MediaWiki REL1_35
SvgHandler.php
Go to the documentation of this file.
1<?php
26use Wikimedia\ScopedCallback;
27
33class SvgHandler extends ImageHandler {
34 public const SVG_METADATA_VERSION = 2;
35
40 private static $metaConversion = [
41 'originalwidth' => 'ImageWidth',
42 'originalheight' => 'ImageLength',
43 'description' => 'ImageDescription',
44 'title' => 'ObjectName',
45 ];
46
47 public function isEnabled() {
49 if ( !isset( $wgSVGConverters[$wgSVGConverter] ) ) {
50 wfDebug( "\$wgSVGConverter is invalid, disabling SVG rendering." );
51
52 return false;
53 } else {
54 return true;
55 }
56 }
57
58 public function mustRender( $file ) {
59 return true;
60 }
61
62 public function isVectorized( $file ) {
63 return true;
64 }
65
70 public function isAnimatedImage( $file ) {
71 # @todo Detect animated SVGs
72 $metadata = $file->getMetadata();
73 if ( $metadata ) {
74 $metadata = $this->unpackMetadata( $metadata );
75 if ( isset( $metadata['animated'] ) ) {
76 return $metadata['animated'];
77 }
78 }
79
80 return false;
81 }
82
95 public function getAvailableLanguages( File $file ) {
96 $metadata = $file->getMetadata();
97 $langList = [];
98 if ( $metadata ) {
99 $metadata = $this->unpackMetadata( $metadata );
100 if ( isset( $metadata['translations'] ) ) {
101 foreach ( $metadata['translations'] as $lang => $langType ) {
102 if ( $langType === SVGReader::LANG_FULL_MATCH ) {
103 $langList[] = strtolower( $lang );
104 }
105 }
106 }
107 }
108 return array_unique( $langList );
109 }
110
126 public function getMatchedLanguage( $userPreferredLanguage, array $svgLanguages ) {
127 foreach ( $svgLanguages as $svgLang ) {
128 if ( strcasecmp( $svgLang, $userPreferredLanguage ) === 0 ) {
129 return $svgLang;
130 }
131 $trimmedSvgLang = $svgLang;
132 while ( strpos( $trimmedSvgLang, '-' ) !== false ) {
133 $trimmedSvgLang = substr( $trimmedSvgLang, 0, strrpos( $trimmedSvgLang, '-' ) );
134 if ( strcasecmp( $trimmedSvgLang, $userPreferredLanguage ) === 0 ) {
135 return $svgLang;
136 }
137 }
138 }
139 return null;
140 }
141
148 protected function getLanguageFromParams( array $params ) {
149 return $params['lang'] ?? $params['targetlang'] ?? 'en';
150 }
151
159 return 'en';
160 }
161
167 public function canAnimateThumbnail( $file ) {
168 return false;
169 }
170
176 public function normaliseParams( $image, &$params ) {
177 if ( parent::normaliseParams( $image, $params ) ) {
178 $params = $this->normaliseParamsInternal( $image, $params );
179 return true;
180 }
181
182 return false;
183 }
184
194 protected function normaliseParamsInternal( $image, $params ) {
195 global $wgSVGMaxSize;
196
197 # Don't make an image bigger than wgMaxSVGSize on the smaller side
198 if ( $params['physicalWidth'] <= $params['physicalHeight'] ) {
199 if ( $params['physicalWidth'] > $wgSVGMaxSize ) {
200 $srcWidth = $image->getWidth( $params['page'] );
201 $srcHeight = $image->getHeight( $params['page'] );
202 $params['physicalWidth'] = $wgSVGMaxSize;
203 $params['physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight, $wgSVGMaxSize );
204 }
205 } elseif ( $params['physicalHeight'] > $wgSVGMaxSize ) {
206 $srcWidth = $image->getWidth( $params['page'] );
207 $srcHeight = $image->getHeight( $params['page'] );
208 $params['physicalWidth'] = File::scaleHeight( $srcHeight, $srcWidth, $wgSVGMaxSize );
209 $params['physicalHeight'] = $wgSVGMaxSize;
210 }
211 // To prevent the proliferation of thumbnails in languages not present in SVGs, unless
212 // explicitly forced by user.
213 if ( isset( $params['targetlang'] ) && !$image->getMatchedLanguage( $params['targetlang'] ) ) {
214 unset( $params['targetlang'] );
215 }
216
217 return $params;
218 }
219
228 public function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
229 if ( !$this->normaliseParams( $image, $params ) ) {
230 return new TransformParameterError( $params );
231 }
232 $clientWidth = $params['width'];
233 $clientHeight = $params['height'];
234 $physicalWidth = $params['physicalWidth'];
235 $physicalHeight = $params['physicalHeight'];
236 $lang = $this->getLanguageFromParams( $params );
237
238 if ( $flags & self::TRANSFORM_LATER ) {
239 return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
240 }
241
242 $metadata = $this->unpackMetadata( $image->getMetadata() );
243 if ( isset( $metadata['error'] ) ) { // sanity check
244 $err = wfMessage( 'svg-long-error', $metadata['error']['message'] );
245
246 return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
247 }
248
249 if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
250 return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight,
251 wfMessage( 'thumbnail_dest_directory' ) );
252 }
253
254 $srcPath = $image->getLocalRefPath();
255 if ( $srcPath === false ) { // Failed to get local copy
256 wfDebugLog( 'thumbnail',
257 sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
258 wfHostname(), $image->getName() ) );
259
260 return new MediaTransformError( 'thumbnail_error',
261 $params['width'], $params['height'],
262 wfMessage( 'filemissing' )
263 );
264 }
265
266 // Make a temp dir with a symlink to the local copy in it.
267 // This plays well with rsvg-convert policy for external entities.
268 // https://git.gnome.org/browse/librsvg/commit/?id=f01aded72c38f0e18bc7ff67dee800e380251c8e
269 $tmpDir = wfTempDir() . '/svg_' . wfRandomString( 24 );
270 $lnPath = "$tmpDir/" . basename( $srcPath );
271 $ok = mkdir( $tmpDir, 0771 );
272 if ( !$ok ) {
273 wfDebugLog( 'thumbnail',
274 sprintf( 'Thumbnail failed on %s: could not create temporary directory %s',
275 wfHostname(), $tmpDir ) );
276 return new MediaTransformError( 'thumbnail_error',
277 $params['width'], $params['height'],
278 wfMessage( 'thumbnail-temp-create' )->text()
279 );
280 }
281 $ok = symlink( $srcPath, $lnPath );
283 $cleaner = new ScopedCallback( function () use ( $tmpDir, $lnPath ) {
284 Wikimedia\suppressWarnings();
285 unlink( $lnPath );
286 rmdir( $tmpDir );
287 Wikimedia\restoreWarnings();
288 } );
289 if ( !$ok ) {
290 wfDebugLog( 'thumbnail',
291 sprintf( 'Thumbnail failed on %s: could not link %s to %s',
292 wfHostname(), $lnPath, $srcPath ) );
293 return new MediaTransformError( 'thumbnail_error',
294 $params['width'], $params['height'],
295 wfMessage( 'thumbnail-temp-create' )
296 );
297 }
298
299 $status = $this->rasterize( $lnPath, $dstPath, $physicalWidth, $physicalHeight, $lang );
300 if ( $status === true ) {
301 return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
302 } else {
303 return $status; // MediaTransformError
304 }
305 }
306
318 public function rasterize( $srcPath, $dstPath, $width, $height, $lang = false ) {
320 $err = false;
321 $retval = '';
322 if ( isset( $wgSVGConverters[$wgSVGConverter] ) ) {
323 if ( is_array( $wgSVGConverters[$wgSVGConverter] ) ) {
324 // This is a PHP callable
326 if ( !is_callable( $func ) ) {
327 throw new MWException( "$func is not callable" );
328 }
329 $err = $func( $srcPath,
330 $dstPath,
331 $width,
332 $height,
333 $lang,
334 ...array_slice( $wgSVGConverters[$wgSVGConverter], 1 )
335 );
336 $retval = (bool)$err;
337 } else {
338 // External command
339 $cmd = str_replace(
340 [ '$path/', '$width', '$height', '$input', '$output' ],
341 [ $wgSVGConverterPath ? Shell::escape( "$wgSVGConverterPath/" ) : "",
342 intval( $width ),
343 intval( $height ),
344 Shell::escape( $srcPath ),
345 Shell::escape( $dstPath ) ],
347 );
348
349 $env = [];
350 if ( $lang !== false ) {
351 $env['LANG'] = $lang;
352 }
353
354 wfDebug( __METHOD__ . ": $cmd" );
355 $err = wfShellExecWithStderr( $cmd, $retval, $env );
356 }
357 }
358 $removed = $this->removeBadFile( $dstPath, $retval );
359 if ( $retval != 0 || $removed ) {
360 $this->logErrorForExternalProcess( $retval, $err, $cmd );
361 return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
362 }
363
364 return true;
365 }
366
367 public static function rasterizeImagickExt( $srcPath, $dstPath, $width, $height ) {
368 $im = new Imagick( $srcPath );
369 $im->setImageFormat( 'png' );
370 $im->setBackgroundColor( 'transparent' );
371 $im->setImageDepth( 8 );
372
373 if ( !$im->thumbnailImage( intval( $width ), intval( $height ), /* fit */ false ) ) {
374 return 'Could not resize image';
375 }
376 if ( !$im->writeImage( $dstPath ) ) {
377 return "Could not write to $dstPath";
378 }
379 }
380
387 public function getImageSize( $file, $path, $metadata = false ) {
388 if ( $metadata === false && $file instanceof File ) {
389 $metadata = $file->getMetadata();
390 }
391 $metadata = $this->unpackMetadata( $metadata );
392
393 if ( isset( $metadata['width'] ) && isset( $metadata['height'] ) ) {
394 return [ $metadata['width'], $metadata['height'], 'SVG',
395 "width=\"{$metadata['width']}\" height=\"{$metadata['height']}\"" ];
396 } else { // error
397 return [ 0, 0, 'SVG', "width=\"0\" height=\"0\"" ];
398 }
399 }
400
401 public function getThumbType( $ext, $mime, $params = null ) {
402 return [ 'png', 'image/png' ];
403 }
404
414 public function getLongDesc( $file ) {
415 global $wgLang;
416
417 $metadata = $this->unpackMetadata( $file->getMetadata() );
418 if ( isset( $metadata['error'] ) ) {
419 return wfMessage( 'svg-long-error', $metadata['error']['message'] )->text();
420 }
421
422 $size = $wgLang->formatSize( $file->getSize() );
423
424 if ( $this->isAnimatedImage( $file ) ) {
425 $msg = wfMessage( 'svg-long-desc-animated' );
426 } else {
427 $msg = wfMessage( 'svg-long-desc' );
428 }
429
430 $msg->numParams( $file->getWidth(), $file->getHeight() )->params( $size );
431
432 return $msg->parse();
433 }
434
440 public function getMetadata( $file, $filename ) {
441 $metadata = [ 'version' => self::SVG_METADATA_VERSION ];
442
443 try {
444 $svgReader = new SVGReader( $filename );
445 $metadata += $svgReader->getMetadata();
446 } catch ( Exception $e ) { // @todo SVG specific exceptions
447 // File not found, broken, etc.
448 $metadata['error'] = [
449 'message' => $e->getMessage(),
450 'code' => $e->getCode()
451 ];
452 wfDebug( __METHOD__ . ': ' . $e->getMessage() );
453 }
454
455 return serialize( $metadata );
456 }
457
458 protected function unpackMetadata( $metadata ) {
459 Wikimedia\suppressWarnings();
460 $unser = unserialize( $metadata );
461 Wikimedia\restoreWarnings();
462 if ( isset( $unser['version'] ) && $unser['version'] == self::SVG_METADATA_VERSION ) {
463 return $unser;
464 } else {
465 return false;
466 }
467 }
468
469 public function getMetadataType( $image ) {
470 return 'parsed-svg';
471 }
472
473 public function isMetadataValid( $image, $metadata ) {
474 $meta = $this->unpackMetadata( $metadata );
475 if ( $meta === false ) {
476 return self::METADATA_BAD;
477 }
478 if ( !isset( $meta['originalWidth'] ) ) {
479 // Old but compatible
481 }
482
483 return self::METADATA_GOOD;
484 }
485
486 protected function visibleMetadataFields() {
487 $fields = [ 'objectname', 'imagedescription' ];
488
489 return $fields;
490 }
491
497 public function formatMetadata( $file, $context = false ) {
498 $result = [
499 'visible' => [],
500 'collapsed' => []
501 ];
502 $metadata = $file->getMetadata();
503 if ( !$metadata ) {
504 return false;
505 }
506 $metadata = $this->unpackMetadata( $metadata );
507 if ( !$metadata || isset( $metadata['error'] ) ) {
508 return false;
509 }
510
511 /* @todo Add a formatter
512 $format = new FormatSVG( $metadata );
513 $formatted = $format->getFormattedData();
514 */
515
516 // Sort fields into visible and collapsed
517 $visibleFields = $this->visibleMetadataFields();
518
519 $showMeta = false;
520 foreach ( $metadata as $name => $value ) {
521 $tag = strtolower( $name );
522 if ( isset( self::$metaConversion[$tag] ) ) {
523 $tag = strtolower( self::$metaConversion[$tag] );
524 } else {
525 // Do not output other metadata not in list
526 continue;
527 }
528 $showMeta = true;
529 self::addMeta( $result,
530 in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
531 'exif',
532 $tag,
533 $value
534 );
535 }
536
537 return $showMeta ? $result : false;
538 }
539
545 public function validateParam( $name, $value ) {
546 if ( in_array( $name, [ 'width', 'height' ] ) ) {
547 // Reject negative heights, widths
548 return ( $value > 0 );
549 } elseif ( $name == 'lang' ) {
550 // Validate $code
551 if ( $value === ''
552 || !MediaWikiServices::getInstance()->getLanguageNameUtils()
553 ->isValidCode( $value )
554 ) {
555 return false;
556 }
557
558 return true;
559 }
560
561 // Only lang, width and height are acceptable keys
562 return false;
563 }
564
569 public function makeParamString( $params ) {
570 $lang = '';
571 $code = $this->getLanguageFromParams( $params );
572 if ( $code !== 'en' ) {
573 $lang = 'lang' . strtolower( $code ) . '-';
574 }
575 if ( !isset( $params['width'] ) ) {
576 return false;
577 }
578
579 return "$lang{$params['width']}px";
580 }
581
582 public function parseParamString( $str ) {
583 $m = false;
584 if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/i', $str, $m ) ) {
585 return [ 'width' => array_pop( $m ), 'lang' => $m[1] ];
586 } elseif ( preg_match( '/^(\d+)px$/', $str, $m ) ) {
587 return [ 'width' => $m[1], 'lang' => 'en' ];
588 } else {
589 return false;
590 }
591 }
592
593 public function getParamMap() {
594 return [ 'img_lang' => 'lang', 'img_width' => 'width' ];
595 }
596
601 protected function getScriptParams( $params ) {
602 $scriptParams = [ 'width' => $params['width'] ];
603 if ( isset( $params['lang'] ) ) {
604 $scriptParams['lang'] = $params['lang'];
605 }
606
607 return $scriptParams;
608 }
609
610 public function getCommonMetaArray( File $file ) {
611 $metadata = $file->getMetadata();
612 if ( !$metadata ) {
613 return [];
614 }
615 $metadata = $this->unpackMetadata( $metadata );
616 if ( !$metadata || isset( $metadata['error'] ) ) {
617 return [];
618 }
619 $stdMetadata = [];
620 foreach ( $metadata as $name => $value ) {
621 $tag = strtolower( $name );
622 if ( $tag === 'originalwidth' || $tag === 'originalheight' ) {
623 // Skip these. In the exif metadata stuff, it is assumed these
624 // are measured in px, which is not the case here.
625 continue;
626 }
627 if ( isset( self::$metaConversion[$tag] ) ) {
628 $tag = self::$metaConversion[$tag];
629 $stdMetadata[$tag] = $value;
630 }
631 }
632
633 return $stdMetadata;
634 }
635}
serialize()
unserialize( $serialized)
$wgSVGConverter
Pick a converter defined in $wgSVGConverters.
$wgSVGMaxSize
Don't scale a SVG larger than this.
$wgSVGConverterPath
If not in the executable PATH, specify the SVG converter path.
$wgSVGConverters
Scalable Vector Graphics (SVG) may be uploaded as images.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfTempDir()
Tries to get the system directory for temporary files.
wfRandomString( $length=32)
Get a random string containing a number of pseudo-random hex characters.
wfHostname()
Get host name of the current machine, for use in error reporting.
wfShellExecWithStderr( $cmd, &$retval=null, $environ=[], $limits=[])
Execute a shell command, returning both stdout and stderr.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfMkdirParents( $dir, $mode=null, $caller=null)
Make directory, and make all parent directories if they don't exist.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
$wgLang
Definition Setup.php:781
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:63
Media handler abstract base class for images.
MediaWiki exception.
const METADATA_COMPATIBLE
static addMeta(&$array, $visibility, $type, $id, $value, $param=false)
This is used to generate an array element for each metadata value That array is then used to generate...
logErrorForExternalProcess( $retval, $err, $cmd)
Log an error that occurred in an external process.
const METADATA_GOOD
removeBadFile( $dstPath, $retval=0)
Check for zero-sized thumbnails.
Basic media transform error class.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Executes shell commands.
Definition Shell.php:44
const LANG_FULL_MATCH
Definition SVGReader.php:36
Handler for SVG images.
validateParam( $name, $value)
isVectorized( $file)
The material is vectorized and thus scaling is lossless.
normaliseParams( $image, &$params)
formatMetadata( $file, $context=false)
parseParamString( $str)
Parse a param string made with makeParamString back into an array.array|bool Array of parameters or f...
getMetadata( $file, $filename)
mustRender( $file)
True if handled types cannot be displayed directly in a browser but can be rendered.
getScriptParams( $params)
makeParamString( $params)
getCommonMetaArray(File $file)
Get an array of standard (FormatMetadata type) metadata values.
unpackMetadata( $metadata)
static array $metaConversion
A list of metadata tags that can be converted to the commonly used exif tags.
doTransform( $image, $dstPath, $dstUrl, $params, $flags=0)
getImageSize( $file, $path, $metadata=false)
getLanguageFromParams(array $params)
Determines render language from image parameters.
getAvailableLanguages(File $file)
Which languages (systemLanguage attribute) is supported.
getLongDesc( $file)
Subtitle for the image.
normaliseParamsInternal( $image, $params)
Code taken out of normaliseParams() for testability.
getMetadataType( $image)
Get a string describing the type of metadata, for display purposes.
getThumbType( $ext, $mime, $params=null)
Get the thumbnail extension and MIME type for a given source MIME type.
getDefaultRenderLanguage(File $file)
What language to render file in if none selected.
rasterize( $srcPath, $dstPath, $width, $height, $lang=false)
Transform an SVG file to PNG This function can be called outside of thumbnail contexts.
isEnabled()
False if the handler is disabled for all files Stable to override.
canAnimateThumbnail( $file)
We do not support making animated svg thumbnails.
visibleMetadataFields()
Get a list of metadata items which should be displayed when the metadata table is collapsed.
static rasterizeImagickExt( $srcPath, $dstPath, $width, $height)
isAnimatedImage( $file)
getMatchedLanguage( $userPreferredLanguage, array $svgLanguages)
SVG's systemLanguage matching rules state: 'The systemLanguage attribute ... [e]valuates to "true" if...
const SVG_METADATA_VERSION
isMetadataValid( $image, $metadata)
Check if the metadata string is valid for this handler.
getParamMap()
Get an associative array mapping magic word IDs to parameter names.Will be used by the parser to iden...
Media transform output for images.
Shortcut class for parameter validation errors.
$mime
Definition router.php:60
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42
if(!is_readable( $file)) $ext
Definition router.php:48
if(!isset( $args[0])) $lang