getNamespace() !== NS_AV_ANNOT ) { return false; } return parent::canBeUsedOn( $title ); } public function supportsPreloadContent(): bool { return true; } public function serializeContent( Content $content, $format = null ) { return parent::serializeContent( $content, $format ); } public function unserializeContent( $text, $format = null ) { return new AnnotationContent( $text ); } public function makeEmptyContent() { return new AnnotationContent( '' ); } protected function fillParserOutput( Content $content, ContentParseParams $cpoParams, ParserOutput &$output ) { self::console_log('Fill Parser Output', true); parent::fillParserOutput( $content, $cpoParams, $output ); try { $metadata = toml_decode($content->getText(), true); } catch (TomlError $e) { $output->addWarningMsg( $e->getMessage() ); $output->setText( 'No image available' ); return; } $output->setText( '
' . self::buildSvg($metadata) . '
' ); } /** * Build annotated SVG from TOML metadata * * This takes in the metadata text from the file page (or the current editor) * and produces the html string for the svg with base image and annotations. * * @param string $metadata The metadata object parsed from the text * @return string Html string of the complete svg */ private function buildSvg($metadata) { //Set SVG UID-ish for element IDs $svgID = wfRandomString(12); //Check if the image to annotate has been set if (isset($metadata['baseImage'])) { $imageTitle = Title::makeTitleSafe( NS_FILE, $metadata['baseImage'] ); $baseImage = MediaWikiServices::getInstance()->getRepoGroup()->findFile($imageTitle); } else { $baseImage = false; } //Gather basic data $elBaseImg = ''; if ($baseImage) { $imageStr = 'data:image/png;base64,' . base64_encode(file_get_contents($baseImage->getLocalRefPath())); $baseHeight = $baseImage->getHeight(); $baseWidth = $baseImage->getWidth(); $baseAspect = $baseHeight / $baseWidth; $fixScale = $baseWidth / 100 * sqrt($baseAspect); $attrBase = array( 'class' => 'annot-base', 'preserveAspectRatio' => 'none', 'width' => '100%', 'height' => '100%', 'href' => $imageStr ); $elBaseImg = Html::rawElement('image', $attrBase); } //Render and return svg $attrSvg = array( 'class' => 'annot-svg', 'version' => '1.1', 'viewBox' => "0 0 {$baseWidth} {$baseHeight}", 'xml:space' => 'preserve', 'xmlns' => 'http://www.w3.org/2000/svg', 'width' => $baseWidth, 'height' => $baseHeight, 'style' => 'width: 100%; height: auto;', 'mwScale' => $fixScale ); $elCompass = ''; $useCompass = ''; if (isset($metadata['view'])) { $viewLabels = self::getCompassLabels($metadata['view']['direction'], $metadata['view']['rostral']); $rotate = $metadata['view']['rotate'] ?? 0; $textRotate = $rotate * -1; $compSize = $metadata['view']['size'] ?? 25; if (isset($metadata['view']['right'])) { $compX = ($baseWidth / $fixScale) - $metadata['view']['right'] - $compSize; } else { $compX = $metadata['view']['left'] ?? 1; } if (isset($metadata['view']['bottom'])) { $compY = ($baseHeight / $fixScale) - $metadata['view']['bottom'] - $compSize; } else { $compY = $metadata['view']['top'] ?? 1; } $elCompass = << {$viewLabels[0]} {$viewLabels[1]} {$viewLabels[2]} {$viewLabels[3]} {$viewLabels[4]} comp; $compRotX = $compX + ($compSize * .5); $compRotY = $compY + ($compSize * .5); $attrCompass = array( 'href'=> "#compass" . $svgID, 'transform' => "scale( $fixScale ) rotate( $rotate $compRotX $compRotY)", 'x' => $compX, 'y' => $compY, 'width' => $compSize, 'height' => $compSize, 'style' => "transform-box: fill-box;" ); $useCompass = Html::rawElement('use', $attrCompass); } $annotations = []; $svgDefs = ''; if (isset($metadata['annotation'])) { $maskCircles = []; $leadLines = []; foreach($metadata['annotation'] as $label => $annot) { if (isset($annot['position'])) { $marker = array(Html::openElement('g',array('transform' => "scale( {$fixScale} )"))); $attrMask = array( 'cx' => $annot['position']['0'], 'cy' => $annot['position']['1'], 'r' => '3.75', 'fill' => 'black' ); $maskCirc = Html::rawElement('circle',$attrMask); array_push($maskCircles,$maskCirc); $markColor = isset($annot['color']) ? $annot['color'] : '#ff0000'; $attrMarkerCirc = array( 'cx' => $annot['position']['0'], 'cy' => $annot['position']['1'], 'r' => '3.75', 'style' => "fill-opacity: .35; stroke-linejoin: bevel; stroke-width: .3793; stroke: {$markColor};", 'fill' => (isset($annot['light']) && $annot['light']) ? 'white' : 'black' ); $markCirc = Html::rawElement('circle',$attrMarkerCirc); array_push($marker,$markCirc); $attrMarkerText = array( 'x' => $annot['position']['0'], 'y' => $annot['position']['1'], 'style' => "fill: {$markColor}; font-size: 5px; stroke-linejoin: bevel; stroke-width: .6968; text-align: center; text-anchor: middle; transform: translateY(1.5px);" ); $markText = Html::rawElement('text',$attrMarkerText, isset($annot['label']) ? $annot['label'] : $label ); array_push($marker,$markText); if(isset($annot['leader'])) { foreach($annot['leader'] as $line) { $lineAttr = array( 'd' => sprintf('M%s %s %s %s',$annot['position']['0'], $annot['position']['1'], $line['0'], $line['1']), 'mask' => "url(#annotMask_{$svgID})", 'style' => "fill-opacity:.35; fill: {$markColor}; stroke-linejoin:bevel; stroke-width:.4; stroke: {$markColor};" ); $lineText = Html::rawElement('path', $lineAttr); array_push($marker, $lineText); } } array_push($marker,Html::closeElement('g')); array_push($annotations, implode($marker)); } } $attrMaskRect = array( 'width' => $baseWidth / $fixScale, 'height' => $baseHeight / $fixScale, 'fill' => 'white' ); $maskRect = Html::rawElement('rect',$attrMaskRect); $markMask = Html::rawElement('mask', array('id' => 'annotMask_' . $svgID), $maskRect . implode($maskCircles)); $svgDefs = Html::rawElement('defs',array(),$markMask); } $paths = []; if (isset($metadata['path'])) { array_push($paths, Html::openElement('g',array('transform' => "scale( {$fixScale} )"))); foreach($metadata['path'] as $drawPath) { if (isset($drawPath['d'])) { $pathColor = $drawPath['color'] ?? '#ff0000'; $pathPattern = $drawPath['pattern'] ?? '2 1'; $attrPath = array( 'd' => $drawPath['d'], 'fill' => 'none', 'stroke-dasharray' => $pathPattern, 'style' => "fill-opacity:.35;stroke-linejoin:bevel;stroke-width:.4;stroke:{$pathColor}" ); array_push($paths,Html::rawElement('path',$attrPath)); } } array_push($paths,Html::closeElement('g')); } return Html::rawElement('svg', $attrSvg, $svgDefs. $elCompass . $elBaseImg . implode($annotations). implode($paths) . $useCompass ); } public function validateSave( Content $content, ValidationParams $validationParams ) { self::console_log('Validate Save', true); $tempDir = sys_get_temp_dir(); $status = Status::newGood(); try { $tomlCheck = toml_decode($content->getText(), true); } catch (TomlError $e) { $status->fatal( 'content-failed-to-parse', 'SVG', "", $e->getMessage() ); return; } $svgText = self::buildSvg($tomlCheck); $pageTitle = $validationParams->getPageIdentity()->__toString(); $tmpFileFactory = MediaWikiServices::getInstance()->getTempFSFileFactory(); $tmpFile = $tmpFileFactory->newTempFSFile( 'annotSvg_', 'svg' ); if ( !$tmpFile ) { $status->fatal( 'svg-error-create-temp', 'Failed to create temp SVG' ); return; } $tmpPath = $tmpFile->getPath(); self::console_log($tmpPath, true); $result = file_put_contents( $tmpPath, $svgText ); if ( $result === false ) { $status->fatal( 'svg-error-write-temp', 'Failed to write SVG text' ); return; } $repoGroup = MediaWikiServices::getInstance()->getRepoGroup(); $localRepo = $repoGroup->getLocalRepo(); $targetTitleText = 'File:' . substr($pageTitle, 11) . '.svg'; $targetTitle = Title::newFromText($targetTitleText); $newImage = new UploadFromFile(); $newImage->initializePathInfo( 'File:' . substr($pageTitle, 11) . '.svg', $tmpPath, filesize( $tmpPath ), true ); $newImage->performUpload('Generate annotation svg','Annotation image automatically generated from [[' . $pageTitle . ']]', false, \User::newSystemUser( 'MediaWiki default' )); return $status; } /** * Return the correct sequence of view compass labels based on the defined view * * @param string $direction the defined view direction * @param bool true if 'rostral' should be used in place of 'cranial' * @return string[] sequence of view labels */ private function getCompassLabels($direction = 'L', $rostral = false) { self::console_log(print_r($direction,true), true); if ($rostral && $direction == 'Cr') { $direction = 'Ro'; } $views = array( 'L' => array('L','D','Cd','V','Cr'), 'Lm' => array('L','D','Cd','V','Cr'), 'R' => array('R','D','Cr','V','Cd'), 'Rm' => array('R','D','Cr','V','Cd'), 'D' => array('D','Cr','R','Cd','L'), 'V' => array('V','Cr','L','Cd','R'), 'Cr' => array('Cr','D','L','V','R'), 'Ro' => array('Ro','D','L','V','R'), 'Cd' => array('Cd','D','R','V','L') ); return $views[$direction]; } /** * Small helper function to display information on the browser console * * Usage: * echo ''; * * @param $data information to display * @param bool $add_script_tags true to put information is inside complete script tag */ public static function console_log($data, $add_script_tags = false) { $command = 'console.log('. json_encode($data, JSON_HEX_TAG).');'; if ($add_script_tags) { $command = ''; } echo $command; } }