MediaWiki  master
TransformationalImageHandler.php
Go to the documentation of this file.
1 <?php
30 
48  public function normaliseParams( $image, &$params ) {
49  if ( !parent::normaliseParams( $image, $params ) ) {
50  return false;
51  }
52 
53  # Obtain the source, pre-rotation dimensions
54  $srcWidth = $image->getWidth( $params['page'] );
55  $srcHeight = $image->getHeight( $params['page'] );
56 
57  # Don't make an image bigger than the source
58  if ( $params['physicalWidth'] >= $srcWidth ) {
59  $params['physicalWidth'] = $srcWidth;
60  $params['physicalHeight'] = $srcHeight;
61 
62  # Skip scaling limit checks if no scaling is required
63  # due to requested size being bigger than source.
64  if ( !$image->mustRender() ) {
65  return true;
66  }
67  }
68 
69  return true;
70  }
71 
84  public function extractPreRotationDimensions( $params, $rotation ) {
85  if ( $rotation == 90 || $rotation == 270 ) {
86  # We'll resize before rotation, so swap the dimensions again
87  $width = $params['physicalHeight'];
88  $height = $params['physicalWidth'];
89  } else {
90  $width = $params['physicalWidth'];
91  $height = $params['physicalHeight'];
92  }
93 
94  return [ $width, $height ];
95  }
96 
111  public function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
112  if ( !$this->normaliseParams( $image, $params ) ) {
113  return new TransformParameterError( $params );
114  }
115 
116  # Create a parameter array to pass to the scaler
117  $scalerParams = [
118  # The size to which the image will be resized
119  'physicalWidth' => $params['physicalWidth'],
120  'physicalHeight' => $params['physicalHeight'],
121  'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}",
122  # The size of the image on the page
123  'clientWidth' => $params['width'],
124  'clientHeight' => $params['height'],
125  # Comment as will be added to the Exif of the thumbnail
126  'comment' => isset( $params['descriptionUrl'] )
127  ? "File source: {$params['descriptionUrl']}"
128  : '',
129  # Properties of the original image
130  'srcWidth' => $image->getWidth(),
131  'srcHeight' => $image->getHeight(),
132  'mimeType' => $image->getMimeType(),
133  'dstPath' => $dstPath,
134  'dstUrl' => $dstUrl,
135  'interlace' => $params['interlace'] ?? false,
136  ];
137 
138  if ( isset( $params['quality'] ) && $params['quality'] === 'low' ) {
139  $scalerParams['quality'] = 30;
140  }
141 
142  // For subclasses that might be paged.
143  if ( $image->isMultipage() && isset( $params['page'] ) ) {
144  $scalerParams['page'] = intval( $params['page'] );
145  }
146 
147  # Determine scaler type
148  $scaler = $this->getScalerType( $dstPath );
149 
150  if ( is_array( $scaler ) ) {
151  $scalerName = get_class( $scaler[0] );
152  } else {
153  $scalerName = $scaler;
154  }
155 
156  wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} " .
157  "thumbnail at $dstPath using scaler $scalerName" );
158 
159  if ( !$image->mustRender() &&
160  $scalerParams['physicalWidth'] == $scalerParams['srcWidth']
161  && $scalerParams['physicalHeight'] == $scalerParams['srcHeight']
162  && !isset( $scalerParams['quality'] )
163  ) {
164  # normaliseParams (or the user) wants us to return the unscaled image
165  wfDebug( __METHOD__ . ": returning unscaled image" );
166 
167  return $this->getClientScalingThumbnailImage( $image, $scalerParams );
168  }
169 
170  if ( $scaler == 'client' ) {
171  # Client-side image scaling, use the source URL
172  # Using the destination URL in a TRANSFORM_LATER request would be incorrect
173  return $this->getClientScalingThumbnailImage( $image, $scalerParams );
174  }
175 
176  if ( $image->isTransformedLocally() && !$this->isImageAreaOkForThumbnaling( $image, $params ) ) {
177  global $wgMaxImageArea;
178  return new TransformTooBigImageAreaError( $params, $wgMaxImageArea );
179  }
180 
181  if ( $flags & self::TRANSFORM_LATER ) {
182  wfDebug( __METHOD__ . ": Transforming later per flags." );
183  $newParams = [
184  'width' => $scalerParams['clientWidth'],
185  'height' => $scalerParams['clientHeight']
186  ];
187  if ( isset( $params['quality'] ) ) {
188  $newParams['quality'] = $params['quality'];
189  }
190  if ( isset( $params['page'] ) && $params['page'] ) {
191  $newParams['page'] = $params['page'];
192  }
193  return new ThumbnailImage( $image, $dstUrl, false, $newParams );
194  }
195 
196  # Try to make a target path for the thumbnail
197  if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
198  wfDebug( __METHOD__ . ": Unable to create thumbnail destination " .
199  "directory, falling back to client scaling" );
200 
201  return $this->getClientScalingThumbnailImage( $image, $scalerParams );
202  }
203 
204  # Transform functions and binaries need a FS source file
205  $thumbnailSource = $this->getThumbnailSource( $image, $params );
206 
207  // If the source isn't the original, disable EXIF rotation because it's already been applied
208  if ( $scalerParams['srcWidth'] != $thumbnailSource['width']
209  || $scalerParams['srcHeight'] != $thumbnailSource['height'] ) {
210  $scalerParams['disableRotation'] = true;
211  }
212 
213  $scalerParams['srcPath'] = $thumbnailSource['path'];
214  $scalerParams['srcWidth'] = $thumbnailSource['width'];
215  $scalerParams['srcHeight'] = $thumbnailSource['height'];
216 
217  if ( $scalerParams['srcPath'] === false ) { // Failed to get local copy
218  wfDebugLog( 'thumbnail',
219  sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
220  wfHostname(), $image->getName() ) );
221 
222  return new MediaTransformError( 'thumbnail_error',
223  $scalerParams['clientWidth'], $scalerParams['clientHeight'],
224  wfMessage( 'filemissing' )
225  );
226  }
227 
228  # Try a hook. Called "Bitmap" for historical reasons.
229 
230  $mto = null;
231  Hooks::runner()->onBitmapHandlerTransform( $this, $image, $scalerParams, $mto );
232  if ( $mto !== null ) {
233  wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto" );
234  $scaler = 'hookaborted';
235  }
236 
237  // $scaler will return a MediaTransformError on failure, or false on success.
238  // If the scaler is successful, it will have created a thumbnail at the destination
239  // path.
240  if ( is_array( $scaler ) && is_callable( $scaler ) ) {
241  // Allow subclasses to specify their own rendering methods.
242  $err = call_user_func( $scaler, $image, $scalerParams );
243  } else {
244  switch ( $scaler ) {
245  case 'hookaborted':
246  # Handled by the hook above
247  $err = $mto->isError() ? $mto : false;
248  break;
249  case 'im':
250  $err = $this->transformImageMagick( $image, $scalerParams );
251  break;
252  case 'custom':
253  $err = $this->transformCustom( $image, $scalerParams );
254  break;
255  case 'imext':
256  $err = $this->transformImageMagickExt( $image, $scalerParams );
257  break;
258  case 'gd':
259  default:
260  $err = $this->transformGd( $image, $scalerParams );
261  break;
262  }
263  }
264 
265  # Remove the file if a zero-byte thumbnail was created, or if there was an error
266  $removed = $this->removeBadFile( $dstPath, (bool)$err );
267  if ( $err ) {
268  # transform returned MediaTransforError
269  return $err;
270  } elseif ( $removed ) {
271  # Thumbnail was zero-byte and had to be removed
272  return new MediaTransformError( 'thumbnail_error',
273  $scalerParams['clientWidth'], $scalerParams['clientHeight'],
274  wfMessage( 'unknown-error' )
275  );
276  } elseif ( $mto ) {
277  // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
278  return $mto;
279  } else {
280  $newParams = [
281  'width' => $scalerParams['clientWidth'],
282  'height' => $scalerParams['clientHeight']
283  ];
284  if ( isset( $params['quality'] ) ) {
285  $newParams['quality'] = $params['quality'];
286  }
287  if ( isset( $params['page'] ) && $params['page'] ) {
288  $newParams['page'] = $params['page'];
289  }
290  return new ThumbnailImage( $image, $dstUrl, $dstPath, $newParams );
291  }
292  }
293 
301  protected function getThumbnailSource( $file, $params ) {
302  return $file->getThumbnailSource( $params );
303  }
304 
326  abstract protected function getScalerType( $dstPath, $checkDstPath = true );
327 
339  protected function getClientScalingThumbnailImage( $image, $scalerParams ) {
340  $params = [
341  'width' => $scalerParams['clientWidth'],
342  'height' => $scalerParams['clientHeight']
343  ];
344 
345  return new ThumbnailImage( $image, $image->getUrl(), null, $params );
346  }
347 
359  protected function transformImageMagick( $image, $params ) {
360  return $this->getMediaTransformError( $params, "Unimplemented" );
361  }
362 
374  protected function transformImageMagickExt( $image, $params ) {
375  return $this->getMediaTransformError( $params, "Unimplemented" );
376  }
377 
389  protected function transformCustom( $image, $params ) {
390  return $this->getMediaTransformError( $params, "Unimplemented" );
391  }
392 
400  public function getMediaTransformError( $params, $errMsg ) {
401  return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
402  $params['clientHeight'], $errMsg );
403  }
404 
415  protected function transformGd( $image, $params ) {
416  return $this->getMediaTransformError( $params, "Unimplemented" );
417  }
418 
425  protected function escapeMagickProperty( $s ) {
426  // Double the backslashes
427  $s = str_replace( '\\', '\\\\', $s );
428  // Double the percents
429  $s = str_replace( '%', '%%', $s );
430  // Escape initial - or @
431  if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
432  $s = '\\' . $s;
433  }
434 
435  return $s;
436  }
437 
455  protected function escapeMagickInput( $path, $scene = false ) {
456  # Die on initial metacharacters (caller should prepend path)
457  $firstChar = substr( $path, 0, 1 );
458  if ( $firstChar === '~' || $firstChar === '@' ) {
459  throw new MWException( __METHOD__ . ': cannot escape this path name' );
460  }
461 
462  # Escape glob chars
463  $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
464 
465  return $this->escapeMagickPath( $path, $scene );
466  }
467 
475  protected function escapeMagickOutput( $path, $scene = false ) {
476  $path = str_replace( '%', '%%', $path );
477 
478  return $this->escapeMagickPath( $path, $scene );
479  }
480 
490  protected function escapeMagickPath( $path, $scene = false ) {
491  # Die on format specifiers (other than drive letters). The regex is
492  # meant to match all the formats you get from "convert -list format"
493  if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
494  if ( wfIsWindows() && is_dir( $m[0] ) ) {
495  // OK, it's a drive letter
496  // ImageMagick has a similar exception, see IsMagickConflict()
497  } else {
498  throw new MWException( __METHOD__ . ': unexpected colon character in path name' );
499  }
500  }
501 
502  # If there are square brackets, add a do-nothing scene specification
503  # to force a literal interpretation
504  if ( $scene === false ) {
505  if ( strpos( $path, '[' ) !== false ) {
506  $path .= '[0--1]';
507  }
508  } else {
509  $path .= "[$scene]";
510  }
511 
512  return $path;
513  }
514 
521  protected function getMagickVersion() {
522  $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
523  $method = __METHOD__;
524  return $cache->getWithSetCallback(
525  $cache->makeGlobalKey( 'imagemagick-version' ),
526  $cache::TTL_HOUR,
527  static function () use ( $method ) {
529 
530  $cmd = Shell::escape( $wgImageMagickConvertCommand ) . ' -version';
531  wfDebug( $method . ": Running convert -version" );
532  $retval = '';
533  $return = wfShellExecWithStderr( $cmd, $retval );
534  $x = preg_match(
535  '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches
536  );
537  if ( $x != 1 ) {
538  wfDebug( $method . ": ImageMagick version check failed" );
539  return false;
540  }
541 
542  return $matches[1];
543  }
544  );
545  }
546 
554  public function canRotate() {
555  return false;
556  }
557 
566  public function autoRotateEnabled() {
567  return false;
568  }
569 
582  public function rotate( $file, $params ) {
583  return new MediaTransformError( 'thumbnail_error', 0, 0,
584  static::class . ' rotation not implemented' );
585  }
586 
595  public function mustRender( $file ) {
596  return $this->canRotate() && $this->getRotation( $file ) != 0;
597  }
598 
610  public function isImageAreaOkForThumbnaling( $file, &$params ) {
611  global $wgMaxImageArea;
612 
613  # For historical reasons, hook starts with BitmapHandler
614  $checkImageAreaHookResult = null;
615  Hooks::runner()->onBitmapHandlerCheckImageArea(
616  $file, $params, $checkImageAreaHookResult );
617 
618  if ( $checkImageAreaHookResult !== null ) {
619  // was set by hook, so return that value
620  return (bool)$checkImageAreaHookResult;
621  }
622 
623  $srcWidth = $file->getWidth( $params['page'] );
624  $srcHeight = $file->getHeight( $params['page'] );
625 
626  if ( $srcWidth * $srcHeight > $wgMaxImageArea
627  && !( $file->getMimeType() == 'image/jpeg'
628  && $this->getScalerType( false, false ) == 'im' )
629  ) {
630  # Only ImageMagick can efficiently downsize jpg images without loading
631  # the entire file in memory
632  return false;
633  }
634  return true;
635  }
636 }
MediaHandler\removeBadFile
removeBadFile( $dstPath, $retval=0)
Check for zero-sized thumbnails.
Definition: MediaHandler.php:924
TransformationalImageHandler\transformImageMagick
transformImageMagick( $image, $params)
Transform an image using ImageMagick.
Definition: TransformationalImageHandler.php:359
MediaWiki\Shell\Shell
Executes shell commands.
Definition: Shell.php:45
MediaTransformError
Basic media transform error class.
Definition: MediaTransformError.php:31
ThumbnailImage
Media transform output for images.
Definition: ThumbnailImage.php:29
wfMkdirParents
wfMkdirParents( $dir, $mode=null, $caller=null)
Make directory, and make all parent directories if they don't exist.
Definition: GlobalFunctions.php:1773
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:193
TransformationalImageHandler\getMediaTransformError
getMediaTransformError( $params, $errMsg)
Get a MediaTransformError with error 'thumbnail_error'.
Definition: TransformationalImageHandler.php:400
TransformationalImageHandler\escapeMagickOutput
escapeMagickOutput( $path, $scene=false)
Escape a string for ImageMagick's output filename.
Definition: TransformationalImageHandler.php:475
TransformationalImageHandler\escapeMagickProperty
escapeMagickProperty( $s)
Escape a string for ImageMagick's property input (e.g.
Definition: TransformationalImageHandler.php:425
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
wfHostname
wfHostname()
Get host name of the current machine, for use in error reporting.
Definition: GlobalFunctions.php:1245
TransformationalImageHandler\doTransform
doTransform( $image, $dstPath, $dstUrl, $params, $flags=0)
Create a thumbnail.
Definition: TransformationalImageHandler.php:111
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1182
TransformationalImageHandler\transformCustom
transformCustom( $image, $params)
Transform an image using a custom command.
Definition: TransformationalImageHandler.php:389
TransformationalImageHandler\rotate
rotate( $file, $params)
Rotate a thumbnail.
Definition: TransformationalImageHandler.php:582
TransformationalImageHandler
Handler for images that need to be transformed.
Definition: TransformationalImageHandler.php:39
TransformationalImageHandler\getClientScalingThumbnailImage
getClientScalingThumbnailImage( $image, $scalerParams)
Get a ThumbnailImage that respresents an image that will be scaled client side.
Definition: TransformationalImageHandler.php:339
wfDebugLog
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
Definition: GlobalFunctions.php:958
TransformationalImageHandler\getThumbnailSource
getThumbnailSource( $file, $params)
Get the source file for the transform.
Definition: TransformationalImageHandler.php:301
TransformationalImageHandler\escapeMagickPath
escapeMagickPath( $path, $scene=false)
Armour a string against ImageMagick's GetPathComponent().
Definition: TransformationalImageHandler.php:490
MWException
MediaWiki exception.
Definition: MWException.php:29
$matches
$matches
Definition: NoLocalSettings.php:24
TransformationalImageHandler\transformGd
transformGd( $image, $params)
Transform an image using the built in GD library.
Definition: TransformationalImageHandler.php:415
ImageHandler
Media handler abstract base class for images.
Definition: ImageHandler.php:31
TransformationalImageHandler\extractPreRotationDimensions
extractPreRotationDimensions( $params, $rotation)
Extracts the width/height if the image will be scaled before rotating.
Definition: TransformationalImageHandler.php:84
TransformationalImageHandler\escapeMagickInput
escapeMagickInput( $path, $scene=false)
Escape a string for ImageMagick's input filenames.
Definition: TransformationalImageHandler.php:455
$wgMaxImageArea
$wgMaxImageArea
The maximum number of pixels a source image can have if it is to be scaled down by a scaler that requ...
Definition: DefaultSettings.php:1409
TransformationalImageHandler\normaliseParams
normaliseParams( $image, &$params)
Definition: TransformationalImageHandler.php:48
TransformTooBigImageAreaError
Shortcut class for parameter file size errors.
Definition: TransformTooBigImageAreaError.php:31
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:894
TransformationalImageHandler\mustRender
mustRender( $file)
Returns whether the file needs to be rendered.
Definition: TransformationalImageHandler.php:595
$s
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
Definition: mergeMessageFileList.php:206
$wgImageMagickConvertCommand
$wgImageMagickConvertCommand
The convert command shipped with ImageMagick.
Definition: DefaultSettings.php:1256
wfIsWindows
wfIsWindows()
Check if the operating system is Windows.
Definition: GlobalFunctions.php:1729
TransformParameterError
Shortcut class for parameter validation errors.
Definition: TransformParameterError.php:30
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
TransformationalImageHandler\getMagickVersion
getMagickVersion()
Retrieve the version of the installed ImageMagick You can use PHPs version_compare() to use this valu...
Definition: TransformationalImageHandler.php:521
TransformationalImageHandler\getScalerType
getScalerType( $dstPath, $checkDstPath=true)
Returns what sort of scaler type should be used.
TransformationalImageHandler\autoRotateEnabled
autoRotateEnabled()
Should we automatically rotate an image based on exif.
Definition: TransformationalImageHandler.php:566
$cache
$cache
Definition: mcc.php:33
TransformationalImageHandler\transformImageMagickExt
transformImageMagickExt( $image, $params)
Transform an image using the Imagick PHP extension.
Definition: TransformationalImageHandler.php:374
$path
$path
Definition: NoLocalSettings.php:25
TransformationalImageHandler\canRotate
canRotate()
Returns whether the current scaler supports rotation.
Definition: TransformationalImageHandler.php:554
TransformationalImageHandler\isImageAreaOkForThumbnaling
isImageAreaOkForThumbnaling( $file, &$params)
Check if the file is smaller than the maximum image area for thumbnailing.
Definition: TransformationalImageHandler.php:610
MediaHandler\getRotation
getRotation( $file)
On supporting image formats, try to read out the low-level orientation of the file and return the ang...
Definition: MediaHandler.php:992
wfShellExecWithStderr
wfShellExecWithStderr( $cmd, &$retval=null, $environ=[], $limits=[])
Execute a shell command, returning both stdout and stderr.
Definition: GlobalFunctions.php:1988