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