MediaWiki  REL1_24
SVG.php
Go to the documentation of this file.
00001 <?php
00029 class SvgHandler extends ImageHandler {
00030     const SVG_METADATA_VERSION = 2;
00031 
00036     private static $metaConversion = array(
00037         'originalwidth' => 'ImageWidth',
00038         'originalheight' => 'ImageLength',
00039         'description' => 'ImageDescription',
00040         'title' => 'ObjectName',
00041     );
00042 
00043     function isEnabled() {
00044         global $wgSVGConverters, $wgSVGConverter;
00045         if ( !isset( $wgSVGConverters[$wgSVGConverter] ) ) {
00046             wfDebug( "\$wgSVGConverter is invalid, disabling SVG rendering.\n" );
00047 
00048             return false;
00049         } else {
00050             return true;
00051         }
00052     }
00053 
00054     function mustRender( $file ) {
00055         return true;
00056     }
00057 
00058     function isVectorized( $file ) {
00059         return true;
00060     }
00061 
00066     function isAnimatedImage( $file ) {
00067         # @todo Detect animated SVGs
00068         $metadata = $file->getMetadata();
00069         if ( $metadata ) {
00070             $metadata = $this->unpackMetadata( $metadata );
00071             if ( isset( $metadata['animated'] ) ) {
00072                 return $metadata['animated'];
00073             }
00074         }
00075 
00076         return false;
00077     }
00078 
00091     public function getAvailableLanguages( File $file ) {
00092         $metadata = $file->getMetadata();
00093         $langList = array();
00094         if ( $metadata ) {
00095             $metadata = $this->unpackMetadata( $metadata );
00096             if ( isset( $metadata['translations'] ) ) {
00097                 foreach ( $metadata['translations'] as $lang => $langType ) {
00098                     if ( $langType === SvgReader::LANG_FULL_MATCH ) {
00099                         $langList[] = $lang;
00100                     }
00101                 }
00102             }
00103         }
00104         return $langList;
00105     }
00106 
00113     public function getDefaultRenderLanguage( File $file ) {
00114         return 'en';
00115     }
00116 
00122     function canAnimateThumbnail( $file ) {
00123         return false;
00124     }
00125 
00131     function normaliseParams( $image, &$params ) {
00132         global $wgSVGMaxSize;
00133         if ( !parent::normaliseParams( $image, $params ) ) {
00134             return false;
00135         }
00136         # Don't make an image bigger than wgMaxSVGSize on the smaller side
00137         if ( $params['physicalWidth'] <= $params['physicalHeight'] ) {
00138             if ( $params['physicalWidth'] > $wgSVGMaxSize ) {
00139                 $srcWidth = $image->getWidth( $params['page'] );
00140                 $srcHeight = $image->getHeight( $params['page'] );
00141                 $params['physicalWidth'] = $wgSVGMaxSize;
00142                 $params['physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight, $wgSVGMaxSize );
00143             }
00144         } else {
00145             if ( $params['physicalHeight'] > $wgSVGMaxSize ) {
00146                 $srcWidth = $image->getWidth( $params['page'] );
00147                 $srcHeight = $image->getHeight( $params['page'] );
00148                 $params['physicalWidth'] = File::scaleHeight( $srcHeight, $srcWidth, $wgSVGMaxSize );
00149                 $params['physicalHeight'] = $wgSVGMaxSize;
00150             }
00151         }
00152 
00153         return true;
00154     }
00155 
00164     function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
00165         if ( !$this->normaliseParams( $image, $params ) ) {
00166             return new TransformParameterError( $params );
00167         }
00168         $clientWidth = $params['width'];
00169         $clientHeight = $params['height'];
00170         $physicalWidth = $params['physicalWidth'];
00171         $physicalHeight = $params['physicalHeight'];
00172         $lang = isset( $params['lang'] ) ? $params['lang'] : $this->getDefaultRenderLanguage( $image );
00173 
00174         if ( $flags & self::TRANSFORM_LATER ) {
00175             return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
00176         }
00177 
00178         $metadata = $this->unpackMetadata( $image->getMetadata() );
00179         if ( isset( $metadata['error'] ) ) { // sanity check
00180             $err = wfMessage( 'svg-long-error', $metadata['error']['message'] )->text();
00181 
00182             return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
00183         }
00184 
00185         if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
00186             return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight,
00187                 wfMessage( 'thumbnail_dest_directory' )->text() );
00188         }
00189 
00190         $srcPath = $image->getLocalRefPath();
00191         if ( $srcPath === false ) { // Failed to get local copy
00192             wfDebugLog( 'thumbnail',
00193                 sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
00194                     wfHostname(), $image->getName() ) );
00195 
00196             return new MediaTransformError( 'thumbnail_error',
00197                 $params['width'], $params['height'],
00198                 wfMessage( 'filemissing' )->text()
00199             );
00200         }
00201 
00202         // Make a temp dir with a symlink to the local copy in it.
00203         // This plays well with rsvg-convert policy for external entities.
00204         // https://git.gnome.org/browse/librsvg/commit/?id=f01aded72c38f0e18bc7ff67dee800e380251c8e
00205         $tmpDir = wfTempDir() . '/svg_' . wfRandomString( 24 );
00206         $lnPath = "$tmpDir/" . basename( $srcPath );
00207         $ok = mkdir( $tmpDir, 0771 ) && symlink( $srcPath, $lnPath );
00208         $cleaner = new ScopedCallback( function () use ( $tmpDir, $lnPath ) {
00209             wfSuppressWarnings();
00210             unlink( $lnPath );
00211             rmdir( $tmpDir );
00212             wfRestoreWarnings();
00213         } );
00214         if ( !$ok ) {
00215             wfDebugLog( 'thumbnail',
00216                 sprintf( 'Thumbnail failed on %s: could not link %s to %s',
00217                     wfHostname(), $lnPath, $srcPath ) );
00218             return new MediaTransformError( 'thumbnail_error',
00219                 $params['width'], $params['height'],
00220                 wfMessage( 'thumbnail-temp-create' )->text()
00221             );
00222         }
00223 
00224         $status = $this->rasterize( $lnPath, $dstPath, $physicalWidth, $physicalHeight, $lang );
00225         if ( $status === true ) {
00226             return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
00227         } else {
00228             return $status; // MediaTransformError
00229         }
00230     }
00231 
00243     public function rasterize( $srcPath, $dstPath, $width, $height, $lang = false ) {
00244         global $wgSVGConverters, $wgSVGConverter, $wgSVGConverterPath;
00245         $err = false;
00246         $retval = '';
00247         if ( isset( $wgSVGConverters[$wgSVGConverter] ) ) {
00248             if ( is_array( $wgSVGConverters[$wgSVGConverter] ) ) {
00249                 // This is a PHP callable
00250                 $func = $wgSVGConverters[$wgSVGConverter][0];
00251                 $args = array_merge( array( $srcPath, $dstPath, $width, $height, $lang ),
00252                     array_slice( $wgSVGConverters[$wgSVGConverter], 1 ) );
00253                 if ( !is_callable( $func ) ) {
00254                     throw new MWException( "$func is not callable" );
00255                 }
00256                 $err = call_user_func_array( $func, $args );
00257                 $retval = (bool)$err;
00258             } else {
00259                 // External command
00260                 $cmd = str_replace(
00261                     array( '$path/', '$width', '$height', '$input', '$output' ),
00262                     array( $wgSVGConverterPath ? wfEscapeShellArg( "$wgSVGConverterPath/" ) : "",
00263                         intval( $width ),
00264                         intval( $height ),
00265                         wfEscapeShellArg( $srcPath ),
00266                         wfEscapeShellArg( $dstPath ) ),
00267                     $wgSVGConverters[$wgSVGConverter]
00268                 );
00269 
00270                 $env = array();
00271                 if ( $lang !== false ) {
00272                     $env['LANG'] = $lang;
00273                 }
00274 
00275                 wfProfileIn( 'rsvg' );
00276                 wfDebug( __METHOD__ . ": $cmd\n" );
00277                 $err = wfShellExecWithStderr( $cmd, $retval, $env );
00278                 wfProfileOut( 'rsvg' );
00279             }
00280         }
00281         $removed = $this->removeBadFile( $dstPath, $retval );
00282         if ( $retval != 0 || $removed ) {
00283             $this->logErrorForExternalProcess( $retval, $err, $cmd );
00284             return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
00285         }
00286 
00287         return true;
00288     }
00289 
00290     public static function rasterizeImagickExt( $srcPath, $dstPath, $width, $height ) {
00291         $im = new Imagick( $srcPath );
00292         $im->setImageFormat( 'png' );
00293         $im->setBackgroundColor( 'transparent' );
00294         $im->setImageDepth( 8 );
00295 
00296         if ( !$im->thumbnailImage( intval( $width ), intval( $height ), /* fit */ false ) ) {
00297             return 'Could not resize image';
00298         }
00299         if ( !$im->writeImage( $dstPath ) ) {
00300             return "Could not write to $dstPath";
00301         }
00302     }
00303 
00310     function getImageSize( $file, $path, $metadata = false ) {
00311         if ( $metadata === false ) {
00312             $metadata = $file->getMetaData();
00313         }
00314         $metadata = $this->unpackMetaData( $metadata );
00315 
00316         if ( isset( $metadata['width'] ) && isset( $metadata['height'] ) ) {
00317             return array( $metadata['width'], $metadata['height'], 'SVG',
00318                 "width=\"{$metadata['width']}\" height=\"{$metadata['height']}\"" );
00319         } else { // error
00320             return array( 0, 0, 'SVG', "width=\"0\" height=\"0\"" );
00321         }
00322     }
00323 
00324     function getThumbType( $ext, $mime, $params = null ) {
00325         return array( 'png', 'image/png' );
00326     }
00327 
00337     function getLongDesc( $file ) {
00338         global $wgLang;
00339 
00340         $metadata = $this->unpackMetadata( $file->getMetadata() );
00341         if ( isset( $metadata['error'] ) ) {
00342             return wfMessage( 'svg-long-error', $metadata['error']['message'] )->text();
00343         }
00344 
00345         $size = $wgLang->formatSize( $file->getSize() );
00346 
00347         if ( $this->isAnimatedImage( $file ) ) {
00348             $msg = wfMessage( 'svg-long-desc-animated' );
00349         } else {
00350             $msg = wfMessage( 'svg-long-desc' );
00351         }
00352 
00353         $msg->numParams( $file->getWidth(), $file->getHeight() )->params( $size );
00354 
00355         return $msg->parse();
00356     }
00357 
00363     function getMetadata( $file, $filename ) {
00364         $metadata = array( 'version' => self::SVG_METADATA_VERSION );
00365         try {
00366             $metadata += SVGMetadataExtractor::getMetadata( $filename );
00367         } catch ( MWException $e ) { // @todo SVG specific exceptions
00368             // File not found, broken, etc.
00369             $metadata['error'] = array(
00370                 'message' => $e->getMessage(),
00371                 'code' => $e->getCode()
00372             );
00373             wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
00374         }
00375 
00376         return serialize( $metadata );
00377     }
00378 
00379     function unpackMetadata( $metadata ) {
00380         wfSuppressWarnings();
00381         $unser = unserialize( $metadata );
00382         wfRestoreWarnings();
00383         if ( isset( $unser['version'] ) && $unser['version'] == self::SVG_METADATA_VERSION ) {
00384             return $unser;
00385         } else {
00386             return false;
00387         }
00388     }
00389 
00390     function getMetadataType( $image ) {
00391         return 'parsed-svg';
00392     }
00393 
00394     function isMetadataValid( $image, $metadata ) {
00395         $meta = $this->unpackMetadata( $metadata );
00396         if ( $meta === false ) {
00397             return self::METADATA_BAD;
00398         }
00399         if ( !isset( $meta['originalWidth'] ) ) {
00400             // Old but compatible
00401             return self::METADATA_COMPATIBLE;
00402         }
00403 
00404         return self::METADATA_GOOD;
00405     }
00406 
00407     protected function visibleMetadataFields() {
00408         $fields = array( 'objectname', 'imagedescription' );
00409 
00410         return $fields;
00411     }
00412 
00417     function formatMetadata( $file ) {
00418         $result = array(
00419             'visible' => array(),
00420             'collapsed' => array()
00421         );
00422         $metadata = $file->getMetadata();
00423         if ( !$metadata ) {
00424             return false;
00425         }
00426         $metadata = $this->unpackMetadata( $metadata );
00427         if ( !$metadata || isset( $metadata['error'] ) ) {
00428             return false;
00429         }
00430 
00431         /* @todo Add a formatter
00432         $format = new FormatSVG( $metadata );
00433         $formatted = $format->getFormattedData();
00434         */
00435 
00436         // Sort fields into visible and collapsed
00437         $visibleFields = $this->visibleMetadataFields();
00438 
00439         $showMeta = false;
00440         foreach ( $metadata as $name => $value ) {
00441             $tag = strtolower( $name );
00442             if ( isset( self::$metaConversion[$tag] ) ) {
00443                 $tag = strtolower( self::$metaConversion[$tag] );
00444             } else {
00445                 // Do not output other metadata not in list
00446                 continue;
00447             }
00448             $showMeta = true;
00449             self::addMeta( $result,
00450                 in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
00451                 'exif',
00452                 $tag,
00453                 $value
00454             );
00455         }
00456 
00457         return $showMeta ? $result : false;
00458     }
00459 
00465     function validateParam( $name, $value ) {
00466         if ( in_array( $name, array( 'width', 'height' ) ) ) {
00467             // Reject negative heights, widths
00468             return ( $value > 0 );
00469         } elseif ( $name == 'lang' ) {
00470             // Validate $code
00471             if ( $value === '' || !Language::isValidBuiltinCode( $value ) ) {
00472                 wfDebug( "Invalid user language code\n" );
00473 
00474                 return false;
00475             }
00476 
00477             return true;
00478         }
00479 
00480         // Only lang, width and height are acceptable keys
00481         return false;
00482     }
00483 
00488     function makeParamString( $params ) {
00489         $lang = '';
00490         if ( isset( $params['lang'] ) && $params['lang'] !== 'en' ) {
00491             $params['lang'] = mb_strtolower( $params['lang'] );
00492             $lang = "lang{$params['lang']}-";
00493         }
00494         if ( !isset( $params['width'] ) ) {
00495             return false;
00496         }
00497 
00498         return "$lang{$params['width']}px";
00499     }
00500 
00501     function parseParamString( $str ) {
00502         $m = false;
00503         if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/', $str, $m ) ) {
00504             return array( 'width' => array_pop( $m ), 'lang' => $m[1] );
00505         } elseif ( preg_match( '/^(\d+)px$/', $str, $m ) ) {
00506             return array( 'width' => $m[1], 'lang' => 'en' );
00507         } else {
00508             return false;
00509         }
00510     }
00511 
00512     function getParamMap() {
00513         return array( 'img_lang' => 'lang', 'img_width' => 'width' );
00514     }
00515 
00520     function getScriptParams( $params ) {
00521         $scriptParams = array( 'width' => $params['width'] );
00522         if ( isset( $params['lang'] ) ) {
00523             $scriptParams['lang'] = $params['lang'];
00524         }
00525 
00526         return $scriptParams;
00527     }
00528 
00529     public function getCommonMetaArray( File $file ) {
00530         $metadata = $file->getMetadata();
00531         if ( !$metadata ) {
00532             return array();
00533         }
00534         $metadata = $this->unpackMetadata( $metadata );
00535         if ( !$metadata || isset( $metadata['error'] ) ) {
00536             return array();
00537         }
00538         $stdMetadata = array();
00539         foreach ( $metadata as $name => $value ) {
00540             $tag = strtolower( $name );
00541             if ( $tag === 'originalwidth' || $tag === 'originalheight' ) {
00542                 // Skip these. In the exif metadata stuff, it is assumed these
00543                 // are measured in px, which is not the case here.
00544                 continue;
00545             }
00546             if ( isset( self::$metaConversion[$tag] ) ) {
00547                 $tag = self::$metaConversion[$tag];
00548                 $stdMetadata[$tag] = $value;
00549             }
00550         }
00551 
00552         return $stdMetadata;
00553     }
00554 }