MediaWiki REL1_39
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 } else {
60 return true;
61 }
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 } else {
312 return $status; // MediaTransformError
313 }
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 $cmd = str_replace(
352 [ '$path/', '$width', '$height', '$input', '$output' ],
353 [ $svgConverterPath ? Shell::escape( "{$svgConverterPath}/" ) : "",
354 intval( $width ),
355 intval( $height ),
356 Shell::escape( $srcPath ),
357 Shell::escape( $dstPath ) ],
358 $svgConverters[$svgConverter]
359 );
360
361 $env = [];
362 if ( $lang !== false ) {
363 $env['LANG'] = $lang;
364 }
365
366 wfDebug( __METHOD__ . ": $cmd" );
367 $err = wfShellExecWithStderr( $cmd, $retval, $env );
368 }
369 }
370 $removed = $this->removeBadFile( $dstPath, $retval );
371 if ( $retval != 0 || $removed ) {
372 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable cmd is set when used
373 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable cmd is set when used
374 $this->logErrorForExternalProcess( $retval, $err, $cmd );
375 return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
376 }
377
378 return true;
379 }
380
381 public static function rasterizeImagickExt( $srcPath, $dstPath, $width, $height ) {
382 $im = new Imagick( $srcPath );
383 $im->setBackgroundColor( 'transparent' );
384 $im->readImage( $srcPath );
385 $im->setImageFormat( 'png' );
386 $im->setImageDepth( 8 );
387
388 if ( !$im->thumbnailImage( intval( $width ), intval( $height ), /* fit */ false ) ) {
389 return 'Could not resize image';
390 }
391 if ( !$im->writeImage( $dstPath ) ) {
392 return "Could not write to $dstPath";
393 }
394 }
395
396 public function getThumbType( $ext, $mime, $params = null ) {
397 return [ 'png', 'image/png' ];
398 }
399
409 public function getLongDesc( $file ) {
410 $metadata = $this->validateMetadata( $file->getMetadataArray() );
411 if ( isset( $metadata['error'] ) ) {
412 return wfMessage( 'svg-long-error', $metadata['error']['message'] )->text();
413 }
414
415 if ( $this->isAnimatedImage( $file ) ) {
416 $msg = wfMessage( 'svg-long-desc-animated' );
417 } else {
418 $msg = wfMessage( 'svg-long-desc' );
419 }
420
421 return $msg->numParams( $file->getWidth(), $file->getHeight() )->sizeParams( $file->getSize() )->parse();
422 }
423
429 public function getSizeAndMetadata( $state, $filename ) {
430 $metadata = [ 'version' => self::SVG_METADATA_VERSION ];
431
432 try {
433 $svgReader = new SVGReader( $filename );
434 $metadata += $svgReader->getMetadata();
435 } catch ( TimeoutException $e ) {
436 throw $e;
437 } catch ( Exception $e ) { // @todo SVG specific exceptions
438 // File not found, broken, etc.
439 $metadata['error'] = [
440 'message' => $e->getMessage(),
441 'code' => $e->getCode()
442 ];
443 wfDebug( __METHOD__ . ': ' . $e->getMessage() );
444 }
445
446 return [
447 'width' => $metadata['width'] ?? 0,
448 'height' => $metadata['height'] ?? 0,
449 'metadata' => $metadata
450 ];
451 }
452
453 protected function validateMetadata( $unser ) {
454 if ( isset( $unser['version'] ) && $unser['version'] == self::SVG_METADATA_VERSION ) {
455 return $unser;
456 } else {
457 return null;
458 }
459 }
460
461 public function getMetadataType( $image ) {
462 return 'parsed-svg';
463 }
464
465 public function isFileMetadataValid( $image ) {
466 $meta = $this->validateMetadata( $image->getMetadataArray() );
467 if ( !$meta ) {
468 return self::METADATA_BAD;
469 }
470 if ( !isset( $meta['originalWidth'] ) ) {
471 // Old but compatible
472 return self::METADATA_COMPATIBLE;
473 }
474
475 return self::METADATA_GOOD;
476 }
477
478 protected function visibleMetadataFields() {
479 $fields = [ 'objectname', 'imagedescription' ];
480
481 return $fields;
482 }
483
489 public function formatMetadata( $file, $context = false ) {
490 $result = [
491 'visible' => [],
492 'collapsed' => []
493 ];
494 $metadata = $this->validateMetadata( $file->getMetadataArray() );
495 if ( !$metadata || isset( $metadata['error'] ) ) {
496 return false;
497 }
498
499 /* @todo Add a formatter
500 $format = new FormatSVG( $metadata );
501 $formatted = $format->getFormattedData();
502 */
503
504 // Sort fields into visible and collapsed
505 $visibleFields = $this->visibleMetadataFields();
506
507 $showMeta = false;
508 foreach ( $metadata as $name => $value ) {
509 $tag = strtolower( $name );
510 if ( isset( self::$metaConversion[$tag] ) ) {
511 $tag = strtolower( self::$metaConversion[$tag] );
512 } else {
513 // Do not output other metadata not in list
514 continue;
515 }
516 $showMeta = true;
517 self::addMeta( $result,
518 in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
519 'exif',
520 $tag,
521 $value
522 );
523 }
524
525 return $showMeta ? $result : false;
526 }
527
533 public function validateParam( $name, $value ) {
534 if ( in_array( $name, [ 'width', 'height' ] ) ) {
535 // Reject negative heights, widths
536 return ( $value > 0 );
537 }
538 if ( $name == 'lang' ) {
539 // Validate $code
540 if ( $value === ''
541 || !LanguageCode::isWellFormedLanguageTag( $value )
542 ) {
543 return false;
544 }
545
546 return true;
547 }
548
549 // Only lang, width and height are acceptable keys
550 return false;
551 }
552
557 public function makeParamString( $params ) {
558 $lang = '';
559 $code = $this->getLanguageFromParams( $params );
560 if ( $code !== self::SVG_DEFAULT_RENDER_LANG ) {
561 $lang = 'lang' . strtolower( $code ) . '-';
562 }
563 if ( !isset( $params['width'] ) ) {
564 return false;
565 }
566
567 return "$lang{$params['width']}px";
568 }
569
570 public function parseParamString( $str ) {
571 $m = false;
572 // Language codes are supposed to be lowercase
573 if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/', $str, $m ) ) {
574 if ( LanguageCode::isWellFormedLanguageTag( $m[1] ) ) {
575 return [ 'width' => array_pop( $m ), 'lang' => $m[1] ];
576 }
577 return [ 'width' => array_pop( $m ), 'lang' => self::SVG_DEFAULT_RENDER_LANG ];
578 }
579 if ( preg_match( '/^(\d+)px$/', $str, $m ) ) {
580 return [ 'width' => $m[1], 'lang' => self::SVG_DEFAULT_RENDER_LANG ];
581 }
582 return false;
583 }
584
585 public function getParamMap() {
586 return [ 'img_lang' => 'lang', 'img_width' => 'width' ];
587 }
588
593 protected function getScriptParams( $params ) {
594 $scriptParams = [ 'width' => $params['width'] ];
595 if ( isset( $params['lang'] ) ) {
596 $scriptParams['lang'] = $params['lang'];
597 }
598
599 return $scriptParams;
600 }
601
602 public function getCommonMetaArray( File $file ) {
603 $metadata = $this->validateMetadata( $file->getMetadataArray() );
604 if ( !$metadata || isset( $metadata['error'] ) ) {
605 return [];
606 }
607 $stdMetadata = [];
608 foreach ( $metadata as $name => $value ) {
609 $tag = strtolower( $name );
610 if ( $tag === 'originalwidth' || $tag === 'originalheight' ) {
611 // Skip these. In the exif metadata stuff, it is assumed these
612 // are measured in px, which is not the case here.
613 continue;
614 }
615 if ( isset( self::$metaConversion[$tag] ) ) {
616 $tag = self::$metaConversion[$tag];
617 $stdMetadata[$tag] = $value;
618 }
619 }
620
621 return $stdMetadata;
622 }
623}
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:67
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