Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
28.98% covered (danger)
28.98%
82 / 283
13.33% covered (danger)
13.33%
4 / 30
CRAP
0.00% covered (danger)
0.00%
0 / 1
SvgHandler
29.08% covered (danger)
29.08%
82 / 282
13.33% covered (danger)
13.33%
4 / 30
4191.23
0.00% covered (danger)
0.00%
0 / 1
 isEnabled
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 allowRenderingByUserAgent
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
6.02
 mustRender
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isVectorized
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isAnimatedImage
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getAvailableLanguages
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getMatchedLanguage
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
6.03
 getLanguageFromParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDefaultRenderLanguage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 canAnimateThumbnail
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 normaliseParams
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 normaliseParamsInternal
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
6
 doTransform
0.00% covered (danger)
0.00%
0 / 57
0.00% covered (danger)
0.00%
0 / 1
132
 rasterize
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
156
 rasterizeImagickExt
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getClientScalingThumbnailImage
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getThumbType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLongDesc
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 getSizeAndMetadata
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 validateMetadata
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 getMetadataType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isFileMetadataValid
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 visibleMetadataFields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 formatMetadata
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
56
 validateParam
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
 makeParamString
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 parseParamString
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 getParamMap
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getScriptParams
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getCommonMetaArray
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
7.03
1<?php
2/**
3 * Handler for SVG images.
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 * @ingroup Media
8 */
9
10namespace MediaWiki\Media;
11
12use Imagick;
13use MediaWiki\Context\IContextSource;
14use MediaWiki\FileRepo\File\File;
15use MediaWiki\Language\LanguageCode;
16use MediaWiki\MainConfigNames;
17use MediaWiki\MediaWikiServices;
18use MediaWiki\Shell\Shell;
19use UnexpectedValueException;
20use Wikimedia\ScopedCallback;
21
22/**
23 * Handler for SVG images.
24 *
25 * @ingroup Media
26 */
27class SvgHandler extends ImageHandler {
28    public const SVG_METADATA_VERSION = 2;
29
30    private const SVG_DEFAULT_RENDER_LANG = 'en';
31
32    /** @var array A list of metadata tags that can be converted
33     *  to the commonly used exif tags. This allows messages
34     *  to be reused, and consistent tag names for {{#formatmetadata:..}}
35     */
36    private static $metaConversion = [
37        'originalwidth' => 'ImageWidth',
38        'originalheight' => 'ImageLength',
39        'description' => 'ImageDescription',
40        'title' => 'ObjectName',
41    ];
42
43    /** @inheritDoc */
44    public function isEnabled() {
45        $config = MediaWikiServices::getInstance()->getMainConfig();
46        $svgConverters = $config->get( MainConfigNames::SVGConverters );
47        $svgConverter = $config->get( MainConfigNames::SVGConverter );
48        if ( $config->get( MainConfigNames::SVGNativeRendering ) === true ) {
49            return true;
50        }
51        if ( !isset( $svgConverters[$svgConverter] ) ) {
52            wfDebug( "\$wgSVGConverter is invalid, disabling SVG rendering." );
53
54            return false;
55        }
56
57        return true;
58    }
59
60    public function allowRenderingByUserAgent( File $file ): bool {
61        $svgNativeRendering = MediaWikiServices::getInstance()
62            ->getMainConfig()->get( MainConfigNames::SVGNativeRendering );
63        if ( $svgNativeRendering === false ) {
64            // SVG images are always rasterized to PNG
65            return false;
66        }
67
68        // Files bigger than the limit have to be rendered as PNG, as big files might be a tax on the user agent
69        $maxSVGFilesize = MediaWikiServices::getInstance()
70            ->getMainConfig()->get( MainConfigNames::SVGNativeRenderingSizeLimit );
71        if ( $maxSVGFilesize && $file->getSize() >= $maxSVGFilesize ) {
72            return false;
73        }
74
75        if ( $svgNativeRendering === true ) {
76            return true;
77        }
78
79        // 'partial' mode: only allow if considered safe
80        // Browsers don't really support SVG translations, so always render those to PNG
81        if ( $svgNativeRendering === 'partial' ) {
82            return count( $this->getAvailableLanguages( $file ) ) <= 1;
83        }
84        return false;
85    }
86
87    /** @inheritDoc */
88    public function mustRender( $file ) {
89        return !$this->allowRenderingByUserAgent( $file );
90    }
91
92    /** @inheritDoc */
93    public function isVectorized( $file ) {
94        return true;
95    }
96
97    /**
98     * @param File $file
99     * @return bool
100     */
101    public function isAnimatedImage( $file ) {
102        # @todo Detect animated SVGs
103        $metadata = $this->validateMetadata( $file->getMetadataArray() );
104        if ( isset( $metadata['animated'] ) ) {
105            return $metadata['animated'];
106        }
107
108        return false;
109    }
110
111    /**
112     * Which languages (systemLanguage attribute) is supported.
113     *
114     * @note This list is not guaranteed to be exhaustive.
115     * To avoid OOM errors, we only look at first bit of a file.
116     * Thus all languages on this list are present in the file,
117     * but its possible for the file to have a language not on
118     * this list.
119     *
120     * @param File $file
121     * @return string[] Array of language codes, or empty if no language switching supported.
122     */
123    public function getAvailableLanguages( File $file ) {
124        $langList = [];
125        $metadata = $this->validateMetadata( $file->getMetadataArray() );
126        if ( isset( $metadata['translations'] ) ) {
127            foreach ( $metadata['translations'] as $lang => $langType ) {
128                if ( $langType === SVGReader::LANG_FULL_MATCH ) {
129                    $langList[] = strtolower( $lang );
130                }
131            }
132        }
133        return array_unique( $langList );
134    }
135
136    /**
137     * SVG's systemLanguage matching rules state:
138     * 'The `systemLanguage` attribute ... [e]valuates to "true" if one of the languages indicated
139     * by user preferences exactly equals one of the languages given in the value of this parameter,
140     * or if one of the languages indicated by user preferences exactly equals a prefix of one of
141     * the languages given in the value of this parameter such that the first tag character
142     * following the prefix is "-".'
143     *
144     * Return the first element of $svgLanguages that matches $userPreferredLanguage
145     *
146     * @see https://www.w3.org/TR/SVG/struct.html#SystemLanguageAttribute
147     * @param string $userPreferredLanguage
148     * @param string[] $svgLanguages
149     * @return string|null
150     */
151    public function getMatchedLanguage( $userPreferredLanguage, array $svgLanguages ) {
152        // Explicitly requested undetermined language (text without svg systemLanguage attribute)
153        if ( $userPreferredLanguage === 'und' ) {
154            return 'und';
155        }
156        foreach ( $svgLanguages as $svgLang ) {
157            if ( strcasecmp( $svgLang, $userPreferredLanguage ) === 0 ) {
158                return $svgLang;
159            }
160            $trimmedSvgLang = $svgLang;
161            while ( str_contains( $trimmedSvgLang, '-' ) ) {
162                $trimmedSvgLang = substr( $trimmedSvgLang, 0, strrpos( $trimmedSvgLang, '-' ) );
163                if ( strcasecmp( $trimmedSvgLang, $userPreferredLanguage ) === 0 ) {
164                    return $svgLang;
165                }
166            }
167        }
168        return null;
169    }
170
171    /**
172     * Determines render language from image parameters
173     * This is a lowercase IETF language
174     *
175     * @param array $params
176     * @return string
177     */
178    protected function getLanguageFromParams( array $params ) {
179        return $params['lang'] ?? $params['targetlang'] ?? self::SVG_DEFAULT_RENDER_LANG;
180    }
181
182    /**
183     * What language to render file in if none selected
184     *
185     * @param File $file Language code
186     * @return string
187     */
188    public function getDefaultRenderLanguage( File $file ) {
189        return self::SVG_DEFAULT_RENDER_LANG;
190    }
191
192    /**
193     * We do not support making animated svg thumbnails
194     * @param File $file
195     * @return bool
196     */
197    public function canAnimateThumbnail( $file ) {
198        return $this->allowRenderingByUserAgent( $file );
199    }
200
201    /**
202     * @param File $image
203     * @param array &$params
204     * @return bool
205     */
206    public function normaliseParams( $image, &$params ) {
207        if ( parent::normaliseParams( $image, $params ) ) {
208            $params = $this->normaliseParamsInternal( $image, $params );
209            return true;
210        }
211
212        return false;
213    }
214
215    /**
216     * Code taken out of normaliseParams() for testability
217     *
218     * @since 1.33
219     *
220     * @param File $image
221     * @param array $params
222     * @return array Modified $params
223     */
224    protected function normaliseParamsInternal( $image, $params ) {
225        $svgMaxSize = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::SVGMaxSize );
226
227        $srcWidth = $image->getWidth( $params['page'] );
228        $srcHeight = $image->getHeight( $params['page'] );
229        $params['physicalWidth'] = $this->getSteppedThumbWidth(
230            $image, $params['physicalWidth'], $srcWidth, $srcHeight
231        );
232        $params['physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight, $params['physicalWidth'] );
233
234        # Don't make an image bigger than wgMaxSVGSize on the smaller side
235        if ( $params['physicalWidth'] <= $params['physicalHeight'] ) {
236            if ( $params['physicalWidth'] > $svgMaxSize ) {
237                $params['physicalWidth'] = $svgMaxSize;
238                $params['physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight, $svgMaxSize );
239            }
240        } elseif ( $params['physicalHeight'] > $svgMaxSize ) {
241            $params['physicalWidth'] = File::scaleHeight( $srcHeight, $srcWidth, $svgMaxSize );
242            $params['physicalHeight'] = $svgMaxSize;
243        }
244        // To prevent the proliferation of thumbnails in languages not present in SVGs, unless
245        // explicitly forced by user.
246        if ( isset( $params['targetlang'] ) && !$image->getMatchedLanguage( $params['targetlang'] ) ) {
247            unset( $params['targetlang'] );
248        }
249
250        return $params;
251    }
252
253    /**
254     * @param File $image
255     * @param string $dstPath
256     * @param string $dstUrl
257     * @param array $params
258     * @param int $flags
259     * @return MediaTransformError|ThumbnailImage|TransformParameterError|false
260     */
261    public function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
262        if ( !$this->normaliseParams( $image, $params ) ) {
263            return new TransformParameterError( $params );
264        }
265        $clientWidth = $params['width'];
266        $clientHeight = $params['height'];
267        $physicalWidth = $params['physicalWidth'];
268        $physicalHeight = $params['physicalHeight'];
269        $lang = $this->getLanguageFromParams( $params );
270
271        if ( $this->allowRenderingByUserAgent( $image ) ) {
272            return $this->getClientScalingThumbnailImage( $image, $params );
273        }
274
275        if ( $flags & self::TRANSFORM_LATER ) {
276            return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
277        }
278
279        $metadata = $this->validateMetadata( $image->getMetadataArray() );
280        if ( isset( $metadata['error'] ) ) {
281            $err = wfMessage( 'svg-long-error', $metadata['error']['message'] );
282
283            return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
284        }
285
286        if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
287            return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight,
288                wfMessage( 'thumbnail_dest_directory' ) );
289        }
290
291        $srcPath = $image->getLocalRefPath();
292        if ( $srcPath === false ) { // Failed to get local copy
293            wfDebugLog( 'thumbnail',
294                sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
295                    wfHostname(), $image->getName() ) );
296
297            return new MediaTransformError( 'thumbnail_error',
298                $params['width'], $params['height'],
299                wfMessage( 'filemissing' )
300            );
301        }
302
303        // Make a temp dir with a symlink to the local copy in it.
304        // This plays well with rsvg-convert policy for external entities.
305        // https://git.gnome.org/browse/librsvg/commit/?id=f01aded72c38f0e18bc7ff67dee800e380251c8e
306        $tmpDir = wfTempDir() . '/svg_' . wfRandomString( 24 );
307        $lnPath = "$tmpDir/" . basename( $srcPath );
308        $ok = mkdir( $tmpDir, 0771 );
309        if ( !$ok ) {
310            wfDebugLog( 'thumbnail',
311                sprintf( 'Thumbnail failed on %s: could not create temporary directory %s',
312                    wfHostname(), $tmpDir ) );
313            return new MediaTransformError( 'thumbnail_error',
314                $params['width'], $params['height'],
315                wfMessage( 'thumbnail-temp-create' )->text()
316            );
317        }
318        // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
319        $ok = @symlink( $srcPath, $lnPath );
320        /** @noinspection PhpUnusedLocalVariableInspection */
321        $cleaner = new ScopedCallback( static function () use ( $tmpDir, $lnPath ) {
322            // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
323            @unlink( $lnPath );
324            // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
325            @rmdir( $tmpDir );
326        } );
327        if ( !$ok ) {
328            // Fallback because symlink often fails on Windows
329            $ok = copy( $srcPath, $lnPath );
330        }
331        if ( !$ok ) {
332            wfDebugLog( 'thumbnail',
333                sprintf( 'Thumbnail failed on %s: could not link %s to %s',
334                    wfHostname(), $lnPath, $srcPath ) );
335            return new MediaTransformError( 'thumbnail_error',
336                $params['width'], $params['height'],
337                wfMessage( 'thumbnail-temp-create' )
338            );
339        }
340
341        $status = $this->rasterize( $lnPath, $dstPath, $physicalWidth, $physicalHeight, $lang );
342        if ( $status === true ) {
343            return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
344        }
345
346        return $status; // MediaTransformError
347    }
348
349    /**
350     * Transform an SVG file to PNG
351     * This function can be called outside of thumbnail contexts
352     * @param string $srcPath
353     * @param string $dstPath
354     * @param int $width
355     * @param int $height
356     * @param string|false $lang Language code of the language to render the SVG in
357     * @return bool|MediaTransformError
358     */
359    public function rasterize( $srcPath, $dstPath, $width, $height, $lang = false ) {
360        $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
361        $svgConverters = $mainConfig->get( MainConfigNames::SVGConverters );
362        $svgConverter = $mainConfig->get( MainConfigNames::SVGConverter );
363        $svgConverterPath = $mainConfig->get( MainConfigNames::SVGConverterPath );
364        $err = '';
365        $retval = null;
366        $cmd = '';
367        if ( isset( $svgConverters[$svgConverter] ) ) {
368            // Handling largely for imagick PHP extension support (T16706) - ::rasterizeImagickExt()
369            if ( is_array( $svgConverters[$svgConverter] ) ) {
370                // This is a PHP callable
371                $func = $svgConverters[$svgConverter][0];
372                if ( !is_callable( $func ) ) {
373                    throw new UnexpectedValueException( "$func is not callable" );
374                }
375                // Returns string on error else void
376                $err = $func( $srcPath,
377                    $dstPath,
378                    $width,
379                    $height,
380                    $lang,
381                    ...array_slice( $svgConverters[$svgConverter], 1 )
382                );
383                $retval = is_string( $err ) ? 1 : 0;
384                $err = is_string( $err ) ? $err : '';
385                $cmd = 'PHP Callback: ' . (string)$func;
386            } else {
387                // External command
388                $cmd = strtr( $svgConverters[$svgConverter], [
389                    '$path/' => $svgConverterPath ? Shell::escape( "$svgConverterPath/" ) : '',
390                    '$width' => (int)$width,
391                    '$height' => (int)$height,
392                    '$input' => Shell::escape( $srcPath ),
393                    '$output' => Shell::escape( $dstPath ),
394                ] );
395
396                $env = [];
397                if ( $lang !== false ) {
398                    $env['LANG'] = $lang;
399                }
400
401                wfDebug( __METHOD__ . "$cmd" );
402                $err = Shell::command()->unsafeCommand( $cmd )->environment( $env )->execute();
403                $retval = $err->getExitCode();
404                $err = $err->getStderr();
405                $err = $err === null ? '' : $err;
406            }
407        }
408        $removed = $this->removeBadFile( $dstPath, (int)$retval );
409        if ( ( $retval != 0 || $removed ) && $retval !== null ) {
410            $this->logErrorForExternalProcess( $retval, $err, $cmd );
411            return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
412        }
413
414        return true;
415    }
416
417    /**
418     * @param string $srcPath
419     * @param string $dstPath
420     * @param int $width
421     * @param int $height
422     * @return string|void
423     */
424    public static function rasterizeImagickExt( $srcPath, $dstPath, $width, $height ) {
425        $im = new Imagick( $srcPath );
426        $im->setBackgroundColor( 'transparent' );
427        $im->readImage( $srcPath );
428        $im->setImageFormat( 'png' );
429        $im->setImageDepth( 8 );
430
431        if ( !$im->thumbnailImage( (int)$width, (int)$height, /* fit */ false ) ) {
432            return 'Could not resize image';
433        }
434        if ( !$im->writeImage( $dstPath ) ) {
435            return "Could not write to $dstPath";
436        }
437    }
438
439    /**
440     * Get a ThumbnailImage that represents an image that will be scaled
441     * client side
442     *
443     * @stable to override
444     * @param File $image File associated with this thumbnail
445     * @param array $params Media handler parameters
446     * @return ThumbnailImage
447     */
448    protected function getClientScalingThumbnailImage( $image, $params ) {
449        $url = $image->modifyClientThumbUrl( $image->getUrl(), $params );
450        return new ThumbnailImage( $image, $url, null, $params );
451    }
452
453    /** @inheritDoc */
454    public function getThumbType( $ext, $mime, $params = null ) {
455        return [ 'png', 'image/png' ];
456    }
457
458    /**
459     * Subtitle for the image. Different from the base
460     * class so it can be denoted that SVG's have
461     * a "nominal" resolution, and not a fixed one,
462     * as well as so animation can be denoted.
463     *
464     * @param File $file
465     * @return string
466     */
467    public function getLongDesc( $file ) {
468        $metadata = $this->validateMetadata( $file->getMetadataArray() );
469        if ( isset( $metadata['error'] ) ) {
470            return wfMessage( 'svg-long-error', $metadata['error']['message'] )
471                ->inLanguage( $this->getLanguage() )->escaped();
472        }
473
474        if ( $this->isAnimatedImage( $file ) ) {
475            $msg = wfMessage( 'svg-long-desc-animated' );
476        } else {
477            $msg = wfMessage( 'svg-long-desc' );
478        }
479
480        return $msg
481            ->numParams( $file->getWidth(), $file->getHeight() )
482            ->sizeParams( $file->getSize() )
483            ->inLanguage( $this->getLanguage() )
484            ->parse();
485    }
486
487    /**
488     * @param MediaHandlerState $state
489     * @param string $filename
490     * @return array
491     */
492    public function getSizeAndMetadata( $state, $filename ) {
493        $metadata = [ 'version' => self::SVG_METADATA_VERSION ];
494
495        try {
496            $svgReader = new SVGReader( $filename );
497            $metadata += $svgReader->getMetadata();
498        } catch ( InvalidSVGException $e ) {
499            // File not found, broken, etc.
500            $metadata['error'] = [
501                'message' => $e->getMessage(),
502                'code' => $e->getCode()
503            ];
504            wfDebug( __METHOD__ . ': ' . $e->getMessage() );
505        }
506
507        return [
508            'width' => $metadata['width'] ?? 0,
509            'height' => $metadata['height'] ?? 0,
510            'metadata' => $metadata
511        ];
512    }
513
514    /** @inheritDoc */
515    protected function validateMetadata( $unser ) {
516        if ( isset( $unser['version'] ) && $unser['version'] === self::SVG_METADATA_VERSION ) {
517            return $unser;
518        }
519
520        return null;
521    }
522
523    /** @inheritDoc */
524    public function getMetadataType( $image ) {
525        return 'parsed-svg';
526    }
527
528    /** @inheritDoc */
529    public function isFileMetadataValid( $image ) {
530        $meta = $this->validateMetadata( $image->getMetadataArray() );
531        if ( !$meta ) {
532            return self::METADATA_BAD;
533        }
534        if ( !isset( $meta['originalWidth'] ) ) {
535            // Old but compatible
536            return self::METADATA_COMPATIBLE;
537        }
538
539        return self::METADATA_GOOD;
540    }
541
542    /** @inheritDoc */
543    protected function visibleMetadataFields() {
544        return [ 'objectname', 'imagedescription' ];
545    }
546
547    /**
548     * @param File $file
549     * @param IContextSource|false $context
550     * @return array<string,array[]>|false
551     */
552    public function formatMetadata( $file, $context = false ) {
553        $result = [
554            'visible' => [],
555            'collapsed' => []
556        ];
557        $metadata = $this->validateMetadata( $file->getMetadataArray() );
558        if ( !$metadata || isset( $metadata['error'] ) ) {
559            return false;
560        }
561
562        /* @todo Add a formatter
563        $format = new FormatSVG( $metadata );
564        $formatted = $format->getFormattedData();
565        */
566
567        // Sort fields into visible and collapsed
568        $visibleFields = $this->visibleMetadataFields();
569
570        $showMeta = false;
571        foreach ( $metadata as $name => $value ) {
572            $tag = strtolower( $name );
573            if ( isset( self::$metaConversion[$tag] ) ) {
574                $tag = strtolower( self::$metaConversion[$tag] );
575            } else {
576                // Do not output other metadata not in list
577                continue;
578            }
579            $showMeta = true;
580            self::addMeta( $result,
581                in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
582                'exif',
583                $tag,
584                $value
585            );
586        }
587
588        return $showMeta ? $result : false;
589    }
590
591    /**
592     * @param string $name Parameter name
593     * @param mixed $value Parameter value
594     * @return bool Validity
595     */
596    public function validateParam( $name, $value ) {
597        if ( in_array( $name, [ 'width', 'height' ] ) ) {
598            // Reject negative heights, widths
599            return (int)$value > 0;
600        }
601        if ( $name === 'lang' ) {
602            // Validate $code
603            if ( !is_string( $value ) || $value === ''
604                || !LanguageCode::isWellFormedLanguageTag( $value )
605            ) {
606                return false;
607            }
608
609            return true;
610        }
611
612        // Only lang, width and height are acceptable keys
613        return false;
614    }
615
616    /**
617     * @param array $params Name=>value pairs of parameters
618     * @return string|false Filename to use
619     */
620    public function makeParamString( $params ) {
621        $lang = '';
622        $code = $this->getLanguageFromParams( $params );
623        if ( $code !== self::SVG_DEFAULT_RENDER_LANG ) {
624            $lang = 'lang' . strtolower( $code ) . '-';
625        }
626
627        if ( isset( $params['physicalWidth'] ) && $params['physicalWidth'] ) {
628            return "$lang{$params['physicalWidth']}px";
629        }
630
631        if ( !isset( $params['width'] ) ) {
632            return false;
633        }
634
635        return "$lang{$params['width']}px";
636    }
637
638    /** @inheritDoc */
639    public function parseParamString( $str ) {
640        $m = false;
641        // Language codes are supposed to be lowercase
642        if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/', $str, $m ) ) {
643            if ( LanguageCode::isWellFormedLanguageTag( $m[1] ) ) {
644                return [ 'width' => array_pop( $m ), 'lang' => $m[1] ];
645            }
646            return [ 'width' => array_pop( $m ), 'lang' => self::SVG_DEFAULT_RENDER_LANG ];
647        }
648        if ( preg_match( '/^(\d+)px$/', $str, $m ) ) {
649            return [ 'width' => $m[1], 'lang' => self::SVG_DEFAULT_RENDER_LANG ];
650        }
651        return false;
652    }
653
654    /** @inheritDoc */
655    public function getParamMap() {
656        return [ 'img_lang' => 'lang', 'img_width' => 'width' ];
657    }
658
659    /**
660     * @param array $params
661     * @return array
662     */
663    protected function getScriptParams( $params ) {
664        $scriptParams = [ 'width' => $params['width'] ];
665        if ( isset( $params['lang'] ) ) {
666            $scriptParams['lang'] = $params['lang'];
667        }
668
669        return $scriptParams;
670    }
671
672    /** @inheritDoc */
673    public function getCommonMetaArray( File $file ) {
674        $metadata = $this->validateMetadata( $file->getMetadataArray() );
675        if ( !$metadata || isset( $metadata['error'] ) ) {
676            return [];
677        }
678        $stdMetadata = [];
679        foreach ( $metadata as $name => $value ) {
680            $tag = strtolower( $name );
681            if ( $tag === 'originalwidth' || $tag === 'originalheight' ) {
682                // Skip these. In the exif metadata stuff, it is assumed these
683                // are measured in px, which is not the case here.
684                continue;
685            }
686            if ( isset( self::$metaConversion[$tag] ) ) {
687                $tag = self::$metaConversion[$tag];
688                $stdMetadata[$tag] = $value;
689            }
690        }
691
692        return $stdMetadata;
693    }
694}
695
696/** @deprecated class alias since 1.46 */
697class_alias( SvgHandler::class, 'SvgHandler' );