MediaWiki REL1_40
TransformationalImageHandler.php
Go to the documentation of this file.
1<?php
32
50 public function normaliseParams( $image, &$params ) {
51 if ( !parent::normaliseParams( $image, $params ) ) {
52 return false;
53 }
54
55 # Obtain the source, pre-rotation dimensions
56 $srcWidth = $image->getWidth( $params['page'] );
57 $srcHeight = $image->getHeight( $params['page'] );
58
59 # Don't make an image bigger than the source
60 if ( $params['physicalWidth'] >= $srcWidth ) {
61 $params['physicalWidth'] = $srcWidth;
62 $params['physicalHeight'] = $srcHeight;
63
64 # Skip scaling limit checks if no scaling is required
65 # due to requested size being bigger than source.
66 if ( !$image->mustRender() ) {
67 return true;
68 }
69 }
70
71 return true;
72 }
73
86 public function extractPreRotationDimensions( $params, $rotation ) {
87 if ( $rotation === 90 || $rotation === 270 ) {
88 // We'll resize before rotation, so swap the dimensions again
89 $width = $params['physicalHeight'];
90 $height = $params['physicalWidth'];
91 } else {
92 $width = $params['physicalWidth'];
93 $height = $params['physicalHeight'];
94 }
95
96 return [ $width, $height ];
97 }
98
113 public function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
114 if ( !$this->normaliseParams( $image, $params ) ) {
115 return new TransformParameterError( $params );
116 }
117
118 // Create a parameter array to pass to the scaler
119 $scalerParams = [
120 // The size to which the image will be resized
121 'physicalWidth' => $params['physicalWidth'],
122 'physicalHeight' => $params['physicalHeight'],
123 'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}",
124 // The size of the image on the page
125 'clientWidth' => $params['width'],
126 'clientHeight' => $params['height'],
127 // Comment as will be added to the Exif of the thumbnail
128 'comment' => isset( $params['descriptionUrl'] )
129 ? "File source: {$params['descriptionUrl']}"
130 : '',
131 // Properties of the original image
132 'srcWidth' => $image->getWidth(),
133 'srcHeight' => $image->getHeight(),
134 'mimeType' => $image->getMimeType(),
135 'dstPath' => $dstPath,
136 'dstUrl' => $dstUrl,
137 'interlace' => $params['interlace'] ?? false,
138 'isFilePageThumb' => $params['isFilePageThumb'] ?? false,
139 ];
140
141 if ( isset( $params['quality'] ) && $params['quality'] === 'low' ) {
142 $scalerParams['quality'] = 30;
143 }
144
145 // For subclasses that might be paged.
146 if ( $image->isMultipage() && isset( $params['page'] ) ) {
147 $scalerParams['page'] = (int)$params['page'];
148 }
149
150 # Determine scaler type
151 $scaler = $this->getScalerType( $dstPath );
152
153 if ( is_array( $scaler ) ) {
154 $scalerName = get_class( $scaler[0] );
155 } else {
156 $scalerName = $scaler;
157 }
158
159 wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} " .
160 "thumbnail at $dstPath using scaler $scalerName" );
161
162 if ( !$image->mustRender() &&
163 $scalerParams['physicalWidth'] == $scalerParams['srcWidth']
164 && $scalerParams['physicalHeight'] == $scalerParams['srcHeight']
165 && !isset( $scalerParams['quality'] )
166 ) {
167 # normaliseParams (or the user) wants us to return the unscaled image
168 wfDebug( __METHOD__ . ": returning unscaled image" );
169
170 return $this->getClientScalingThumbnailImage( $image, $scalerParams );
171 }
172
173 if ( $scaler === 'client' ) {
174 # Client-side image scaling, use the source URL
175 # Using the destination URL in a TRANSFORM_LATER request would be incorrect
176 return $this->getClientScalingThumbnailImage( $image, $scalerParams );
177 }
178
179 if ( $image->isTransformedLocally() && !$this->isImageAreaOkForThumbnaling( $image, $params ) ) {
180 $maxImageArea = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::MaxImageArea );
181 return new TransformTooBigImageAreaError( $params, $maxImageArea );
182 }
183
184 if ( $flags & self::TRANSFORM_LATER ) {
185 wfDebug( __METHOD__ . ": Transforming later per flags." );
186 $newParams = [
187 'width' => $scalerParams['clientWidth'],
188 'height' => $scalerParams['clientHeight']
189 ];
190 if ( isset( $params['quality'] ) ) {
191 $newParams['quality'] = $params['quality'];
192 }
193 if ( isset( $params['page'] ) && $params['page'] ) {
194 $newParams['page'] = $params['page'];
195 }
196 return new ThumbnailImage( $image, $dstUrl, false, $newParams );
197 }
198
199 # Try to make a target path for the thumbnail
200 if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
201 wfDebug( __METHOD__ . ": Unable to create thumbnail destination " .
202 "directory, falling back to client scaling" );
203
204 return $this->getClientScalingThumbnailImage( $image, $scalerParams );
205 }
206
207 # Transform functions and binaries need a FS source file
208 $thumbnailSource = $this->getThumbnailSource( $image, $params );
209
210 // If the source isn't the original, disable EXIF rotation because it's already been applied
211 if ( $scalerParams['srcWidth'] != $thumbnailSource['width']
212 || $scalerParams['srcHeight'] != $thumbnailSource['height'] ) {
213 $scalerParams['disableRotation'] = true;
214 }
215
216 $scalerParams['srcPath'] = $thumbnailSource['path'];
217 $scalerParams['srcWidth'] = $thumbnailSource['width'];
218 $scalerParams['srcHeight'] = $thumbnailSource['height'];
219
220 if ( $scalerParams['srcPath'] === false ) { // Failed to get local copy
221 wfDebugLog( 'thumbnail',
222 sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
223 wfHostname(), $image->getName() ) );
224
225 return new MediaTransformError( 'thumbnail_error',
226 $scalerParams['clientWidth'], $scalerParams['clientHeight'],
227 wfMessage( 'filemissing' )
228 );
229 }
230
231 // Try a hook. Called "Bitmap" for historical reasons.
233 $mto = null;
234 Hooks::runner()->onBitmapHandlerTransform( $this, $image, $scalerParams, $mto );
235 if ( $mto !== null ) {
236 wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto" );
237 $scaler = 'hookaborted';
238 }
239
240 // $scaler will return a MediaTransformError on failure, or false on success.
241 // If the scaler is successful, it will have created a thumbnail at the destination
242 // path.
243 if ( is_array( $scaler ) && is_callable( $scaler ) ) {
244 // Allow subclasses to specify their own rendering methods.
245 $err = call_user_func( $scaler, $image, $scalerParams );
246 } else {
247 switch ( $scaler ) {
248 case 'hookaborted':
249 # Handled by the hook above
250 $err = $mto->isError() ? $mto : false;
251 break;
252 case 'im':
253 $err = $this->transformImageMagick( $image, $scalerParams );
254 break;
255 case 'custom':
256 $err = $this->transformCustom( $image, $scalerParams );
257 break;
258 case 'imext':
259 $err = $this->transformImageMagickExt( $image, $scalerParams );
260 break;
261 case 'gd':
262 default:
263 $err = $this->transformGd( $image, $scalerParams );
264 break;
265 }
266 }
267
268 // Remove the file if a zero-byte thumbnail was created, or if there was an error
269 // @phan-suppress-next-line PhanTypeMismatchArgument Relaying on bool/int conversion to cast objects correct
270 $removed = $this->removeBadFile( $dstPath, (bool)$err );
271 if ( $err ) {
272 # transform returned MediaTransforError
273 return $err;
274 }
275
276 if ( $removed ) {
277 // Thumbnail was zero-byte and had to be removed
278 return new MediaTransformError( 'thumbnail_error',
279 $scalerParams['clientWidth'], $scalerParams['clientHeight'],
280 wfMessage( 'unknown-error' )
281 );
282 }
283
284 if ( $mto ) {
285 // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
286 return $mto;
287 }
288
289 $newParams = [
290 'width' => $scalerParams['clientWidth'],
291 'height' => $scalerParams['clientHeight']
292 ];
293 if ( isset( $params['quality'] ) ) {
294 $newParams['quality'] = $params['quality'];
295 }
296 if ( isset( $params['page'] ) && $params['page'] ) {
297 $newParams['page'] = $params['page'];
298 }
299 return new ThumbnailImage( $image, $dstUrl, $dstPath, $newParams );
300 }
301
309 protected function getThumbnailSource( $file, $params ) {
310 return $file->getThumbnailSource( $params );
311 }
312
334 abstract protected function getScalerType( $dstPath, $checkDstPath = true );
335
347 protected function getClientScalingThumbnailImage( $image, $scalerParams ) {
348 $params = [
349 'width' => $scalerParams['clientWidth'],
350 'height' => $scalerParams['clientHeight']
351 ];
352
353 $url = $image->getUrl();
354 if ( isset( $scalerParams['isFilePageThumb'] ) && $scalerParams['isFilePageThumb'] ) {
355 // Use a versioned URL on file description pages
356 $url = $image->getFilePageThumbUrl( $url );
357 }
358
359 return new ThumbnailImage( $image, $url, null, $params );
360 }
361
373 protected function transformImageMagick( $image, $params ) {
374 return $this->getMediaTransformError( $params, "Unimplemented" );
375 }
376
388 protected function transformImageMagickExt( $image, $params ) {
389 return $this->getMediaTransformError( $params, "Unimplemented" );
390 }
391
403 protected function transformCustom( $image, $params ) {
404 return $this->getMediaTransformError( $params, "Unimplemented" );
405 }
406
414 public function getMediaTransformError( $params, $errMsg ) {
415 return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
416 $params['clientHeight'], $errMsg );
417 }
418
429 protected function transformGd( $image, $params ) {
430 return $this->getMediaTransformError( $params, "Unimplemented" );
431 }
432
439 protected function escapeMagickProperty( $s ) {
440 // Double the backslashes
441 $s = str_replace( '\\', '\\\\', $s );
442 // Double the percents
443 $s = str_replace( '%', '%%', $s );
444 // Escape initial - or @
445 if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
446 $s = '\\' . $s;
447 }
448
449 return $s;
450 }
451
469 protected function escapeMagickInput( $path, $scene = false ) {
470 # Die on initial metacharacters (caller should prepend path)
471 $firstChar = substr( $path, 0, 1 );
472 if ( $firstChar === '~' || $firstChar === '@' ) {
473 throw new MWException( __METHOD__ . ': cannot escape this path name' );
474 }
475
476 # Escape glob chars
477 $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
478
479 return $this->escapeMagickPath( $path, $scene );
480 }
481
489 protected function escapeMagickOutput( $path, $scene = false ) {
490 $path = str_replace( '%', '%%', $path );
491
492 return $this->escapeMagickPath( $path, $scene );
493 }
494
504 protected function escapeMagickPath( $path, $scene = false ) {
505 # Die on format specifiers (other than drive letters). The regex is
506 # meant to match all the formats you get from "convert -list format"
507 if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
508 if ( wfIsWindows() && is_dir( $m[0] ) ) {
509 // OK, it's a drive letter
510 // ImageMagick has a similar exception, see IsMagickConflict()
511 } else {
512 throw new MWException( __METHOD__ . ': unexpected colon character in path name' );
513 }
514 }
515
516 # If there are square brackets, add a do-nothing scene specification
517 # to force a literal interpretation
518 if ( $scene === false ) {
519 if ( strpos( $path, '[' ) !== false ) {
520 $path .= '[0--1]';
521 }
522 } else {
523 $path .= "[$scene]";
524 }
525
526 return $path;
527 }
528
535 protected function getMagickVersion() {
536 $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
537 $method = __METHOD__;
538 return $cache->getWithSetCallback(
539 $cache->makeGlobalKey( 'imagemagick-version' ),
540 $cache::TTL_HOUR,
541 static function () use ( $method ) {
542 $imageMagickConvertCommand = MediaWikiServices::getInstance()
543 ->getMainConfig()->get( MainConfigNames::ImageMagickConvertCommand );
544
545 $cmd = Shell::escape( $imageMagickConvertCommand ) . ' -version';
546 wfDebug( $method . ": Running convert -version" );
547 $retval = '';
548 $return = wfShellExecWithStderr( $cmd, $retval );
549 $x = preg_match(
550 '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches
551 );
552 if ( $x != 1 ) {
553 wfDebug( $method . ": ImageMagick version check failed" );
554 return false;
555 }
556
557 return $matches[1];
558 }
559 );
560 }
561
569 public function canRotate() {
570 return false;
571 }
572
581 public function autoRotateEnabled() {
582 return false;
583 }
584
597 public function rotate( $file, $params ) {
598 return new MediaTransformError( 'thumbnail_error', 0, 0,
599 static::class . ' rotation not implemented' );
600 }
601
610 public function mustRender( $file ) {
611 return $this->canRotate() && $this->getRotation( $file ) != 0;
612 }
613
625 public function isImageAreaOkForThumbnaling( $file, &$params ) {
626 $maxImageArea = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::MaxImageArea );
627
628 # For historical reasons, hook starts with BitmapHandler
629 $checkImageAreaHookResult = null;
630 Hooks::runner()->onBitmapHandlerCheckImageArea(
631 $file, $params, $checkImageAreaHookResult );
632
633 if ( $checkImageAreaHookResult !== null ) {
634 // was set by hook, so return that value
635 return (bool)$checkImageAreaHookResult;
636 }
637
638 if ( $maxImageArea === false ) {
639 // Checking is disabled, fine to thumbnail
640 return true;
641 }
642
643 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Checked by normaliseParams
644 $srcWidth = $file->getWidth( $params['page'] );
645 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Checked by normaliseParams
646 $srcHeight = $file->getHeight( $params['page'] );
647
648 if ( $srcWidth * $srcHeight > $maxImageArea
649 && !( $file->getMimeType() === 'image/jpeg'
650 && $this->getScalerType( null, false ) === 'im' )
651 ) {
652 # Only ImageMagick can efficiently downsize jpg images without loading
653 # the entire file in memory
654 return false;
655 }
656 return true;
657 }
658}
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.
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.
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.
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42