[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/media/ -> SVG.php (source)

   1  <?php
   2  /**
   3   * Handler for SVG images.
   4   *
   5   * This program is free software; you can redistribute it and/or modify
   6   * it under the terms of the GNU General Public License as published by
   7   * the Free Software Foundation; either version 2 of the License, or
   8   * (at your option) any later version.
   9   *
  10   * This program is distributed in the hope that it will be useful,
  11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13   * GNU General Public License for more details.
  14   *
  15   * You should have received a copy of the GNU General Public License along
  16   * with this program; if not, write to the Free Software Foundation, Inc.,
  17   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18   * http://www.gnu.org/copyleft/gpl.html
  19   *
  20   * @file
  21   * @ingroup Media
  22   */
  23  
  24  /**
  25   * Handler for SVG images.
  26   *
  27   * @ingroup Media
  28   */
  29  class SvgHandler extends ImageHandler {
  30      const SVG_METADATA_VERSION = 2;
  31  
  32      /** @var array A list of metadata tags that can be converted
  33       *  to the commonly used exif tags. This allows messages
  34       *  to be reused, and consistent tag names for {{#formatmetadata:..}}
  35       */
  36      private static $metaConversion = array(
  37          'originalwidth' => 'ImageWidth',
  38          'originalheight' => 'ImageLength',
  39          'description' => 'ImageDescription',
  40          'title' => 'ObjectName',
  41      );
  42  
  43  	function isEnabled() {
  44          global $wgSVGConverters, $wgSVGConverter;
  45          if ( !isset( $wgSVGConverters[$wgSVGConverter] ) ) {
  46              wfDebug( "\$wgSVGConverter is invalid, disabling SVG rendering.\n" );
  47  
  48              return false;
  49          } else {
  50              return true;
  51          }
  52      }
  53  
  54  	function mustRender( $file ) {
  55          return true;
  56      }
  57  
  58  	function isVectorized( $file ) {
  59          return true;
  60      }
  61  
  62      /**
  63       * @param File $file
  64       * @return bool
  65       */
  66  	function isAnimatedImage( $file ) {
  67          # @todo Detect animated SVGs
  68          $metadata = $file->getMetadata();
  69          if ( $metadata ) {
  70              $metadata = $this->unpackMetadata( $metadata );
  71              if ( isset( $metadata['animated'] ) ) {
  72                  return $metadata['animated'];
  73              }
  74          }
  75  
  76          return false;
  77      }
  78  
  79      /**
  80       * Which languages (systemLanguage attribute) is supported.
  81       *
  82       * @note This list is not guaranteed to be exhaustive.
  83       * To avoid OOM errors, we only look at first bit of a file.
  84       * Thus all languages on this list are present in the file,
  85       * but its possible for the file to have a language not on
  86       * this list.
  87       *
  88       * @param File $file
  89       * @return array Array of language codes, or empty if no language switching supported.
  90       */
  91  	public function getAvailableLanguages( File $file ) {
  92          $metadata = $file->getMetadata();
  93          $langList = array();
  94          if ( $metadata ) {
  95              $metadata = $this->unpackMetadata( $metadata );
  96              if ( isset( $metadata['translations'] ) ) {
  97                  foreach ( $metadata['translations'] as $lang => $langType ) {
  98                      if ( $langType === SvgReader::LANG_FULL_MATCH ) {
  99                          $langList[] = $lang;
 100                      }
 101                  }
 102              }
 103          }
 104          return $langList;
 105      }
 106  
 107      /**
 108       * What language to render file in if none selected.
 109       *
 110       * @param File $file
 111       * @return string Language code.
 112       */
 113  	public function getDefaultRenderLanguage( File $file ) {
 114          return 'en';
 115      }
 116  
 117      /**
 118       * We do not support making animated svg thumbnails
 119       * @param File $file
 120       * @return bool
 121       */
 122  	function canAnimateThumbnail( $file ) {
 123          return false;
 124      }
 125  
 126      /**
 127       * @param File $image
 128       * @param array $params
 129       * @return bool
 130       */
 131  	function normaliseParams( $image, &$params ) {
 132          global $wgSVGMaxSize;
 133          if ( !parent::normaliseParams( $image, $params ) ) {
 134              return false;
 135          }
 136          # Don't make an image bigger than wgMaxSVGSize on the smaller side
 137          if ( $params['physicalWidth'] <= $params['physicalHeight'] ) {
 138              if ( $params['physicalWidth'] > $wgSVGMaxSize ) {
 139                  $srcWidth = $image->getWidth( $params['page'] );
 140                  $srcHeight = $image->getHeight( $params['page'] );
 141                  $params['physicalWidth'] = $wgSVGMaxSize;
 142                  $params['physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight, $wgSVGMaxSize );
 143              }
 144          } else {
 145              if ( $params['physicalHeight'] > $wgSVGMaxSize ) {
 146                  $srcWidth = $image->getWidth( $params['page'] );
 147                  $srcHeight = $image->getHeight( $params['page'] );
 148                  $params['physicalWidth'] = File::scaleHeight( $srcHeight, $srcWidth, $wgSVGMaxSize );
 149                  $params['physicalHeight'] = $wgSVGMaxSize;
 150              }
 151          }
 152  
 153          return true;
 154      }
 155  
 156      /**
 157       * @param File $image
 158       * @param string $dstPath
 159       * @param string $dstUrl
 160       * @param array $params
 161       * @param int $flags
 162       * @return bool|MediaTransformError|ThumbnailImage|TransformParameterError
 163       */
 164  	function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
 165          if ( !$this->normaliseParams( $image, $params ) ) {
 166              return new TransformParameterError( $params );
 167          }
 168          $clientWidth = $params['width'];
 169          $clientHeight = $params['height'];
 170          $physicalWidth = $params['physicalWidth'];
 171          $physicalHeight = $params['physicalHeight'];
 172          $lang = isset( $params['lang'] ) ? $params['lang'] : $this->getDefaultRenderLanguage( $image );
 173  
 174          if ( $flags & self::TRANSFORM_LATER ) {
 175              return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
 176          }
 177  
 178          $metadata = $this->unpackMetadata( $image->getMetadata() );
 179          if ( isset( $metadata['error'] ) ) { // sanity check
 180              $err = wfMessage( 'svg-long-error', $metadata['error']['message'] )->text();
 181  
 182              return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
 183          }
 184  
 185          if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
 186              return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight,
 187                  wfMessage( 'thumbnail_dest_directory' )->text() );
 188          }
 189  
 190          $srcPath = $image->getLocalRefPath();
 191          if ( $srcPath === false ) { // Failed to get local copy
 192              wfDebugLog( 'thumbnail',
 193                  sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
 194                      wfHostname(), $image->getName() ) );
 195  
 196              return new MediaTransformError( 'thumbnail_error',
 197                  $params['width'], $params['height'],
 198                  wfMessage( 'filemissing' )->text()
 199              );
 200          }
 201  
 202          // Make a temp dir with a symlink to the local copy in it.
 203          // This plays well with rsvg-convert policy for external entities.
 204          // https://git.gnome.org/browse/librsvg/commit/?id=f01aded72c38f0e18bc7ff67dee800e380251c8e
 205          $tmpDir = wfTempDir() . '/svg_' . wfRandomString( 24 );
 206          $lnPath = "$tmpDir/" . basename( $srcPath );
 207          $ok = mkdir( $tmpDir, 0771 ) && symlink( $srcPath, $lnPath );
 208          $cleaner = new ScopedCallback( function () use ( $tmpDir, $lnPath ) {
 209              wfSuppressWarnings();
 210              unlink( $lnPath );
 211              rmdir( $tmpDir );
 212              wfRestoreWarnings();
 213          } );
 214          if ( !$ok ) {
 215              wfDebugLog( 'thumbnail',
 216                  sprintf( 'Thumbnail failed on %s: could not link %s to %s',
 217                      wfHostname(), $lnPath, $srcPath ) );
 218              return new MediaTransformError( 'thumbnail_error',
 219                  $params['width'], $params['height'],
 220                  wfMessage( 'thumbnail-temp-create' )->text()
 221              );
 222          }
 223  
 224          $status = $this->rasterize( $lnPath, $dstPath, $physicalWidth, $physicalHeight, $lang );
 225          if ( $status === true ) {
 226              return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
 227          } else {
 228              return $status; // MediaTransformError
 229          }
 230      }
 231  
 232      /**
 233       * Transform an SVG file to PNG
 234       * This function can be called outside of thumbnail contexts
 235       * @param string $srcPath
 236       * @param string $dstPath
 237       * @param string $width
 238       * @param string $height
 239       * @param bool|string $lang Language code of the language to render the SVG in
 240       * @throws MWException
 241       * @return bool|MediaTransformError
 242       */
 243  	public function rasterize( $srcPath, $dstPath, $width, $height, $lang = false ) {
 244          global $wgSVGConverters, $wgSVGConverter, $wgSVGConverterPath;
 245          $err = false;
 246          $retval = '';
 247          if ( isset( $wgSVGConverters[$wgSVGConverter] ) ) {
 248              if ( is_array( $wgSVGConverters[$wgSVGConverter] ) ) {
 249                  // This is a PHP callable
 250                  $func = $wgSVGConverters[$wgSVGConverter][0];
 251                  $args = array_merge( array( $srcPath, $dstPath, $width, $height, $lang ),
 252                      array_slice( $wgSVGConverters[$wgSVGConverter], 1 ) );
 253                  if ( !is_callable( $func ) ) {
 254                      throw new MWException( "$func is not callable" );
 255                  }
 256                  $err = call_user_func_array( $func, $args );
 257                  $retval = (bool)$err;
 258              } else {
 259                  // External command
 260                  $cmd = str_replace(
 261                      array( '$path/', '$width', '$height', '$input', '$output' ),
 262                      array( $wgSVGConverterPath ? wfEscapeShellArg( "$wgSVGConverterPath/" ) : "",
 263                          intval( $width ),
 264                          intval( $height ),
 265                          wfEscapeShellArg( $srcPath ),
 266                          wfEscapeShellArg( $dstPath ) ),
 267                      $wgSVGConverters[$wgSVGConverter]
 268                  );
 269  
 270                  $env = array();
 271                  if ( $lang !== false ) {
 272                      $env['LANG'] = $lang;
 273                  }
 274  
 275                  wfProfileIn( 'rsvg' );
 276                  wfDebug( __METHOD__ . ": $cmd\n" );
 277                  $err = wfShellExecWithStderr( $cmd, $retval, $env );
 278                  wfProfileOut( 'rsvg' );
 279              }
 280          }
 281          $removed = $this->removeBadFile( $dstPath, $retval );
 282          if ( $retval != 0 || $removed ) {
 283              $this->logErrorForExternalProcess( $retval, $err, $cmd );
 284              return new MediaTransformError( 'thumbnail_error', $width, $height, $err );
 285          }
 286  
 287          return true;
 288      }
 289  
 290  	public static function rasterizeImagickExt( $srcPath, $dstPath, $width, $height ) {
 291          $im = new Imagick( $srcPath );
 292          $im->setImageFormat( 'png' );
 293          $im->setBackgroundColor( 'transparent' );
 294          $im->setImageDepth( 8 );
 295  
 296          if ( !$im->thumbnailImage( intval( $width ), intval( $height ), /* fit */ false ) ) {
 297              return 'Could not resize image';
 298          }
 299          if ( !$im->writeImage( $dstPath ) ) {
 300              return "Could not write to $dstPath";
 301          }
 302      }
 303  
 304      /**
 305       * @param File $file
 306       * @param string $path Unused
 307       * @param bool|array $metadata
 308       * @return array
 309       */
 310  	function getImageSize( $file, $path, $metadata = false ) {
 311          if ( $metadata === false ) {
 312              $metadata = $file->getMetaData();
 313          }
 314          $metadata = $this->unpackMetaData( $metadata );
 315  
 316          if ( isset( $metadata['width'] ) && isset( $metadata['height'] ) ) {
 317              return array( $metadata['width'], $metadata['height'], 'SVG',
 318                  "width=\"{$metadata['width']}\" height=\"{$metadata['height']}\"" );
 319          } else { // error
 320              return array( 0, 0, 'SVG', "width=\"0\" height=\"0\"" );
 321          }
 322      }
 323  
 324  	function getThumbType( $ext, $mime, $params = null ) {
 325          return array( 'png', 'image/png' );
 326      }
 327  
 328      /**
 329       * Subtitle for the image. Different from the base
 330       * class so it can be denoted that SVG's have
 331       * a "nominal" resolution, and not a fixed one,
 332       * as well as so animation can be denoted.
 333       *
 334       * @param File $file
 335       * @return string
 336       */
 337  	function getLongDesc( $file ) {
 338          global $wgLang;
 339  
 340          $metadata = $this->unpackMetadata( $file->getMetadata() );
 341          if ( isset( $metadata['error'] ) ) {
 342              return wfMessage( 'svg-long-error', $metadata['error']['message'] )->text();
 343          }
 344  
 345          $size = $wgLang->formatSize( $file->getSize() );
 346  
 347          if ( $this->isAnimatedImage( $file ) ) {
 348              $msg = wfMessage( 'svg-long-desc-animated' );
 349          } else {
 350              $msg = wfMessage( 'svg-long-desc' );
 351          }
 352  
 353          $msg->numParams( $file->getWidth(), $file->getHeight() )->params( $size );
 354  
 355          return $msg->parse();
 356      }
 357  
 358      /**
 359       * @param File $file
 360       * @param string $filename
 361       * @return string Serialised metadata
 362       */
 363  	function getMetadata( $file, $filename ) {
 364          $metadata = array( 'version' => self::SVG_METADATA_VERSION );
 365          try {
 366              $metadata += SVGMetadataExtractor::getMetadata( $filename );
 367          } catch ( MWException $e ) { // @todo SVG specific exceptions
 368              // File not found, broken, etc.
 369              $metadata['error'] = array(
 370                  'message' => $e->getMessage(),
 371                  'code' => $e->getCode()
 372              );
 373              wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
 374          }
 375  
 376          return serialize( $metadata );
 377      }
 378  
 379  	function unpackMetadata( $metadata ) {
 380          wfSuppressWarnings();
 381          $unser = unserialize( $metadata );
 382          wfRestoreWarnings();
 383          if ( isset( $unser['version'] ) && $unser['version'] == self::SVG_METADATA_VERSION ) {
 384              return $unser;
 385          } else {
 386              return false;
 387          }
 388      }
 389  
 390  	function getMetadataType( $image ) {
 391          return 'parsed-svg';
 392      }
 393  
 394  	function isMetadataValid( $image, $metadata ) {
 395          $meta = $this->unpackMetadata( $metadata );
 396          if ( $meta === false ) {
 397              return self::METADATA_BAD;
 398          }
 399          if ( !isset( $meta['originalWidth'] ) ) {
 400              // Old but compatible
 401              return self::METADATA_COMPATIBLE;
 402          }
 403  
 404          return self::METADATA_GOOD;
 405      }
 406  
 407  	protected function visibleMetadataFields() {
 408          $fields = array( 'objectname', 'imagedescription' );
 409  
 410          return $fields;
 411      }
 412  
 413      /**
 414       * @param File $file
 415       * @return array|bool
 416       */
 417  	function formatMetadata( $file ) {
 418          $result = array(
 419              'visible' => array(),
 420              'collapsed' => array()
 421          );
 422          $metadata = $file->getMetadata();
 423          if ( !$metadata ) {
 424              return false;
 425          }
 426          $metadata = $this->unpackMetadata( $metadata );
 427          if ( !$metadata || isset( $metadata['error'] ) ) {
 428              return false;
 429          }
 430  
 431          /* @todo Add a formatter
 432          $format = new FormatSVG( $metadata );
 433          $formatted = $format->getFormattedData();
 434          */
 435  
 436          // Sort fields into visible and collapsed
 437          $visibleFields = $this->visibleMetadataFields();
 438  
 439          $showMeta = false;
 440          foreach ( $metadata as $name => $value ) {
 441              $tag = strtolower( $name );
 442              if ( isset( self::$metaConversion[$tag] ) ) {
 443                  $tag = strtolower( self::$metaConversion[$tag] );
 444              } else {
 445                  // Do not output other metadata not in list
 446                  continue;
 447              }
 448              $showMeta = true;
 449              self::addMeta( $result,
 450                  in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
 451                  'exif',
 452                  $tag,
 453                  $value
 454              );
 455          }
 456  
 457          return $showMeta ? $result : false;
 458      }
 459  
 460      /**
 461       * @param string $name Parameter name
 462       * @param mixed $value Parameter value
 463       * @return bool Validity
 464       */
 465  	function validateParam( $name, $value ) {
 466          if ( in_array( $name, array( 'width', 'height' ) ) ) {
 467              // Reject negative heights, widths
 468              return ( $value > 0 );
 469          } elseif ( $name == 'lang' ) {
 470              // Validate $code
 471              if ( $value === '' || !Language::isValidBuiltinCode( $value ) ) {
 472                  wfDebug( "Invalid user language code\n" );
 473  
 474                  return false;
 475              }
 476  
 477              return true;
 478          }
 479  
 480          // Only lang, width and height are acceptable keys
 481          return false;
 482      }
 483  
 484      /**
 485       * @param array $params Name=>value pairs of parameters
 486       * @return string Filename to use
 487       */
 488  	function makeParamString( $params ) {
 489          $lang = '';
 490          if ( isset( $params['lang'] ) && $params['lang'] !== 'en' ) {
 491              $params['lang'] = mb_strtolower( $params['lang'] );
 492              $lang = "lang{$params['lang']}-";
 493          }
 494          if ( !isset( $params['width'] ) ) {
 495              return false;
 496          }
 497  
 498          return "$lang{$params['width']}px";
 499      }
 500  
 501  	function parseParamString( $str ) {
 502          $m = false;
 503          if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/', $str, $m ) ) {
 504              return array( 'width' => array_pop( $m ), 'lang' => $m[1] );
 505          } elseif ( preg_match( '/^(\d+)px$/', $str, $m ) ) {
 506              return array( 'width' => $m[1], 'lang' => 'en' );
 507          } else {
 508              return false;
 509          }
 510      }
 511  
 512  	function getParamMap() {
 513          return array( 'img_lang' => 'lang', 'img_width' => 'width' );
 514      }
 515  
 516      /**
 517       * @param array $params
 518       * @return array
 519       */
 520  	function getScriptParams( $params ) {
 521          $scriptParams = array( 'width' => $params['width'] );
 522          if ( isset( $params['lang'] ) ) {
 523              $scriptParams['lang'] = $params['lang'];
 524          }
 525  
 526          return $scriptParams;
 527      }
 528  
 529  	public function getCommonMetaArray( File $file ) {
 530          $metadata = $file->getMetadata();
 531          if ( !$metadata ) {
 532              return array();
 533          }
 534          $metadata = $this->unpackMetadata( $metadata );
 535          if ( !$metadata || isset( $metadata['error'] ) ) {
 536              return array();
 537          }
 538          $stdMetadata = array();
 539          foreach ( $metadata as $name => $value ) {
 540              $tag = strtolower( $name );
 541              if ( $tag === 'originalwidth' || $tag === 'originalheight' ) {
 542                  // Skip these. In the exif metadata stuff, it is assumed these
 543                  // are measured in px, which is not the case here.
 544                  continue;
 545              }
 546              if ( isset( self::$metaConversion[$tag] ) ) {
 547                  $tag = self::$metaConversion[$tag];
 548                  $stdMetadata[$tag] = $value;
 549              }
 550          }
 551  
 552          return $stdMetadata;
 553      }
 554  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1