Display SVGs in target language
authorMax Semenik <maxsem.wiki@gmail.com>
Sat, 6 Oct 2018 00:56:53 +0000 (17:56 -0700)
committerMax Semenik <maxsem.wiki@gmail.com>
Tue, 30 Oct 2018 23:12:11 +0000 (16:12 -0700)
Previously, they were always displayed in defult language unless
forced explicitly in wikitext, e.g. [[File:Foo.svg|lang=ru]].
This change adds a feature flag that would enable always trying to
display in page language.

* If enabled, Parser will pass a new parameter - 'pagelang' - to
  the media handler.
* SvgHandler uses page language when determining what language to
  render the image in.
* 'pagelang' can always be overridden by 'lang'.
* If no translation in page language is available, the default
  language (English) will be used for thumbnail URLs, to prevent
  cluttering media storage and HTTP caches with useless copies.

Performance: this requires accessing image's metadata during parsing.
My testing indicates there were no code path where this wasn't the
case already, so no performance hit is expected, however we should
still keep an eye on page save performance.

Bug: T205040
Change-Id: I348840ef405e1370cc0c17d69051bce30153c9c0

RELEASE-NOTES-1.33
includes/DefaultSettings.php
includes/Linker.php
includes/media/ImageHandler.php
includes/media/SvgHandler.php
includes/parser/Parser.php
tests/parser/ParserTestRunner.php
tests/parser/parserTests.txt
tests/phpunit/includes/media/SVGTest.php [deleted file]
tests/phpunit/includes/media/SvgHandlerTest.php [new file with mode: 0644]

index 3e7e496..5ed9d5a 100644 (file)
@@ -11,7 +11,8 @@ production.
 * $wgEnablePartialBlocks – This enables the Partial Blocks feature, which gives
   accounts with block permissions the ability to block users, IPs, and IP ranges
   from editing specific pages, while allowing them to edit the rest of the wiki.
-* …
+* $wgMediaInTargetLanguage – whether multilingual images should be dispalyed in
+  the current parse language where available.
 
 ==== Changed configuration ====
 * …
index 82dbecf..9c26a28 100644 (file)
@@ -1225,6 +1225,16 @@ $wgSVGMetadataCutoff = 262144;
  */
 $wgAllowTitlesInSVG = false;
 
+/**
+ * Whether thumbnails should be generated in target language (usually, same as
+ * page language), if available.
+ * Currently, applies only to SVG images that use the systemLanguage attribute
+ * to specify text language.
+ *
+ * @since 1.33
+ */
+$wgMediaInTargetLanguage = false;
+
 /**
  * The maximum number of pixels a source image can have if it is to be scaled
  * down by a scaler that requires the full source image to be decompressed
index a3fba83..1d1ad06 100644 (file)
@@ -289,6 +289,7 @@ class Linker {
         *          link-title      Title object to link to
         *          link-target     Value for the target attribute, only with link-url
         *          no-link         Boolean, suppress description link
+        *          targetlang      (optional) Target language code, see Parser::getTargetLanguage()
         *
         * @param array $handlerParams Associative array of media handler parameters, to be passed
         *       to transform(). Typical keys are "width" and "page".
index a0a1603..e88c1b0 100644 (file)
@@ -83,7 +83,7 @@ abstract class ImageHandler extends MediaHandler {
         * @param array &$params
         * @return bool
         */
