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