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  return $mto;
278  } else {
279  $newParams = [
280  'width' => $scalerParams['clientWidth'],
281  'height' => $scalerParams['clientHeight']
282  ];
283  if ( isset( $params['quality'] ) ) {
284  $newParams['quality'] = $params['quality'];
285  }
286  if ( isset( $params['page'] ) && $params['page'] ) {
287  $newParams['page'] = $params['page'];
288  }
289  return new ThumbnailImage( $image, $dstUrl, $dstPath, $newParams );
290  }
291  }
292 
300  protected function getThumbnailSource( $file, $params ) {
301  return $file->getThumbnailSource( $params );
302  }
303 
325  abstract protected function getScalerType( $dstPath, $checkDstPath = true );
326 
338  protected function getClientScalingThumbnailImage( $image, $scalerParams ) {
339  $params = [
340  'width' => $scalerParams['clientWidth'],
341  'height' => $scalerParams['clientHeight']
342  ];
343 
344  return new ThumbnailImage( $image, $image->getUrl(), null, $params );
345  }
346 
358  protected function transformImageMagick( $image, $params ) {
359  return $this->getMediaTransformError( $params, "Unimplemented" );
360  }
361 
373  protected function transformImageMagickExt( $image, $params ) {
374  return $this->getMediaTransformError( $params, "Unimplemented" );
375  }
376 
388  protected function transformCustom( $image, $params ) {
389  return $this->getMediaTransformError( $params, "Unimplemented" );
390  }
391 
399  public function getMediaTransformError( $params, $errMsg ) {
400  return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
401  $params['clientHeight'], $errMsg );
402  }
403 
414  protected function transformGd( $image, $params ) {
415  return $this->getMediaTransformError( $params, "Unimplemented" );
416  }
417 
424  protected function escapeMagickProperty( $s ) {
425  // Double the backslashes
426  $s = str_replace( '\\', '\\\\', $s );
427  // Double the percents
428  $s = str_replace( '%', '%%', $s );
429  // Escape initial - or @
430  if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
431  $s = '\\' . $s;
432  }
433 
434  return $s;
435  }
436 
454  protected function escapeMagickInput( $path, $scene = false ) {
455  # Die on initial metacharacters (caller should prepend path)
456  $firstChar = substr( $path, 0, 1 );
457  if ( $firstChar === '~' || $firstChar === '@' ) {
458  throw new MWException( __METHOD__ . ': cannot escape this path name' );
459  }
460 
461  # Escape glob chars
462  $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
463 
464  return $this->escapeMagickPath( $path, $scene );
465  }
466 
474  protected function escapeMagickOutput( $path, $scene = false ) {
475  $path = str_replace( '%', '%%', $path );
476 
477  return $this->escapeMagickPath( $path, $scene );
478  }
479 
489  protected function escapeMagickPath( $path, $scene = false ) {
490  # Die on format specifiers (other than drive letters). The regex is
491  # meant to match all the formats you get from "convert -list format"
492  if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
493  if ( wfIsWindows() && is_dir( $m[0] ) ) {
494  // OK, it's a drive letter
495  // ImageMagick has a similar exception, see IsMagickConflict()
496  } else {
497  throw new MWException( __METHOD__ . ': unexpected colon character in path name' );
498  }
499  }
500 
501  # If there are square brackets, add a do-nothing scene specification
502  # to force a literal interpretation
503  if ( $scene === false ) {
504  if ( strpos( $path, '[' ) !== false ) {
505  $path .= '[0--1]';
506  }
507  } else {
508  $path .= "[$scene]";
509  }
510 
511  return $path;
512  }
513 
520  protected function getMagickVersion() {
521  $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
522  $method = __METHOD__;
523  return $cache->getWithSetCallback(
524  $cache->makeGlobalKey( 'imagemagick-version' ),
525  $cache::TTL_HOUR,
526  function () use ( $method ) {
528 
529  $cmd = Shell::escape( $wgImageMagickConvertCommand ) . ' -version';
530  wfDebug( $method . ": Running convert -version" );
531  $retval = '';
532  $return = wfShellExecWithStderr( $cmd, $retval );
533  $x = preg_match(
534  '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches
535  );
536  if ( $x != 1 ) {
537  wfDebug( $method . ": ImageMagick version check failed" );
538  return false;
539  }
540 
541  return $matches[1];
542  }
543  );
544  }
545 
553  public function canRotate() {
554  return false;
555  }
556 
565  public function autoRotateEnabled() {
566  return false;
567  }
568 
581  public function rotate( $file, $params ) {
582  return new MediaTransformError( 'thumbnail_error', 0, 0,
583  static::class . ' rotation not implemented' );
584  }
585 
594  public function mustRender( $file ) {
595  return $this->canRotate() && $this->getRotation( $file ) != 0;
596  }
597 
609  public function isImageAreaOkForThumbnaling( $file, &$params ) {
610  global $wgMaxImageArea;
611 
612  # For historical reasons, hook starts with BitmapHandler
613  $checkImageAreaHookResult = null;
614  Hooks::runner()->onBitmapHandlerCheckImageArea(
615  $file, $params, $checkImageAreaHookResult );
616 
617  if ( $checkImageAreaHookResult !== null ) {
618  // was set by hook, so return that value
619  return (bool)$checkImageAreaHookResult;
620  }
621 
622  $srcWidth = $file->getWidth( $params['page'] );
623  $srcHeight = $file->getHeight( $params['page'] );
624 
625  if ( $srcWidth * $srcHeight > $wgMaxImageArea
626  && !( $file->getMimeType() == 'image/jpeg'
627  && $this->getScalerType( false, false ) == 'im' )
628  ) {
629  # Only ImageMagick can efficiently downsize jpg images without loading
630  # the entire file in memory
631  return false;
632  }
633  return true;
634  }
635 }
MediaHandler\removeBadFile
removeBadFile( $dstPath, $retval=0)
Check for zero-sized thumbnails.
Definition: MediaHandler.php:738
TransformationalImageHandler\transformImageMagick
transformImageMagick( $image, $params)
Transform an image using ImageMagick.
Definition: TransformationalImageHandler.php:358
MediaWiki\Shell\Shell
Executes shell commands.
Definition: Shell.php:44
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:1890
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:154
TransformationalImageHandler\getMediaTransformError
getMediaTransformError( $params, $errMsg)
Get a MediaTransformError with error 'thumbnail_error'.
Definition: TransformationalImageHandler.php:399
TransformationalImageHandler\escapeMagickOutput
escapeMagickOutput( $path, $scene=false)
Escape a string for ImageMagick's output filename.
Definition: TransformationalImageHandler.php:474
TransformationalImageHandler\escapeMagickProperty
escapeMagickProperty( $s)
Escape a string for ImageMagick's property input (e.g.
Definition: TransformationalImageHandler.php:424
$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:1282
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:1219
$s
$s
Definition: mergeMessageFileList.php:185
TransformationalImageHandler\transformCustom
transformCustom( $image, $params)
Transform an image using a custom command.
Definition: TransformationalImageHandler.php:388
TransformationalImageHandler\rotate
rotate( $file, $params)
Rotate a thumbnail.
Definition: TransformationalImageHandler.php:581
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:338
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:988
TransformationalImageHandler\getThumbnailSource
getThumbnailSource( $file, $params)
Get the source file for the transform.
Definition: TransformationalImageHandler.php:300
TransformationalImageHandler\escapeMagickPath
escapeMagickPath( $path, $scene=false)
Armour a string against ImageMagick's GetPathComponent().
Definition: TransformationalImageHandler.php:489
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:414
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:454
$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:1336
TransformationalImageHandler\normaliseParams
normaliseParams( $image, &$params)
Stable to override.
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:909
TransformationalImageHandler\mustRender
mustRender( $file)
Returns whether the file needs to be rendered.
Definition: TransformationalImageHandler.php:594
$wgImageMagickConvertCommand
$wgImageMagickConvertCommand
The convert command shipped with ImageMagick.
Definition: DefaultSettings.php:1183
wfIsWindows
wfIsWindows()
Check if the operating system is Windows.
Definition: GlobalFunctions.php:1846
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:172
TransformationalImageHandler\getMagickVersion
getMagickVersion()
Retrieve the version of the installed ImageMagick You can use PHPs version_compare() to use this valu...
Definition: TransformationalImageHandler.php:520
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:565
$cache
$cache
Definition: mcc.php:33
TransformationalImageHandler\transformImageMagickExt
transformImageMagickExt( $image, $params)
Transform an image using the Imagick PHP extension.
Definition: TransformationalImageHandler.php:373
$path
$path
Definition: NoLocalSettings.php:25
TransformationalImageHandler\canRotate
canRotate()
Returns whether the current scaler supports rotation.
Definition: TransformationalImageHandler.php:553
TransformationalImageHandler\isImageAreaOkForThumbnaling
isImageAreaOkForThumbnaling( $file, &$params)
Check if the file is smaller than the maximum image area for thumbnailing.
Definition: TransformationalImageHandler.php:609
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:806
wfShellExecWithStderr
wfShellExecWithStderr( $cmd, &$retval=null, $environ=[], $limits=[])
Execute a shell command, returning both stdout and stderr.
Definition: GlobalFunctions.php:2105