Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
15.69% covered (danger)
15.69%
32 / 204
0.00% covered (danger)
0.00%
0 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
TransformationalImageHandler
15.69% covered (danger)
15.69%
32 / 204
0.00% covered (danger)
0.00%
0 / 20
4112.16
0.00% covered (danger)
0.00%
0 / 1
 normaliseParams
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 extractPreRotationDimensions
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 doTransform
28.57% covered (danger)
28.57%
32 / 112
0.00% covered (danger)
0.00%
0 / 1
593.30
 getThumbnailSource
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getScalerType
n/a
0 / 0
n/a
0 / 0
0
 getClientScalingThumbnailImage
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 transformImageMagick
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 transformImageMagickExt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 transformCustom
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMediaTransformError
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 transformGd
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 escapeMagickProperty
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 escapeMagickInput
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 escapeMagickOutput
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 escapeMagickPath
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
 getMagickVersion
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
6
 canRotate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 autoRotateEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 rotate
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 mustRender
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 isImageAreaOkForThumbnaling
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2/**
3 * Base class for handlers which require transforming images in a
4 * similar way as BitmapHandler does.
5 *
6 * This was split from BitmapHandler on the basis that some extensions
7 * might want to work in a similar way to BitmapHandler, but for
8 * different formats.
9 *
10 * @license GPL-2.0-or-later
11 * @file
12 * @ingroup Media
13 */
14
15use MediaWiki\FileRepo\File\File;
16use MediaWiki\HookContainer\HookRunner;
17use MediaWiki\MainConfigNames;
18use MediaWiki\MediaWikiServices;
19use MediaWiki\Shell\Shell;
20
21/**
22 * Handler for images that need to be transformed
23 *
24 * @stable to extend
25 *
26 * @since 1.24
27 * @ingroup Media
28 */
29abstract class TransformationalImageHandler extends ImageHandler {
30    /**
31     * @stable to override
32     * @param File $image
33     * @param array &$params Transform parameters. Entries with the keys 'width'
34     * and 'height' are the respective screen width and height, while the keys
35     * 'physicalWidth' and 'physicalHeight' indicate the thumbnail dimensions.
36     * @return bool
37     */
38    public function normaliseParams( $image, &$params ) {
39        if ( !parent::normaliseParams( $image, $params ) ) {
40            return false;
41        }
42
43        # Obtain the source, pre-rotation dimensions
44        $srcWidth = $image->getWidth( $params['page'] );
45        $srcHeight = $image->getHeight( $params['page'] );
46
47        # Don't make an image bigger than the source
48        if ( $params['physicalWidth'] >= $srcWidth ) {
49            $params['physicalWidth'] = $srcWidth;
50            $params['physicalHeight'] = $srcHeight;
51
52            # Skip scaling limit checks if no scaling is required
53            # due to requested size being bigger than source.
54            if ( !$image->mustRender() ) {
55                return true;
56            }
57        }
58
59        return true;
60    }
61
62    /**
63     * Extracts the width/height if the image will be scaled before rotating
64     *
65     * This will match the physical size/aspect ratio of the original image
66     * prior to application of the rotation -- so for a portrait image that's
67     * stored as raw landscape with 90-degress rotation, the resulting size
68     * will be wider than it is tall.
69     *
70     * @param array $params Parameters as returned by normaliseParams
71     * @param int $rotation The rotation angle that will be applied
72     * @return array ($width, $height) array
73     */
74    public function extractPreRotationDimensions( $params, $rotation ) {
75        if ( $rotation === 90 || $rotation === 270 ) {
76            // We'll resize before rotation, so swap the dimensions again
77            $width = $params['physicalHeight'];
78            $height = $params['physicalWidth'];
79        } else {
80            $width = $params['physicalWidth'];
81            $height = $params['physicalHeight'];
82        }
83
84        return [ $width, $height ];
85    }
86
87    /**
88     * Create a thumbnail.
89     *
90     * This sets up various parameters, and then calls a helper method
91     * based on $this->getScalerType in order to scale the image.
92     * @stable to override
93     *
94     * @param File $image
95     * @param string $dstPath
96     * @param string $dstUrl
97     * @param array $params
98     * @param int $flags
99     * @return MediaTransformError|ThumbnailImage|TransformParameterError
100     */
101    public function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
102        if ( !$this->normaliseParams( $image, $params ) ) {
103            return new TransformParameterError( $params );
104        }
105
106        // Create a parameter array to pass to the scaler
107        $scalerParams = [
108            // The size to which the image will be resized
109            'physicalWidth' => $params['physicalWidth'],
110            'physicalHeight' => $params['physicalHeight'],
111            'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}",
112            // The size of the image on the page
113            'clientWidth' => $params['width'],
114            'clientHeight' => $params['height'],
115            // Comment as will be added to the Exif of the thumbnail
116            'comment' => isset( $params['descriptionUrl'] )
117                ? "File source: {$params['descriptionUrl']}"
118                : '',
119            // Properties of the original image
120            'srcWidth' => $image->getWidth(),
121            'srcHeight' => $image->getHeight(),
122            'mimeType' => $image->getMimeType(),
123            'dstPath' => $dstPath,
124            'dstUrl' => $dstUrl,
125            'interlace' => $params['interlace'] ?? false,
126            'isFilePageThumb' => $params['isFilePageThumb'] ?? false,
127        ];
128
129        if ( isset( $params['quality'] ) && $params['quality'] === 'low' ) {
130            $scalerParams['quality'] = 30;
131        }
132
133        // For subclasses that might be paged.
134        if ( $image->isMultipage() && isset( $params['page'] ) ) {
135            $scalerParams['page'] = (int)$params['page'];
136        }
137
138        # Determine scaler type
139        $scaler = $this->getScalerType( $dstPath );
140
141        if ( is_array( $scaler ) ) {
142            $scalerName = get_class( $scaler[0] );
143        } else {
144            $scalerName = $scaler;
145        }
146
147        wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} " .
148            "thumbnail of {$image->getPath()} at $dstPath using scaler $scalerName" );
149
150        if ( !$image->mustRender() &&
151            $scalerParams['physicalWidth'] == $scalerParams['srcWidth']
152            && $scalerParams['physicalHeight'] == $scalerParams['srcHeight']
153            && !isset( $scalerParams['quality'] )
154        ) {
155            # normaliseParams (or the user) wants us to return the unscaled image
156            wfDebug( __METHOD__ . ": returning unscaled image" );
157
158            return $this->getClientScalingThumbnailImage( $image, $scalerParams );
159        }
160
161        if ( $scaler === 'client' ) {
162            # Client-side image scaling, use the source URL
163            # Using the destination URL in a TRANSFORM_LATER request would be incorrect
164            return $this->getClientScalingThumbnailImage( $image, $scalerParams );
165        }
166
167        if ( $image->isTransformedLocally() && !$this->isImageAreaOkForThumbnaling( $image, $params ) ) {
168            $maxImageArea = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::MaxImageArea );
169            return new TransformTooBigImageAreaError( $params, $maxImageArea );
170        }
171
172        if ( $flags & self::TRANSFORM_LATER ) {
173            wfDebug( __METHOD__ . ": Transforming later per flags." );
174            $newParams = [
175                'width' => $scalerParams['clientWidth'],
176                'height' => $scalerParams['clientHeight']
177            ];
178            if ( isset( $params['quality'] ) ) {
179                $newParams['quality'] = $params['quality'];
180            }
181            if ( isset( $params['page'] ) && $params['page'] ) {
182                $newParams['page'] = $params['page'];
183            }
184            return new ThumbnailImage( $image, $dstUrl, false, $newParams );
185        }
186
187        # Try to make a target path for the thumbnail
188        if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
189            wfDebug( __METHOD__ . ": Unable to create thumbnail destination " .
190                "directory, falling back to client scaling" );
191
192            return $this->getClientScalingThumbnailImage( $image, $scalerParams );
193        }
194
195        # Transform functions and binaries need a FS source file
196        $thumbnailSource = $this->getThumbnailSource( $image, $params );
197
198        // If the source isn't the original, disable EXIF rotation because it's already been applied
199        if ( $scalerParams['srcWidth'] != $thumbnailSource['width']
200            || $scalerParams['srcHeight'] != $thumbnailSource['height'] ) {
201            $scalerParams['disableRotation'] = true;
202        }
203
204        $scalerParams['srcPath'] = $thumbnailSource['path'];
205        $scalerParams['srcWidth'] = $thumbnailSource['width'];
206        $scalerParams['srcHeight'] = $thumbnailSource['height'];
207
208        if ( $scalerParams['srcPath'] === false ) { // Failed to get local copy
209            wfDebugLog( 'thumbnail',
210                sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
211                    wfHostname(), $image->getName() ) );
212
213            return new MediaTransformError( 'thumbnail_error',
214                $scalerParams['clientWidth'], $scalerParams['clientHeight'],
215                wfMessage( 'filemissing' )
216            );
217        }
218
219        // Try a hook. Called "Bitmap" for historical reasons.
220        /** @var MediaTransformOutput $mto */
221        $mto = null;
222        ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
223            ->onBitmapHandlerTransform( $this, $image, $scalerParams, $mto );
224        if ( $mto !== null ) {
225            wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto" );
226            $scaler = 'hookaborted';
227        }
228
229        // $scaler will return a MediaTransformError on failure, or false on success.
230        // If the scaler is successful, it will have created a thumbnail at the destination
231        // path.
232        if ( is_array( $scaler ) && is_callable( $scaler ) ) {
233            // Allow subclasses to specify their own rendering methods.
234            $err = $scaler( $image, $scalerParams );
235        } else {
236            switch ( $scaler ) {
237                case 'hookaborted':
238                    # Handled by the hook above
239                    $err = $mto->isError() ? $mto : false;
240                    break;
241                case 'im':
242                    $err = $this->transformImageMagick( $image, $scalerParams );
243                    break;
244                case 'custom':
245                    $err = $this->transformCustom( $image, $scalerParams );
246                    break;
247                case 'imext':
248                    $err = $this->transformImageMagickExt( $image, $scalerParams );
249                    break;
250                case 'gd':
251                default:
252                    $err = $this->transformGd( $image, $scalerParams );
253                    break;
254            }
255        }
256
257        // Remove the file if a zero-byte thumbnail was created, or if there was an error
258        // @phan-suppress-next-line PhanTypeMismatchArgument Relaying on bool/int conversion to cast objects correct
259        $removed = $this->removeBadFile( $dstPath, (bool)$err );
260        if ( $err ) {
261            # transform returned MediaTransforError
262            return $err;
263        }
264
265        if ( $removed ) {
266            // Thumbnail was zero-byte and had to be removed
267            return new MediaTransformError( 'thumbnail_error',
268                $scalerParams['clientWidth'], $scalerParams['clientHeight'],
269                wfMessage( 'unknown-error' )
270            );
271        }
272
273        if ( $mto ) {
274            // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
275            return $mto;
276        }
277
278        $newParams = [
279            'width' => $scalerParams['clientWidth'],
280            'height' => $scalerParams['clientHeight']
281        ];
282        if ( isset( $params['quality'] ) ) {
283            $newParams['quality'] = $params['quality'];
284        }
285        if ( isset( $params['page'] ) && $params['page'] ) {
286            $newParams['page'] = $params['page'];
287        }
288        return new ThumbnailImage( $image, $dstUrl, $dstPath, $newParams );
289    }
290
291    /**
292     * Get the source file for the transform
293     *
294     * @param File $file
295     * @param array $params
296     * @return array Array with keys  width, height and path.
297     */
298    protected function getThumbnailSource( $file, $params ) {
299        return $file->getThumbnailSource( $params );
300    }
301
302    /**
303     * Returns what sort of scaler type should be used.
304     *
305     * Values can be one of client, im, custom, gd, imext, or an array
306     * of object, method-name to call that specific method.
307     *
308     * If specifying a custom scaler command with [ Obj, method ],
309     * the method in question should take 2 parameters, a File object,
310     * and a $scalerParams array with various options (See doTransform
311     * for what is in $scalerParams). On error it should return a
312     * MediaTransformError object. On success it should return false,
313     * and simply make sure the thumbnail file is located at
314     * $scalerParams['dstPath'].
315     *
316     * If there is a problem with the output path, it returns "client"
317     * to do client side scaling.
318     *
319     * @param string|null $dstPath
320     * @param bool $checkDstPath Check that $dstPath is valid
321     * @return string|callable One of client, im, custom, gd, imext, or a callable
322     */
323    abstract protected function getScalerType( $dstPath, $checkDstPath = true );
324
325    /**
326     * Get a ThumbnailImage that respresents an image that will be scaled
327     * client side
328     *
329     * @stable to override
330     * @param File $image File associated with this thumbnail
331     * @param array $scalerParams Array with scaler params
332     * @return ThumbnailImage
333     *
334     * @todo FIXME: No rotation support
335     */
336    protected function getClientScalingThumbnailImage( $image, $scalerParams ) {
337        $params = [
338            'width' => $scalerParams['clientWidth'],
339            'height' => $scalerParams['clientHeight']
340        ];
341
342        $url = $image->getUrl();
343        if ( isset( $scalerParams['isFilePageThumb'] ) && $scalerParams['isFilePageThumb'] ) {
344            // Use a versioned URL on file description pages
345            $url = $image->getFilePageThumbUrl( $url );
346        }
347
348        return new ThumbnailImage( $image, $url, null, $params );
349    }
350
351    /**
352     * Transform an image using ImageMagick
353     *
354     * This is a stub method. The real method is in BitmapHandler.
355     *
356     * @stable to override
357     * @param File $image File associated with this thumbnail
358     * @param array $params Array with scaler params
359     *
360     * @return MediaTransformError|false Error object if error occurred, false (=no error) otherwise
361     */
362    protected function transformImageMagick( $image, $params ) {
363        return $this->getMediaTransformError( $params, "Unimplemented" );
364    }
365
366    /**
367     * Transform an image using the Imagick PHP extension
368     *
369     * This is a stub method. The real method is in BitmapHandler.
370     *
371     * @stable to override
372     * @param File $image File associated with this thumbnail
373     * @param array $params Array with scaler params
374     *
375     * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
376     */
377    protected function transformImageMagickExt( $image, $params ) {
378        return $this->getMediaTransformError( $params, "Unimplemented" );
379    }
380
381    /**
382     * Transform an image using a custom command
383     *
384     * This is a stub method. The real method is in BitmapHandler.
385     *
386     * @stable to override
387     * @param File $image File associated with this thumbnail
388     * @param array $params Array with scaler params
389     *
390     * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
391     */
392    protected function transformCustom( $image, $params ) {
393        return $this->getMediaTransformError( $params, "Unimplemented" );
394    }
395
396    /**
397     * Get a MediaTransformError with error 'thumbnail_error'
398     *
399     * @param array $params Parameter array as passed to the transform* functions
400     * @param string $errMsg Error message
401     * @return MediaTransformError
402     */
403    public function getMediaTransformError( $params, $errMsg ) {
404        return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
405            $params['clientHeight'], $errMsg );
406    }
407
408    /**
409     * Transform an image using the built in GD library
410     *
411     * This is a stub method. The real method is in BitmapHandler.
412     *
413     * @param File $image File associated with this thumbnail
414     * @param array $params Array with scaler params
415     *
416     * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
417     */
418    protected function transformGd( $image, $params ) {
419        return $this->getMediaTransformError( $params, "Unimplemented" );
420    }
421
422    /**
423     * Escape a string for ImageMagick's property input (e.g. -set -comment)
424     * See InterpretImageProperties() in magick/property.c
425     * @param string $s
426     * @return string
427     */
428    protected function escapeMagickProperty( $s ) {
429        // Double the backslashes
430        $s = str_replace( '\\', '\\\\', $s );
431        // Double the percents
432        $s = str_replace( '%', '%%', $s );
433        // Escape initial - or @
434        if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
435            $s = '\\' . $s;
436        }
437
438        return $s;
439    }
440
441    /**
442     * Escape a string for ImageMagick's input filenames. See ExpandFilenames()
443     * and GetPathComponent() in magick/utility.c.
444     *
445     * This won't work with an initial ~ or @, so input files should be prefixed
446     * with the directory name.
447     *
448     * Glob character unescaping is broken in ImageMagick before 6.6.1-5, but
449     * it's broken in a way that doesn't involve trying to convert every file
450     * in a directory, so we're better off escaping and waiting for the bugfix
451     * to filter down to users.
452     *
453     * @param string $path The file path
454     * @param string|false $scene The scene specification, or false if there is none
455     * @return string
456     */
457    protected function escapeMagickInput( $path, $scene = false ) {
458        # Die on initial metacharacters (caller should prepend path)
459        $firstChar = substr( $path, 0, 1 );
460        if ( $firstChar === '~' || $firstChar === '@' ) {
461            throw new InvalidArgumentException( __METHOD__ . ': cannot escape this path name' );
462        }
463
464        # Escape glob chars
465        $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
466
467        return $this->escapeMagickPath( $path, $scene );
468    }
469
470    /**
471     * Escape a string for ImageMagick's output filename. See
472     * InterpretImageFilename() in magick/image.c.
473     * @param string $path The file path
474     * @param string|false $scene The scene specification, or false if there is none
475     * @return string
476     */
477    protected function escapeMagickOutput( $path, $scene = false ) {
478        $path = str_replace( '%', '%%', $path );
479
480        return $this->escapeMagickPath( $path, $scene );
481    }
482
483    /**
484     * Armour a string against ImageMagick's GetPathComponent(). This is a
485     * helper function for escapeMagickInput() and escapeMagickOutput().
486     *
487     * @param string $path The file path
488     * @param string|false $scene The scene specification, or false if there is none
489     * @return string
490     */
491    protected function escapeMagickPath( $path, $scene = false ) {
492        # Die on format specifiers (other than drive letters). The regex is
493        # meant to match all the formats you get from "convert -list format"
494        if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
495            if ( wfIsWindows() && is_dir( $m[0] ) ) {
496                // OK, it's a drive letter
497                // ImageMagick has a similar exception, see IsMagickConflict()
498            } else {
499                throw new InvalidArgumentException( __METHOD__ . ': unexpected colon character in path name' );
500            }
501        }
502
503        # If there are square brackets, add a do-nothing scene specification
504        # to force a literal interpretation
505        if ( $scene === false ) {
506            if ( str_contains( $path, '[' ) ) {
507                $path .= '[0--1]';
508            }
509        } else {
510            $path .= "[$scene]";
511        }
512
513        return $path;
514    }
515
516    /**
517     * Retrieve the version of the installed ImageMagick
518     * You can use PHPs version_compare() to use this value
519     * Value is cached for one hour.
520     * @return string|false Representing the IM version; false on error
521     */
522    protected function getMagickVersion() {
523        $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
524        $method = __METHOD__;
525        return $cache->getWithSetCallback(
526            $cache->makeGlobalKey( 'imagemagick-version' ),
527            $cache::TTL_HOUR,
528            static function () use ( $method ) {
529                $imageMagickConvertCommand = MediaWikiServices::getInstance()
530                    ->getMainConfig()->get( MainConfigNames::ImageMagickConvertCommand );
531
532                $cmd = Shell::escape( $imageMagickConvertCommand ) . ' -version';
533                wfDebug( $method . ": Running convert -version" );
534                $retval = '';
535                $return = wfShellExecWithStderr( $cmd, $retval );
536                $x = preg_match(
537                    '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches
538                );
539                if ( $x != 1 ) {
540                    wfDebug( $method . ": ImageMagick version check failed" );
541                    return false;
542                }
543
544                return $matches[1];
545            }
546        );
547    }
548
549    /**
550     * Returns whether the current scaler supports rotation.
551     *
552     * @since 1.24 No longer static
553     * @stable to override
554     * @return bool
555     */
556    public function canRotate() {
557        return false;
558    }
559
560    /**
561     * Should we automatically rotate an image based on exif
562     *
563     * @since 1.24 No longer static
564     * @stable to override
565     * @see $wgEnableAutoRotation
566     * @return bool Whether auto rotation is enabled
567     */
568    public function autoRotateEnabled() {
569        return false;
570    }
571
572    /**
573     * Rotate a thumbnail.
574     *
575     * This is a stub. See BitmapHandler::rotate.
576     *
577     * @stable to override
578     * @param File $file
579     * @param array{rotation:int,srcPath:string,dstPath:string} $params Rotate parameters.
580     *   'rotation' clockwise rotation in degrees, allowed are multiples of 90
581     * @since 1.24 Is non-static. From 1.21 it was static
582     * @return MediaTransformError|false
583     */
584    public function rotate( $file, $params ) {
585        return new MediaTransformError( 'thumbnail_error', 0, 0,
586            static::class . ' rotation not implemented' );
587    }
588
589    /**
590     * Returns whether the file needs to be rendered. Returns true if the
591     * file requires rotation and we are able to rotate it.
592     *
593     * @stable to override
594     * @param File $file
595     * @return bool
596     */
597    public function mustRender( $file ) {
598        return $this->canRotate() && $this->getRotation( $file ) != 0;
599    }
600
601    /**
602     * Check if the file is smaller than the maximum image area for thumbnailing.
603     *
604     * Runs the 'BitmapHandlerCheckImageArea' hook.
605     *
606     * @stable to override
607     * @param File $file
608     * @param array &$params
609     * @return bool
610     * @since 1.25
611     */
612    public function isImageAreaOkForThumbnaling( $file, &$params ) {
613        $maxImageArea = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::MaxImageArea );
614
615        # For historical reasons, hook starts with BitmapHandler
616        $checkImageAreaHookResult = null;
617        ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )->onBitmapHandlerCheckImageArea(
618            $file, $params, $checkImageAreaHookResult );
619
620        if ( $checkImageAreaHookResult !== null ) {
621            // was set by hook, so return that value
622            return (bool)$checkImageAreaHookResult;
623        }
624
625        if ( $maxImageArea === false ) {
626            // Checking is disabled, fine to thumbnail
627            return true;
628        }
629
630        $srcWidth = $file->getWidth( $params['page'] );
631        $srcHeight = $file->getHeight( $params['page'] );
632
633        if ( $srcWidth * $srcHeight > $maxImageArea
634            && !( $file->getMimeType() === 'image/jpeg'
635                && $this->getScalerType( null, false ) === 'im' )
636        ) {
637            # Only ImageMagick can efficiently downsize jpg images without loading
638            # the entire file in memory
639            return false;
640        }
641        return true;
642    }
643}