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;' ); if (isset($metadata['view'])) { $viewLabels = self::getCompassLabels($metadata['view']['direction'], $metadata['view']['rostral']); $attrCompass = array( 'id' => "compass{$svgID}", 'viewBox' => '0 0 100 100', 'preserveAspectRatio' => 'xMinYMin meet', 'refX' => 'left', 'refY' => 'top', 'style' => "font-family:'TeX Gyre DejaVu Math';font-size:11.686px;letter-spacing:0px;line-height:125%;text-align:center;text-anchor:middle;word-spacing:0px;" ); $compass = array( Html::openElement('symbol',$attrCompass) ); $attrCompBack = array( 'cx' => '50', 'cy' => '50', 'r' => '50', 'style' => 'fill-opacity:.35;fill:#ffffff;' ); array_push($compass,Html::rawElement('circle',$attrCompBack)); $attrCompBkArrow = array( 'd' => 'M 50,81.3 41.4,58.6 18.7,50 41.4,41.4 50,18.7 58.6,41.4 81.3,50 58.6,58.6 Z', 'style' => 'fill:#ffffff;stroke-width:1.5025' ); array_push($compass, Html::rawElement('path',$attrCompBkArrow)); array_push($compass,Html::closeElement('symbol')); } if (isset($metadata['annotation'])) { $annotations = []; $maskCircles = []; $leadLines = []; foreach($metadata['annotation'] as $label => $annot) { if (isset($annot['position'])) { $marker = array(Html::openElement('g',array('transform' => "scale( {$fixScale} {$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); } return Html::rawElement('svg', $attrSvg, $svgDefs. implode($compass) . $elBaseImg . implode($annotations) ); } 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; } }