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