MediaWiki 1.44.0
TransformationalImageHandler.php
Go to the documentation of this file.
1<?php
34
52 public function normaliseParams( $image, &$params ) {
53 if ( !parent::normaliseParams( $image, $params ) ) {
54 return false;
55 }
56
57 # Obtain the source, pre-rotation dimensions
58 $srcWidth = $image->getWidth( $params['page'] );
59 $srcHeight = $image->getHeight( $params['page'] );
60
61 # Don't make an image bigger than the source
62 if ( $params['physicalWidth'] >= $srcWidth ) {
63 $params['physicalWidth'] = $srcWidth;
64 $params['physicalHeight'] = $srcHeight;
65
66 # Skip scaling limit checks if no scaling is required
67 # due to requested size being bigger than source.
68 if ( !$image->mustRender() ) {
69 return true;
70 }
71 }
72
73 return true;
74 }
75
88 public function extractPreRotationDimensions( $params, $rotation ) {
89 if ( $rotation === 90 || $rotation === 270 ) {
90 // We'll resize before rotation, so swap the dimensions again
91 $width = $params['physicalHeight'];
92 $height = $params['physicalWidth'];
93 } else {
94 $width = $params['physicalWidth'];
95 $height = $params['physicalHeight'];
96 }
97
98 return [ $width, $height ];
99 }
100
115 public function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
116 if ( !$this->normaliseParams( $image, $params ) ) {
117 return new TransformParameterError( $params );
118 }
119
120 // Create a parameter array to pass to the scaler
121 $scalerParams = [
122 // The size to which the image will be resized
123 'physicalWidth' => $params['physicalWidth'],
124 'physicalHeight' => $params['physicalHeight'],
125 'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}",
126 // The size of the image on the page
127 'clientWidth' => $params['width'],
128 'clientHeight' => $params['height'],
129 // Comment as will be added to the Exif of the thumbnail
130 'comment' => isset( $params['descriptionUrl'] )
131 ? "File source: {$params['descriptionUrl']}"
132 : '',
133 // Properties of the original image
134 'srcWidth' => $image->getWidth(),
135 'srcHeight' => $image->getHeight(),
136 'mimeType' => $image->getMimeType(),
137 'dstPath' => $dstPath,
138 'dstUrl' => $dstUrl,
139 'interlace' => $params['interlace'] ?? false,
140 'isFilePageThumb' => $params['isFilePageThumb'] ?? false,
141 ];
142
143 if ( isset( $params['quality'] ) && $params['quality'] === 'low' ) {
144 $scalerParams['quality'] = 30;
145 }
146
147 // For subclasses that might be paged.
148 if ( $image->isMultipage() && isset( $params['page'] ) ) {
149 $scalerParams['page'] = (int)$params['page'];
150 }
151
152 # Determine scaler type
153 $scaler = $this->getScalerType( $dstPath );
154
155 if ( is_array( $scaler ) ) {
156 $scalerName = get_class( $scaler[0] );
157 } else {
158 $scalerName = $scaler;
159 }
160
161 wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} " .
162 "thumbnail of {$image->getPath()} at $dstPath using scaler $scalerName" );
163
164 if ( !$image->mustRender() &&
165 $scalerParams['physicalWidth'] == $scalerParams['srcWidth']
166 && $scalerParams['physicalHeight'] == $scalerParams['srcHeight']
167 && !isset( $scalerParams['quality'] )
168 ) {
169 # normaliseParams (or the user) wants us to return the unscaled image
170 wfDebug( __METHOD__ . ": returning unscaled image" );
171
172 return $this->getClientScalingThumbnailImage( $image, $scalerParams );
173 }
174
175 if ( $scaler === 'client' ) {
176 # Client-side image scaling, use the source URL
177 # Using the destination URL in a TRANSFORM_LATER request would be incorrect
178 return $this->getClientScalingThumbnailImage( $image, $scalerParams );
179 }
180
181 if ( $image->isTransformedLocally() && !$this->isImageAreaOkForThumbnaling( $image, $params ) ) {
182 $maxImageArea = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::MaxImageArea );
183 return new TransformTooBigImageAreaError( $params, $maxImageArea );
184 }
185
186 if ( $flags & self::TRANSFORM_LATER ) {
187 wfDebug( __METHOD__ . ": Transforming later per flags." );
188 $newParams = [
189 'width' => $scalerParams['clientWidth'],
190 'height' => $scalerParams['clientHeight']
191 ];
192 if ( isset( $params['quality'] ) ) {
193 $newParams['quality'] = $params['quality'];
194 }
195 if ( isset( $params['page'] ) && $params['page'] ) {
196 $newParams['page'] = $params['page'];
197 }
198 return new ThumbnailImage( $image, $dstUrl, false, $newParams );
199 }
200
201 # Try to make a target path for the thumbnail
202 if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
203 wfDebug( __METHOD__ . ": Unable to create thumbnail destination " .
204 "directory, falling back to client scaling" );
205
206 return $this->getClientScalingThumbnailImage( $image, $scalerParams );
207 }
208
209 # Transform functions and binaries need a FS source file
210 $thumbnailSource = $this->getThumbnailSource( $image, $params );
211
212 // If the source isn't the original, disable EXIF rotation because it's already been applied
213 if ( $scalerParams['srcWidth'] != $thumbnailSource['width']
214 || $scalerParams['srcHeight'] != $thumbnailSource['height'] ) {
215 $scalerParams['disableRotation'] = true;
216 }
217
218 $scalerParams['srcPath'] = $thumbnailSource['path'];
219 $scalerParams['srcWidth'] = $thumbnailSource['width'];
220 $scalerParams['srcHeight'] = $thumbnailSource['height'];
221
222 if ( $scalerParams['srcPath'] === false ) { // Failed to get local copy
223 wfDebugLog( 'thumbnail',
224 sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
225 wfHostname(), $image->getName() ) );
226
227 return new MediaTransformError( 'thumbnail_error',
228 $scalerParams['clientWidth'], $scalerParams['clientHeight'],
229 wfMessage( 'filemissing' )
230 );
231 }
232
233 // Try a hook. Called "Bitmap" for historical reasons.
235 $mto = null;
236 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
237 ->onBitmapHandlerTransform( $this, $image, $scalerParams, $mto );
238 if ( $mto !== null ) {
239 wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto" );
240 $scaler = 'hookaborted';
241 }
242
243 // $scaler will return a MediaTransformError on failure, or false on success.
244 // If the scaler is successful, 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 = $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 // @phan-suppress-next-line PhanTypeMismatchArgument Relaying on bool/int conversion to cast objects correct
273 $removed = $this->removeBadFile( $dstPath, (bool)$err );
274 if ( $err ) {
275 # transform returned MediaTransforError
276 return $err;
277 }
278
279 if ( $removed ) {
280 // Thumbnail was zero-byte and had to be removed
281 return new MediaTransformError( 'thumbnail_error',
282 $scalerParams['clientWidth'], $scalerParams['clientHeight'],
283 wfMessage( 'unknown-error' )
284 );
285 }
286
287 if ( $mto ) {
288 // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
289 return $mto;
290 }
291
292 $newParams = [
293 'width' => $scalerParams['clientWidth'],
294 'height' => $scalerParams['clientHeight']
295 ];
296 if ( isset( $params['quality'] ) ) {
297 $newParams['quality'] = $params['quality'];
298 }
299 if ( isset( $params['page'] ) && $params['page'] ) {
300 $newParams['page'] = $params['page'];
301 }
302 return new ThumbnailImage( $image, $dstUrl, $dstPath, $newParams );
303 }
304
312 protected function getThumbnailSource( $file, $params ) {
313 return $file->getThumbnailSource( $params );
314 }
315
337 abstract protected function getScalerType( $dstPath, $checkDstPath = true );
338
350 protected function getClientScalingThumbnailImage( $image, $scalerParams ) {
351 $params = [
352 'width' => $scalerParams['clientWidth'],
353 'height' => $scalerParams['clientHeight']
354 ];
355
356 $url = $image->getUrl();
357 if ( isset( $scalerParams['isFilePageThumb'] ) && $scalerParams['isFilePageThumb'] ) {
358 // Use a versioned URL on file description pages
359 $url = $image->getFilePageThumbUrl( $url );
360 }
361
362 return new ThumbnailImage( $image, $url, null, $params );
363 }
364
376 protected function transformImageMagick( $image, $params ) {
377 return $this->getMediaTransformError( $params, "Unimplemented" );
378 }
379
391 protected function transformImageMagickExt( $image, $params ) {
392 return $this->getMediaTransformError( $params, "Unimplemented" );
393 }
394
406 protected function transformCustom( $image, $params ) {
407 return $this->getMediaTransformError( $params, "Unimplemented" );
408 }
409
417 public function getMediaTransformError( $params, $errMsg ) {
418 return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
419 $params['clientHeight'], $errMsg );
420 }
421
432 protected function transformGd( $image, $params ) {
433 return $this->getMediaTransformError( $params, "Unimplemented" );
434 }
435
442 protected function escapeMagickProperty( $s ) {
443 // Double the backslashes
444 $s = str_replace( '\\', '\\\\', $s );
445 // Double the percents
446 $s = str_replace( '%', '%%', $s );
447 // Escape initial - or @
448 if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
449 $s = '\\' . $s;
450 }
451
452 return $s;
453 }
454
471 protected function escapeMagickInput( $path, $scene = false ) {
472 # Die on initial metacharacters (caller should prepend path)
473 $firstChar = substr( $path, 0, 1 );
474 if ( $firstChar === '~' || $firstChar === '@' ) {
475 throw new InvalidArgumentException( __METHOD__ . ': cannot escape this path name' );
476 }
477
478 # Escape glob chars
479 $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
480
481 return $this->escapeMagickPath( $path, $scene );
482 }
483
491 protected function escapeMagickOutput( $path, $scene = false ) {
492 $path = str_replace( '%', '%%', $path );
493
494 return $this->escapeMagickPath( $path, $scene );
495 }
496
505 protected function escapeMagickPath( $path, $scene = false ) {
506 # Die on format specifiers (other than drive letters). The regex is
507 # meant to match all the formats you get from "convert -list format"
508 if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
509 if ( wfIsWindows() && is_dir( $m[0] ) ) {
510 // OK, it's a drive letter
511 // ImageMagick has a similar exception, see IsMagickConflict()
512 } else {
513 throw new InvalidArgumentException( __METHOD__ . ': unexpected colon character in path name' );
514 }
515 }
516
517 # If there are square brackets, add a do-nothing scene specification
518 # to force a literal interpretation
519 if ( $scene === false ) {
520 if ( strpos( $path, '[' ) !== false ) {
521 $path .= '[0--1]';
522 }
523 } else {
524 $path .= "[$scene]";
525 }
526
527 return $path;
528 }
529
536 protected function getMagickVersion() {
537 $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
538 $method = __METHOD__;
539 return $cache->getWithSetCallback(
540 $cache->makeGlobalKey( 'imagemagick-version' ),
541 $cache::TTL_HOUR,
542 static function () use ( $method ) {
543 $imageMagickConvertCommand = MediaWikiServices::getInstance()
544 ->getMainConfig()->get( MainConfigNames::ImageMagickConvertCommand );
545
546 $cmd = Shell::escape( $imageMagickConvertCommand ) . ' -version';
547 wfDebug( $method . ": Running convert -version" );
548 $retval = '';
549 $return = wfShellExecWithStderr( $cmd, $retval );
550 $x = preg_match(
551 '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches
552 );
553 if ( $x != 1 ) {
554 wfDebug( $method . ": ImageMagick version check failed" );
555 return false;
556 }
557
558 return $matches[1];
559 }
560 );
561 }
562
570 public function canRotate() {
571 return false;
572 }
573
582 public function autoRotateEnabled() {
583 return false;
584 }
585
598 public function rotate( $file, $params ) {
599 return new MediaTransformError( 'thumbnail_error', 0, 0,
600 static::class . ' rotation not implemented' );
601 }
602
611 public function mustRender( $file ) {
612 return $this->canRotate() && $this->getRotation( $file ) != 0;
613 }
614
626 public function isImageAreaOkForThumbnaling( $file, &$params ) {
627 $maxImageArea = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::MaxImageArea );
628
629 # For historical reasons, hook starts with BitmapHandler
630 $checkImageAreaHookResult = null;
631 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )->onBitmapHandlerCheckImageArea(
632 $file, $params, $checkImageAreaHookResult );
633
634 if ( $checkImageAreaHookResult !== null ) {
635 // was set by hook, so return that value
636 return (bool)$checkImageAreaHookResult;
637 }
638
639 if ( $maxImageArea === false ) {
640 // Checking is disabled, fine to thumbnail
641 return true;
642 }
643
644 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Checked by normaliseParams
645 $srcWidth = $file->getWidth( $params['page'] );
646 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Checked by normaliseParams
647 $srcHeight = $file->getHeight( $params['page'] );
648
649 if ( $srcWidth * $srcHeight > $maxImageArea
650 && !( $file->getMimeType() === 'image/jpeg'
651 && $this->getScalerType( null, false ) === 'im' )
652 ) {
653 # Only ImageMagick can efficiently downsize jpg images without loading
654 # the entire file in memory
655 return false;
656 }
657 return true;
658 }
659}
wfIsWindows()
Check if the operating system is Windows.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfHostname()
Get host name of the current machine, for use in error reporting.
wfShellExecWithStderr( $cmd, &$retval=null, $environ=[], $limits=[])
Execute a shell command, returning both stdout and stderr.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfMkdirParents( $dir, $mode=null, $caller=null)
Make directory, and make all parent directories if they don't exist.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Media handler abstract base class for images.
getRotation( $file)
On supporting image formats, try to read out the low-level orientation of the file and return the ang...
removeBadFile( $dstPath, $retval=0)
Check for zero-sized thumbnails.
Basic media transform error class.
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:93
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Executes shell commands.
Definition Shell.php:46
Media transform output for images.
Shortcut class for parameter validation errors.
Shortcut class for parameter file size errors.
Handler for images that need to be transformed.
getMagickVersion()
Retrieve the version of the installed ImageMagick You can use PHPs version_compare() to use this valu...
escapeMagickInput( $path, $scene=false)
Escape a string for ImageMagick's input filenames.
autoRotateEnabled()
Should we automatically rotate an image based on exif.
isImageAreaOkForThumbnaling( $file, &$params)
Check if the file is smaller than the maximum image area for thumbnailing.
getThumbnailSource( $file, $params)
Get the source file for the transform.
getClientScalingThumbnailImage( $image, $scalerParams)
Get a ThumbnailImage that respresents an image that will be scaled client side.
getScalerType( $dstPath, $checkDstPath=true)
Returns what sort of scaler type should be used.
doTransform( $image, $dstPath, $dstUrl, $params, $flags=0)
Create a thumbnail.
escapeMagickPath( $path, $scene=false)
Armour a string against ImageMagick's GetPathComponent().
escapeMagickProperty( $s)
Escape a string for ImageMagick's property input (e.g.
canRotate()
Returns whether the current scaler supports rotation.
transformImageMagick( $image, $params)
Transform an image using ImageMagick.
transformGd( $image, $params)
Transform an image using the built in GD library.
escapeMagickOutput( $path, $scene=false)
Escape a string for ImageMagick's output filename.
extractPreRotationDimensions( $params, $rotation)
Extracts the width/height if the image will be scaled before rotating.
transformCustom( $image, $params)
Transform an image using a custom command.
mustRender( $file)
Returns whether the file needs to be rendered.
transformImageMagickExt( $image, $params)
Transform an image using the Imagick PHP extension.
getMediaTransformError( $params, $errMsg)
Get a MediaTransformError with error 'thumbnail_error'.
rotate( $file, $params)
Rotate a thumbnail.