MediaWiki REL1_37
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 = $this->validateMetadata( $file->getMetadataArray() );
73 if ( isset( $metadata['animated'] ) ) {
74 return $metadata['animated'];
75 }
76
77 return false;
78 }
79
92 public function getAvailableLanguages( File $file ) {
93 $langList = [];
94 $metadata = $this->validateMetadata( $file->getMetadataArray() );
95 if ( isset( $metadata['translations'] ) ) {
96 foreach ( $metadata['translations'] as $lang => $langType ) {
97 if ( $langType === SVGReader::LANG_FULL_MATCH ) {
98 $langList[] = strtolower( $lang );
99 }
100 }
101 }
102 return array_unique( $langList );
103 }
104
120 public function getMatchedLanguage( $userPreferredLanguage, array $svgLanguages ) {
121 foreach ( $svgLanguages as $svgLang ) {
122 if ( strcasecmp( $svgLang, $userPreferredLanguage ) === 0 ) {
123 return $svgLang;
124 }
125 $trimmedSvgLang = $svgLang;
126 while ( strpos( $trimmedSvgLang, '-' ) !== false ) {
127 $trimmedSvgLang = substr( $trimmedSvgLang, 0, strrpos( $trimmedSvgLang, '-' ) );
128 if ( strcasecmp( $trimmedSvgLang, $userPreferredLanguage ) === 0 ) {
129 return $svgLang;
130 }
131 }
132 }
133 return null;
134 }
135
142 protected function getLanguageFromParams( array $params ) {
143 return $params['lang'] ?? $params['targetlang'] ?? 'en';
144 }
145
153 return 'en';
154 }
155
161 public function canAnimateThumbnail( $file ) {
162 return false;
163 }
164
170 public function normaliseParams( $image, &$params ) {
171 if ( parent::normaliseParams( $image, $params ) ) {
172 $params = $this->normaliseParamsInternal( $image, $params );
173 return true;
174 }
175
176 return false;
177 }
178
188 protected function normaliseParamsInternal( $image, $params ) {
189 global $wgSVGMaxSize;
190
191 # Don't make an image bigger than wgMaxSVGSize on the smaller side
192 if ( $params['physicalWidth'] <= $params['physicalHeight'] ) {
193 if ( $params['physicalWidth'] > $wgSVGMaxSize ) {
194 $srcWidth = $image->getWidth( $params['page'] );
195 $srcHeight = $image->getHeight( $params['page'] );
196 $params['physicalWidth'] = $wgSVGMaxSize;
197 $params['physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight, $wgSVGMaxSize );
198 }
199 } elseif ( $params['physicalHeight'] > $wgSVGMaxSize ) {
200 $srcWidth = $image->getWidth( $params['page'] );
201 $srcHeight = $image->getHeight( $params['page'] );
202 $params['physicalWidth'] = File::scaleHeight( $srcHeight, $srcWidth, $wgSVGMaxSize );
203 $params['physicalHeight'] = $wgSVGMaxSize;
204 }
205 // To prevent the proliferation of thumbnails in languages not present in SVGs, unless
206 // explicitly forced by user.
207 if ( isset( $params['targetlang'] ) && !$image->getMatchedLanguage( $params['targetlang'] ) ) {
208 unset( $params['targetlang'] );
209 }
210
211 return $params;
212 }
213
222 public function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
223 if ( !$this->normaliseParams( $image, $params ) ) {
224 return new TransformParameterError( $params );
225 }
226 $clientWidth = $params['width'];
227 $clientHeight = $params['height'];
228 $physicalWidth = $params['physicalWidth'];
229 $physicalHeight = $params['physicalHeight'];
230 $lang = $this->getLanguageFromParams( $params );
231
232 if ( $flags & self::TRANSFORM_LATER ) {
233 return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
234 }
235
236 $metadata = $this->validateMetadata( $image->getMetadataArray() );
237 if ( isset( $metadata['error'] ) ) { // sanity check
238 $err = wfMessage( 'svg-long-error', $metadata['error']['message'] );
239
240 return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
241 }
242
243 if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
244 return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight,
245 wfMessage( 'thumbnail_dest_directory' ) );
246 }
247
248 $srcPath = $image->getLocalRefPath();
249 if ( $srcPath === false ) { // Failed to get local copy
250 wfDebugLog( 'thumbnail',
251 sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
252 wfHostname(), $image->getName() ) );
253
254 return new MediaTransformError( 'thumbnail_error',
255 $params['width'], $params['height'],
256 wfMessage( 'filemissing' )
257 );
258 }
259
260 // Make a temp dir with a symlink to the local copy in it.
261 // This plays well with rsvg-convert policy for external entities.
262 // https://git.gnome.org/browse/librsvg/commit/?id=f01aded72c38f0e18bc7ff67dee800e380251c8e
263 $tmpDir = wfTempDir() . '/svg_' . wfRandomString( 24 );
264 $lnPath = "$tmpDir/" . basename( $srcPath );
265 $ok = mkdir( $tmpDir, 0771 );
266 if ( !$ok ) {
267 wfDebugLog( 'thumbnail',
268 sprintf( 'Thumbnail failed on %s: could not create temporary directory %s',
269 wfHostname(), $tmpDir ) );
270 return new MediaTransformError( 'thumbnail_error',
271 $params['width'], $params['height'],
272 wfMessage( 'thumbnail-temp-create' )->text()
273 );
274 }
275 $ok = symlink( $srcPath, $lnPath );
277 $cleaner = new ScopedCallback( static function () use ( $tmpDir, $lnPath ) {
278 Wikimedia\suppressWarnings();
279 unlink( $lnPath );
280 rmdir( $tmpDir );
281 Wikimedia\restoreWarnings();
282 } );
283 if ( !$ok ) {
284 wfDebugLog( 'thumbnail',
285 sprintf( 'Thumbnail failed on %s: could not link %s to %s',
286 wfHostname(), $lnPath, $srcPath ) );
287 return new MediaTransformError( 'thumbnail_error',
288 $params['width'], $params['height'],
289 wfMessage( 'thumbnail-temp-create' )
290 );
291 }
292
293 $status = $this->rasterize( $lnPath, $dstPath, $physicalWidth, $physicalHeight, $lang );
294 if ( $status === true ) {
295 return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
296 } else {
297 return $status; // MediaTransformError
298 }
299 }
300
312 public function rasterize( $srcPath, $dstPath, $width, $height, $lang = false ) {
314 $err = false;
315 $retval = '';
316 if ( isset( $wgSVGConverters[$wgSVGConverter] ) ) {
317 if ( is_array( $wgSVGConverters[$wgSVGConverter] ) ) {
318 // This is a PHP callable
320 if ( !is_callable( $func ) ) {
321 throw new MWException( "$func is not callable" );
322 }
323 $err = $func( $srcPath,
324 $dstPath,
325 $width,
326 $height,
327 $lang,
328 ...array_slice( $wgSVGConverters[$wgSVGConverter], 1 )
329 );
330 $retval = (bool)$err;
331 } else {
332 // External command
333 $cmd = str_replace(
334 [ '$path/', '$width', '$height', '$input', '$output' ],
335 [ $wgSVGConverterPath ? Shell::escape( "$wgSVGConverterPath/" ) : "",
336 intval( $width ),
337 intval( $height ),
338 Shell::escape( $srcPath ),
339 Shell::escape( $dstPath ) ],
341 );
342
343 $env = [];
344 if ( $lang !== false ) {
345 $env['LANG'] = $lang;
346 }
347
348 wfDebug( __METHOD__ . ": $cmd" );
349 $err = wfShellExecWithStderr( $cmd, $retval, $env );
350 }
351 }
352 $removed = $this->removeBadFile( $dstPath, $retval );
353 if ( $retval != 0 || $removed ) {
354 $this->logErrorForExternalProcess( $retval, $err, $cmd );
355 return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
356 }
357
358 return true;
359 }
360
361 public static function rasterizeImagickExt( $srcPath, $dstPath, $width, $height ) {
362 $im = new Imagick( $srcPath );
363 $im->setImageFormat( 'png' );
364 $im->setBackgroundColor( 'transparent' );
365 $im->setImageDepth( 8 );
366
367 if ( !$im->thumbnailImage( intval( $width ), intval( $height ), /* fit */ false ) ) {
368 return 'Could not resize image';
369 }
370 if ( !$im->writeImage( $dstPath ) ) {
371 return "Could not write to $dstPath";
372 }
373 }
374
375 public function getThumbType( $ext, $mime, $params = null ) {
376 return [ 'png', 'image/png' ];
377 }
378
388 public function getLongDesc( $file ) {
389 global $wgLang;
390
391 $metadata = $this->validateMetadata( $file->getMetadataArray() );
392 if ( isset( $metadata['error'] ) ) {
393 return wfMessage( 'svg-long-error', $metadata['error']['message'] )->text();
394 }
395
396 $size = $wgLang->formatSize( $file->getSize() );
397
398 if ( $this->isAnimatedImage( $file ) ) {
399 $msg = wfMessage( 'svg-long-desc-animated' );
400 } else {
401 $msg = wfMessage( 'svg-long-desc' );
402 }
403
404 $msg->numParams( $file->getWidth(), $file->getHeight() )->params( $size );
405
406 return $msg->parse();
407 }
408
414 public function getSizeAndMetadata( $state, $filename ) {
415 $metadata = [ 'version' => self::SVG_METADATA_VERSION ];
416
417 try {
418 $svgReader = new SVGReader( $filename );
419 $metadata += $svgReader->getMetadata();
420 } catch ( Exception $e ) { // @todo SVG specific exceptions
421 // File not found, broken, etc.
422 $metadata['error'] = [
423 'message' => $e->getMessage(),
424 'code' => $e->getCode()
425 ];
426 wfDebug( __METHOD__ . ': ' . $e->getMessage() );
427 }
428
429 return [
430 'width' => $metadata['width'] ?? 0,
431 'height' => $metadata['height'] ?? 0,
432 'metadata' => $metadata
433 ];
434 }
435
436 protected function validateMetadata( $unser ) {
437 if ( isset( $unser['version'] ) && $unser['version'] == self::SVG_METADATA_VERSION ) {
438 return $unser;
439 } else {
440 return null;
441 }
442 }
443
444 public function getMetadataType( $image ) {
445 return 'parsed-svg';
446 }
447
448 public function isFileMetadataValid( $image ) {
449 $meta = $this->validateMetadata( $image->getMetadataArray() );
450 if ( !$meta ) {
451 return self::METADATA_BAD;
452 }
453 if ( !isset( $meta['originalWidth'] ) ) {
454 // Old but compatible
456 }
457
458 return self::METADATA_GOOD;
459 }
460
461 protected function visibleMetadataFields() {
462 $fields = [ 'objectname', 'imagedescription' ];
463
464 return $fields;
465 }
466
472 public function formatMetadata( $file, $context = false ) {
473 $result = [
474 'visible' => [],
475 'collapsed' => []
476 ];
477 $metadata = $this->validateMetadata( $file->getMetadataArray() );
478 if ( !$metadata || isset( $metadata['error'] ) ) {
479 return false;
480 }
481
482 /* @todo Add a formatter
483 $format = new FormatSVG( $metadata );
484 $formatted = $format->getFormattedData();
485 */
486
487 // Sort fields into visible and collapsed
488 $visibleFields = $this->visibleMetadataFields();
489
490 $showMeta = false;
491 foreach ( $metadata as $name => $value ) {
492 $tag = strtolower( $name );
493 if ( isset( self::$metaConversion[$tag] ) ) {
494 $tag = strtolower( self::$metaConversion[$tag] );
495 } else {
496 // Do not output other metadata not in list
497 continue;
498 }
499 $showMeta = true;
500 self::addMeta( $result,
501 in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
502 'exif',
503 $tag,
504 $value
505 );
506 }
507
508 return $showMeta ? $result : false;
509 }
510
516 public function validateParam( $name, $value ) {
517 if ( in_array( $name, [ 'width', 'height' ] ) ) {
518 // Reject negative heights, widths
519 return ( $value > 0 );
520 } elseif ( $name == 'lang' ) {
521 // Validate $code
522 if ( $value === ''
523 || !MediaWikiServices::getInstance()->getLanguageNameUtils()
524 ->isValidCode( $value )
525 ) {
526 return false;
527 }
528
529 return true;
530 }
531
532 // Only lang, width and height are acceptable keys
533 return false;
534 }
535
540 public function makeParamString( $params ) {
541 $lang = '';
542 $code = $this->getLanguageFromParams( $params );
543 if ( $code !== 'en' ) {
544 $lang = 'lang' . strtolower( $code ) . '-';
545 }
546 if ( !isset( $params['width'] ) ) {
547 return false;
548 }
549
550 return "$lang{$params['width']}px";
551 }
552
553 public function parseParamString( $str ) {
554 $m = false;
555 if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/i', $str, $m ) ) {
556 return [ 'width' => array_pop( $m ), 'lang' => $m[1] ];
557 } elseif ( preg_match( '/^(\d+)px$/', $str, $m ) ) {
558 return [ 'width' => $m[1], 'lang' => 'en' ];
559 } else {
560 return false;
561 }
562 }
563
564 public function getParamMap() {
565 return [ 'img_lang' => 'lang', 'img_width' => 'width' ];
566 }
567
572 protected function getScriptParams( $params ) {
573 $scriptParams = [ 'width' => $params['width'] ];
574 if ( isset( $params['lang'] ) ) {
575 $scriptParams['lang'] = $params['lang'];
576 }
577
578 return $scriptParams;
579 }
580
581 public function getCommonMetaArray( File $file ) {
582 $metadata = $this->validateMetadata( $file->getMetadataArray() );
583 if ( !$metadata || isset( $metadata['error'] ) ) {
584 return [];
585 }
586 $stdMetadata = [];
587 foreach ( $metadata as $name => $value ) {
588 $tag = strtolower( $name );
589 if ( $tag === 'originalwidth' || $tag === 'originalheight' ) {
590 // Skip these. In the exif metadata stuff, it is assumed these
591 // are measured in px, which is not the case here.
592 continue;
593 }
594 if ( isset( self::$metaConversion[$tag] ) ) {
595 $tag = self::$metaConversion[$tag];
596 $stdMetadata[$tag] = $value;
597 }
598 }
599
600 return $stdMetadata;
601 }
602}
$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:831
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:66
getMetadataArray()
Get the unserialized handler-specific metadata STUB.
Definition File.php:764
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:45
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...
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.
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)
validateMetadata( $unser)
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.
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