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\HookContainer\HookRunner; |
30 | use MediaWiki\MainConfigNames; |
31 | use MediaWiki\MediaWikiServices; |
32 | use 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 | */ |
42 | abstract 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 BitmapHandler. |
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 BitmapHandler. |
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 BitmapHandler. |
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 BitmapHandler. |
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 | } |