Ebe123 has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/370209 )
Change subject: Output SVG in scores ...................................................................... Output SVG in scores This change makes scores be outputted in SVG, and rendered in- browser if supported. A .png fallback is also created through the MediaHandler, for both files to be output in an <img tag with scrset as the SVG, as discussed in T134455. The SVG output uses the '-dcrop' option, of which is under code review: https://codereview.appspot.com/326960043/ . This option automatically places all the music on 1 file, of which is trimmed. As the '-dcrop' option outputs a trimmed file, the $wgScoreTrim global has been removed as it is now automatic. Further, the multipage functionality has been removed since all is outputted in 1 file. DO NOT MERGE UNTIL -DCROP IS RELEASED UPSTREAM. Change-Id: Ieb13455825053cb187e6240b238fa7b6bd5c18c1 Task: T49578 --- M README M Score.body.php M Score.hooks.php M extension.json M i18n/en.json M i18n/qqq.json 6 files changed, 78 insertions(+), 110 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Score refs/changes/09/370209/1 diff --git a/README b/README index bece82e..24c481c 100644 --- a/README +++ b/README @@ -4,8 +4,7 @@ ============= This extension uses LilyPond to render score images, so you need a working -LilyPond installation. If you want the extension to trim the score files for -you, you will also need ImageMagick. +LilyPond installation. The extension is also capable of creating Ogg/Vorbis files from the MIDI files generated by LilyPond. If you want to make use of this functionality, you need @@ -38,7 +37,6 @@ LilyPond conversion */ $wgScoreTimidty = '/path/to/your/timidty/executable'; /* if you want MIDI to Vorbis conversion */ - $wgScoreTrim = true; /* Set to false if you don't want score trimming */ to your LocalSettings.php file. If you get unexpected out-of-memory errors, you may also have to increase $wgMaxShellMemory (see @@ -107,4 +105,3 @@ provided you installed and configured the OggHandler extension. An Ogg/Vorbis player will be embedded in the HTML below the score image(s). - diff --git a/Score.body.php b/Score.body.php index 6cb8304..d19b782 100644 --- a/Score.body.php +++ b/Score.body.php @@ -360,9 +360,9 @@ $existingFiles[$file] = true; } - /* Generate PNG and MIDI files if necessary */ - $imageFileName = "{$options['file_name_prefix']}.png"; - $multi1FileName = "{$options['file_name_prefix']}-page1.png"; + /* Generate SVG and MIDI files if necessary */ + $svgFileName = "{$options['file_name_prefix']}.svg"; + $pngFileName = "{$options['file_name_prefix']}.png"; $midiFileName = "{$options['file_name_prefix']}.midi"; $metaDataFileName = "{$options['file_name_prefix']}.json"; @@ -380,11 +380,11 @@ if ( !isset( $existingFiles[$metaDataFileName] ) || ( - !isset( $existingFiles[$imageFileName] ) - && !isset( $existingFiles[$multi1FileName] ) - ) + !isset( $existingFiles[$svgFileName] ) + && !isset( $existingFiles[$pngFileName] ) + ) || !isset( $existingFiles[$midiFileName] ) ) { - $existingFiles += self::generatePngAndMidi( $code, $options, $metaData ); + $existingFiles += self::generateImageAndMidi( $code, $options, $metaData ); } /* Generate Ogg/Vorbis file if necessary */ @@ -420,34 +420,16 @@ } /* return output link(s) */ - if ( isset( $existingFiles[$imageFileName] ) ) { - list( $width, $height ) = $metaData[$imageFileName]['size']; + if ( isset( $existingFiles[$svgFileName] ) ) { + list( $width, $height ) = $metaData[$svgFileName]['size']; $link = Html::rawElement( 'img', [ - 'src' => "{$options['dest_url']}/$imageFileName", + 'src' => "{$options['dest_url']}/$pngFileName", + 'srcset' => "{$options['dest_url']}/$svgFileName 1x", 'width' => $width, 'height' => $height, 'alt' => $code, + 'class' => 'ScoreSVG', ] ); - } elseif ( isset( $existingFiles[$multi1FileName] ) ) { - $link = ''; - for ( $i = 1; ; ++$i ) { - $fileName = "{$options['file_name_prefix']}-page$i.png"; - if ( !isset( $existingFiles[$fileName] ) ) { - break; - } - $pageNumb = wfMessage( 'score-page' ) - ->inContentLanguage() - ->numParams( $i ) - ->plain(); - list( $width, $height ) = $metaData[$fileName]['size']; - $link .= Html::rawElement( 'img', [ - 'src' => "{$options['dest_url']}/$fileName", - 'width' => $width, - 'height' => $height, - 'alt' => $pageNumb, - 'title' => $pageNumb - ] ); - } } else { /* No images; this may happen in raw mode or when the user omits the score code */ throw new ScoreException( wfMessage( 'score-noimages' ) ); @@ -504,7 +486,7 @@ } /** - * Generates score PNG file(s) and a MIDI file. + * Generates score SVG, PNG, and MIDI files. * * @param $code string Score code. * @param $options array Rendering options. They are the same as for @@ -516,8 +498,8 @@ * * @throws ScoreException on error. */ - private static function generatePngAndMidi( $code, $options, &$metaData ) { - global $wgScoreLilyPond, $wgScoreTrim; + private static function generateImageAndMidi( $code, $options, &$metaData ) { + global $wgScoreLilyPond; if ( !is_executable( $wgScoreLilyPond ) ) { throw new ScoreException( wfMessage( 'score-notexecutable', $wgScoreLilyPond ) ); @@ -528,8 +510,9 @@ self::createDirectory( $factoryDirectory, 0700 ); $factoryLy = "$factoryDirectory/file.ly"; $factoryMidi = "$factoryDirectory/file.midi"; - $factoryImage = "$factoryDirectory/file.png"; - $factoryImageTrimmed = "$factoryDirectory/file-trimmed.png"; + $factoryPng = "$factoryDirectory/file.png"; + $factorySvg = "$factoryDirectory/file.cropped.svg"; + $factorySvgProcessed = "$factoryDirectory/file.svg"; /* Generate LilyPond input file */ if ( $options['lang'] == 'lilypond' ) { @@ -565,8 +548,8 @@ $env = [ 'LILYPOND_GC_YIELD' => '25' ]; $cmd = wfEscapeShellArg( $wgScoreLilyPond ) - . ' ' . wfEscapeShellArg( '-dsafe=#t' ) - . ' -dbackend=ps --png --header=texidoc ' + . ' -dsafe=#t -dbackend=svg -dno-point-and-click -dno-print-pages' + . ' -dcrop --header=texidoc ' . wfEscapeShellArg( $factoryLy ) . ' 2>&1'; $output = wfShellExec( $cmd, $rc2, $env ); @@ -589,21 +572,11 @@ } } - /* trim output images if wanted */ - if ( $wgScoreTrim ) { - if ( file_exists( $factoryImage ) ) { - self::trimImage( $factoryImage, $factoryImageTrimmed ); - } else { - for ( $i = 1; ; ++$i ) { - $src = "$factoryDirectory/file-page$i.png"; - if ( !file_exists( $src ) ) { - break; - } - $dest = "$factoryDirectory/file-page$i-trimmed.png"; - self::trimImage( $src, $dest ); - } - } - } + // Process SVG file + self::processSvg( $factorySvg, $factorySvgProcessed ); + + // Create PNG fallback + self::createPngFallback( $factorySvg, $factoryPng ); // Create the destination directory if it doesn't exist $backend = self::getBackend(); @@ -629,43 +602,26 @@ } } - // Add the PNGs - if ( $wgScoreTrim ) { - $src = $factoryImageTrimmed; - } else { - $src = $factoryImage; - } - if ( file_exists( $src ) ) { + // Add the SVG + if ( file_exists( $factorySvgProcessed ) ) { + $dstFileName = "{$options['file_name_prefix']}.svg"; + $ops[] = [ + 'op' => 'store', + 'src' => $factorySvgProcessed, + 'dst' => "{$options['dest_storage_path']}/$dstFileName" ]; + + list( $width, $height ) = self::imageSize( $factorySvgProcessed ); + $metaData[$dstFileName]['size'] = [ $width, $height ]; + $newFiles[$dstFileName] = true; + + // Now for the PNG $dstFileName = "{$options['file_name_prefix']}.png"; $ops[] = [ 'op' => 'store', - 'src' => $src, + 'src' => $factoryPng, 'dst' => "{$options['dest_storage_path']}/$dstFileName" ]; - list( $width, $height ) = self::imageSize( $src ); - $metaData[$dstFileName]['size'] = [ $width, $height ]; $newFiles[$dstFileName] = true; - } else { - for ( $i = 1; ; ++$i ) { - if ( $wgScoreTrim ) { - $src = "$factoryDirectory/file-page$i-trimmed.png"; - } else { - $src = "$factoryDirectory/file-page$i.png"; - } - if ( !file_exists( $src ) ) { - break; - } - $dstFileName = "{$options['file_name_prefix']}-page$i.png"; - $dest = "{$options['dest_storage_path']}/$dstFileName"; - $ops[] = [ - 'op' => 'store', - 'src' => $src, - 'dst' => $dest ]; - - list( $width, $height ) = self::imageSize( $src ); - $metaData[$dstFileName]['size'] = [ $width, $height ]; - $newFiles[$dstFileName] = true; - } } $dstFileName = "{$options['file_name_prefix']}.json"; @@ -692,8 +648,34 @@ * @return array of ints (width, height) */ private static function imageSize( $filename ) { - list( $width, $height ) = getimagesize( $filename ); + $svg = simplexml_load_file( $filename ); + + $width = intval($svg['width']); + $height = intval($svg['height']); return [ $width, $height ]; + } + + /** + * Convert height and width from SVG file + * + * @param string $source + * @param string $dest + */ + private static function processSvg( $source, $dest ) { + $svg = simplexml_load_file( $source ); + + // Remove mm + $width = floatval( strstr( $svg['width'], 'mm', true ) ); + $height = floatval( strstr( $svg['height'], 'mm', true ) ); + + // Convert to px + $mmToPxFactor = 3.543307; + $width = intval( ceil( $width * $mmToPxFactor ) ); + $height = intval( ceil( $height * $mmToPxFactor ) ); + + $svg['width'] = $width; + $svg['height'] = $height; + file_put_contents( $dest, $svg->asXML() ); } /** @@ -890,24 +872,19 @@ } /** - * Trims an image with ImageMagick. + * Use convert to create png of given SVG file * - * @param $source string Local path to the source image. - * @param $dest string Local path to the target (trimmed) image. + * @param $source string Local path to the source svg image. + * @param $dest string Local path to the target png image. * * @throws ScoreException on error. */ - private static function trimImage( $source, $dest ) { - global $wgImageMagickConvertCommand; - - $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) - . ' -trim -transparent white ' - . wfEscapeShellArg( $source ) . ' ' - . wfEscapeShellArg( $dest ) - . ' 2>&1'; - $output = wfShellExec( $cmd, $rc ); - if ( $rc != 0 ) { - self::throwCallException( wfMessage( 'score-trimerr' ), $output ); + private static function createPngFallback( $source, $dest ) { + list( $width, $height ) = self::imageSize( $source ); + $handler = MediaHandler::getHandler( 'image/svg' ); + $status = $handler->rasterize( $source, $dest, $width, $height ); + if ( $status !== true ) { + self::throwCallException( wfMessage( 'score-pngerr' ), $status->toText() ); } } diff --git a/Score.hooks.php b/Score.hooks.php index 855a7b4..859f25d 100644 --- a/Score.hooks.php +++ b/Score.hooks.php @@ -6,11 +6,6 @@ * @return bool Returns true */ public static function onParserFirstCallInit( Parser &$parser ) { - global $wgUseImageMagick, $wgScoreTrim; - if ( $wgScoreTrim === null ) { - // Default to if we use Image Magick, since it requires Image Magick. - $wgScoreTrim = $wgUseImageMagick; - } $parser->setHook( 'score', 'Score::render' ); return true; } diff --git a/extension.json b/extension.json index f585433..ce7b601 100644 --- a/extension.json +++ b/extension.json @@ -67,7 +67,6 @@ ] }, "config": { - "ScoreTrim": null, "ScoreLilyPond": "/usr/bin/lilypond", "ScoreAbc2Ly": "/usr/bin/abc2ly", "ScoreTimidity": "/usr/bin/timidity", diff --git a/i18n/en.json b/i18n/en.json index 263e931..d4c6552 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -26,11 +26,11 @@ "score-oggconversionerr": "Unable to convert MIDI to Ogg/Vorbis:\n$1", "score-oggoverridenotfound": "The file \"<nowiki>$1</nowiki>\" you specified with override_ogg does not exist.", "score-page": "Page $1", + "score-pngerr": "SVG could not be converted to PNG:\n$1", "score-pregreplaceerr": "PCRE regular expression replacement failed", "score-readerr": "Unable to read file $1.", "score-timiditynotexecutable": "TiMidity++ could not be executed: $1 is not an executable file. Make sure <code>$wgScoreTimidity</code> is set correctly.", "score-renameerr": "Error moving score files to upload directory.", - "score-trimerr": "Image could not be trimmed:\n$1\nSet <code>$wgScoreTrim=false</code> if this problem persists.", "score-versionerr": "Unable to obtain LilyPond version:\n$1", "score-visualeditor-mwscoreinspector-card-advanced": "Advanced", "score-visualeditor-mwscoreinspector-card-audio": "Audio", diff --git a/i18n/qqq.json b/i18n/qqq.json index bcbc759..ba5d20c 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -32,11 +32,11 @@ "score-oggconversionerr": "Displayed if the MIDI to Ogg/Vorbis conversion failed. $1 is the error (generally big block of text in a pre tag)", "score-oggoverridenotfound": "Displayed if the file specified with the override_ogg=\"…\" attribute could not be found. $1 is the value of the override_ogg attribute.", "score-page": "The word \"Page\" as used in pagination. Parameters:\n* $1 - the page number\n{{Identical|Page}}", + "score-pngerr": "Displayed if the extension failed to convert SVG to PNG. $1 is the error (generally big block of text in a pre tag)", "score-pregreplaceerr": "Displayed if a PCRE regular expression replacement failed.", "score-readerr": "Displayed if the extension could not read a file. $1 is the path to the file that could not be read.", "score-timiditynotexecutable": "Displayed if TiMidity++ could not be executed. $1 is the path to the TiMidity++ binary.", "score-renameerr": "Displayed if moving the resultant files from the working environment to the upload directory fails.", - "score-trimerr": "Displayed if the extension failed to trim an output image. $1 is the error (generally big block of text in a pre tag)", "score-versionerr": "Displayed if the extension failed to obtain the version string of LilyPond. $1 is the LilyPond stdout output generated by the attempt.", "score-visualeditor-mwscoreinspector-card-advanced": "Label for the advanced card of the score inspector\n{{Identical|Advanced}}", "score-visualeditor-mwscoreinspector-card-audio": "Label for the audio card of the score inspector\n{{Identical|Audio}}", -- To view, visit https://gerrit.wikimedia.org/r/370209 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Ieb13455825053cb187e6240b238fa7b6bd5c18c1 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/Score Gerrit-Branch: master Gerrit-Owner: Ebe123 <[email protected]> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
