[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

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

   1  <?php
   2  /**
   3   * Generic handler for bitmap 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   * Generic handler for bitmap images
  26   *
  27   * @ingroup Media
  28   */
  29  class BitmapHandler extends TransformationalImageHandler {
  30  
  31      /**
  32       * Returns which scaler type should be used. Creates parent directories
  33       * for $dstPath and returns 'client' on error
  34       *
  35       * @param string $dstPath
  36       * @param bool $checkDstPath
  37       * @return string|Callable One of client, im, custom, gd, imext or an array( object, method )
  38       */
  39  	protected function getScalerType( $dstPath, $checkDstPath = true ) {
  40          global $wgUseImageResize, $wgUseImageMagick, $wgCustomConvertCommand;
  41  
  42          if ( !$dstPath && $checkDstPath ) {
  43              # No output path available, client side scaling only
  44              $scaler = 'client';
  45          } elseif ( !$wgUseImageResize ) {
  46              $scaler = 'client';
  47          } elseif ( $wgUseImageMagick ) {
  48              $scaler = 'im';
  49          } elseif ( $wgCustomConvertCommand ) {
  50              $scaler = 'custom';
  51          } elseif ( function_exists( 'imagecreatetruecolor' ) ) {
  52              $scaler = 'gd';
  53          } elseif ( class_exists( 'Imagick' ) ) {
  54              $scaler = 'imext';
  55          } else {
  56              $scaler = 'client';
  57          }
  58  
  59          return $scaler;
  60      }
  61  
  62      /**
  63       * Transform an image using ImageMagick
  64       *
  65       * @param File $image File associated with this thumbnail
  66       * @param array $params Array with scaler params
  67       *
  68       * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
  69       */
  70  	protected function transformImageMagick( $image, $params ) {
  71          # use ImageMagick
  72          global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea,
  73              $wgImageMagickTempDir, $wgImageMagickConvertCommand;
  74  
  75          $quality = array();
  76          $sharpen = array();
  77          $scene = false;
  78          $animation_pre = array();
  79          $animation_post = array();
  80          $decoderHint = array();
  81          if ( $params['mimeType'] == 'image/jpeg' ) {
  82              $qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null;
  83              $quality = array( '-quality', $qualityVal ?: '80' ); // 80%
  84              # Sharpening, see bug 6193
  85              if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
  86                  / ( $params['srcWidth'] + $params['srcHeight'] )
  87                  < $wgSharpenReductionThreshold
  88              ) {
  89                  $sharpen = array( '-sharpen', $wgSharpenParameter );
  90              }
  91              if ( version_compare( $this->getMagickVersion(), "6.5.6" ) >= 0 ) {
  92                  // JPEG decoder hint to reduce memory, available since IM 6.5.6-2
  93                  $decoderHint = array( '-define', "jpeg:size={$params['physicalDimensions']}" );
  94              }
  95          } elseif ( $params['mimeType'] == 'image/png' ) {
  96              $quality = array( '-quality', '95' ); // zlib 9, adaptive filtering
  97  
  98          } elseif ( $params['mimeType'] == 'image/gif' ) {
  99              if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
 100                  // Extract initial frame only; we're so big it'll
 101                  // be a total drag. :P
 102                  $scene = 0;
 103              } elseif ( $this->isAnimatedImage( $image ) ) {
 104                  // Coalesce is needed to scale animated GIFs properly (bug 1017).
 105                  $animation_pre = array( '-coalesce' );
 106                  // We optimize the output, but -optimize is broken,
 107                  // use optimizeTransparency instead (bug 11822)
 108                  if ( version_compare( $this->getMagickVersion(), "6.3.5" ) >= 0 ) {
 109                      $animation_post = array( '-fuzz', '5%', '-layers', 'optimizeTransparency' );
 110                  }
 111              }
 112          } elseif ( $params['mimeType'] == 'image/x-xcf' ) {
 113              // Before merging layers, we need to set the background
 114              // to be transparent to preserve alpha, as -layers merge
 115              // merges all layers on to a canvas filled with the
 116              // background colour. After merging we reset the background
 117              // to be white for the default background colour setting
 118              // in the PNG image (which is used in old IE)
 119              $animation_pre = array(
 120                  '-background', 'transparent',
 121                  '-layers', 'merge',
 122                  '-background', 'white',
 123              );
 124              wfSuppressWarnings();
 125              $xcfMeta = unserialize( $image->getMetadata() );
 126              wfRestoreWarnings();
 127              if ( $xcfMeta
 128                  && isset( $xcfMeta['colorType'] )
 129                  && $xcfMeta['colorType'] === 'greyscale-alpha'
 130                  && version_compare( $this->getMagickVersion(), "6.8.9-3" ) < 0
 131              ) {
 132                  // bug 66323 - Greyscale images not rendered properly.
 133                  // So only take the "red" channel.
 134                  $channelOnly = array( '-channel', 'R', '-separate' );
 135                  $animation_pre = array_merge( $animation_pre, $channelOnly );
 136              }
 137          }
 138  
 139          // Use one thread only, to avoid deadlock bugs on OOM
 140          $env = array( 'OMP_NUM_THREADS' => 1 );
 141          if ( strval( $wgImageMagickTempDir ) !== '' ) {
 142              $env['MAGICK_TMPDIR'] = $wgImageMagickTempDir;
 143          }
 144  
 145          $rotation = $this->getRotation( $image );
 146          list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
 147  
 148          $cmd = call_user_func_array( 'wfEscapeShellArg', array_merge(
 149              array( $wgImageMagickConvertCommand ),
 150              $quality,
 151              // Specify white background color, will be used for transparent images
 152              // in Internet Explorer/Windows instead of default black.
 153              array( '-background', 'white' ),
 154              $decoderHint,
 155              array( $this->escapeMagickInput( $params['srcPath'], $scene ) ),
 156              $animation_pre,
 157              // For the -thumbnail option a "!" is needed to force exact size,
 158              // or ImageMagick may decide your ratio is wrong and slice off
 159              // a pixel.
 160              array( '-thumbnail', "{$width}x{$height}!" ),
 161              // Add the source url as a comment to the thumb, but don't add the flag if there's no comment
 162              ( $params['comment'] !== ''
 163                  ? array( '-set', 'comment', $this->escapeMagickProperty( $params['comment'] ) )
 164                  : array() ),
 165              array( '-depth', 8 ),
 166              $sharpen,
 167              array( '-rotate', "-$rotation" ),
 168              $animation_post,
 169              array( $this->escapeMagickOutput( $params['dstPath'] ) ) ) );
 170  
 171          wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
 172          wfProfileIn( 'convert' );
 173          $retval = 0;
 174          $err = wfShellExecWithStderr( $cmd, $retval, $env );
 175          wfProfileOut( 'convert' );
 176  
 177          if ( $retval !== 0 ) {
 178              $this->logErrorForExternalProcess( $retval, $err, $cmd );
 179  
 180              return $this->getMediaTransformError( $params, "$err\nError code: $retval" );
 181          }
 182  
 183          return false; # No error
 184      }
 185  
 186      /**
 187       * Transform an image using the Imagick PHP extension
 188       *
 189       * @param File $image File associated with this thumbnail
 190       * @param array $params Array with scaler params
 191       *
 192       * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
 193       */
 194  	protected function transformImageMagickExt( $image, $params ) {
 195          global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea;
 196  
 197          try {
 198              $im = new Imagick();
 199              $im->readImage( $params['srcPath'] );
 200  
 201              if ( $params['mimeType'] == 'image/jpeg' ) {
 202                  // Sharpening, see bug 6193
 203                  if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
 204                      / ( $params['srcWidth'] + $params['srcHeight'] )
 205                      < $wgSharpenReductionThreshold
 206                  ) {
 207                      // Hack, since $wgSharpenParamater is written specifically for the command line convert
 208                      list( $radius, $sigma ) = explode( 'x', $wgSharpenParameter );
 209                      $im->sharpenImage( $radius, $sigma );
 210                  }
 211                  $qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null;
 212                  $im->setCompressionQuality( $qualityVal ?: 80 );
 213              } elseif ( $params['mimeType'] == 'image/png' ) {
 214                  $im->setCompressionQuality( 95 );
 215              } elseif ( $params['mimeType'] == 'image/gif' ) {
 216                  if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
 217                      // Extract initial frame only; we're so big it'll
 218                      // be a total drag. :P
 219                      $im->setImageScene( 0 );
 220                  } elseif ( $this->isAnimatedImage( $image ) ) {
 221                      // Coalesce is needed to scale animated GIFs properly (bug 1017).
 222                      $im = $im->coalesceImages();
 223                  }
 224              }
 225  
 226              $rotation = $this->getRotation( $image );
 227              list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
 228  
 229              $im->setImageBackgroundColor( new ImagickPixel( 'white' ) );
 230  
 231              // Call Imagick::thumbnailImage on each frame
 232              foreach ( $im as $i => $frame ) {
 233                  if ( !$frame->thumbnailImage( $width, $height, /* fit */ false ) ) {
 234                      return $this->getMediaTransformError( $params, "Error scaling frame $i" );
 235                  }
 236              }
 237              $im->setImageDepth( 8 );
 238  
 239              if ( $rotation ) {
 240                  if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
 241                      return $this->getMediaTransformError( $params, "Error rotating $rotation degrees" );
 242                  }
 243              }
 244  
 245              if ( $this->isAnimatedImage( $image ) ) {
 246                  wfDebug( __METHOD__ . ": Writing animated thumbnail\n" );
 247                  // This is broken somehow... can't find out how to fix it
 248                  $result = $im->writeImages( $params['dstPath'], true );
 249              } else {
 250                  $result = $im->writeImage( $params['dstPath'] );
 251              }
 252              if ( !$result ) {
 253                  return $this->getMediaTransformError( $params,
 254                      "Unable to write thumbnail to {$params['dstPath']}" );
 255              }
 256          } catch ( ImagickException $e ) {
 257              return $this->getMediaTransformError( $params, $e->getMessage() );
 258          }
 259  
 260          return false;
 261      }
 262  
 263      /**
 264       * Transform an image using a custom command
 265       *
 266       * @param File $image File associated with this thumbnail
 267       * @param array $params Array with scaler params
 268       *
 269       * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
 270       */
 271  	protected function transformCustom( $image, $params ) {
 272          # Use a custom convert command
 273          global $wgCustomConvertCommand;
 274  
 275          # Variables: %s %d %w %h
 276          $src = wfEscapeShellArg( $params['srcPath'] );
 277          $dst = wfEscapeShellArg( $params['dstPath'] );
 278          $cmd = $wgCustomConvertCommand;
 279          $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames
 280          $cmd = str_replace( '%h', wfEscapeShellArg( $params['physicalHeight'] ),
 281              str_replace( '%w', wfEscapeShellArg( $params['physicalWidth'] ), $cmd ) ); # Size
 282          wfDebug( __METHOD__ . ": Running custom convert command $cmd\n" );
 283          wfProfileIn( 'convert' );
 284          $retval = 0;
 285          $err = wfShellExecWithStderr( $cmd, $retval );
 286          wfProfileOut( 'convert' );
 287  
 288          if ( $retval !== 0 ) {
 289              $this->logErrorForExternalProcess( $retval, $err, $cmd );
 290  
 291              return $this->getMediaTransformError( $params, $err );
 292          }
 293  
 294          return false; # No error
 295      }
 296  
 297      /**
 298       * Transform an image using the built in GD library
 299       *
 300       * @param File $image File associated with this thumbnail
 301       * @param array $params Array with scaler params
 302       *
 303       * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
 304       */
 305  	protected function transformGd( $image, $params ) {
 306          # Use PHP's builtin GD library functions.
 307          #
 308          # First find out what kind of file this is, and select the correct
 309          # input routine for this.
 310  
 311          $typemap = array(
 312              'image/gif' => array( 'imagecreatefromgif', 'palette', false, 'imagegif' ),
 313              'image/jpeg' => array( 'imagecreatefromjpeg', 'truecolor', true,
 314                  array( __CLASS__, 'imageJpegWrapper' ) ),
 315              'image/png' => array( 'imagecreatefrompng', 'bits', false, 'imagepng' ),
 316              'image/vnd.wap.wbmp' => array( 'imagecreatefromwbmp', 'palette', false, 'imagewbmp' ),
 317              'image/xbm' => array( 'imagecreatefromxbm', 'palette', false, 'imagexbm' ),
 318          );
 319  
 320          if ( !isset( $typemap[$params['mimeType']] ) ) {
 321              $err = 'Image type not supported';
 322              wfDebug( "$err\n" );
 323              $errMsg = wfMessage( 'thumbnail_image-type' )->text();
 324  
 325              return $this->getMediaTransformError( $params, $errMsg );
 326          }
 327          list( $loader, $colorStyle, $useQuality, $saveType ) = $typemap[$params['mimeType']];
 328  
 329          if ( !function_exists( $loader ) ) {
 330              $err = "Incomplete GD library configuration: missing function $loader";
 331              wfDebug( "$err\n" );
 332              $errMsg = wfMessage( 'thumbnail_gd-library', $loader )->text();
 333  
 334              return $this->getMediaTransformError( $params, $errMsg );
 335          }
 336  
 337          if ( !file_exists( $params['srcPath'] ) ) {
 338              $err = "File seems to be missing: {$params['srcPath']}";
 339              wfDebug( "$err\n" );
 340              $errMsg = wfMessage( 'thumbnail_image-missing', $params['srcPath'] )->text();
 341  
 342              return $this->getMediaTransformError( $params, $errMsg );
 343          }
 344  
 345          $src_image = call_user_func( $loader, $params['srcPath'] );
 346  
 347          $rotation = function_exists( 'imagerotate' ) ? $this->getRotation( $image ) : 0;
 348          list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
 349          $dst_image = imagecreatetruecolor( $width, $height );
 350  
 351          // Initialise the destination image to transparent instead of
 352          // the default solid black, to support PNG and GIF transparency nicely
 353          $background = imagecolorallocate( $dst_image, 0, 0, 0 );
 354          imagecolortransparent( $dst_image, $background );
 355          imagealphablending( $dst_image, false );
 356  
 357          if ( $colorStyle == 'palette' ) {
 358              // Don't resample for paletted GIF images.
 359              // It may just uglify them, and completely breaks transparency.
 360              imagecopyresized( $dst_image, $src_image,
 361                  0, 0, 0, 0,
 362                  $width, $height,
 363                  imagesx( $src_image ), imagesy( $src_image ) );
 364          } else {
 365              imagecopyresampled( $dst_image, $src_image,
 366                  0, 0, 0, 0,
 367                  $width, $height,
 368                  imagesx( $src_image ), imagesy( $src_image ) );
 369          }
 370  
 371          if ( $rotation % 360 != 0 && $rotation % 90 == 0 ) {
 372              $rot_image = imagerotate( $dst_image, $rotation, 0 );
 373              imagedestroy( $dst_image );
 374              $dst_image = $rot_image;
 375          }
 376  
 377          imagesavealpha( $dst_image, true );
 378  
 379          $funcParams = array( $dst_image, $params['dstPath'] );
 380          if ( $useQuality && isset( $params['quality'] ) ) {
 381              $funcParams[] = $params['quality'];
 382          }
 383          call_user_func_array( $saveType, $funcParams );
 384  
 385          imagedestroy( $dst_image );
 386          imagedestroy( $src_image );
 387  
 388          return false; # No error
 389      }
 390  
 391      /**
 392       * Callback for transformGd when transforming jpeg images.
 393       */
 394      // FIXME: transformImageMagick() & transformImageMagickExt() uses JPEG quality 80, here it's 95?
 395  	static function imageJpegWrapper( $dst_image, $thumbPath, $quality = 95 ) {
 396          imageinterlace( $dst_image );
 397          imagejpeg( $dst_image, $thumbPath, $quality );
 398      }
 399  
 400      /**
 401       * Returns whether the current scaler supports rotation (im and gd do)
 402       *
 403       * @return bool
 404       */
 405  	public function canRotate() {
 406          $scaler = $this->getScalerType( null, false );
 407          switch ( $scaler ) {
 408              case 'im':
 409                  # ImageMagick supports autorotation
 410                  return true;
 411              case 'imext':
 412                  # Imagick::rotateImage
 413                  return true;
 414              case 'gd':
 415                  # GD's imagerotate function is used to rotate images, but not
 416                  # all precompiled PHP versions have that function
 417                  return function_exists( 'imagerotate' );
 418              default:
 419                  # Other scalers don't support rotation
 420                  return false;
 421          }
 422      }
 423  
 424      /**
 425       * @see $wgEnableAutoRotation
 426       * @return bool Whether auto rotation is enabled
 427       */
 428  	public function autoRotateEnabled() {
 429          global $wgEnableAutoRotation;
 430  
 431          if ( $wgEnableAutoRotation === null ) {
 432              // Only enable auto-rotation when we actually can
 433              return $this->canRotate();
 434          }
 435  
 436          return $wgEnableAutoRotation;
 437      }
 438  
 439      /**
 440       * @param File $file
 441       * @param array $params Rotate parameters.
 442       *   'rotation' clockwise rotation in degrees, allowed are multiples of 90
 443       * @since 1.21
 444       * @return bool
 445       */
 446  	public function rotate( $file, $params ) {
 447          global $wgImageMagickConvertCommand;
 448  
 449          $rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
 450          $scene = false;
 451  
 452          $scaler = $this->getScalerType( null, false );
 453          switch ( $scaler ) {
 454              case 'im':
 455                  $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . " " .
 456                      wfEscapeShellArg( $this->escapeMagickInput( $params['srcPath'], $scene ) ) .
 457                      " -rotate " . wfEscapeShellArg( "-$rotation" ) . " " .
 458                      wfEscapeShellArg( $this->escapeMagickOutput( $params['dstPath'] ) );
 459                  wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
 460                  wfProfileIn( 'convert' );
 461                  $retval = 0;
 462                  $err = wfShellExecWithStderr( $cmd, $retval );
 463                  wfProfileOut( 'convert' );
 464                  if ( $retval !== 0 ) {
 465                      $this->logErrorForExternalProcess( $retval, $err, $cmd );
 466  
 467                      return new MediaTransformError( 'thumbnail_error', 0, 0, $err );
 468                  }
 469  
 470                  return false;
 471              case 'imext':
 472                  $im = new Imagick();
 473                  $im->readImage( $params['srcPath'] );
 474                  if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
 475                      return new MediaTransformError( 'thumbnail_error', 0, 0,
 476                          "Error rotating $rotation degrees" );
 477                  }
 478                  $result = $im->writeImage( $params['dstPath'] );
 479                  if ( !$result ) {
 480                      return new MediaTransformError( 'thumbnail_error', 0, 0,
 481                          "Unable to write image to {$params['dstPath']}" );
 482                  }
 483  
 484                  return false;
 485              default:
 486                  return new MediaTransformError( 'thumbnail_error', 0, 0,
 487                      "$scaler rotation not implemented" );
 488          }
 489      }
 490  }


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