MediaWiki REL1_37
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.
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}
$wgMaxImageArea
The maximum number of pixels a source image can have if it is to be scaled down by a scaler that requ...
$wgImageMagickConvertCommand
The convert command shipped with ImageMagick.
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.
wfIsWindows()
Check if the operating system is Windows.
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.
MediaWiki exception.
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.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Executes shell commands.
Definition Shell.php:45
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.
$cache
Definition mcc.php:33
foreach( $mmfl['setupFiles'] as $fileName) if($queue) if(empty( $mmfl['quiet'])) $s
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42