[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

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

   1  <?php
   2  /**
   3   * Base class for handlers which require transforming images in a
   4   * similar way as BitmapHandler does.
   5   *
   6   * This was split from BitmapHandler on the basis that some extensions
   7   * might want to work in a similar way to BitmapHandler, but for
   8   * different formats.
   9   *
  10   * This program is free software; you can redistribute it and/or modify
  11   * it under the terms of the GNU General Public License as published by
  12   * the Free Software Foundation; either version 2 of the License, or
  13   * (at your option) any later version.
  14   *
  15   * This program is distributed in the hope that it will be useful,
  16   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18   * GNU General Public License for more details.
  19   *
  20   * You should have received a copy of the GNU General Public License along
  21   * with this program; if not, write to the Free Software Foundation, Inc.,
  22   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  23   * http://www.gnu.org/copyleft/gpl.html
  24   *
  25   * @file
  26   * @ingroup Media
  27   */
  28  
  29  /**
  30   * Handler for images that need to be transformed
  31   *
  32   * @since 1.24
  33   * @ingroup Media
  34   */
  35  abstract class TransformationalImageHandler extends ImageHandler {
  36      /**
  37       * @param File $image
  38       * @param array $params Transform parameters. Entries with the keys 'width'
  39       * and 'height' are the respective screen width and height, while the keys
  40       * 'physicalWidth' and 'physicalHeight' indicate the thumbnail dimensions.
  41       * @return bool
  42       */
  43  	function normaliseParams( $image, &$params ) {
  44          if ( !parent::normaliseParams( $image, $params ) ) {
  45              return false;
  46          }
  47  
  48          # Obtain the source, pre-rotation dimensions
  49          $srcWidth = $image->getWidth( $params['page'] );
  50          $srcHeight = $image->getHeight( $params['page'] );
  51  
  52          # Don't make an image bigger than the source
  53          if ( $params['physicalWidth'] >= $srcWidth ) {
  54              $params['physicalWidth'] = $srcWidth;
  55              $params['physicalHeight'] = $srcHeight;
  56  
  57              # Skip scaling limit checks if no scaling is required
  58              # due to requested size being bigger than source.
  59              if ( !$image->mustRender() ) {
  60                  return true;
  61              }
  62          }
  63  
  64          # Check if the file is smaller than the maximum image area for thumbnailing
  65          # For historical reasons, hook starts with BitmapHandler
  66          $checkImageAreaHookResult = null;
  67          wfRunHooks(
  68              'BitmapHandlerCheckImageArea',
  69              array( $image, &$params, &$checkImageAreaHookResult )
  70          );
  71  
  72          if ( is_null( $checkImageAreaHookResult ) ) {
  73              global $wgMaxImageArea;
  74  
  75              if ( $srcWidth * $srcHeight > $wgMaxImageArea
  76                  && !( $image->getMimeType() == 'image/jpeg'
  77                      && $this->getScalerType( false, false ) == 'im' )
  78              ) {
  79                  # Only ImageMagick can efficiently downsize jpg images without loading
  80                  # the entire file in memory
  81                  return false;
  82              }
  83          } else {
  84              return $checkImageAreaHookResult;
  85          }
  86  
  87          return true;
  88      }
  89  
  90      /**
  91       * Extracts the width/height if the image will be scaled before rotating
  92       *
  93       * This will match the physical size/aspect ratio of the original image
  94       * prior to application of the rotation -- so for a portrait image that's
  95       * stored as raw landscape with 90-degress rotation, the resulting size
  96       * will be wider than it is tall.
  97       *
  98       * @param array $params Parameters as returned by normaliseParams
  99       * @param int $rotation The rotation angle that will be applied
 100       * @return array ($width, $height) array
 101       */
 102  	public function extractPreRotationDimensions( $params, $rotation ) {
 103          if ( $rotation == 90 || $rotation == 270 ) {
 104              # We'll resize before rotation, so swap the dimensions again
 105              $width = $params['physicalHeight'];
 106              $height = $params['physicalWidth'];
 107          } else {
 108              $width = $params['physicalWidth'];
 109              $height = $params['physicalHeight'];
 110          }
 111  
 112          return array( $width, $height );
 113      }
 114  
 115      /**
 116       * Create a thumbnail.
 117       *
 118       * This sets up various parameters, and then calls a helper method
 119       * based on $this->getScalerType in order to scale the image.
 120       *
 121       * @param File $image
 122       * @param string $dstPath
 123       * @param string $dstUrl
 124       * @param array $params
 125       * @param int $flags
 126       * @return MediaTransformError|ThumbnailImage|TransformParameterError
 127       */
 128  	function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
 129          if ( !$this->normaliseParams( $image, $params ) ) {
 130              return new TransformParameterError( $params );
 131          }
 132  
 133          # Create a parameter array to pass to the scaler
 134          $scalerParams = array(
 135              # The size to which the image will be resized
 136              'physicalWidth' => $params['physicalWidth'],
 137              'physicalHeight' => $params['physicalHeight'],
 138              'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}",
 139              # The size of the image on the page
 140              'clientWidth' => $params['width'],
 141              'clientHeight' => $params['height'],
 142              # Comment as will be added to the Exif of the thumbnail
 143              'comment' => isset( $params['descriptionUrl'] )
 144                  ? "File source: {$params['descriptionUrl']}"
 145                  : '',
 146              # Properties of the original image
 147              'srcWidth' => $image->getWidth(),
 148              'srcHeight' => $image->getHeight(),
 149              'mimeType' => $image->getMimeType(),
 150              'dstPath' => $dstPath,
 151              'dstUrl' => $dstUrl,
 152          );
 153  
 154          if ( isset( $params['quality'] ) && $params['quality'] === 'low' ) {
 155              $scalerParams['quality'] = 30;
 156          }
 157  
 158          // For subclasses that might be paged.
 159          if ( $image->isMultipage() && isset( $params['page'] ) ) {
 160              $scalerParams['page'] = intval( $params['page'] );
 161          }
 162  
 163          # Determine scaler type
 164          $scaler = $this->getScalerType( $dstPath );
 165  
 166          if ( is_array( $scaler ) ) {
 167              $scalerName = get_class( $scaler[0] );
 168          } else {
 169              $scalerName = $scaler;
 170          }
 171  
 172          wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} " .
 173              "thumbnail at $dstPath using scaler $scalerName\n" );
 174  
 175          if ( !$image->mustRender() &&
 176              $scalerParams['physicalWidth'] == $scalerParams['srcWidth']
 177              && $scalerParams['physicalHeight'] == $scalerParams['srcHeight']
 178              && !isset( $scalerParams['quality'] )
 179          ) {
 180  
 181              # normaliseParams (or the user) wants us to return the unscaled image
 182              wfDebug( __METHOD__ . ": returning unscaled image\n" );
 183  
 184              return $this->getClientScalingThumbnailImage( $image, $scalerParams );
 185          }
 186  
 187          if ( $scaler == 'client' ) {
 188              # Client-side image scaling, use the source URL
 189              # Using the destination URL in a TRANSFORM_LATER request would be incorrect
 190              return $this->getClientScalingThumbnailImage( $image, $scalerParams );
 191          }
 192  
 193          if ( $flags & self::TRANSFORM_LATER ) {
 194              wfDebug( __METHOD__ . ": Transforming later per flags.\n" );
 195              $newParams = array(
 196                  'width' => $scalerParams['clientWidth'],
 197                  'height' => $scalerParams['clientHeight']
 198              );
 199              if ( isset( $params['quality'] ) ) {
 200                  $newParams['quality'] = $params['quality'];
 201              }
 202              if ( isset( $params['page'] ) && $params['page'] ) {
 203                  $newParams['page'] = $params['page'];
 204              }
 205              return new ThumbnailImage( $image, $dstUrl, false, $newParams );
 206          }
 207  
 208          # Try to make a target path for the thumbnail
 209          if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
 210              wfDebug( __METHOD__ . ": Unable to create thumbnail destination " .
 211                  "directory, falling back to client scaling\n" );
 212  
 213              return $this->getClientScalingThumbnailImage( $image, $scalerParams );
 214          }
 215  
 216          # Transform functions and binaries need a FS source file
 217          $thumbnailSource = $this->getThumbnailSource( $image, $params );
 218  
 219          $scalerParams['srcPath'] = $thumbnailSource['path'];
 220          $scalerParams['srcWidth'] = $thumbnailSource['width'];
 221          $scalerParams['srcHeight'] = $thumbnailSource['height'];
 222  
 223          if ( $scalerParams['srcPath'] === false ) { // Failed to get local copy
 224              wfDebugLog( 'thumbnail',
 225                  sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
 226                      wfHostname(), $image->getName() ) );
 227  
 228              return new MediaTransformError( 'thumbnail_error',
 229                  $scalerParams['clientWidth'], $scalerParams['clientHeight'],
 230                  wfMessage( 'filemissing' )->text()
 231              );
 232          }
 233  
 234          # Try a hook. Called "Bitmap" for historical reasons.
 235          /** @var $mto MediaTransformOutput */
 236          $mto = null;
 237          wfRunHooks( 'BitmapHandlerTransform', array( $this, $image, &$scalerParams, &$mto ) );
 238          if ( !is_null( $mto ) ) {
 239              wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto\n" );
 240              $scaler = 'hookaborted';
 241          }
 242  
 243          // $scaler will return a MediaTransformError on failure, or false on success.
 244          // If the scaler is succesful, it will have created a thumbnail at the destination
 245          // path.
 246          if ( is_array( $scaler ) && is_callable( $scaler ) ) {
 247              // Allow subclasses to specify their own rendering methods.
 248              $err = call_user_func( $scaler, $image, $scalerParams );
 249          } else {
 250              switch ( $scaler ) {
 251                  case 'hookaborted':
 252                      # Handled by the hook above
 253                      $err = $mto->isError() ? $mto : false;
 254                      break;
 255                  case 'im':
 256                      $err = $this->transformImageMagick( $image, $scalerParams );
 257                      break;
 258                  case 'custom':
 259                      $err = $this->transformCustom( $image, $scalerParams );
 260                      break;
 261                  case 'imext':
 262                      $err = $this->transformImageMagickExt( $image, $scalerParams );
 263                      break;
 264                  case 'gd':
 265                  default:
 266                      $err = $this->transformGd( $image, $scalerParams );
 267                      break;
 268              }
 269          }
 270  
 271          # Remove the file if a zero-byte thumbnail was created, or if there was an error
 272          $removed = $this->removeBadFile( $dstPath, (bool)$err );
 273          if ( $err ) {
 274              # transform returned MediaTransforError
 275              return $err;
 276          } elseif ( $removed ) {
 277              # Thumbnail was zero-byte and had to be removed
 278              return new MediaTransformError( 'thumbnail_error',
 279                  $scalerParams['clientWidth'], $scalerParams['clientHeight'],
 280                  wfMessage( 'unknown-error' )->text()
 281              );
 282          } elseif ( $mto ) {
 283              return $mto;
 284          } else {
 285              $newParams = array(
 286                  'width' => $scalerParams['clientWidth'],
 287                  'height' => $scalerParams['clientHeight']
 288              );
 289              if ( isset( $params['quality'] ) ) {
 290                  $newParams['quality'] = $params['quality'];
 291              }
 292              if ( isset( $params['page'] ) && $params['page'] ) {
 293                  $newParams['page'] = $params['page'];
 294              }
 295              return new ThumbnailImage( $image, $dstUrl, $dstPath, $newParams );
 296          }
 297      }
 298  
 299      /**
 300       * Get the source file for the transform
 301       *
 302       * @param $file File
 303       * @param $params Array
 304       * @return Array Array with keys  width, height and path.
 305       */
 306  	protected function getThumbnailSource( $file, $params ) {
 307          return $file->getThumbnailSource( $params );
 308      }
 309  
 310      /**
 311       * Returns what sort of scaler type should be used.
 312       *
 313       * Values can be one of client, im, custom, gd, imext, or an array
 314       * of object, method-name to call that specific method.
 315       *
 316       * If specifying a custom scaler command with array( Obj, method ),
 317       * the method in question should take 2 parameters, a File object,
 318       * and a $scalerParams array with various options (See doTransform
 319       * for what is in $scalerParams). On error it should return a
 320       * MediaTransformError object. On success it should return false,
 321       * and simply make sure the thumbnail file is located at
 322       * $scalerParams['dstPath'].
 323       *
 324       * If there is a problem with the output path, it returns "client"
 325       * to do client side scaling.
 326       *
 327       * @param string $dstPath
 328       * @param bool $checkDstPath Check that $dstPath is valid
 329       * @return string|Callable One of client, im, custom, gd, imext, or a Callable array.
 330       */
 331      abstract protected function getScalerType( $dstPath, $checkDstPath = true );
 332  
 333      /**
 334       * Get a ThumbnailImage that respresents an image that will be scaled
 335       * client side
 336       *
 337       * @param File $image File associated with this thumbnail
 338       * @param array $scalerParams Array with scaler params
 339       * @return ThumbnailImage
 340       *
 341       * @todo FIXME: No rotation support
 342       */
 343  	protected function getClientScalingThumbnailImage( $image, $scalerParams ) {
 344          $params = array(
 345              'width' => $scalerParams['clientWidth'],
 346              'height' => $scalerParams['clientHeight']
 347          );
 348  
 349          return new ThumbnailImage( $image, $image->getURL(), null, $params );
 350      }
 351  
 352      /**
 353       * Transform an image using ImageMagick
 354       *
 355       * This is a stub method. The real method is in BitmapHander.
 356       *
 357       * @param File $image File associated with this thumbnail
 358       * @param array $params Array with scaler params
 359       *
 360       * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
 361       */
 362  	protected function transformImageMagick( $image, $params ) {
 363          return $this->getMediaTransformError( $params, "Unimplemented" );
 364      }
 365  
 366      /**
 367       * Transform an image using the Imagick PHP extension
 368       *
 369       * This is a stub method. The real method is in BitmapHander.
 370       *
 371       * @param File $image File associated with this thumbnail
 372       * @param array $params Array with scaler params
 373       *
 374       * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
 375       */
 376  	protected function transformImageMagickExt( $image, $params ) {
 377          return $this->getMediaTransformError( $params, "Unimplemented" );
 378      }
 379  
 380      /**
 381       * Transform an image using a custom command
 382       *
 383       * This is a stub method. The real method is in BitmapHander.
 384       *
 385       * @param File $image File associated with this thumbnail
 386       * @param array $params Array with scaler params
 387       *
 388       * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
 389       */
 390  	protected function transformCustom( $image, $params ) {
 391          return $this->getMediaTransformError( $params, "Unimplemented" );
 392      }
 393  
 394      /**
 395       * Get a MediaTransformError with error 'thumbnail_error'
 396       *
 397       * @param array $params Parameter array as passed to the transform* functions
 398       * @param string $errMsg Error message
 399       * @return MediaTransformError
 400       */
 401  	public function getMediaTransformError( $params, $errMsg ) {
 402          return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
 403              $params['clientHeight'], $errMsg );
 404      }
 405  
 406      /**
 407       * Transform an image using the built in GD library
 408       *
 409       * This is a stub method. The real method is in BitmapHander.
 410       *
 411       * @param File $image File associated with this thumbnail
 412       * @param array $params Array with scaler params
 413       *
 414       * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
 415       */
 416  	protected function transformGd( $image, $params ) {
 417          return $this->getMediaTransformError( $params, "Unimplemented" );
 418      }
 419  
 420      /**
 421       * Escape a string for ImageMagick's property input (e.g. -set -comment)
 422       * See InterpretImageProperties() in magick/property.c
 423       * @param string $s
 424       * @return string
 425       */
 426  	function escapeMagickProperty( $s ) {
 427          // Double the backslashes
 428          $s = str_replace( '\\', '\\\\', $s );
 429          // Double the percents
 430          $s = str_replace( '%', '%%', $s );
 431          // Escape initial - or @
 432          if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
 433              $s = '\\' . $s;
 434          }
 435  
 436          return $s;
 437      }
 438  
 439      /**
 440       * Escape a string for ImageMagick's input filenames. See ExpandFilenames()
 441       * and GetPathComponent() in magick/utility.c.
 442       *
 443       * This won't work with an initial ~ or @, so input files should be prefixed
 444       * with the directory name.
 445       *
 446       * Glob character unescaping is broken in ImageMagick before 6.6.1-5, but
 447       * it's broken in a way that doesn't involve trying to convert every file
 448       * in a directory, so we're better off escaping and waiting for the bugfix
 449       * to filter down to users.
 450       *
 451       * @param string $path The file path
 452       * @param bool|string $scene The scene specification, or false if there is none
 453       * @throws MWException
 454       * @return string
 455       */
 456  	function escapeMagickInput( $path, $scene = false ) {
 457          # Die on initial metacharacters (caller should prepend path)
 458          $firstChar = substr( $path, 0, 1 );
 459          if ( $firstChar === '~' || $firstChar === '@' ) {
 460              throw new MWException( __METHOD__ . ': cannot escape this path name' );
 461          }
 462  
 463          # Escape glob chars
 464          $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
 465  
 466          return $this->escapeMagickPath( $path, $scene );
 467      }
 468  
 469      /**
 470       * Escape a string for ImageMagick's output filename. See
 471       * InterpretImageFilename() in magick/image.c.
 472       * @param string $path The file path
 473       * @param bool|string $scene The scene specification, or false if there is none
 474       * @return string
 475       */
 476  	function escapeMagickOutput( $path, $scene = false ) {
 477          $path = str_replace( '%', '%%', $path );
 478  
 479          return $this->escapeMagickPath( $path, $scene );
 480      }
 481  
 482      /**
 483       * Armour a string against ImageMagick's GetPathComponent(). This is a
 484       * helper function for escapeMagickInput() and escapeMagickOutput().
 485       *
 486       * @param string $path The file path
 487       * @param bool|string $scene The scene specification, or false if there is none
 488       * @throws MWException
 489       * @return string
 490       */
 491  	protected function escapeMagickPath( $path, $scene = false ) {
 492          # Die on format specifiers (other than drive letters). The regex is
 493          # meant to match all the formats you get from "convert -list format"
 494          if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
 495              if ( wfIsWindows() && is_dir( $m[0] ) ) {
 496                  // OK, it's a drive letter
 497                  // ImageMagick has a similar exception, see IsMagickConflict()
 498              } else {
 499                  throw new MWException( __METHOD__ . ': unexpected colon character in path name' );
 500              }
 501          }
 502  
 503          # If there are square brackets, add a do-nothing scene specification
 504          # to force a literal interpretation
 505          if ( $scene === false ) {
 506              if ( strpos( $path, '[' ) !== false ) {
 507                  $path .= '[0--1]';
 508              }
 509          } else {
 510              $path .= "[$scene]";
 511          }
 512  
 513          return $path;
 514      }
 515  
 516      /**
 517       * Retrieve the version of the installed ImageMagick
 518       * You can use PHPs version_compare() to use this value
 519       * Value is cached for one hour.
 520       * @return string Representing the IM version.
 521       */
 522  	protected function getMagickVersion() {
 523          global $wgMemc;
 524  
 525          $cache = $wgMemc->get( "imagemagick-version" );
 526          if ( !$cache ) {
 527              global $wgImageMagickConvertCommand;
 528              $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . ' -version';
 529              wfDebug( __METHOD__ . ": Running convert -version\n" );
 530              $retval = '';
 531              $return = wfShellExec( $cmd, $retval );
 532              $x = preg_match( '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches );
 533              if ( $x != 1 ) {
 534                  wfDebug( __METHOD__ . ": ImageMagick version check failed\n" );
 535  
 536                  return null;
 537              }
 538              $wgMemc->set( "imagemagick-version", $matches[1], 3600 );
 539  
 540              return $matches[1];
 541          }
 542  
 543          return $cache;
 544      }
 545  
 546      /**
 547       * Returns whether the current scaler supports rotation.
 548       *
 549       * @since 1.24 No longer static
 550       * @return bool
 551       */
 552  	public function canRotate() {
 553          return false;
 554      }
 555  
 556      /**
 557       * Should we automatically rotate an image based on exif
 558       *
 559       * @since 1.24 No longer static
 560       * @see $wgEnableAutoRotation
 561       * @return bool Whether auto rotation is enabled
 562       */
 563  	public function autoRotateEnabled() {
 564          return false;
 565      }
 566  
 567      /**
 568       * Rotate a thumbnail.
 569       *
 570       * This is a stub. See BitmapHandler::rotate.
 571       *
 572       * @param File $file
 573       * @param array $params Rotate parameters.
 574       *   'rotation' clockwise rotation in degrees, allowed are multiples of 90
 575       * @since 1.24 Is non-static. From 1.21 it was static
 576       * @return bool
 577       */
 578  	public function rotate( $file, $params ) {
 579          return new MediaTransformError( 'thumbnail_error', 0, 0,
 580              get_class( $this ) . ' rotation not implemented' );
 581      }
 582  
 583      /**
 584       * Returns whether the file needs to be rendered. Returns true if the
 585       * file requires rotation and we are able to rotate it.
 586       *
 587       * @param File $file
 588       * @return bool
 589       */
 590  	public function mustRender( $file ) {
 591          return $this->canRotate() && $this->getRotation( $file ) != 0;
 592      }
 593  }


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