MediaWiki  master
TransformationalImageHandler.php
Go to the documentation of this file.
1 <?php
31 
49  public function normaliseParams( $image, &$params ) {
50  if ( !parent::normaliseParams( $image, $params ) ) {
51  return false;
52  }
53 
54  # Obtain the source, pre-rotation dimensions
55  $srcWidth = $image->getWidth( $params['page'] );
56  $srcHeight = $image->getHeight( $params['page'] );
57 
58  # Don't make an image bigger than the source
59  if ( $params['physicalWidth'] >= $srcWidth ) {
60  $params['physicalWidth'] = $srcWidth;
61  $params['physicalHeight'] = $srcHeight;
62 
63  # Skip scaling limit checks if no scaling is required
64  # due to requested size being bigger than source.
65  if ( !$image->mustRender() ) {
66  return true;
67  }
68  }
69 
70  return true;
71  }
72 
85  public function extractPreRotationDimensions( $params, $rotation ) {
86  if ( $rotation == 90 || $rotation == 270 ) {
87  # We'll resize before rotation, so swap the dimensions again
88  $width = $params['physicalHeight'];
89  $height = $params['physicalWidth'];
90  } else {
91  $width = $params['physicalWidth'];
92  $height = $params['physicalHeight'];
93  }
94 
95  return [ $width, $height ];
96  }
97 
112  public function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
113  if ( !$this->normaliseParams( $image, $params ) ) {
114  return new TransformParameterError( $params );
115  }
116 
117  # Create a parameter array to pass to the scaler
118  $scalerParams = [
119  # The size to which the image will be resized
120  'physicalWidth' => $params['physicalWidth'],
121  'physicalHeight' => $params['physicalHeight'],
122  'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}",
123  # The size of the image on the page
124  'clientWidth' => $params['width'],
125  'clientHeight' => $params['height'],
126  # Comment as will be added to the Exif of the thumbnail
127  'comment' => isset( $params['descriptionUrl'] )
128  ? "File source: {$params['descriptionUrl']}"
129  : '',
130  # Properties of the original image
131  'srcWidth' => $image->getWidth(),
132  'srcHeight' => $image->getHeight(),
133  'mimeType' => $image->getMimeType(),
134  'dstPath' => $dstPath,
135  'dstUrl' => $dstUrl,
136  'interlace' => $params['interlace'] ?? false,
137  ];
138 
139  if ( isset( $params['quality'] ) && $params['quality'] === 'low' ) {
140  $scalerParams['quality'] = 30;
141  }
142 
143  // For subclasses that might be paged.
144  if ( $image->isMultipage() && isset( $params['page'] ) ) {
145  $scalerParams['page'] = intval( $params['page'] );
146  }
147 
148  # Determine scaler type
149  $scaler = $this->getScalerType( $dstPath );
150 
151  if ( is_array( $scaler ) ) {
152  $scalerName = get_class( $scaler[0] );
153  } else {
154  $scalerName = $scaler;
155  }
156 
157  wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} " .
158  "thumbnail at $dstPath using scaler $scalerName" );
159 
160  if ( !$image->mustRender() &&
161  $scalerParams['physicalWidth'] == $scalerParams['srcWidth']
162  && $scalerParams['physicalHeight'] == $scalerParams['srcHeight']
163  && !isset( $scalerParams['quality'] )
164  ) {
165  # normaliseParams (or the user) wants us to return the unscaled image
166  wfDebug( __METHOD__ . ": returning unscaled image" );
167 
168  return $this->getClientScalingThumbnailImage( $image, $scalerParams );
169  }
170 
171  if ( $scaler == 'client' ) {
172  # Client-side image scaling, use the source URL
173  # Using the destination URL in a TRANSFORM_LATER request would be incorrect
174  return $this->getClientScalingThumbnailImage( $image, $scalerParams );
175  }
176 
177  if ( $image->isTransformedLocally() && !$this->isImageAreaOkForThumbnaling( $image, $params ) ) {
178  $maxImageArea = MediaWikiServices::getInstance()->getMainConfig()->get( 'MaxImageArea' );
179  return new TransformTooBigImageAreaError( $params, $maxImageArea );
180  }
181 
182  if ( $flags & self::TRANSFORM_LATER ) {
183  wfDebug( __METHOD__ . ": Transforming later per flags." );
184  $newParams = [
185  'width' => $scalerParams['clientWidth'],
186  'height' => $scalerParams['clientHeight']
187  ];
188  if ( isset( $params['quality'] ) ) {
189  $newParams['quality'] = $params['quality'];
190  }
191  if ( isset( $params['page'] ) && $params['page'] ) {
192  $newParams['page'] = $params['page'];
193  }
194  return new ThumbnailImage( $image, $dstUrl, false, $newParams );
195  }
196 
197  # Try to make a target path for the thumbnail
198  if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
199  wfDebug( __METHOD__ . ": Unable to create thumbnail destination " .
200  "directory, falling back to client scaling" );
201 
202  return $this->getClientScalingThumbnailImage( $image, $scalerParams );
203  }
204 
205  # Transform functions and binaries need a FS source file
206  $thumbnailSource = $this->getThumbnailSource( $image, $params );
207 
208  // If the source isn't the original, disable EXIF rotation because it's already been applied
209  if ( $scalerParams['srcWidth'] != $thumbnailSource['width']
210  || $scalerParams['srcHeight'] != $thumbnailSource['height'] ) {
211  $scalerParams['disableRotation'] = true;
212  }
213 
214  $scalerParams['srcPath'] = $thumbnailSource['path'];
215  $scalerParams['srcWidth'] = $thumbnailSource['width'];
216  $scalerParams['srcHeight'] = $thumbnailSource['height'];
217 
218  if ( $scalerParams['srcPath'] === false ) { // Failed to get local copy
219  wfDebugLog( 'thumbnail',
220  sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
221  wfHostname(), $image->getName() ) );
222 
223  return new MediaTransformError( 'thumbnail_error',
224  $scalerParams['clientWidth'], $scalerParams['clientHeight'],
225  wfMessage( 'filemissing' )
226  );
227  }
228 
229  # Try a hook. Called "Bitmap" for historical reasons.
230 
231  $mto = null;
232  Hooks::runner()->onBitmapHandlerTransform( $this, $image, $scalerParams, $mto );
233  if ( $mto !== null ) {
234  wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto" );
235  $scaler = 'hookaborted';
236  }
237 
238  // $scaler will return a MediaTransformError on failure, or false on success.
239  // If the scaler is successful, it will have created a thumbnail at the destination
240  // path.
241  if ( is_array( $scaler ) && is_callable( $scaler ) ) {
242  // Allow subclasses to specify their own rendering methods.
243  $err = call_user_func( $scaler, $image, $scalerParams );
244  } else {
245  switch ( $scaler ) {
246  case 'hookaborted':
247  # Handled by the hook above
248  $err = $mto->isError() ? $mto : false;
249  break;
250  case 'im':
251  $err = $this->transformImageMagick( $image, $scalerParams );
252  break;
253  case 'custom':
254  $err = $this->transformCustom( $image, $scalerParams );
255  break;
256  case 'imext':
257  $err = $this->transformImageMagickExt( $image, $scalerParams );
258  break;
259  case 'gd':
260  default:
261  $err = $this->transformGd( $image, $scalerParams );
262  break;
263  }
264  }
265 
266  # Remove the file if a zero-byte thumbnail was created, or if there was an error
267  $removed = $this->removeBadFile( $dstPath, (bool)$err );
268  if ( $err ) {
269  # transform returned MediaTransforError
270  return $err;
271  } elseif ( $removed ) {
272  # Thumbnail was zero-byte and had to be removed
273  return new MediaTransformError( 'thumbnail_error',
274  $scalerParams['clientWidth'], $scalerParams['clientHeight'],
275  wfMessage( 'unknown-error' )
276  );
277  } elseif ( $mto ) {
278  // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
279  return $mto;
280  } else {
281  $newParams = [
282  'width' => $scalerParams['clientWidth'],
283  'height' => $scalerParams['clientHeight']
284  ];
285  if ( isset( $params['quality'] ) ) {
286  $newParams['quality'] = $params['quality'];
287  }
288  if ( isset( $params['page'] ) && $params['page'] ) {
289  $newParams['page'] = $params['page'];
290  }
291  return new ThumbnailImage( $image, $dstUrl, $dstPath, $newParams );
292  }
293  }
294 
302  protected function getThumbnailSource( $file, $params ) {
303  return $file->getThumbnailSource( $params );
304  }
305 
327  abstract protected function getScalerType( $dstPath, $checkDstPath = true );
328 
340  protected function getClientScalingThumbnailImage( $image, $scalerParams ) {
341  $params = [
342  'width' => $scalerParams['clientWidth'],
343  'height' => $scalerParams['clientHeight']
344  ];
345 
346  return new ThumbnailImage( $image, $image->getUrl(), null, $params );
347  }
348 
360  protected function transformImageMagick( $image, $params ) {
361  return $this->getMediaTransformError( $params, "Unimplemented" );
362  }
363 
375  protected function transformImageMagickExt( $image, $params ) {
376  return $this->getMediaTransformError( $params, "Unimplemented" );
377  }
378 
390  protected function transformCustom( $image, $params ) {
391  return $this->getMediaTransformError( $params, "Unimplemented" );
392  }
393 
401  public function getMediaTransformError( $params, $errMsg ) {
402  return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
403  $params['clientHeight'], $errMsg );
404  }
405 
416  protected function transformGd( $image, $params ) {
417  return $this->getMediaTransformError( $params, "Unimplemented" );
418  }
419 
426  protected 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 
456  protected 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 
476  protected function escapeMagickOutput( $path, $scene = false ) {
477  $path = str_replace( '%', '%%', $path );
478 
479  return $this->escapeMagickPath( $path, $scene );
480  }
481 
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 
522  protected function getMagickVersion() {
523  $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
524  $method = __METHOD__;
525  return $cache->getWithSetCallback(
526  $cache->makeGlobalKey( 'imagemagick-version' ),
527  $cache::TTL_HOUR,
528  static function () use ( $method ) {
529  $imageMagickConvertCommand = MediaWikiServices::getInstance()
530  ->getMainConfig()->get( 'ImageMagickConvertCommand' );
531 
532  $cmd = Shell::escape( $imageMagickConvertCommand ) . ' -version';
533  wfDebug( $method . ": Running convert -version" );
534  $retval = '';
535  $return = wfShellExecWithStderr( $cmd, $retval );
536  $x = preg_match(
537  '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches
538  );
539  if ( $x != 1 ) {
540  wfDebug( $method . ": ImageMagick version check failed" );
541  return false;
542  }
543 
544  return $matches[1];
545  }
546  );
547  }
548 
556  public function canRotate() {
557  return false;
558  }
559 
568  public function autoRotateEnabled() {
569  return false;
570  }
571 
584  public function rotate( $file, $params ) {
585  return new MediaTransformError( 'thumbnail_error', 0, 0,
586  static::class . ' rotation not implemented' );
587  }
588 
597  public function mustRender( $file ) {
598  return $this->canRotate() && $this->getRotation( $file ) != 0;
599  }
600 
612  public function isImageAreaOkForThumbnaling( $file, &$params ) {
613  $maxImageArea = MediaWikiServices::getInstance()->getMainConfig()->get( 'MaxImageArea' );
614 
615  # For historical reasons, hook starts with BitmapHandler
616  $checkImageAreaHookResult = null;
617  Hooks::runner()->onBitmapHandlerCheckImageArea(
618  $file, $params, $checkImageAreaHookResult );
619 
620  if ( $checkImageAreaHookResult !== null ) {
621  // was set by hook, so return that value
622  return (bool)$checkImageAreaHookResult;
623  }
624 
625  if ( $maxImageArea === false ) {
626  // Checking is disabled, fine to thumbnail
627  return true;
628  }
629 
630  $srcWidth = $file->getWidth( $params['page'] );
631  $srcHeight = $file->getHeight( $params['page'] );
632 
633  if ( $srcWidth * $srcHeight > $maxImageArea
634  && !( $file->getMimeType() == 'image/jpeg'
635  && $this->getScalerType( false, false ) == 'im' )
636  ) {
637  # Only ImageMagick can efficiently downsize jpg images without loading
638  # the entire file in memory
639  return false;
640  }
641  return true;
642  }
643 }
MediaHandler\removeBadFile
removeBadFile( $dstPath, $retval=0)
Check for zero-sized thumbnails.
Definition: MediaHandler.php:925
TransformationalImageHandler\transformImageMagick
transformImageMagick( $image, $params)
Transform an image using ImageMagick.
Definition: TransformationalImageHandler.php:360
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:32
wfMkdirParents
wfMkdirParents( $dir, $mode=null, $caller=null)
Make directory, and make all parent directories if they don't exist.
Definition: GlobalFunctions.php:1731
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:203
TransformationalImageHandler\getMediaTransformError
getMediaTransformError( $params, $errMsg)
Get a MediaTransformError with error 'thumbnail_error'.
Definition: TransformationalImageHandler.php:401
TransformationalImageHandler\escapeMagickOutput
escapeMagickOutput( $path, $scene=false)
Escape a string for ImageMagick's output filename.
Definition: TransformationalImageHandler.php:476
TransformationalImageHandler\escapeMagickProperty
escapeMagickProperty( $s)
Escape a string for ImageMagick's property input (e.g.
Definition: TransformationalImageHandler.php:426
$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:1230
TransformationalImageHandler\doTransform
doTransform( $image, $dstPath, $dstUrl, $params, $flags=0)
Create a thumbnail.
Definition: TransformationalImageHandler.php:112
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1167
TransformationalImageHandler\transformCustom
transformCustom( $image, $params)
Transform an image using a custom command.
Definition: TransformationalImageHandler.php:390
TransformationalImageHandler\rotate
rotate( $file, $params)
Rotate a thumbnail.
Definition: TransformationalImageHandler.php:584
TransformationalImageHandler
Handler for images that need to be transformed.
Definition: TransformationalImageHandler.php:40
TransformationalImageHandler\getClientScalingThumbnailImage
getClientScalingThumbnailImage( $image, $scalerParams)
Get a ThumbnailImage that respresents an image that will be scaled client side.
Definition: TransformationalImageHandler.php:340
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:302
TransformationalImageHandler\escapeMagickPath
escapeMagickPath( $path, $scene=false)
Armour a string against ImageMagick's GetPathComponent().
Definition: TransformationalImageHandler.php:491
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:416
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:85
TransformationalImageHandler\escapeMagickInput
escapeMagickInput( $path, $scene=false)
Escape a string for ImageMagick's input filenames.
Definition: TransformationalImageHandler.php:456
TransformationalImageHandler\normaliseParams
normaliseParams( $image, &$params)
Definition: TransformationalImageHandler.php:49
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:597
$s
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
Definition: mergeMessageFileList.php:206
wfIsWindows
wfIsWindows()
Check if the operating system is Windows.
Definition: GlobalFunctions.php:1687
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:522
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:568
$cache
$cache
Definition: mcc.php:33
TransformationalImageHandler\transformImageMagickExt
transformImageMagickExt( $image, $params)
Transform an image using the Imagick PHP extension.
Definition: TransformationalImageHandler.php:375
$path
$path
Definition: NoLocalSettings.php:25
TransformationalImageHandler\canRotate
canRotate()
Returns whether the current scaler supports rotation.
Definition: TransformationalImageHandler.php:556
TransformationalImageHandler\isImageAreaOkForThumbnaling
isImageAreaOkForThumbnaling( $file, &$params)
Check if the file is smaller than the maximum image area for thumbnailing.
Definition: TransformationalImageHandler.php:612
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:993
wfShellExecWithStderr
wfShellExecWithStderr( $cmd, &$retval=null, $environ=[], $limits=[])
Execute a shell command, returning both stdout and stderr.
Definition: GlobalFunctions.php:1947