MediaWiki REL1_40
SvgHandler.php
Go to the documentation of this file.
1<?php
27use Wikimedia\AtEase\AtEase;
28use Wikimedia\RequestTimeout\TimeoutException;
29use Wikimedia\ScopedCallback;
30
36class SvgHandler extends ImageHandler {
37 public const SVG_METADATA_VERSION = 2;
38
39 private const SVG_DEFAULT_RENDER_LANG = 'en';
40
45 private static $metaConversion = [
46 'originalwidth' => 'ImageWidth',
47 'originalheight' => 'ImageLength',
48 'description' => 'ImageDescription',
49 'title' => 'ObjectName',
50 ];
51
52 public function isEnabled() {
53 $svgConverters = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::SVGConverters );
54 $svgConverter = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::SVGConverter );
55 if ( !isset( $svgConverters[$svgConverter] ) ) {
56 wfDebug( "\$wgSVGConverter is invalid, disabling SVG rendering." );
57
58 return false;
59 }
60
61 return true;
62 }
63
64 public function mustRender( $file ) {
65 return true;
66 }
67
68 public function isVectorized( $file ) {
69 return true;
70 }
71
76 public function isAnimatedImage( $file ) {
77 # @todo Detect animated SVGs
78 $metadata = $this->validateMetadata( $file->getMetadataArray() );
79 if ( isset( $metadata['animated'] ) ) {
80 return $metadata['animated'];
81 }
82
83 return false;
84 }
85
98 public function getAvailableLanguages( File $file ) {
99 $langList = [];
100 $metadata = $this->validateMetadata( $file->getMetadataArray() );
101 if ( isset( $metadata['translations'] ) ) {
102 foreach ( $metadata['translations'] as $lang => $langType ) {
103 if ( $langType === SVGReader::LANG_FULL_MATCH ) {
104 $langList[] = strtolower( $lang );
105 }
106 }
107 }
108 return array_unique( $langList );
109 }
110
126 public function getMatchedLanguage( $userPreferredLanguage, array $svgLanguages ) {
127 // Explicitly requested undetermined language (text without svg systemLanguage attribute)
128 if ( $userPreferredLanguage === 'und' ) {
129 return 'und';
130 }
131 foreach ( $svgLanguages as $svgLang ) {
132 if ( strcasecmp( $svgLang, $userPreferredLanguage ) === 0 ) {
133 return $svgLang;
134 }
135 $trimmedSvgLang = $svgLang;
136 while ( strpos( $trimmedSvgLang, '-' ) !== false ) {
137 $trimmedSvgLang = substr( $trimmedSvgLang, 0, strrpos( $trimmedSvgLang, '-' ) );
138 if ( strcasecmp( $trimmedSvgLang, $userPreferredLanguage ) === 0 ) {
139 return $svgLang;
140 }
141 }
142 }
143 return null;
144 }
145
153 protected function getLanguageFromParams( array $params ) {
154 return $params['lang'] ?? $params['targetlang'] ?? self::SVG_DEFAULT_RENDER_LANG;
155 }
156
164 return self::SVG_DEFAULT_RENDER_LANG;
165 }
166
172 public function canAnimateThumbnail( $file ) {
173 return false;
174 }
175
181 public function normaliseParams( $image, &$params ) {
182 if ( parent::normaliseParams( $image, $params ) ) {
183 $params = $this->normaliseParamsInternal( $image, $params );
184 return true;
185 }
186
187 return false;
188 }
189
199 protected function normaliseParamsInternal( $image, $params ) {
200 $svgMaxSize = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::SVGMaxSize );
201
202 # Don't make an image bigger than wgMaxSVGSize on the smaller side
203 if ( $params['physicalWidth'] <= $params['physicalHeight'] ) {
204 if ( $params['physicalWidth'] > $svgMaxSize ) {
205 $srcWidth = $image->getWidth( $params['page'] );
206 $srcHeight = $image->getHeight( $params['page'] );
207 $params['physicalWidth'] = $svgMaxSize;
208 $params['physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight, $svgMaxSize );
209 }
210 } elseif ( $params['physicalHeight'] > $svgMaxSize ) {
211 $srcWidth = $image->getWidth( $params['page'] );
212 $srcHeight = $image->getHeight( $params['page'] );
213 $params['physicalWidth'] = File::scaleHeight( $srcHeight, $srcWidth, $svgMaxSize );
214 $params['physicalHeight'] = $svgMaxSize;
215 }
216 // To prevent the proliferation of thumbnails in languages not present in SVGs, unless
217 // explicitly forced by user.
218 if ( isset( $params['targetlang'] ) && !$image->getMatchedLanguage( $params['targetlang'] ) ) {
219 unset( $params['targetlang'] );
220 }
221
222 return $params;
223 }
224
233 public function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
234 if ( !$this->normaliseParams( $image, $params ) ) {
235 return new TransformParameterError( $params );
236 }
237 $clientWidth = $params['width'];
238 $clientHeight = $params['height'];
239 $physicalWidth = $params['physicalWidth'];
240 $physicalHeight = $params['physicalHeight'];
241 $lang = $this->getLanguageFromParams( $params );
242
243 if ( $flags & self::TRANSFORM_LATER ) {
244 return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
245 }
246
247 $metadata = $this->validateMetadata( $image->getMetadataArray() );
248 if ( isset( $metadata['error'] ) ) {
249 $err = wfMessage( 'svg-long-error', $metadata['error']['message'] );
250
251 return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
252 }
253
254 if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
255 return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight,
256 wfMessage( 'thumbnail_dest_directory' ) );
257 }
258
259 $srcPath = $image->getLocalRefPath();
260 if ( $srcPath === false ) { // Failed to get local copy
261 wfDebugLog( 'thumbnail',
262 sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
263 wfHostname(), $image->getName() ) );
264
265 return new MediaTransformError( 'thumbnail_error',
266 $params['width'], $params['height'],
267 wfMessage( 'filemissing' )
268 );
269 }
270
271 // Make a temp dir with a symlink to the local copy in it.
272 // This plays well with rsvg-convert policy for external entities.
273 // https://git.gnome.org/browse/librsvg/commit/?id=f01aded72c38f0e18bc7ff67dee800e380251c8e
274 $tmpDir = wfTempDir() . '/svg_' . wfRandomString( 24 );
275 $lnPath = "$tmpDir/" . basename( $srcPath );
276 $ok = mkdir( $tmpDir, 0771 );
277 if ( !$ok ) {
278 wfDebugLog( 'thumbnail',
279 sprintf( 'Thumbnail failed on %s: could not create temporary directory %s',
280 wfHostname(), $tmpDir ) );
281 return new MediaTransformError( 'thumbnail_error',
282 $params['width'], $params['height'],
283 wfMessage( 'thumbnail-temp-create' )->text()
284 );
285 }
286 $ok = symlink( $srcPath, $lnPath );
288 $cleaner = new ScopedCallback( static function () use ( $tmpDir, $lnPath ) {
289 AtEase::suppressWarnings();
290 unlink( $lnPath );
291 rmdir( $tmpDir );
292 AtEase::restoreWarnings();
293 } );
294 if ( !$ok ) {
295 // Fallback because symlink often fails on Windows
296 $ok = copy( $srcPath, $lnPath );
297 }
298 if ( !$ok ) {
299 wfDebugLog( 'thumbnail',
300 sprintf( 'Thumbnail failed on %s: could not link %s to %s',
301 wfHostname(), $lnPath, $srcPath ) );
302 return new MediaTransformError( 'thumbnail_error',
303 $params['width'], $params['height'],
304 wfMessage( 'thumbnail-temp-create' )
305 );
306 }
307
308 $status = $this->rasterize( $lnPath, $dstPath, $physicalWidth, $physicalHeight, $lang );
309 if ( $status === true ) {
310 return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
311 }
312
313 return $status; // MediaTransformError
314 }
315
327 public function rasterize( $srcPath, $dstPath, $width, $height, $lang = false ) {
328 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
329 $svgConverters = $mainConfig->get( MainConfigNames::SVGConverters );
330 $svgConverter = $mainConfig->get( MainConfigNames::SVGConverter );
331 $svgConverterPath = $mainConfig->get( MainConfigNames::SVGConverterPath );
332 $err = false;
333 $retval = '';
334 if ( isset( $svgConverters[$svgConverter] ) ) {
335 if ( is_array( $svgConverters[$svgConverter] ) ) {
336 // This is a PHP callable
337 $func = $svgConverters[$svgConverter][0];
338 if ( !is_callable( $func ) ) {
339 throw new MWException( "$func is not callable" );
340 }
341 $err = $func( $srcPath,
342 $dstPath,
343 $width,
344 $height,
345 $lang,
346 ...array_slice( $svgConverters[$svgConverter], 1 )
347 );
348 $retval = (bool)$err;
349 } else {
350 // External command
351 $path = $svgConverterPath ? Shell::escape( "{$svgConverterPath}/" ) : '';
352 $cmd = preg_replace_callback( '/\$(path\/|width|height|input|output)/',
353 static function ( $m ) use ( $path, $width, $height, $srcPath, $dstPath ) {
354 return [
355 '$path/' => $path,
356 '$width' => intval( $width ),
357 '$height' => intval( $height ),
358 '$input' => Shell::escape( $srcPath ),
359 '$output' => Shell::escape( $dstPath ),
360 ][$m[0]];
361 },
362 $svgConverters[$svgConverter]
363 );
364
365 $env = [];
366 if ( $lang !== false ) {
367 $env['LANG'] = $lang;
368 }
369
370 wfDebug( __METHOD__ . ": $cmd" );
371 $err = wfShellExecWithStderr( $cmd, $retval, $env );
372 }
373 }
374 $removed = $this->removeBadFile( $dstPath, $retval );
375 if ( $retval != 0 || $removed ) {
376 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable cmd is set when used
377 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable cmd is set when used
378 $this->logErrorForExternalProcess( $retval, $err, $cmd );
379 return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
380 }
381
382 return true;
383 }
384
385 public static function rasterizeImagickExt( $srcPath, $dstPath, $width, $height ) {
386 $im = new Imagick( $srcPath );
387 $im->setBackgroundColor( 'transparent' );
388 $im->readImage( $srcPath );
389 $im->setImageFormat( 'png' );
390 $im->setImageDepth( 8 );
391
392 if ( !$im->thumbnailImage( (int)$width, (int)$height, /* fit */ false ) ) {
393 return 'Could not resize image';
394 }
395 if ( !$im->writeImage( $dstPath ) ) {
396 return "Could not write to $dstPath";
397 }
398 }
399
400 public function getThumbType( $ext, $mime, $params = null ) {
401 return [ 'png', 'image/png' ];
402 }
403
413 public function getLongDesc( $file ) {
414 $metadata = $this->validateMetadata( $file->getMetadataArray() );
415 if ( isset( $metadata['error'] ) ) {
416 return wfMessage( 'svg-long-error', $metadata['error']['message'] )->text();
417 }
418
419 if ( $this->isAnimatedImage( $file ) ) {
420 $msg = wfMessage( 'svg-long-desc-animated' );
421 } else {
422 $msg = wfMessage( 'svg-long-desc' );
423 }
424
425 return $msg->numParams( $file->getWidth(), $file->getHeight() )->sizeParams( $file->getSize() )->parse();
426 }
427
433 public function getSizeAndMetadata( $state, $filename ) {
434 $metadata = [ 'version' => self::SVG_METADATA_VERSION ];
435
436 try {
437 $svgReader = new SVGReader( $filename );
438 $metadata += $svgReader->getMetadata();
439 } catch ( TimeoutException $e ) {
440 throw $e;
441 } catch ( Exception $e ) { // @todo SVG specific exceptions
442 // File not found, broken, etc.
443 $metadata['error'] = [
444 'message' => $e->getMessage(),
445 'code' => $e->getCode()
446 ];
447 wfDebug( __METHOD__ . ': ' . $e->getMessage() );
448 }
449
450 return [
451 'width' => $metadata['width'] ?? 0,
452 'height' => $metadata['height'] ?? 0,
453 'metadata' => $metadata
454 ];
455 }
456
457 protected function validateMetadata( $unser ) {
458 if ( isset( $unser['version'] ) && $unser['version'] === self::SVG_METADATA_VERSION ) {
459 return $unser;
460 }
461
462 return null;
463 }
464
465 public function getMetadataType( $image ) {
466 return 'parsed-svg';
467 }
468
469 public function isFileMetadataValid( $image ) {
470 $meta = $this->validateMetadata( $image->getMetadataArray() );
471 if ( !$meta ) {
472 return self::METADATA_BAD;
473 }
474 if ( !isset( $meta['originalWidth'] ) ) {
475 // Old but compatible
476 return self::METADATA_COMPATIBLE;
477 }
478
479 return self::METADATA_GOOD;
480 }
481
482 protected function visibleMetadataFields() {
483 return [ 'objectname', 'imagedescription' ];
484 }
485
491 public function formatMetadata( $file, $context = false ) {
492 $result = [
493 'visible' => [],
494 'collapsed' => []
495 ];
496 $metadata = $this->validateMetadata( $file->getMetadataArray() );
497 if ( !$metadata || isset( $metadata['error'] ) ) {
498 return false;
499 }
500
501 /* @todo Add a formatter
502 $format = new FormatSVG( $metadata );
503 $formatted = $format->getFormattedData();
504 */
505
506 // Sort fields into visible and collapsed
507 $visibleFields = $this->visibleMetadataFields();
508
509 $showMeta = false;
510 foreach ( $metadata as $name => $value ) {
511 $tag = strtolower( $name );
512 if ( isset( self::$metaConversion[$tag] ) ) {
513 $tag = strtolower( self::$metaConversion[$tag] );
514 } else {
515 // Do not output other metadata not in list
516 continue;
517 }
518 $showMeta = true;
519 self::addMeta( $result,
520 in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
521 'exif',
522 $tag,
523 $value
524 );
525 }
526
527 return $showMeta ? $result : false;
528 }
529
535 public function validateParam( $name, $value ) {
536 if ( in_array( $name, [ 'width', 'height' ] ) ) {
537 // Reject negative heights, widths
538 return ( $value > 0 );
539 }
540 if ( $name === 'lang' ) {
541 // Validate $code
542 if ( $value === ''
543 || !LanguageCode::isWellFormedLanguageTag( $value )
544 ) {
545 return false;
546 }
547
548 return true;
549 }
550
551 // Only lang, width and height are acceptable keys
552 return false;
553 }
554
559 public function makeParamString( $params ) {
560 $lang = '';
561 $code = $this->getLanguageFromParams( $params );
562 if ( $code !== self::SVG_DEFAULT_RENDER_LANG ) {
563 $lang = 'lang' . strtolower( $code ) . '-';
564 }
565 if ( !isset( $params['width'] ) ) {
566 return false;
567 }
568
569 return "$lang{$params['width']}px";
570 }
571
572 public function parseParamString( $str ) {
573 $m = false;
574 // Language codes are supposed to be lowercase
575 if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/', $str, $m ) ) {
576 if ( LanguageCode::isWellFormedLanguageTag( $m[1] ) ) {
577 return [ 'width' => array_pop( $m ), 'lang' => $m[1] ];
578 }
579 return [ 'width' => array_pop( $m ), 'lang' => self::SVG_DEFAULT_RENDER_LANG ];
580 }
581 if ( preg_match( '/^(\d+)px$/', $str, $m ) ) {
582 return [ 'width' => $m[1], 'lang' => self::SVG_DEFAULT_RENDER_LANG ];
583 }
584 return false;
585 }
586
587 public function getParamMap() {
588 return [ 'img_lang' => 'lang', 'img_width' => 'width' ];
589 }
590
595 protected function getScriptParams( $params ) {
596 $scriptParams = [ 'width' => $params['width'] ];
597 if ( isset( $params['lang'] ) ) {
598 $scriptParams['lang'] = $params['lang'];
599 }
600
601 return $scriptParams;
602 }
603
604 public function getCommonMetaArray( File $file ) {
605 $metadata = $this->validateMetadata( $file->getMetadataArray() );
606 if ( !$metadata || isset( $metadata['error'] ) ) {
607 return [];
608 }
609 $stdMetadata = [];
610 foreach ( $metadata as $name => $value ) {
611 $tag = strtolower( $name );
612 if ( $tag === 'originalwidth' || $tag === 'originalheight' ) {
613 // Skip these. In the exif metadata stuff, it is assumed these
614 // are measured in px, which is not the case here.
615 continue;
616 }
617 if ( isset( self::$metaConversion[$tag] ) ) {
618 $tag = self::$metaConversion[$tag];
619 $stdMetadata[$tag] = $value;
620 }
621 }
622
623 return $stdMetadata;
624 }
625}
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.
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:68
Media handler abstract base class for images.
MediaWiki exception.
logErrorForExternalProcess( $retval, $err, $cmd)
Log an error that occurred in an external process.
removeBadFile( $dstPath, $retval=0)
Check for zero-sized thumbnails.
Basic media transform error class.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Executes shell commands.
Definition Shell.php:46
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|false Array of parameters or ...
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.
doTransform( $image, $dstPath, $dstUrl, $params, $flags=0)
validateMetadata( $unser)
getLanguageFromParams(array $params)
Determines render language from image parameters This is a lowercase IETF language.
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.
getSizeAndMetadata( $state, $filename)
isEnabled()
False if the handler is disabled for all files.
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)
isFileMetadataValid( $image)
Check if the metadata is valid for this handler.
isAnimatedImage( $file)
getMatchedLanguage( $userPreferredLanguage, array $svgLanguages)
SVG's systemLanguage matching rules state: 'The systemLanguage attribute ... [e]valuates to "true" if...
const SVG_METADATA_VERSION
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