MediaWiki REL1_35
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 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}
$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:44
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.
normaliseParams( $image, &$params)
Stable to override.
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
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42