Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
30.74% covered (danger)
30.74%
91 / 296
7.14% covered (danger)
7.14%
1 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
BitmapHandler
30.74% covered (danger)
30.74%
91 / 296
7.14% covered (danger)
7.14%
1 / 14
3157.46
0.00% covered (danger)
0.00%
0 / 1
 getScalerType
55.56% covered (warning)
55.56%
10 / 18
0.00% covered (danger)
0.00%
0 / 1
13.62
 makeParamString
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 parseParamString
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 validateParam
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 normaliseParams
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
5.03
 imageMagickSubsampling
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
5.07
 transformImageMagick
66.67% covered (warning)
66.67%
52 / 78
0.00% covered (danger)
0.00%
0 / 1
34.81
 transformImageMagickExt
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
462
 transformCustom
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 transformGd
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 1
156
 imageJpegWrapper
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 canRotate
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
5.39
 autoRotateEnabled
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 rotate
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2/**
3 * Generic handler for bitmap images.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Media
22 */
23
24use MediaWiki\MainConfigNames;
25use MediaWiki\MediaWikiServices;
26use MediaWiki\Shell\Shell;
27
28/**
29 * Generic handler for bitmap images
30 *
31 * @stable to extend
32 * @ingroup Media
33 */
34class BitmapHandler extends TransformationalImageHandler {
35
36    /**
37     * Returns which scaler type should be used. Creates parent directories
38     * for $dstPath and returns 'client' on error
39     * @stable to override
40     *
41     * @param string|null $dstPath
42     * @param bool $checkDstPath
43     * @return string|callable One of client, im, custom, gd, imext or an array( object, method )
44     */
45    protected function getScalerType( $dstPath, $checkDstPath = true ) {
46        $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
47        $useImageResize = $mainConfig->get( MainConfigNames::UseImageResize );
48        $useImageMagick = $mainConfig->get( MainConfigNames::UseImageMagick );
49        $customConvertCommand = $mainConfig->get( MainConfigNames::CustomConvertCommand );
50        if ( !$dstPath && $checkDstPath ) {
51            # No output path available, client side scaling only
52            $scaler = 'client';
53        } elseif ( !$useImageResize ) {
54            $scaler = 'client';
55        } elseif ( $useImageMagick ) {
56            $scaler = 'im';
57        } elseif ( $customConvertCommand ) {
58            $scaler = 'custom';
59        } elseif ( function_exists( 'imagecreatetruecolor' ) ) {
60            $scaler = 'gd';
61        } elseif ( class_exists( 'Imagick' ) ) {
62            $scaler = 'imext';
63        } else {
64            $scaler = 'client';
65        }
66
67        return $scaler;
68    }
69
70    /**
71     * @inheritDoc
72     * @stable to override
73     */
74    public function makeParamString( $params ) {
75        $res = parent::makeParamString( $params );
76        if ( isset( $params['interlace'] ) && $params['interlace'] ) {
77            return "interlaced-$res";
78        }
79        return $res;
80    }
81
82    /**
83     * @inheritDoc
84     * @stable to override
85     */
86    public function parseParamString( $str ) {
87        $remainder = preg_replace( '/^interlaced-/', '', $str );
88        $params = parent::parseParamString( $remainder );
89        if ( $params === false ) {
90            return false;
91        }
92        $params['interlace'] = $str !== $remainder;
93        return $params;
94    }
95
96    /**
97     * @inheritDoc
98     * @stable to override
99     */
100    public function validateParam( $name, $value ) {
101        if ( $name === 'interlace' ) {
102            return $value === false || $value === true;
103        }
104        return parent::validateParam( $name, $value );
105    }
106
107    /**
108     * @stable to override
109     * @param File $image
110     * @param array &$params
111     * @return bool
112     */
113    public function normaliseParams( $image, &$params ) {
114        $maxInterlacingAreas = MediaWikiServices::getInstance()->getMainConfig()
115            ->get( MainConfigNames::MaxInterlacingAreas );
116        if ( !parent::normaliseParams( $image, $params ) ) {
117            return false;
118        }
119        $mimeType = $image->getMimeType();
120        $interlace = isset( $params['interlace'] ) && $params['interlace']
121            && isset( $maxInterlacingAreas[$mimeType] )
122            && $this->getImageArea( $image ) <= $maxInterlacingAreas[$mimeType];
123        $params['interlace'] = $interlace;
124        return true;
125    }
126
127    /**
128     * Get ImageMagick subsampling factors for the target JPEG pixel format.
129     *
130     * @param string $pixelFormat one of 'yuv444', 'yuv422', 'yuv420'
131     * @return string[] List of sampling factors
132     */
133    protected function imageMagickSubsampling( $pixelFormat ) {
134        switch ( $pixelFormat ) {
135            case 'yuv444':
136                return [ '1x1', '1x1', '1x1' ];
137            case 'yuv422':
138                return [ '2x1', '1x1', '1x1' ];
139            case 'yuv420':
140                return [ '2x2', '1x1', '1x1' ];
141            default:
142                throw new UnexpectedValueException( 'Invalid pixel format for JPEG output' );
143        }
144    }
145
146    /**
147     * Transform an image using ImageMagick
148     * @stable to override
149     *
150     * @param File $image File associated with this thumbnail
151     * @param array $params Array with scaler params
152     *
153     * @return MediaTransformError|false Error object if error occurred, false (=no error) otherwise
154     */
155    protected function transformImageMagick( $image, $params ) {
156        # use ImageMagick
157        $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
158        $sharpenReductionThreshold = $mainConfig->get( MainConfigNames::SharpenReductionThreshold );
159        $sharpenParameter = $mainConfig->get( MainConfigNames::SharpenParameter );
160        $maxAnimatedGifArea = $mainConfig->get( MainConfigNames::MaxAnimatedGifArea );
161        $imageMagickTempDir = $mainConfig->get( MainConfigNames::ImageMagickTempDir );
162        $imageMagickConvertCommand = $mainConfig->get( MainConfigNames::ImageMagickConvertCommand );
163        $jpegPixelFormat = $mainConfig->get( MainConfigNames::JpegPixelFormat );
164        $jpegQuality = $mainConfig->get( MainConfigNames::JpegQuality );
165        $quality = [];
166        $sharpen = [];
167        $scene = false;
168        $animation_pre = [];
169        $animation_post = [];
170        $decoderHint = [];
171        $subsampling = [];
172
173        if ( $params['mimeType'] === 'image/jpeg' ) {
174            $qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null;
175            $quality = [ '-quality', $qualityVal ?: (string)$jpegQuality ]; // 80% by default
176            if ( $params['interlace'] ) {
177                $animation_post = [ '-interlace', 'JPEG' ];
178            }
179            # Sharpening, see T8193
180            if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
181                / ( $params['srcWidth'] + $params['srcHeight'] )
182                < $sharpenReductionThreshold
183            ) {
184                $sharpen = [ '-sharpen', $sharpenParameter ];
185            }
186
187            // JPEG decoder hint to reduce memory, available since IM 6.5.6-2
188            $decoderHint = [ '-define', "jpeg:size={$params['physicalDimensions']}" ];
189
190            if ( $jpegPixelFormat ) {
191                $factors = $this->imageMagickSubsampling( $jpegPixelFormat );
192                $subsampling = [ '-sampling-factor', implode( ',', $factors ) ];
193            }
194        } elseif ( $params['mimeType'] === 'image/png' ) {
195            $quality = [ '-quality', '95' ]; // zlib 9, adaptive filtering
196            if ( $params['interlace'] ) {
197                $animation_post = [ '-interlace', 'PNG' ];
198            }
199        } elseif ( $params['mimeType'] === 'image/webp' ) {
200            $quality = [ '-quality', '95' ]; // zlib 9, adaptive filtering
201        } elseif ( $params['mimeType'] === 'image/gif' ) {
202            if ( $this->getImageArea( $image ) > $maxAnimatedGifArea ) {
203                // Extract initial frame only; we're so big it'll
204                // be a total drag. :P
205                $scene = 0;
206            } elseif ( $this->isAnimatedImage( $image ) ) {
207                // Coalesce is needed to scale animated GIFs properly (T3017).
208                $animation_pre = [ '-coalesce' ];
209
210                // We optimize the output, but -optimize is broken,
211                // use optimizeTransparency instead (T13822). Version >= IM 6.3.5
212                $animation_post = [ '-fuzz', '5%', '-layers', 'optimizeTransparency' ];
213            }
214            if ( $params['interlace'] && !$this->isAnimatedImage( $image ) ) {
215                // Version >= IM 6.3.4
216                // interlacing animated GIFs is a bad idea
217                $animation_post[] = '-interlace';
218                $animation_post[] = 'GIF';
219            }
220        } elseif ( $params['mimeType'] === 'image/x-xcf' ) {
221            // Before merging layers, we need to set the background
222            // to be transparent to preserve alpha, as -layers merge
223            // merges all layers on to a canvas filled with the
224            // background colour. After merging we reset the background
225            // to be white for the default background colour setting
226            // in the PNG image (which is used in old IE)
227            $animation_pre = [
228                '-background', 'transparent',
229                '-layers', 'merge',
230                '-background', 'white',
231            ];
232        }
233
234        // Use one thread only, to avoid deadlock bugs on OOM
235        $env = [ 'OMP_NUM_THREADS' => 1 ];
236        if ( (string)$imageMagickTempDir !== '' ) {
237            $env['MAGICK_TMPDIR'] = $imageMagickTempDir;
238        }
239
240        $rotation = isset( $params['disableRotation'] ) ? 0 : $this->getRotation( $image );
241        [ $width, $height ] = $this->extractPreRotationDimensions( $params, $rotation );
242
243        $cmd = Shell::escape( ...array_merge(
244            [ $imageMagickConvertCommand ],
245            $quality,
246            // Specify white background color, will be used for transparent images
247            // in Internet Explorer/Windows instead of default black.
248            [ '-background', 'white' ],
249            $decoderHint,
250            [ $this->escapeMagickInput( $params['srcPath'], $scene ) ],
251            $animation_pre,
252            // For the -thumbnail option a "!" is needed to force exact size,
253            // or ImageMagick may decide your ratio is wrong and slice off
254            // a pixel.
255            [ '-thumbnail', "{$width}x{$height}!" ],
256            // Add the source url as a comment to the thumb, but don't add the flag if there's no comment
257            ( $params['comment'] !== ''
258                ? [ '-set', 'comment', $this->escapeMagickProperty( $params['comment'] ) ]
259                : [] ),
260            // T108616: Avoid exposure of local file path
261            [ '+set', 'Thumb::URI' ],
262            [ '-depth', 8 ],
263            $sharpen,
264            [ '-rotate', "-$rotation" ],
265            $subsampling,
266            $animation_post,
267            [ $this->escapeMagickOutput( $params['dstPath'] ) ] ) );
268
269        wfDebug( __METHOD__ . ": running ImageMagick: $cmd" );
270        $retval = 0;
271        $err = wfShellExecWithStderr( $cmd, $retval, $env );
272
273        if ( $retval !== 0 ) {
274            $this->logErrorForExternalProcess( $retval, $err, $cmd );
275
276            return $this->getMediaTransformError( $params, "$err\nError code: $retval" );
277        }
278
279        return false; # No error
280    }
281
282    /**
283     * Transform an image using the Imagick PHP extension
284     *
285     * @param File $image File associated with this thumbnail
286     * @param array $params Array with scaler params
287     *
288     * @return MediaTransformError|false Error object if error occurred, false (=no error) otherwise
289     */
290    protected function transformImageMagickExt( $image, $params ) {
291        $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
292        $sharpenReductionThreshold = $mainConfig->get( MainConfigNames::SharpenReductionThreshold );
293        $sharpenParameter = $mainConfig->get( MainConfigNames::SharpenParameter );
294        $maxAnimatedGifArea = $mainConfig->get( MainConfigNames::MaxAnimatedGifArea );
295        $jpegPixelFormat = $mainConfig->get( MainConfigNames::JpegPixelFormat );
296        $jpegQuality = $mainConfig->get( MainConfigNames::JpegQuality );
297        try {
298            $im = new Imagick();
299            $im->readImage( $params['srcPath'] );
300
301            if ( $params['mimeType'] === 'image/jpeg' ) {
302                // Sharpening, see T8193
303                if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
304                    / ( $params['srcWidth'] + $params['srcHeight'] )
305                    < $sharpenReductionThreshold
306                ) {
307                    // Hack, since $wgSharpenParameter is written specifically for the command line convert
308                    [ $radius, $sigma ] = explode( 'x', $sharpenParameter, 2 );
309                    $im->sharpenImage( (float)$radius, (float)$sigma );
310                }
311                $qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null;
312                $im->setCompressionQuality( $qualityVal ?: $jpegQuality );
313                if ( $params['interlace'] ) {
314                    $im->setInterlaceScheme( Imagick::INTERLACE_JPEG );
315                }
316                if ( $jpegPixelFormat ) {
317                    $factors = $this->imageMagickSubsampling( $jpegPixelFormat );
318                    $im->setSamplingFactors( $factors );
319                }
320            } elseif ( $params['mimeType'] === 'image/png' ) {
321                $im->setCompressionQuality( 95 );
322                if ( $params['interlace'] ) {
323                    $im->setInterlaceScheme( Imagick::INTERLACE_PNG );
324                }
325            } elseif ( $params['mimeType'] === 'image/gif' ) {
326                if ( $this->getImageArea( $image ) > $maxAnimatedGifArea ) {
327                    // Extract initial frame only; we're so big it'll
328                    // be a total drag. :P
329                    $im->setImageScene( 0 );
330                } elseif ( $this->isAnimatedImage( $image ) ) {
331                    // Coalesce is needed to scale animated GIFs properly (T3017).
332                    $im = $im->coalesceImages();
333                }
334                // GIF interlacing is only available since 6.3.4
335                if ( $params['interlace'] ) {
336                    $im->setInterlaceScheme( Imagick::INTERLACE_GIF );
337                }
338            }
339
340            $rotation = isset( $params['disableRotation'] ) ? 0 : $this->getRotation( $image );
341            [ $width, $height ] = $this->extractPreRotationDimensions( $params, $rotation );
342
343            $im->setImageBackgroundColor( new ImagickPixel( 'white' ) );
344
345            // Call Imagick::thumbnailImage on each frame
346            foreach ( $im as $i => $frame ) {
347                if ( !$frame->thumbnailImage( $width, $height, /* fit */ false ) ) {
348                    return $this->getMediaTransformError( $params, "Error scaling frame $i" );
349                }
350            }
351            $im->setImageDepth( 8 );
352
353            if ( $rotation && !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
354                return $this->getMediaTransformError( $params, "Error rotating $rotation degrees" );
355            }
356
357            if ( $this->isAnimatedImage( $image ) ) {
358                wfDebug( __METHOD__ . ": Writing animated thumbnail" );
359                // This is broken somehow... can't find out how to fix it
360                $result = $im->writeImages( $params['dstPath'], true );
361            } else {
362                $result = $im->writeImage( $params['dstPath'] );
363            }
364            if ( !$result ) {
365                return $this->getMediaTransformError( $params,
366                    "Unable to write thumbnail to {$params['dstPath']}" );
367            }
368        } catch ( ImagickException $e ) {
369            return $this->getMediaTransformError( $params, $e->getMessage() );
370        }
371
372        return false;
373    }
374
375    /**
376     * Transform an image using a custom command
377     *
378     * @param File $image File associated with this thumbnail
379     * @param array $params Array with scaler params
380     *
381     * @return MediaTransformError|false Error object if error occurred, false (=no error) otherwise
382     */
383    protected function transformCustom( $image, $params ) {
384        // Use a custom convert command
385        $customConvertCommand = MediaWikiServices::getInstance()->getMainConfig()
386            ->get( MainConfigNames::CustomConvertCommand );
387
388        // Find all variables in the original command at once,
389        // so that replacement values cannot inject variable placeholders
390        $cmd = strtr( $customConvertCommand, [
391            '%s' => Shell::escape( $params['srcPath'] ),
392            '%d' => Shell::escape( $params['dstPath'] ),
393            '%w' => Shell::escape( $params['physicalWidth'] ),
394            '%h' => Shell::escape( $params['physicalHeight'] ),
395        ] );
396        wfDebug( __METHOD__ . ": Running custom convert command $cmd" );
397        $retval = 0;
398        $err = wfShellExecWithStderr( $cmd, $retval );
399
400        if ( $retval !== 0 ) {
401            $this->logErrorForExternalProcess( $retval, $err, $cmd );
402
403            return $this->getMediaTransformError( $params, $err );
404        }
405
406        return false; # No error
407    }
408
409    /**
410     * Transform an image using the built in GD library
411     *
412     * @param File $image File associated with this thumbnail
413     * @param array $params Array with scaler params
414     *
415     * @return MediaTransformError|false Error object if error occurred, false (=no error) otherwise
416     */
417    protected function transformGd( $image, $params ) {
418        # Use PHP's builtin GD library functions.
419        # First find out what kind of file this is, and select the correct
420        # input routine for this.
421
422        $typemap = [
423            'image/gif' => [ 'imagecreatefromgif', 'palette', false, 'imagegif' ],
424            'image/jpeg' => [ 'imagecreatefromjpeg', 'truecolor', true,
425                [ __CLASS__, 'imageJpegWrapper' ] ],
426            'image/png' => [ 'imagecreatefrompng', 'bits', false, 'imagepng' ],
427            'image/vnd.wap.wbmp' => [ 'imagecreatefromwbmp', 'palette', false, 'imagewbmp' ],
428            'image/xbm' => [ 'imagecreatefromxbm', 'palette', false, 'imagexbm' ],
429        ];
430
431        if ( !isset( $typemap[$params['mimeType']] ) ) {
432            $err = 'Image type not supported';
433            wfDebug( $err );
434            $errMsg = wfMessage( 'thumbnail_image-type' )->text();
435
436            return $this->getMediaTransformError( $params, $errMsg );
437        }
438        [ $loader, $colorStyle, $useQuality, $saveType ] = $typemap[$params['mimeType']];
439
440        if ( !function_exists( $loader ) ) {
441            $err = "Incomplete GD library configuration: missing function $loader";
442            wfDebug( $err );
443            $errMsg = wfMessage( 'thumbnail_gd-library', $loader )->text();
444
445            return $this->getMediaTransformError( $params, $errMsg );
446        }
447
448        if ( !file_exists( $params['srcPath'] ) ) {
449            $err = "File seems to be missing: {$params['srcPath']}";
450            wfDebug( $err );
451            $errMsg = wfMessage( 'thumbnail_image-missing', $params['srcPath'] )->text();
452
453            return $this->getMediaTransformError( $params, $errMsg );
454        }
455
456        if ( filesize( $params['srcPath'] ) === 0 ) {
457            $err = "Image file size seems to be zero.";
458            wfDebug( $err );
459            $errMsg = wfMessage( 'thumbnail_image-size-zero', $params['srcPath'] )->text();
460
461            return $this->getMediaTransformError( $params, $errMsg );
462        }
463
464        $src_image = $loader( $params['srcPath'] );
465
466        $rotation = function_exists( 'imagerotate' ) && !isset( $params['disableRotation'] ) ?
467            $this->getRotation( $image ) :
468            0;
469        [ $width, $height ] = $this->extractPreRotationDimensions( $params, $rotation );
470        $dst_image = imagecreatetruecolor( $width, $height );
471
472        // Initialise the destination image to transparent instead of
473        // the default solid black, to support PNG and GIF transparency nicely
474        $background = imagecolorallocate( $dst_image, 0, 0, 0 );
475        imagecolortransparent( $dst_image, $background );
476        imagealphablending( $dst_image, false );
477
478        if ( $colorStyle === 'palette' ) {
479            // Don't resample for paletted GIF images.
480            // It may just uglify them, and completely breaks transparency.
481            imagecopyresized( $dst_image, $src_image,
482                0, 0, 0, 0,
483                $width, $height,
484                imagesx( $src_image ), imagesy( $src_image ) );
485        } else {
486            imagecopyresampled( $dst_image, $src_image,
487                0, 0, 0, 0,
488                $width, $height,
489                imagesx( $src_image ), imagesy( $src_image ) );
490        }
491
492        if ( $rotation % 360 !== 0 && $rotation % 90 === 0 ) {
493            $rot_image = imagerotate( $dst_image, $rotation, 0 );
494            imagedestroy( $dst_image );
495            $dst_image = $rot_image;
496        }
497
498        imagesavealpha( $dst_image, true );
499
500        $funcParams = [ $dst_image, $params['dstPath'] ];
501        if ( $useQuality && isset( $params['quality'] ) ) {
502            $funcParams[] = $params['quality'];
503        }
504        // @phan-suppress-next-line PhanParamTooFewInternalUnpack,PhanParamTooFewUnpack There are at least 2 args
505        $saveType( ...$funcParams );
506
507        imagedestroy( $dst_image );
508        imagedestroy( $src_image );
509
510        return false; # No error
511    }
512
513    /**
514     * Callback for transformGd when transforming jpeg images.
515     *
516     * @phpcs:ignore MediaWiki.Commenting.FunctionComment.ObjectTypeHintParam
517     * @param resource|object $dst_image Image resource of the original image
518     * @param string $thumbPath File path to write the thumbnail image to
519     * @param int|null $quality Quality of the thumbnail from 1-100,
520     *    or null to use default quality.
521     */
522    public static function imageJpegWrapper( $dst_image, $thumbPath, $quality = null ) {
523        $jpegQuality = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::JpegQuality );
524
525        imageinterlace( $dst_image );
526        imagejpeg( $dst_image, $thumbPath, $quality ?? $jpegQuality );
527    }
528
529    /**
530     * Returns whether the current scaler supports rotation (im and gd do)
531     * @stable to override
532     *
533     * @return bool
534     */
535    public function canRotate() {
536        $scaler = $this->getScalerType( null, false );
537        switch ( $scaler ) {
538            case 'im':
539                # ImageMagick supports autorotation
540                return true;
541            case 'imext':
542                # Imagick::rotateImage
543                return true;
544            case 'gd':
545                # GD's imagerotate function is used to rotate images, but not
546                # all precompiled PHP versions have that function
547                return function_exists( 'imagerotate' );
548            default:
549                # Other scalers don't support rotation
550                return false;
551        }
552    }
553
554    /**
555     * @see $wgEnableAutoRotation
556     * @stable to override
557     * @return bool Whether auto rotation is enabled
558     */
559    public function autoRotateEnabled() {
560        $enableAutoRotation = MediaWikiServices::getInstance()->getMainConfig()
561            ->get( MainConfigNames::EnableAutoRotation );
562
563        if ( $enableAutoRotation === null ) {
564            // Only enable auto-rotation when we actually can
565            return $this->canRotate();
566        }
567
568        return $enableAutoRotation;
569    }
570
571    /**
572     * @stable to override
573     * @param File $file
574     * @param array $params Rotate parameters.
575     *   'rotation' clockwise rotation in degrees, allowed are multiples of 90
576     * @since 1.21
577     * @return MediaTransformError|false
578     */
579    public function rotate( $file, $params ) {
580        $imageMagickConvertCommand = MediaWikiServices::getInstance()
581            ->getMainConfig()->get( MainConfigNames::ImageMagickConvertCommand );
582
583        $rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
584        $scene = false;
585
586        $scaler = $this->getScalerType( null, false );
587        switch ( $scaler ) {
588            case 'im':
589                $cmd = Shell::escape( $imageMagickConvertCommand ) . " " .
590                    Shell::escape( $this->escapeMagickInput( $params['srcPath'], $scene ) ) .
591                    " -rotate " . Shell::escape( "-$rotation" ) . " " .
592                    Shell::escape( $this->escapeMagickOutput( $params['dstPath'] ) );
593                wfDebug( __METHOD__ . ": running ImageMagick: $cmd" );
594                $retval = 0;
595                $err = wfShellExecWithStderr( $cmd, $retval );
596                if ( $retval !== 0 ) {
597                    $this->logErrorForExternalProcess( $retval, $err, $cmd );
598
599                    return new MediaTransformError( 'thumbnail_error', 0, 0, $err );
600                }
601
602                return false;
603            case 'imext':
604                $im = new Imagick();
605                $im->readImage( $params['srcPath'] );
606                if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
607                    return new MediaTransformError( 'thumbnail_error', 0, 0,
608                        "Error rotating $rotation degrees" );
609                }
610                $result = $im->writeImage( $params['dstPath'] );
611                if ( !$result ) {
612                    return new MediaTransformError( 'thumbnail_error', 0, 0,
613                        "Unable to write image to {$params['dstPath']}" );
614                }
615
616                return false;
617            default:
618                return new MediaTransformError( 'thumbnail_error', 0, 0,
619                    "$scaler rotation not implemented" );
620        }
621    }
622}