MediaWiki  master
TransformationalImageHandler.php
Go to the documentation of this file.
1 <?php
30 
45  public function normaliseParams( $image, &$params ) {
46  if ( !parent::normaliseParams( $image, $params ) ) {
47  return false;
48  }
49 
50  # Obtain the source, pre-rotation dimensions
51  $srcWidth = $image->getWidth( $params['page'] );
52  $srcHeight = $image->getHeight( $params['page'] );
53 
54  # Don't make an image bigger than the source
55  if ( $params['physicalWidth'] >= $srcWidth ) {
56  $params['physicalWidth'] = $srcWidth;
57  $params['physicalHeight'] = $srcHeight;
58 
59  # Skip scaling limit checks if no scaling is required
60  # due to requested size being bigger than source.
61  if ( !$image->mustRender() ) {
62  return true;
63  }
64  }
65 
66  return true;
67  }
68 
81  public function extractPreRotationDimensions( $params, $rotation ) {
82  if ( $rotation == 90 || $rotation == 270 ) {
83  # We'll resize before rotation, so swap the dimensions again
84  $width = $params['physicalHeight'];
85  $height = $params['physicalWidth'];
86  } else {
87  $width = $params['physicalWidth'];
88  $height = $params['physicalHeight'];
89  }
90 
91  return [ $width, $height ];
92  }
93 
107  function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
108  if ( !$this->normaliseParams( $image, $params ) ) {
109  return new TransformParameterError( $params );
110  }
111 
112  # Create a parameter array to pass to the scaler
113  $scalerParams = [
114  # The size to which the image will be resized
115  'physicalWidth' => $params['physicalWidth'],
116  'physicalHeight' => $params['physicalHeight'],
117  'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}",
118  # The size of the image on the page
119  'clientWidth' => $params['width'],
120  'clientHeight' => $params['height'],
121  # Comment as will be added to the Exif of the thumbnail
122  'comment' => isset( $params['descriptionUrl'] )
123  ? "File source: {$params['descriptionUrl']}"
124  : '',
125  # Properties of the original image
126  'srcWidth' => $image->getWidth(),
127  'srcHeight' => $image->getHeight(),
128  'mimeType' => $image->getMimeType(),
129  'dstPath' => $dstPath,
130  'dstUrl' => $dstUrl,
131  'interlace' => $params['interlace'] ?? false,
132  ];
133 
134  if ( isset( $params['quality'] ) && $params['quality'] === 'low' ) {
135  $scalerParams['quality'] = 30;
136  }
137 
138  // For subclasses that might be paged.
139  if ( $image->isMultipage() && isset( $params['page'] ) ) {
140  $scalerParams['page'] = intval( $params['page'] );
141  }
142 
143  # Determine scaler type
144  $scaler = $this->getScalerType( $dstPath );
145 
146  if ( is_array( $scaler ) ) {
147  $scalerName = get_class( $scaler[0] );
148  } else {
149  $scalerName = $scaler;
150  }
151 
152  wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} " .
153  "thumbnail at $dstPath using scaler $scalerName\n" );
154 
155  if ( !$image->mustRender() &&
156  $scalerParams['physicalWidth'] == $scalerParams['srcWidth']
157  && $scalerParams['physicalHeight'] == $scalerParams['srcHeight']
158  && !isset( $scalerParams['quality'] )
159  ) {
160  # normaliseParams (or the user) wants us to return the unscaled image
161  wfDebug( __METHOD__ . ": returning unscaled image\n" );
162 
163  return $this->getClientScalingThumbnailImage( $image, $scalerParams );
164  }
165 
166  if ( $scaler == 'client' ) {
167  # Client-side image scaling, use the source URL
168  # Using the destination URL in a TRANSFORM_LATER request would be incorrect
169  return $this->getClientScalingThumbnailImage( $image, $scalerParams );
170  }
171 
172  if ( $image->isTransformedLocally() && !$this->isImageAreaOkForThumbnaling( $image, $params ) ) {
173  global $wgMaxImageArea;
174  return new TransformTooBigImageAreaError( $params, $wgMaxImageArea );
175  }
176 
177  if ( $flags & self::TRANSFORM_LATER ) {
178  wfDebug( __METHOD__ . ": Transforming later per flags.\n" );
179  $newParams = [
180  'width' => $scalerParams['clientWidth'],
181  'height' => $scalerParams['clientHeight']
182  ];
183  if ( isset( $params['quality'] ) ) {
184  $newParams['quality'] = $params['quality'];
185  }
186  if ( isset( $params['page'] ) && $params['page'] ) {
187  $newParams['page'] = $params['page'];
188  }
189  return new ThumbnailImage( $image, $dstUrl, false, $newParams );
190  }
191 
192  # Try to make a target path for the thumbnail
193  if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
194  wfDebug( __METHOD__ . ": Unable to create thumbnail destination " .
195  "directory, falling back to client scaling\n" );
196 
197  return $this->getClientScalingThumbnailImage( $image, $scalerParams );
198  }
199 
200  # Transform functions and binaries need a FS source file
201  $thumbnailSource = $this->getThumbnailSource( $image, $params );
202 
203  // If the source isn't the original, disable EXIF rotation because it's already been applied
204  if ( $scalerParams['srcWidth'] != $thumbnailSource['width']
205  || $scalerParams['srcHeight'] != $thumbnailSource['height'] ) {
206  $scalerParams['disableRotation'] = true;
207  }
208 
209  $scalerParams['srcPath'] = $thumbnailSource['path'];
210  $scalerParams['srcWidth'] = $thumbnailSource['width'];
211  $scalerParams['srcHeight'] = $thumbnailSource['height'];
212 
213  if ( $scalerParams['srcPath'] === false ) { // Failed to get local copy
214  wfDebugLog( 'thumbnail',
215  sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
216  wfHostname(), $image->getName() ) );
217 
218  return new MediaTransformError( 'thumbnail_error',
219  $scalerParams['clientWidth'], $scalerParams['clientHeight'],
220  wfMessage( 'filemissing' )
221  );
222  }
223 
224  # Try a hook. Called "Bitmap" for historical reasons.
225 
226  $mto = null;
227  Hooks::run( 'BitmapHandlerTransform', [ $this, $image, &$scalerParams, &$mto ] );
228  if ( !is_null( $mto ) ) {
229  wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto\n" );
230  $scaler = 'hookaborted';
231  }
232 
233  // $scaler will return a MediaTransformError on failure, or false on success.
234  // If the scaler is successful, it will have created a thumbnail at the destination
235  // path.
236  if ( is_array( $scaler ) && is_callable( $scaler ) ) {
237  // Allow subclasses to specify their own rendering methods.
238  $err = call_user_func( $scaler, $image, $scalerParams );
239  } else {
240  switch ( $scaler ) {
241  case 'hookaborted':
242  # Handled by the hook above
243  $err = $mto->isError() ? $mto : false;
244  break;
245  case 'im':
246  $err = $this->transformImageMagick( $image, $scalerParams );
247  break;
248  case 'custom':
249  $err = $this->transformCustom( $image, $scalerParams );
250  break;
251  case 'imext':
252  $err = $this->transformImageMagickExt( $image, $scalerParams );
253  break;
254  case 'gd':
255  default:
256  $err = $this->transformGd( $image, $scalerParams );
257  break;
258  }
259  }
260 
261  # Remove the file if a zero-byte thumbnail was created, or if there was an error
262  $removed = $this->removeBadFile( $dstPath, (bool)$err );
263  if ( $err ) {
264  # transform returned MediaTransforError
265  return $err;
266  } elseif ( $removed ) {
267  # Thumbnail was zero-byte and had to be removed
268  return new MediaTransformError( 'thumbnail_error',
269  $scalerParams['clientWidth'], $scalerParams['clientHeight'],
270  wfMessage( 'unknown-error' )
271  );
272  } elseif ( $mto ) {
273  return $mto;
274  } else {
275  $newParams = [
276  'width' => $scalerParams['clientWidth'],
277  'height' => $scalerParams['clientHeight']
278  ];
279  if ( isset( $params['quality'] ) ) {
280  $newParams['quality'] = $params['quality'];
281  }
282  if ( isset( $params['page'] ) && $params['page'] ) {
283  $newParams['page'] = $params['page'];
284  }
285  return new ThumbnailImage( $image, $dstUrl, $dstPath, $newParams );
286  }
287  }
288 
296  protected function getThumbnailSource( $file, $params ) {
297  return $file->getThumbnailSource( $params );
298  }
299 
321  abstract protected function getScalerType( $dstPath, $checkDstPath = true );
322 
333  protected function getClientScalingThumbnailImage( $image, $scalerParams ) {
334  $params = [
335  'width' => $scalerParams['clientWidth'],
336  'height' => $scalerParams['clientHeight']
337  ];
338 
339  return new ThumbnailImage( $image, $image->getUrl(), null, $params );
340  }
341 
352  protected function transformImageMagick( $image, $params ) {
353  return $this->getMediaTransformError( $params, "Unimplemented" );
354  }
355 
366  protected function transformImageMagickExt( $image, $params ) {
367  return $this->getMediaTransformError( $params, "Unimplemented" );
368  }
369 
380  protected function transformCustom( $image, $params ) {
381  return $this->getMediaTransformError( $params, "Unimplemented" );
382  }
383 
391  public function getMediaTransformError( $params, $errMsg ) {
392  return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
393  $params['clientHeight'], $errMsg );
394  }
395 
406  protected function transformGd( $image, $params ) {
407  return $this->getMediaTransformError( $params, "Unimplemented" );
408  }
409 
416  function escapeMagickProperty( $s ) {
417  // Double the backslashes
418  $s = str_replace( '\\', '\\\\', $s );
419  // Double the percents
420  $s = str_replace( '%', '%%', $s );
421  // Escape initial - or @
422  if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
423  $s = '\\' . $s;
424  }
425 
426  return $s;
427  }
428 
446  function escapeMagickInput( $path, $scene = false ) {
447  # Die on initial metacharacters (caller should prepend path)
448  $firstChar = substr( $path, 0, 1 );
449  if ( $firstChar === '~' || $firstChar === '@' ) {
450  throw new MWException( __METHOD__ . ': cannot escape this path name' );
451  }
452 
453  # Escape glob chars
454  $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
455 
456  return $this->escapeMagickPath( $path, $scene );
457  }
458 
466  function escapeMagickOutput( $path, $scene = false ) {
467  $path = str_replace( '%', '%%', $path );
468 
469  return $this->escapeMagickPath( $path, $scene );
470  }
471 
481  protected function escapeMagickPath( $path, $scene = false ) {
482  # Die on format specifiers (other than drive letters). The regex is
483  # meant to match all the formats you get from "convert -list format"
484  if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
485  if ( wfIsWindows() && is_dir( $m[0] ) ) {
486  // OK, it's a drive letter
487  // ImageMagick has a similar exception, see IsMagickConflict()
488  } else {
489  throw new MWException( __METHOD__ . ': unexpected colon character in path name' );
490  }
491  }
492 
493  # If there are square brackets, add a do-nothing scene specification
494  # to force a literal interpretation
495  if ( $scene === false ) {
496  if ( strpos( $path, '[' ) !== false ) {
497  $path .= '[0--1]';
498  }
499  } else {
500  $path .= "[$scene]";
501  }
502 
503  return $path;
504  }
505 
512  protected function getMagickVersion() {
513  $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
514  $method = __METHOD__;
515  return $cache->getWithSetCallback(
516  $cache->makeGlobalKey( 'imagemagick-version' ),
517  $cache::TTL_HOUR,
518  function () use ( $method ) {
520 
521  $cmd = Shell::escape( $wgImageMagickConvertCommand ) . ' -version';
522  wfDebug( $method . ": Running convert -version\n" );
523  $retval = '';
524  $return = wfShellExecWithStderr( $cmd, $retval );
525  $x = preg_match(
526  '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches
527  );
528  if ( $x != 1 ) {
529  wfDebug( $method . ": ImageMagick version check failed\n" );
530  return false;
531  }
532 
533  return $matches[1];
534  }
535  );
536  }
537 
544  public function canRotate() {
545  return false;
546  }
547 
555  public function autoRotateEnabled() {
556  return false;
557  }
558 
570  public function rotate( $file, $params ) {
571  return new MediaTransformError( 'thumbnail_error', 0, 0,
572  static::class . ' rotation not implemented' );
573  }
574 
582  public function mustRender( $file ) {
583  return $this->canRotate() && $this->getRotation( $file ) != 0;
584  }
585 
596  public function isImageAreaOkForThumbnaling( $file, &$params ) {
597  global $wgMaxImageArea;
598 
599  # For historical reasons, hook starts with BitmapHandler
600  $checkImageAreaHookResult = null;
601  Hooks::run(
602  'BitmapHandlerCheckImageArea',
603  [ $file, &$params, &$checkImageAreaHookResult ]
604  );
605 
606  if ( !is_null( $checkImageAreaHookResult ) ) {
607  // was set by hook, so return that value
608  return (bool)$checkImageAreaHookResult;
609  }
610 
611  $srcWidth = $file->getWidth( $params['page'] );
612  $srcHeight = $file->getHeight( $params['page'] );
613 
614  if ( $srcWidth * $srcHeight > $wgMaxImageArea
615  && !( $file->getMimeType() == 'image/jpeg'
616  && $this->getScalerType( false, false ) == 'im' )
617  ) {
618  # Only ImageMagick can efficiently downsize jpg images without loading
619  # the entire file in memory
620  return false;
621  }
622  return true;
623  }
624 }
canRotate()
Returns whether the current scaler supports rotation.
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
escapeMagickInput( $path, $scene=false)
Escape a string for ImageMagick&#39;s input filenames.
$wgImageMagickConvertCommand
The convert command shipped with ImageMagick.
removeBadFile( $dstPath, $retval=0)
Check for zero-sized thumbnails.
isImageAreaOkForThumbnaling( $file, &$params)
Check if the file is smaller than the maximum image area for thumbnailing.
wfHostname()
Fetch server name for use in error reporting etc.
getClientScalingThumbnailImage( $image, $scalerParams)
Get a ThumbnailImage that respresents an image that will be scaled client side.
Handler for images that need to be transformed.
doTransform( $image, $dstPath, $dstUrl, $params, $flags=0)
Create a thumbnail.
$wgMaxImageArea
The maximum number of pixels a source image can have if it is to be scaled down by a scaler that requ...
wfIsWindows()
Check if the operating system is Windows.
escapeMagickOutput( $path, $scene=false)
Escape a string for ImageMagick&#39;s output filename.
transformCustom( $image, $params)
Transform an image using a custom command.
Shortcut class for parameter validation errors.
escapeMagickProperty( $s)
Escape a string for ImageMagick&#39;s property input (e.g.
getMediaTransformError( $params, $errMsg)
Get a MediaTransformError with error &#39;thumbnail_error&#39;.
mustRender( $file)
Returns whether the file needs to be rendered.
escapeMagickPath( $path, $scene=false)
Armour a string against ImageMagick&#39;s GetPathComponent().
getThumbnailSource( $file, $params)
Get the source file for the transform.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Media transform output for images.
getRotation( $file)
On supporting image formats, try to read out the low-level orientation of the file and return the ang...
$cache
Definition: mcc.php:33
transformGd( $image, $params)
Transform an image using the built in GD library.
getScalerType( $dstPath, $checkDstPath=true)
Returns what sort of scaler type should be used.
Shortcut class for parameter file size errors.
transformImageMagick( $image, $params)
Transform an image using ImageMagick.
autoRotateEnabled()
Should we automatically rotate an image based on exif.
transformImageMagickExt( $image, $params)
Transform an image using the Imagick PHP extension.
Media handler abstract base class for images.
wfMkdirParents( $dir, $mode=null, $caller=null)
Make directory, and make all parent directories if they don&#39;t exist.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not...
wfShellExecWithStderr( $cmd, &$retval=null, $environ=[], $limits=[])
Execute a shell command, returning both stdout and stderr.
getMagickVersion()
Retrieve the version of the installed ImageMagick You can use PHPs version_compare() to use this valu...
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
extractPreRotationDimensions( $params, $rotation)
Extracts the width/height if the image will be scaled before rotating.
Basic media transform error class.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
rotate( $file, $params)
Rotate a thumbnail.
$matches