Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
15.69% |
32 / 204 |
|
0.00% |
0 / 20 |
CRAP | |
0.00% |
0 / 1 |
TransformationalImageHandler | |
15.69% |
32 / 204 |
|
0.00% |
0 / 20 |
4112.16 | |
0.00% |
0 / 1 |
normaliseParams | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
extractPreRotationDimensions | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
doTransform | |
28.57% |
32 / 112 |
|
0.00% |
0 / 1 |
593.30 | |||
getThumbnailSource | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getScalerType | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getClientScalingThumbnailImage | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
transformImageMagick | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
transformImageMagickExt | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
transformCustom | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMediaTransformError | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
transformGd | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
escapeMagickProperty | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
escapeMagickInput | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
escapeMagickOutput | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
escapeMagickPath | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
42 | |||
getMagickVersion | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
6 | |||
canRotate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
autoRotateEnabled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
rotate | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
mustRender | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
isImageAreaOkForThumbnaling | |
0.00% |
0 / 15 |
|
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 | |
29 | use MediaWiki\FileRepo\File\File; |
30 | use MediaWiki\HookContainer\HookRunner; |
31 | use MediaWiki\MainConfigNames; |
32 | use MediaWiki\MediaWikiServices; |
33 | use 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 | */ |
43 | abstract 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 | } |