-       function normaliseParams( $image, &$params ) {
+       public function normaliseParams( $image, &$params ) {
                $mimeType = $image->getMimeType();
 
                if ( !isset( $params['width'] ) ) {
index a9c7b4f..e3057f4 100644 (file)
@@ -136,6 +136,16 @@ class SvgHandler extends ImageHandler {
                return null;
        }
 
+       /**
+        * Determines render language from image parameters
+        *
+        * @param array $params
+        * @return string
+        */
+       protected function getLanguageFromParams( array $params ) {
+               return $params['lang'] ?? $params['targetlang'] ?? 'en';
+       }
+
        /**
         * What language to render file in if none selected
         *
@@ -160,11 +170,27 @@ class SvgHandler extends ImageHandler {
         * @param array &$params
         * @return bool
         */
-       function normaliseParams( $image, &$params ) {
-               global $wgSVGMaxSize;
-               if ( !parent::normaliseParams( $image, $params ) ) {
-                       return false;
+       public function normaliseParams( $image, &$params ) {
+               if ( parent::normaliseParams( $image, $params ) ) {
+                       $params = $this->normaliseParamsInternal( $image, $params );
+                       return true;
                }
+
+               return false;
+       }
+
+       /**
+        * Code taken out of normaliseParams() for testability
+        *
+        * @since 1.33
+        *
+        * @param File $image
+        * @param array $params
+        * @return array Modified $params
+        */
+       protected function normaliseParamsInternal( $image, $params ) {
+               global $wgSVGMaxSize;
+
                # Don't make an image bigger than wgMaxSVGSize on the smaller side
                if ( $params['physicalWidth'] <= $params['physicalHeight'] ) {
                        if ( $params['physicalWidth'] > $wgSVGMaxSize ) {
@@ -181,8 +207,15 @@ class SvgHandler extends ImageHandler {
                                $params['physicalHeight'] = $wgSVGMaxSize;
                        }
                }
+               // To prevent the proliferation of thumbnails in languages not present in SVGs, unless
+               // explicitly forced by user.
+               if ( isset( $params['targetlang'] ) ) {
+                       if ( !$image->getMatchedLanguage( $params['targetlang'] ) ) {
+                               unset( $params['targetlang'] );
+                       }
+               }
 
-               return true;
+               return $params;
        }
 
        /**
@@ -201,7 +234,7 @@ class SvgHandler extends ImageHandler {
                $clientHeight = $params['height'];
                $physicalWidth = $params['physicalWidth'];
                $physicalHeight = $params['physicalHeight'];
-               $lang = $params['lang'] ?? $this->getDefaultRenderLanguage( $image );
+               $lang = $this->getLanguageFromParams( $params );
 
                if ( $flags & self::TRANSFORM_LATER ) {
                        return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
@@ -531,8 +564,9 @@ class SvgHandler extends ImageHandler {
         */
        public function makeParamString( $params ) {
                $lang = '';
-               if ( isset( $params['lang'] ) && $params['lang'] !== 'en' ) {
-                       $lang = 'lang' . strtolower( $params['lang'] ) . '-';
+               $code = $this->getLanguageFromParams( $params );
+               if ( $code !== 'en' ) {
+                       $lang = 'lang' . strtolower( $code ) . '-';
                }
                if ( !isset( $params['width'] ) ) {
                        return false;
index 3509200..721d1fb 100644 (file)
@@ -5258,6 +5258,8 @@ class Parser {
                #  * bottom
                #  * text-bottom
 
+               global $wgMediaInTargetLanguage;
+
                # Protect LanguageConverter markup when splitting into parts
                $parts = StringUtils::delimiterExplode(
                        '-{', '}-', '|', $options, true /* allow nesting */
@@ -5415,6 +5417,9 @@ class Parser {
                        # Use the "caption" for the tooltip text
                        $params['frame']['title'] = $this->stripAltText( $caption, $holders );
                }
+               if ( $wgMediaInTargetLanguage ) {
+                       $params['handler']['targetlang'] = $this->getTargetLanguage()->getCode();
+               }
 
                Hooks::run( 'ParserMakeImageParams', [ $title, $file, &$params, $this ] );
 
index 1f3183f..5c4c48c 100644 (file)
@@ -300,6 +300,7 @@ class ParserTestRunner {
                $setup['wgHtml5'] = true;
                $setup['wgDisableLangConversion'] = false;
                $setup['wgDisableTitleConversion'] = false;
+               $setup['wgMediaInTargetLanguage'] = false;
 
                // "extra language links"
                // see https://gerrit.wikimedia.org/r/111390
@@ -1113,6 +1114,7 @@ class ParserTestRunner {
                                + [ 'ISBN' => true, 'PMID' => true, 'RFC' => true ],
                        // Test with legacy encoding by default until HTML5 is very stable and default
                        'wgFragmentMode' => [ 'legacy' ],
+                       'wgMediaInTargetLanguage' => self::getOptionValue( 'wgMediaInTargetLanguage', $opts, false ),
                ];
 
                $nonIncludable = self::getOptionValue( 'wgNonincludableNamespaces', $opts, false );
@@ -1393,7 +1395,17 @@ class ParserTestRunner {
                                'bits'        => 0,
                                'media_type'  => MEDIATYPE_DRAWING,
                                'mime'        => 'image/svg+xml',
-                               'metadata'    => serialize( [] ),
+                               'metadata'    => serialize( [
+                                       'version'        => SvgHandler::SVG_METADATA_VERSION,
+                                       'width'          => 240,
+                                       'height'         => 180,
+                                       'originalWidth'  => '100%',
+                                       'originalHeight' => '100%',
+                                       'translations'   => [
+                                               'en' => SVGReader::LANG_FULL_MATCH,
+                                               'ru' => SVGReader::LANG_FULL_MATCH,
+                                       ],
+                               ] ),
                                'sha1'        => Wikimedia\base_convert( '', 16, 36, 31 ),
                                'fileExists'  => true
                ], $this->db->timestamp( '20010115123500' ), $user );
index bbd9ecb..d836111 100644 (file)
@@ -37,7 +37,7 @@
 # You can also set the following parser properties via test options:
 #  wgEnableUploads, wgAllowExternalImages, wgMaxTocLevel,
 #  wgLinkHolderBatchSize, wgRawHtml, wgInterwikiMagic,
-#  wgEnableMagicLinks
+#  wgEnableMagicLinks, wgMediaInTargetLanguage
 #
 # For testing purposes, temporary articles can created:
 # !!article / NAMESPACE:TITLE / !!text / ARTICLE TEXT / !!endarticle
@@ -15370,6 +15370,30 @@ parsoid=wt2html,wt2wt,html2html
 <figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.svg"><img resource="./File:Foobar.svg" src="//example.com/images/thumb/f/ff/Foobar.svg/220px-Foobar.svg.png" data-file-width="240" data-file-height="180" data-file-type="drawing" height="165" width="220"/></a><figcaption>lang=invalid:language:code</figcaption></figure>
 !! end
 
+!! test
+SVG thumbnails in page language
+!! options
+language=ru
+wgMediaInTargetLanguage = true
+!! wikitext
+[[File:Foobar.svg]] [[File:Foobar.svg|lang=en]]
+!! html/php
+<p><a href="/wiki/%D0%A4%D0%B0%D0%B9%D0%BB:Foobar.svg" class="image"><img alt="Foobar.svg" src="http://example.com/images/thumb/f/ff/Foobar.svg/langru-240px-Foobar.svg.png" width="240" height="180" srcset="http://example.com/images/thumb/f/ff/Foobar.svg/langru-360px-Foobar.svg.png 1.5x, http://example.com/images/thumb/f/ff/Foobar.svg/langru-480px-Foobar.svg.png 2x" /></a> <a href="/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Foobar.svg&amp;lang=en" class="image"><img alt="Foobar.svg" src="http://example.com/images/thumb/f/ff/Foobar.svg/240px-Foobar.svg.png" width="240" height="180" srcset="http://example.com/images/thumb/f/ff/Foobar.svg/360px-Foobar.svg.png 1.5x, http://example.com/images/thumb/f/ff/Foobar.svg/480px-Foobar.svg.png 2x" /></a>
+</p>
+!! end
+
+!! test
+SVG thumbnails in page language not present in the file
+!! options
+language=de
+wgMediaInTargetLanguage = true
+!! wikitext
+[[File:Foobar.svg]] [[File:Foobar.svg|lang=ru]]
+!! html/php
+<p><a href="/wiki/Datei:Foobar.svg" class="image"><img alt="Foobar.svg" src="http://example.com/images/thumb/f/ff/Foobar.svg/240px-Foobar.svg.png" width="240" height="180" srcset="http://example.com/images/thumb/f/ff/Foobar.svg/360px-Foobar.svg.png 1.5x, http://example.com/images/thumb/f/ff/Foobar.svg/480px-Foobar.svg.png 2x" /></a> <a href="/index.php?title=Datei:Foobar.svg&amp;lang=ru" class="image"><img alt="Foobar.svg" src="http://example.com/images/thumb/f/ff/Foobar.svg/langru-240px-Foobar.svg.png" width="240" height="180" srcset="http://example.com/images/thumb/f/ff/Foobar.svg/langru-360px-Foobar.svg.png 1.5x, http://example.com/images/thumb/f/ff/Foobar.svg/langru-480px-Foobar.svg.png 2x" /></a>
+</p>
+!! end
+
 !! test
 T3887: A ISBN with a thumbnail
 !! wikitext
diff --git a/tests/phpunit/includes/media/SVGTest.php b/tests/phpunit/includes/media/SVGTest.php
deleted file mode 100644 (file)
index b68dd0e..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-<?php
-
-/**
- * @group Media
- */
-class SVGTest extends MediaWikiMediaTestCase {
-
-       /**
-        * @var SvgHandler
-        */
-       private $handler;
-
-       protected function setUp() {
-               parent::setUp();
-
-               $this->filePath = __DIR__ . '/../../data/media/';
-
-               $this->setMwGlobals( 'wgShowEXIF', true );
-
-               $this->handler = new SvgHandler;
-       }
-
-       /**
-        * @param string $filename
-        * @param array $expected The expected independent metadata
-        * @dataProvider providerGetIndependentMetaArray
-        * @covers SvgHandler::getCommonMetaArray
-        */
-       public function testGetIndependentMetaArray( $filename, $expected ) {
-               $file = $this->dataFile( $filename, 'image/svg+xml' );
-               $res = $this->handler->getCommonMetaArray( $file );
-
-               $this->assertEquals( $res, $expected );
-       }
-
-       public static function providerGetIndependentMetaArray() {
-               return [
-                       [ 'Tux.svg', [
-                               'ObjectName' => 'Tux',
-                               'ImageDescription' =>
-                                       'For more information see: http://commons.wikimedia.org/wiki/Image:Tux.svg',
-                       ] ],
-                       [ 'Wikimedia-logo.svg', [] ]
-               ];
-       }
-
-       /**
-        * @param string $userPreferredLanguage
-        * @param array $svgLanguages
-        * @param string $expectedMatch
-        * @dataProvider providerGetMatchedLanguage
-        * @covers SvgHandler::getMatchedLanguage
-        */
-       public function testGetMatchedLanguage( $userPreferredLanguage, $svgLanguages, $expectedMatch ) {
-               $match = $this->handler->getMatchedLanguage( $userPreferredLanguage, $svgLanguages );
-               $this->assertEquals( $expectedMatch, $match );
-       }
-
-       public function providerGetMatchedLanguage() {
-               return [
-                       'no match' => [
-                               'userPreferredLanguage' => 'en',
-                               'svgLanguages' => [ 'de-DE', 'zh', 'ga', 'fr', 'sr-Latn-ME' ],
-                               'expectedMatch' => null,
-                       ],
-                       'no subtags' => [
-                               'userPreferredLanguage' => 'en',
-                               'svgLanguages' => [ 'de', 'zh', 'en', 'fr' ],
-                               'expectedMatch' => 'en',
-                       ],
-                       'user no subtags, svg 1 subtag' => [
-                               'userPreferredLanguage' => 'en',
-                               'svgLanguages' => [ 'de-DE', 'en-GB', 'en-US', 'fr' ],
-                               'expectedMatch' => 'en-GB',
-                       ],
-                       'user no subtags, svg >1 subtag' => [
-                               'userPreferredLanguage' => 'sr',
-                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'en-US', 'fr' ],
-                               'expectedMatch' => 'sr-Cyrl-BA',
-                       ],
-                       'user 1 subtag, svg no subtags' => [
-                               'userPreferredLanguage' => 'en-US',
-                               'svgLanguages' => [ 'de', 'en', 'en', 'fr' ],
-                               'expectedMatch' => null,
-                       ],
-                       'user 1 subtag, svg 1 subtag' => [
-                               'userPreferredLanguage' => 'en-US',
-                               'svgLanguages' => [ 'de-DE', 'en-GB', 'en-US', 'fr' ],
-                               'expectedMatch' => 'en-US',
-                       ],
-                       'user 1 subtag, svg >1 subtag' => [
-                               'userPreferredLanguage' => 'sr-Latn',
-                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'fr' ],
-                               'expectedMatch' => 'sr-Latn-ME',
-                       ],
-                       'user >1 subtag, svg >1 subtag' => [
-                               'userPreferredLanguage' => 'sr-Latn-ME',
-                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'en-US', 'fr' ],
-                               'expectedMatch' => 'sr-Latn-ME',
-                       ],
-                       'user >1 subtag, svg <=1 subtag' => [
-                               'userPreferredLanguage' => 'sr-Latn-ME',
-                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl', 'sr-Latn', 'en-US', 'fr' ],
-                               'expectedMatch' => null,
-                       ],
-                       'ensure case-insensitive' => [
-                               'userPreferredLanguage' => 'sr-latn',
-                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl', 'sr-Latn-ME', 'en-US', 'fr' ],
-                               'expectedMatch' => 'sr-Latn-ME',
-                       ],
-               ];
-       }
-}
diff --git a/tests/phpunit/includes/media/SvgHandlerTest.php b/tests/phpunit/includes/media/SvgHandlerTest.php
new file mode 100644 (file)
index 0000000..9c98ada
--- /dev/null
@@ -0,0 +1,367 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group Media
+ */
+class SvgHandlerTest extends MediaWikiMediaTestCase {
+
+       /**
+        * @covers \SvgHandler::getCommonMetaArray()
+        * @dataProvider provideGetIndependentMetaArray
+        *
+        * @param string $filename
+        * @param array $expected The expected independent metadata
+        */
+       public function testGetIndependentMetaArray( $filename, $expected ) {
+               $this->filePath = __DIR__ . '/../../data/media/';
+               $this->setMwGlobals( 'wgShowEXIF', true );
+
+               $file = $this->dataFile( $filename, 'image/svg+xml' );
+               $handler = new SvgHandler();
+               $res = $handler->getCommonMetaArray( $file );
+
+               self::assertEquals( $res, $expected );
+       }
+
+       public static function provideGetIndependentMetaArray() {
+               return [
+                       [ 'Tux.svg', [
+                               'ObjectName' => 'Tux',
+                               'ImageDescription' =>
+                                       'For more information see: http://commons.wikimedia.org/wiki/Image:Tux.svg',
+                       ] ],
+                       [ 'Wikimedia-logo.svg', [] ]
+               ];
+       }
+
+       /**
+        * @covers SvgHandler::getMatchedLanguage()
+        * @dataProvider provideGetMatchedLanguage
+        *
+        * @param string $userPreferredLanguage
+        * @param array $svgLanguages
+        * @param string $expectedMatch
+        */
+       public function testGetMatchedLanguage( $userPreferredLanguage, $svgLanguages, $expectedMatch ) {
+               $handler = new SvgHandler();
+               $match = $handler->getMatchedLanguage( $userPreferredLanguage, $svgLanguages );
+               self::assertEquals( $expectedMatch, $match );
+       }
+
+       public function provideGetMatchedLanguage() {
+               return [
+                       'no match' => [
+                               'userPreferredLanguage' => 'en',
+                               'svgLanguages' => [ 'de-DE', 'zh', 'ga', 'fr', 'sr-Latn-ME' ],
+                               'expectedMatch' => null,
+                       ],
+                       'no subtags' => [
+                               'userPreferredLanguage' => 'en',
+                               'svgLanguages' => [ 'de', 'zh', 'en', 'fr' ],
+                               'expectedMatch' => 'en',
+                       ],
+                       'user no subtags, svg 1 subtag' => [
+                               'userPreferredLanguage' => 'en',
+                               'svgLanguages' => [ 'de-DE', 'en-GB', 'en-US', 'fr' ],
+                               'expectedMatch' => 'en-GB',
+                       ],
+                       'user no subtags, svg >1 subtag' => [
+                               'userPreferredLanguage' => 'sr',
+                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'en-US', 'fr' ],
+                               'expectedMatch' => 'sr-Cyrl-BA',
+                       ],
+                       'user 1 subtag, svg no subtags' => [
+                               'userPreferredLanguage' => 'en-US',
+                               'svgLanguages' => [ 'de', 'en', 'en', 'fr' ],
+                               'expectedMatch' => null,
+                       ],
+                       'user 1 subtag, svg 1 subtag' => [
+                               'userPreferredLanguage' => 'en-US',
+                               'svgLanguages' => [ 'de-DE', 'en-GB', 'en-US', 'fr' ],
+                               'expectedMatch' => 'en-US',
+                       ],
+                       'user 1 subtag, svg >1 subtag' => [
+                               'userPreferredLanguage' => 'sr-Latn',
+                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'fr' ],
+                               'expectedMatch' => 'sr-Latn-ME',
+                       ],
+                       'user >1 subtag, svg >1 subtag' => [
+                               'userPreferredLanguage' => 'sr-Latn-ME',
+                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'en-US', 'fr' ],
+                               'expectedMatch' => 'sr-Latn-ME',
+                       ],
+                       'user >1 subtag, svg <=1 subtag' => [
+                               'userPreferredLanguage' => 'sr-Latn-ME',
+                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl', 'sr-Latn', 'en-US', 'fr' ],
+                               'expectedMatch' => null,
+                       ],
+                       'ensure case-insensitive' => [
+                               'userPreferredLanguage' => 'sr-latn',
+                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl', 'sr-Latn-ME', 'en-US', 'fr' ],
+                               'expectedMatch' => 'sr-Latn-ME',
+                       ],
+               ];
+       }
+
+       /**
+        * @covers \SvgHandler::makeParamString()
+        * @dataProvider provideMakeParamString
+        *
+        * @param array $params
+        * @param string $expected
+        * @param string $message
+        */
+       public function testMakeParamString( array $params, $expected, $message = '' ) {
+               $handler = new SvgHandler();
+               self::assertEquals( $expected, $handler->makeParamString( $params ), $message );
+       }
+
+       public function provideMakeParamString() {
+               return [
+                       [
+                               [],
+                               false,
+                               "Don't thumbnail without knowing width"
+                       ],
+                       [
+                               [ 'lang' => 'ru' ],
+                               false,
+                               "Don't thumbnail without knowing width, even with lang"
+                       ],
+                       [
+                               [ 'width' => 123, ],
+                               '123px',
+                               'Width in thumb'
+                       ],
+                       [
+                               [ 'width' => 123, 'lang' => 'en' ],
+                               '123px',
+                               'Ignore lang=en'
+                       ],
+                       [
+                               [ 'width' => 123, 'targetlang' => 'en' ],
+                               '123px',
+                               'Ignore targetlang=en'
+                       ],
+                       [
+                               [ 'width' => 123, 'lang' => 'en', 'targetlang' => 'ru' ],
+                               '123px',
+                               "lang should override targetlang even of it's in English"
+                       ],
+                       [
+                               [ 'width' => 123, 'targetlang' => 'en' ],
+                               '123px',
+                               'Ignore targetlang=en'
+                       ],
+                       [
+                               [ 'width' => 123, 'lang' => 'en', 'targetlang' => 'en' ],
+                               '123px',
+                               'Ignore lang=targetlang=en'
+                       ],
+                       [
+                               [ 'width' => 123, 'lang' => 'ru' ],
+                               'langru-123px',
+                               'Include lang in thumb'
+                       ],
+                       [
+                               [ 'width' => 123, 'targetlang' => 'ru' ],
+                               'langru-123px',
+                               'Include targetlang in thumb'
+                       ],
+                       [
+                               [ 'width' => 123, 'lang' => 'fr', 'targetlang' => 'sq' ],
+                               'langfr-123px',
+                               'lang should override targetlang'
+                       ],
+               ];
+       }
+
+       /**
+        * @covers SvgHandler::normaliseParamsInternal()
+        * @dataProvider provideNormaliseParamsInternal
+        *
+        * @param $message
+        * @param int $width
+        * @param int $height
+        * @param array $params
+        * @param array $paramsExpected
+        */
+       public function testNormaliseParamsInternal( $message,
+               $width,
+               $height,
+               array $params,
+               array $paramsExpected = null
+       ) {
+               $this->setMwGlobals( 'wgSVGMaxSize', 1000 );
+
+               /** @var SvgHandler $handler */
+               $handler = TestingAccessWrapper::newFromObject( new SvgHandler() );
+
+               $file = $this->getMockBuilder( File::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [ 'getWidth', 'getHeight', 'getMetadata', 'getHandler' ] )
+                       ->getMock();
+
+               $file->method( 'getWidth' )
+                       ->willReturn( $width );
+               $file->method( 'getHeight' )
+                       ->willReturn( $height );
+               $file->method( 'getMetadata' )
+                       ->willReturn( serialize( [
+                               'version' => SvgHandler::SVG_METADATA_VERSION,
+                               'translations' => [
+                                       'en' => SVGReader::LANG_FULL_MATCH,
+                                       'ru' => SVGReader::LANG_FULL_MATCH,
+                               ],
+                       ] ) );
+               $file->method( 'getHandler' )
+                       ->willReturn( $handler );
+
+               /** @var File $file */
+               $params = $handler->normaliseParamsInternal( $file, $params );
+               self::assertEquals( $paramsExpected, $params, $message );
+       }
+
+       public function provideNormaliseParamsInternal() {
+               return [
+                       [
+                               'No need to change anything',
+                               400, 500,
+                               [ 'physicalWidth' => 400, 'physicalHeight' => 500 ],
+                               [ 'physicalWidth' => 400, 'physicalHeight' => 500 ],
+                       ],
+                       [
+                               'Resize horizontal image',
+                               2000, 1600,
+                               [ 'physicalWidth' => 2000, 'physicalHeight' => 1600, 'page' => 0 ],
+                               [ 'physicalWidth' => 1250, 'physicalHeight' => 1000, 'page' => 0 ],
+                       ],
+                       [
+                               'Resize vertical image',
+                               1600, 2000,
+                               [ 'physicalWidth' => 1600, 'physicalHeight' => 2000, 'page' => 0 ],
+                               [ 'physicalWidth' => 1000, 'physicalHeight' => 1250, 'page' => 0 ],
+                       ],
+                       [
+                               'Preserve targetlang present in the image',
+                               400, 500,
+                               [ 'physicalWidth' => 400, 'physicalHeight' => 500, 'targetlang' => 'en' ],
+                               [ 'physicalWidth' => 400, 'physicalHeight' => 500, 'targetlang' => 'en' ],
+                       ],
+                       [
+                               'Preserve targetlang present in the image 2',
+                               400, 500,
+                               [ 'physicalWidth' => 400, 'physicalHeight' => 500, 'targetlang' => 'en' ],
+                               [ 'physicalWidth' => 400, 'physicalHeight' => 500, 'targetlang' => 'en' ],
+                       ],
+                       [
+                               'Remove targetlang not present in the image',
+                               400, 500,
+                               [ 'physicalWidth' => 400, 'physicalHeight' => 500, 'targetlang' => 'de' ],
+                               [ 'physicalWidth' => 400, 'physicalHeight' => 500 ],
+                       ],
+                       [
+                               'Remove targetlang not present in the image 2',
+                               400, 500,
+                               [ 'physicalWidth' => 400, 'physicalHeight' => 500, 'targetlang' => 'ru-UA' ],
+                               [ 'physicalWidth' => 400, 'physicalHeight' => 500 ],
+                       ],
+               ];
+       }
+
+       /**
+        * @covers \SvgHandler::isEnabled()
+        * @dataProvider provideIsEnabled
+        *
+        * @param string $converter
+        * @param bool $expected
+        */
+       public function testIsEnabled( $converter, $expected ) {
+               $this->setMwGlobals( 'wgSVGConverter', $converter );
+
+               $handler = new SvgHandler();
+               self::assertEquals( $handler->isEnabled(), $expected );
+       }
+
+       public function provideIsEnabled() {
+               return [
+                       [ 'ImageMagick', true ],
+                       [ 'sodipodi', true ],
+                       [ 'invalid', false ],
+               ];
+       }
+
+       /**
+        * @covers \SvgHandler::getAvailableLanguages()
+        * @dataProvider provideAvailableLanguages
+        *
+        * @param array $metadata
+        * @param array $expected
+        */
+       public function testGetAvailableLanguages( array $metadata, array $expected ) {
+               $metadata['version'] = SvgHandler::SVG_METADATA_VERSION;
+               $file = $this->getMockBuilder( File::class )
+                       ->disableOriginalConstructor()
+                       ->setMethods( [ 'getMetadata' ] )
+                       ->getMock();
+               $file->method( 'getMetadata' )
+                       ->willReturn( serialize( $metadata ) );
+
+               $handler = new SvgHandler();
+               /** @var File $file */
+               self::assertEquals( $expected, $handler->getAvailableLanguages( $file ) );
+       }
+
+       public function provideAvailableLanguages() {
+               return [
+                       [ [], [] ],
+                       [ [ 'translations' => [] ], [] ],
+                       [
+                               [
+                                       'translations' => [
+                                               'ru-RU' => SVGReader::LANG_PREFIX_MATCH
+                                       ]
+                               ],
+                               [],
+                       ],
+                       [
+                               [
+                                       'translations' => [
+                                               'en' => SVGReader::LANG_FULL_MATCH,
+                                               'ru-RU' => SVGReader::LANG_PREFIX_MATCH,
+                                               'ru' => SVGReader::LANG_FULL_MATCH,
+                                               'fr-CA' => SVGReader::LANG_PREFIX_MATCH,
+                                       ],
+                               ],
+                               [ 'en', 'ru' ],
+                       ],
+               ];
+       }
+
+       /**
+        * @covers SvgHandler::getLanguageFromParams()
+        * @dataProvider provideGetLanguageFromParams
+        *
+        * @param array $params
+        * @param string $expected
+        * @param string $message
+        */
+       public function testGetLanguageFromParams( array $params, $expected, $message ) {
+               /** @var SvgHandler $handler */
+               $handler = TestingAccessWrapper::newFromObject( new SvgHandler() );
+               self::assertEquals( $expected, $handler->getLanguageFromParams( $params ), $message );
+       }
+
+       public function provideGetLanguageFromParams() {
+               return [
+                       [ [], 'en', 'Default no language to en' ],
+                       [ [ 'preserve' => 'this' ], 'en', 'Default no language to en 2' ],
+                       [ [ 'preserve' => 'this', 'lang' => 'ru' ], 'ru', 'Language from lang' ],
+                       [ [ 'lang' => 'ru' ], 'ru', 'Language from lang 2' ],
+                       [ [ 'targetlang' => 'fr' ], 'fr', 'Language from targetlang' ],
+                       [ [ 'lang' => 'fr', 'targetlang' => 'de' ], 'fr', 'lang overrides targetlang' ],
+               ];
+       }
+}