MediaWiki REL1_39
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'] = intval( $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 } elseif ( $removed ) {
275 # Thumbnail was zero-byte and had to be removed
276 return new MediaTransformError( 'thumbnail_error',
277 $scalerParams['clientWidth'], $scalerParams['clientHeight'],
278 wfMessage( 'unknown-error' )
279 );
280 } elseif ( $mto ) {
281 // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
282 return $mto;
283 } else {
284 $newParams = [
285 'width' => $scalerParams['clientWidth'],
286 'height' => $scalerParams['clientHeight']
287 ];
288 if ( isset( $params['quality'] ) ) {
289 $newParams['quality'] = $params['quality'];
290 }
291 if ( isset( $params['page'] ) && $params['page'] ) {
292 $newParams['page'] = $params['page'];
293 }
294 return new ThumbnailImage( $image, $dstUrl, $dstPath, $newParams );
295 }
296 }
297
305 protected function getThumbnailSource( $file, $params ) {
306 return $file->getThumbnailSource( $params );
307 }
308
330 abstract protected function getScalerType( $dstPath, $checkDstPath = true );
331
343 protected function getClientScalingThumbnailImage( $image, $scalerParams ) {
344 $params = [
345 'width' => $scalerParams['clientWidth'],
346 'height' => $scalerParams['clientHeight']
347 ];
348
349 $url = $image->getUrl();
350 if ( isset( $scalerParams['isFilePageThumb'] ) && $scalerParams['isFilePageThumb'] ) {
351 // Use a versioned URL on file description pages
352 $url = $image->getFilePageThumbUrl( $url );
353 }
354
355 return new ThumbnailImage( $image, $url, null, $params );
356 }
357
369 protected function transformImageMagick( $image, $params ) {
370 return $this->getMediaTransformError( $params, "Unimplemented" );
371 }
372
384 protected function transformImageMagickExt( $image, $params ) {
385 return $this->getMediaTransformError( $params, "Unimplemented" );
386 }
387
399 protected function transformCustom( $image, $params ) {
400 return $this->getMediaTransformError( $params, "Unimplemented" );
401 }
402
410 public function getMediaTransformError( $params, $errMsg ) {
411 return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
412 $params['clientHeight'], $errMsg );
413 }
414
425 protected function transformGd( $image, $params ) {
426 return $this->getMediaTransformError( $params, "Unimplemented" );
427 }
428
435 protected function escapeMagickProperty( $s ) {
436 // Double the backslashes
437 $s = str_replace( '\\', '\\\\', $s );
438 // Double the percents
439 $s = str_replace( '%', '%%', $s );
440 // Escape initial - or @
441 if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
442 $s = '\\' . $s;
443 }
444
445 return $s;
446 }
447
465 protected function escapeMagickInput( $path, $scene = false ) {
466 # Die on initial metacharacters (caller should prepend path)
467 $firstChar = substr( $path, 0, 1 );
468 if ( $firstChar === '~' || $firstChar === '@' ) {
469 throw new MWException( __METHOD__ . ': cannot escape this path name' );
470 }
471
472 # Escape glob chars
473 $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
474
475 return $this->escapeMagickPath( $path, $scene );
476 }
477
485 protected function escapeMagickOutput( $path, $scene = false ) {
486 $path = str_replace( '%', '%%', $path );
487
488 return $this->escapeMagickPath( $path, $scene );
489 }
490
500 protected function escapeMagickPath( $path, $scene = false ) {
501 # Die on format specifiers (other than drive letters). The regex is
502 # meant to match all the formats you get from "convert -list format"
503 if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
504 if ( wfIsWindows() && is_dir( $m[0] ) ) {
505 // OK, it's a drive letter
506 // ImageMagick has a similar exception, see IsMagickConflict()
507 } else {
508 throw new MWException( __METHOD__ . ': unexpected colon character in path name' );
509 }
510 }
511
512 # If there are square brackets, add a do-nothing scene specification
513 # to force a literal interpretation
514 if ( $scene === false ) {
515 if ( strpos( $path, '[' ) !== false ) {
516 $path .= '[0--1]';
517 }
518 } else {
519 $path .= "[$scene]";
520 }
521
522 return $path;
523 }
524
531 protected function getMagickVersion() {
532 $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
533 $method = __METHOD__;
534 return $cache->getWithSetCallback(
535 $cache->makeGlobalKey( 'imagemagick-version' ),
536 $cache::TTL_HOUR,
537 static function () use ( $method ) {
538 $imageMagickConvertCommand = MediaWikiServices::getInstance()
539 ->getMainConfig()->get( MainConfigNames::ImageMagickConvertCommand );
540
541 $cmd = Shell::escape( $imageMagickConvertCommand ) . ' -version';
542 wfDebug( $method . ": Running convert -version" );
543 $retval = '';
544 $return = wfShellExecWithStderr( $cmd, $retval );
545 $x = preg_match(
546 '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches
547 );
548 if ( $x != 1 ) {
549 wfDebug( $method . ": ImageMagick version check failed" );
550 return false;
551 }
552
553 return $matches[1];
554 }
555 );
556 }
557
565 public function canRotate() {
566 return false;
567 }
568
577 public function autoRotateEnabled() {
578 return false;
579 }
580
593 public function rotate( $file, $params ) {
594 return new MediaTransformError( 'thumbnail_error', 0, 0,
595 static::class . ' rotation not implemented' );
596 }
597
606 public function mustRender( $file ) {
607 return $this->canRotate() && $this->getRotation( $file ) != 0;
608 }
609
621 public function isImageAreaOkForThumbnaling( $file, &$params ) {
622 $maxImageArea = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::MaxImageArea );
623
624 # For historical reasons, hook starts with BitmapHandler
625 $checkImageAreaHookResult = null;
626 Hooks::runner()->onBitmapHandlerCheckImageArea(
627 $file, $params, $checkImageAreaHookResult );
628
629 if ( $checkImageAreaHookResult !== null ) {
630 // was set by hook, so return that value
631 return (bool)$checkImageAreaHookResult;
632 }
633
634 if ( $maxImageArea === false ) {
635 // Checking is disabled, fine to thumbnail
636 return true;
637 }
638
639 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Checked by normaliseParams
640 $srcWidth = $file->getWidth( $params['page'] );
641 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Checked by normaliseParams
642 $srcHeight = $file->getHeight( $params['page'] );
643
644 if ( $srcWidth * $srcHeight > $maxImageArea
645 && !( $file->getMimeType() == 'image/jpeg'
646 && $this->getScalerType( null, false ) == 'im' )
647 ) {
648 # Only ImageMagick can efficiently downsize jpg images without loading
649 # the entire file in memory
650 return false;
651 }
652 return true;
653 }
654}
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.
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.
$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