19use UnexpectedValueException;
20use Wikimedia\ScopedCallback;
30 private const SVG_DEFAULT_RENDER_LANG =
'en';
36 private static $metaConversion = [
37 'originalwidth' =>
'ImageWidth',
38 'originalheight' =>
'ImageLength',
39 'description' =>
'ImageDescription',
40 'title' =>
'ObjectName',
51 if ( !isset( $svgConverters[$svgConverter] ) ) {
52 wfDebug(
"\$wgSVGConverter is invalid, disabling SVG rendering." );
63 if ( $svgNativeRendering ===
false ) {
71 if ( $maxSVGFilesize && $file->
getSize() >= $maxSVGFilesize ) {
75 if ( $svgNativeRendering ===
true ) {
81 if ( $svgNativeRendering ===
'partial' ) {
89 return !$this->allowRenderingByUserAgent( $file );
102 # @todo Detect animated SVGs
104 if ( isset( $metadata[
'animated'] ) ) {
105 return $metadata[
'animated'];
126 if ( isset( $metadata[
'translations'] ) ) {
127 foreach ( $metadata[
'translations'] as $lang => $langType ) {
129 $langList[] = strtolower( $lang );
133 return array_unique( $langList );
153 if ( $userPreferredLanguage ===
'und' ) {
156 foreach ( $svgLanguages as $svgLang ) {
157 if ( strcasecmp( $svgLang, $userPreferredLanguage ) === 0 ) {
160 $trimmedSvgLang = $svgLang;
161 while ( str_contains( $trimmedSvgLang,
'-' ) ) {
162 $trimmedSvgLang = substr( $trimmedSvgLang, 0, strrpos( $trimmedSvgLang,
'-' ) );
163 if ( strcasecmp( $trimmedSvgLang, $userPreferredLanguage ) === 0 ) {
179 return $params[
'lang'] ?? $params[
'targetlang'] ?? self::SVG_DEFAULT_RENDER_LANG;
189 return self::SVG_DEFAULT_RENDER_LANG;
198 return $this->allowRenderingByUserAgent( $file );
207 if ( parent::normaliseParams( $image, $params ) ) {
208 $params = $this->normaliseParamsInternal( $image, $params );
227 $srcWidth = $image->getWidth( $params[
'page'] );
228 $srcHeight = $image->getHeight( $params[
'page'] );
229 $params[
'physicalWidth'] = $this->getSteppedThumbWidth(
230 $image, $params[
'physicalWidth'], $srcWidth, $srcHeight
232 $params[
'physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight, $params[
'physicalWidth'] );
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 );
240 } elseif ( $params[
'physicalHeight'] > $svgMaxSize ) {
241 $params[
'physicalWidth'] = File::scaleHeight( $srcHeight, $srcWidth, $svgMaxSize );
242 $params[
'physicalHeight'] = $svgMaxSize;
246 if ( isset( $params[
'targetlang'] ) && !$image->getMatchedLanguage( $params[
'targetlang'] ) ) {
247 unset( $params[
'targetlang'] );
261 public function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
262 if ( !$this->normaliseParams( $image, $params ) ) {
265 $clientWidth = $params[
'width'];
266 $clientHeight = $params[
'height'];
267 $physicalWidth = $params[
'physicalWidth'];
268 $physicalHeight = $params[
'physicalHeight'];
269 $lang = $this->getLanguageFromParams( $params );
271 if ( $this->allowRenderingByUserAgent( $image ) ) {
272 return $this->getClientScalingThumbnailImage( $image, $params );
275 if ( $flags & self::TRANSFORM_LATER ) {
279 $metadata = $this->validateMetadata( $image->getMetadataArray() );
280 if ( isset( $metadata[
'error'] ) ) {
281 $err =
wfMessage(
'svg-long-error', $metadata[
'error'][
'message'] );
288 wfMessage(
'thumbnail_dest_directory' ) );
291 $srcPath = $image->getLocalRefPath();
292 if ( $srcPath ===
false ) {
294 sprintf(
'Thumbnail failed on %s: could not get local copy of "%s"',
298 $params[
'width'], $params[
'height'],
307 $lnPath =
"$tmpDir/" . basename( $srcPath );
308 $ok = mkdir( $tmpDir, 0771 );
311 sprintf(
'Thumbnail failed on %s: could not create temporary directory %s',
314 $params[
'width'], $params[
'height'],
315 wfMessage(
'thumbnail-temp-create' )->text()
319 $ok = @symlink( $srcPath, $lnPath );
321 $cleaner =
new ScopedCallback(
static function () use ( $tmpDir, $lnPath ) {
329 $ok = copy( $srcPath, $lnPath );
333 sprintf(
'Thumbnail failed on %s: could not link %s to %s',
336 $params[
'width'], $params[
'height'],
341 $status = $this->rasterize( $lnPath, $dstPath, $physicalWidth, $physicalHeight, $lang );
342 if ( $status ===
true ) {
359 public function rasterize( $srcPath, $dstPath, $width, $height, $lang =
false ) {
366 if ( isset( $svgConverters[$svgConverter] ) ) {
367 if ( is_array( $svgConverters[$svgConverter] ) ) {
369 $func = $svgConverters[$svgConverter][0];
370 if ( !is_callable( $func ) ) {
371 throw new UnexpectedValueException(
"$func is not callable" );
373 $err = $func( $srcPath,
378 ...array_slice( $svgConverters[$svgConverter], 1 )
380 $retval = (bool)$err;
383 $cmd = strtr( $svgConverters[$svgConverter], [
384 '$path/' => $svgConverterPath ? Shell::escape(
"$svgConverterPath/" ) :
'',
385 '$width' => (int)$width,
386 '$height' => (
int)$height,
387 '$input' => Shell::escape( $srcPath ),
388 '$output' => Shell::escape( $dstPath ),
392 if ( $lang !==
false ) {
393 $env[
'LANG'] = $lang;
396 wfDebug( __METHOD__ .
": $cmd" );
400 $removed = $this->removeBadFile( $dstPath, $retval );
401 if ( $retval != 0 || $removed ) {
403 $this->logErrorForExternalProcess( $retval, $err, $cmd );
418 $im =
new Imagick( $srcPath );
419 $im->setBackgroundColor(
'transparent' );
420 $im->readImage( $srcPath );
421 $im->setImageFormat(
'png' );
422 $im->setImageDepth( 8 );
424 if ( !$im->thumbnailImage( (
int)$width, (
int)$height,
false ) ) {
425 return 'Could not resize image';
427 if ( !$im->writeImage( $dstPath ) ) {
428 return "Could not write to $dstPath";
442 $url = $image->modifyClientThumbUrl( $image->getUrl(), $params );
448 return [
'png',
'image/png' ];
462 if ( isset( $metadata[
'error'] ) ) {
463 return wfMessage(
'svg-long-error', $metadata[
'error'][
'message'] )
464 ->inLanguage( $this->getLanguage() )->escaped();
467 if ( $this->isAnimatedImage( $file ) ) {
468 $msg =
wfMessage(
'svg-long-desc-animated' );
475 ->sizeParams( $file->
getSize() )
476 ->inLanguage( $this->getLanguage() )
486 $metadata = [
'version' => self::SVG_METADATA_VERSION ];
490 $metadata += $svgReader->getMetadata();
493 $metadata[
'error'] = [
494 'message' => $e->getMessage(),
495 'code' => $e->getCode()
497 wfDebug( __METHOD__ .
': ' . $e->getMessage() );
501 'width' => $metadata[
'width'] ?? 0,
502 'height' => $metadata[
'height'] ?? 0,
503 'metadata' => $metadata
509 if ( isset( $unser[
'version'] ) && $unser[
'version'] === self::SVG_METADATA_VERSION ) {
523 $meta = $this->validateMetadata( $image->getMetadataArray() );
525 return self::METADATA_BAD;
527 if ( !isset( $meta[
'originalWidth'] ) ) {
529 return self::METADATA_COMPATIBLE;
532 return self::METADATA_GOOD;
537 return [
'objectname',
'imagedescription' ];
551 if ( !$metadata || isset( $metadata[
'error'] ) ) {
561 $visibleFields = $this->visibleMetadataFields();
564 foreach ( $metadata as $name => $value ) {
565 $tag = strtolower( $name );
566 if ( isset( self::$metaConversion[$tag] ) ) {
567 $tag = strtolower( self::$metaConversion[$tag] );
573 self::addMeta( $result,
574 in_array( $tag, $visibleFields ) ?
'visible' :
'collapsed',
581 return $showMeta ? $result :
false;
590 if ( in_array( $name, [
'width',
'height' ] ) ) {
592 return ( $value > 0 );
594 if ( $name ===
'lang' ) {
597 || !LanguageCode::isWellFormedLanguageTag( $value )
615 $code = $this->getLanguageFromParams( $params );
616 if ( $code !== self::SVG_DEFAULT_RENDER_LANG ) {
617 $lang =
'lang' . strtolower( $code ) .
'-';
620 if ( isset( $params[
'physicalWidth'] ) && $params[
'physicalWidth'] ) {
621 return "$lang{$params['physicalWidth']}px";
624 if ( !isset( $params[
'width'] ) ) {
628 return "$lang{$params['width']}px";
635 if ( preg_match(
'/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/', $str, $m ) ) {
636 if ( LanguageCode::isWellFormedLanguageTag( $m[1] ) ) {
637 return [
'width' => array_pop( $m ),
'lang' => $m[1] ];
639 return [
'width' => array_pop( $m ),
'lang' => self::SVG_DEFAULT_RENDER_LANG ];
641 if ( preg_match(
'/^(\d+)px$/', $str, $m ) ) {
642 return [
'width' => $m[1],
'lang' => self::SVG_DEFAULT_RENDER_LANG ];
649 return [
'img_lang' =>
'lang',
'img_width' =>
'width' ];
657 $scriptParams = [
'width' => $params[
'width'] ];
658 if ( isset( $params[
'lang'] ) ) {
659 $scriptParams[
'lang'] = $params[
'lang'];
662 return $scriptParams;
668 if ( !$metadata || isset( $metadata[
'error'] ) ) {
672 foreach ( $metadata as $name => $value ) {
673 $tag = strtolower( $name );
674 if ( $tag ===
'originalwidth' || $tag ===
'originalheight' ) {
679 if ( isset( self::$metaConversion[$tag] ) ) {
680 $tag = self::$metaConversion[$tag];
681 $stdMetadata[$tag] = $value;
690class_alias( SvgHandler::class,
'SvgHandler' );
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.
A class containing constants representing the names of configuration variables.
const SVGConverter
Name constant for the SVGConverter setting, for use with Config::get()
const SVGConverters
Name constant for the SVGConverters setting, for use with Config::get()
const SVGNativeRenderingSizeLimit
Name constant for the SVGNativeRenderingSizeLimit setting, for use with Config::get()
const SVGMaxSize
Name constant for the SVGMaxSize setting, for use with Config::get()
const SVGConverterPath
Name constant for the SVGConverterPath setting, for use with Config::get()
const SVGNativeRendering
Name constant for the SVGNativeRendering setting, for use with Config::get()
Interface for objects which can provide a MediaWiki context on request.