(bug 31487) Don't specify -auto-orient, but specify image rotations ourselves
[lhc/web/wiklou.git] / includes / media / Bitmap.php
1 <?php
2 /**
3 * Generic handler for bitmap images
4 *
5 * @file
6 * @ingroup Media
7 */
8
9 /**
10 * Generic handler for bitmap images
11 *
12 * @ingroup Media
13 */
14 class BitmapHandler extends ImageHandler {
15
16 /**
17 * @param $image File
18 * @param $params array Transform parameters. Entries with the keys 'width'
19 * and 'height' are the respective screen width and height, while the keys
20 * 'physicalWidth' and 'physicalHeight' indicate the thumbnail dimensions.
21 * @return bool
22 */
23 function normaliseParams( $image, &$params ) {
24 global $wgMaxImageArea;
25 if ( !parent::normaliseParams( $image, $params ) ) {
26 return false;
27 }
28
29 $mimeType = $image->getMimeType();
30 # Obtain the source, pre-rotation dimensions
31 $srcWidth = $image->getWidth( $params['page'] );
32 $srcHeight = $image->getHeight( $params['page'] );
33
34 # Don't make an image bigger than the source
35 if ( $params['physicalWidth'] >= $srcWidth ) {
36 $params['physicalWidth'] = $srcWidth;
37 $params['physicalHeight'] = $srcHeight;
38
39 # Skip scaling limit checks if no scaling is required
40 # due to requested size being bigger than source.
41 if ( !$image->mustRender() ) {
42 return true;
43 }
44 }
45
46 # Don't thumbnail an image so big that it will fill hard drives and send servers into swap
47 # JPEG has the handy property of allowing thumbnailing without full decompression, so we make
48 # an exception for it.
49 # @todo FIXME: This actually only applies to ImageMagick
50 if ( $mimeType !== 'image/jpeg' &&
51 $srcWidth * $srcHeight > $wgMaxImageArea )
52 {
53 return false;
54 }
55
56 return true;
57 }
58
59 /**
60 * Extracts the width/height if the image will be scaled before rotating
61 *
62 * This will match the physical size/aspect ratio of the original image
63 * prior to application of the rotation -- so for a portrait image that's
64 * stored as raw landscape with 90-degress rotation, the resulting size
65 * will be wider than it is tall.
66 *
67 * @param $params array Parameters as returned by normaliseParams
68 * @param $rotation int The rotation angle that will be applied
69 * @return array ($width, $height) array
70 */
71 public function extractPreRotationDimensions( $params, $rotation ) {
72 if ( $rotation == 90 || $rotation == 270 ) {
73 # We'll resize before rotation, so swap the dimensions again
74 $width = $params['physicalHeight'];
75 $height = $params['physicalWidth'];
76 } else {
77 $width = $params['physicalWidth'];
78 $height = $params['physicalHeight'];
79 }
80 return array( $width, $height );
81 }
82
83
84 // Function that returns the number of pixels to be thumbnailed.
85 // Intended for animated GIFs to multiply by the number of frames.
86 function getImageArea( $image, $width, $height ) {
87 return $width * $height;
88 }
89
90 /**
91 * @param $image File
92 * @param $dstPath
93 * @param $dstUrl
94 * @param $params
95 * @param int $flags
96 * @return MediaTransformError|ThumbnailImage|TransformParameterError
97 */
98 function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
99 if ( !$this->normaliseParams( $image, $params ) ) {
100 return new TransformParameterError( $params );
101 }
102 # Create a parameter array to pass to the scaler
103 $scalerParams = array(
104 # The size to which the image will be resized
105 'physicalWidth' => $params['physicalWidth'],
106 'physicalHeight' => $params['physicalHeight'],
107 'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}",
108 # The size of the image on the page
109 'clientWidth' => $params['width'],
110 'clientHeight' => $params['height'],
111 # Comment as will be added to the EXIF of the thumbnail
112 'comment' => isset( $params['descriptionUrl'] ) ?
113 "File source: {$params['descriptionUrl']}" : '',
114 # Properties of the original image
115 'srcWidth' => $image->getWidth(),
116 'srcHeight' => $image->getHeight(),
117 'mimeType' => $image->getMimeType(),
118 'srcPath' => $image->getPath(),
119 'dstPath' => $dstPath,
120 'dstUrl' => $dstUrl,
121 );
122
123 wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} thumbnail at $dstPath\n" );
124
125 if ( !$image->mustRender() &&
126 $scalerParams['physicalWidth'] == $scalerParams['srcWidth']
127 && $scalerParams['physicalHeight'] == $scalerParams['srcHeight'] ) {
128
129 # normaliseParams (or the user) wants us to return the unscaled image
130 wfDebug( __METHOD__ . ": returning unscaled image\n" );
131 return $this->getClientScalingThumbnailImage( $image, $scalerParams );
132 }
133
134 # Determine scaler type
135 $scaler = self::getScalerType( $dstPath );
136 wfDebug( __METHOD__ . ": scaler $scaler\n" );
137
138 if ( $scaler == 'client' ) {
139 # Client-side image scaling, use the source URL
140 # Using the destination URL in a TRANSFORM_LATER request would be incorrect
141 return $this->getClientScalingThumbnailImage( $image, $scalerParams );
142 }
143
144 if ( $flags & self::TRANSFORM_LATER ) {
145 wfDebug( __METHOD__ . ": Transforming later per flags.\n" );
146 return new ThumbnailImage( $image, $dstUrl, $scalerParams['clientWidth'],
147 $scalerParams['clientHeight'], $dstPath );
148 }
149
150 # Try to make a target path for the thumbnail
151 if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
152 wfDebug( __METHOD__ . ": Unable to create thumbnail destination directory, falling back to client scaling\n" );
153 return $this->getClientScalingThumbnailImage( $image, $scalerParams );
154 }
155
156 # Try a hook
157 $mto = null;
158 wfRunHooks( 'BitmapHandlerTransform', array( $this, $image, &$scalerParams, &$mto ) );
159 if ( !is_null( $mto ) ) {
160 wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto\n" );
161 $scaler = 'hookaborted';
162 }
163
164 switch ( $scaler ) {
165 case 'hookaborted':
166 # Handled by the hook above
167 $err = $mto->isError() ? $mto : false;
168 break;
169 case 'im':
170 $err = $this->transformImageMagick( $image, $scalerParams );
171 break;
172 case 'custom':
173 $err = $this->transformCustom( $image, $scalerParams );
174 break;
175 case 'imext':
176 $err = $this->transformImageMagickExt( $image, $scalerParams );
177 break;
178 case 'gd':
179 default:
180 $err = $this->transformGd( $image, $scalerParams );
181 break;
182 }
183
184 # Remove the file if a zero-byte thumbnail was created, or if there was an error
185 $removed = $this->removeBadFile( $dstPath, (bool)$err );
186 if ( $err ) {
187 # transform returned MediaTransforError
188 return $err;
189 } elseif ( $removed ) {
190 # Thumbnail was zero-byte and had to be removed
191 return new MediaTransformError( 'thumbnail_error',
192 $scalerParams['clientWidth'], $scalerParams['clientHeight'] );
193 } elseif ( $mto ) {
194 return $mto;
195 } else {
196 return new ThumbnailImage( $image, $dstUrl, $scalerParams['clientWidth'],
197 $scalerParams['clientHeight'], $dstPath );
198 }
199 }
200
201 /**
202 * Returns which scaler type should be used. Creates parent directories
203 * for $dstPath and returns 'client' on error
204 *
205 * @return string client,im,custom,gd
206 */
207 protected static function getScalerType( $dstPath, $checkDstPath = true ) {
208 global $wgUseImageResize, $wgUseImageMagick, $wgCustomConvertCommand;
209
210 if ( !$dstPath && $checkDstPath ) {
211 # No output path available, client side scaling only
212 $scaler = 'client';
213 } elseif ( !$wgUseImageResize ) {
214 $scaler = 'client';
215 } elseif ( $wgUseImageMagick ) {
216 $scaler = 'im';
217 } elseif ( $wgCustomConvertCommand ) {
218 $scaler = 'custom';
219 } elseif ( function_exists( 'imagecreatetruecolor' ) ) {
220 $scaler = 'gd';
221 } elseif ( class_exists( 'Imagick' ) ) {
222 $scaler = 'imext';
223 } else {
224 $scaler = 'client';
225 }
226 return $scaler;
227 }
228
229 /**
230 * Get a ThumbnailImage that respresents an image that will be scaled
231 * client side
232 *
233 * @param $image File File associated with this thumbnail
234 * @param $params array Array with scaler params
235 * @return ThumbnailImage
236 *
237 * @fixme no rotation support
238 */
239 protected function getClientScalingThumbnailImage( $image, $params ) {
240 return new ThumbnailImage( $image, $image->getURL(),
241 $params['clientWidth'], $params['clientHeight'], $params['srcPath'] );
242 }
243
244 /**
245 * Transform an image using ImageMagick
246 *
247 * @param $image File File associated with this thumbnail
248 * @param $params array Array with scaler params
249 *
250 * @return MediaTransformError Error object if error occured, false (=no error) otherwise
251 */
252 protected function transformImageMagick( $image, $params ) {
253 # use ImageMagick
254 global $wgSharpenReductionThreshold, $wgSharpenParameter,
255 $wgMaxAnimatedGifArea,
256 $wgImageMagickTempDir, $wgImageMagickConvertCommand;
257
258 $quality = '';
259 $sharpen = '';
260 $scene = false;
261 $animation_pre = '';
262 $animation_post = '';
263 $decoderHint = '';
264 if ( $params['mimeType'] == 'image/jpeg' ) {
265 $quality = "-quality 80"; // 80%
266 # Sharpening, see bug 6193
267 if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
268 / ( $params['srcWidth'] + $params['srcHeight'] )
269 < $wgSharpenReductionThreshold ) {
270 $sharpen = "-sharpen " . wfEscapeShellArg( $wgSharpenParameter );
271 }
272 // JPEG decoder hint to reduce memory, available since IM 6.5.6-2
273 $decoderHint = "-define jpeg:size={$params['physicalDimensions']}";
274
275 } elseif ( $params['mimeType'] == 'image/png' ) {
276 $quality = "-quality 95"; // zlib 9, adaptive filtering
277
278 } elseif ( $params['mimeType'] == 'image/gif' ) {
279 if ( $this->getImageArea( $image, $params['srcWidth'],
280 $params['srcHeight'] ) > $wgMaxAnimatedGifArea ) {
281 // Extract initial frame only; we're so big it'll
282 // be a total drag. :P
283 $scene = 0;
284
285 } elseif ( $this->isAnimatedImage( $image ) ) {
286 // Coalesce is needed to scale animated GIFs properly (bug 1017).
287 $animation_pre = '-coalesce';
288 // We optimize the output, but -optimize is broken,
289 // use optimizeTransparency instead (bug 11822)
290 if ( version_compare( $this->getMagickVersion(), "6.3.5" ) >= 0 ) {
291 $animation_post = '-fuzz 5% -layers optimizeTransparency';
292 }
293 }
294 }
295
296 // Use one thread only, to avoid deadlock bugs on OOM
297 $env = array( 'OMP_NUM_THREADS' => 1 );
298 if ( strval( $wgImageMagickTempDir ) !== '' ) {
299 $env['MAGICK_TMPDIR'] = $wgImageMagickTempDir;
300 }
301
302 $rotation = $this->getRotation( $image );
303 list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
304
305 $cmd =
306 wfEscapeShellArg( $wgImageMagickConvertCommand ) .
307 // Specify white background color, will be used for transparent images
308 // in Internet Explorer/Windows instead of default black.
309 " {$quality} -background white" .
310 " {$decoderHint} " .
311 wfEscapeShellArg( $this->escapeMagickInput( $params['srcPath'], $scene ) ) .
312 " {$animation_pre}" .
313 // For the -thumbnail option a "!" is needed to force exact size,
314 // or ImageMagick may decide your ratio is wrong and slice off
315 // a pixel.
316 " -thumbnail " . wfEscapeShellArg( "{$width}x{$height}!" ) .
317 // Add the source url as a comment to the thumb, but don't add the flag if there's no comment
318 ( $params['comment'] !== ''
319 ? " -set comment " . wfEscapeShellArg( $this->escapeMagickProperty( $params['comment'] ) )
320 : '' ) .
321 " -depth 8 $sharpen " .
322 " -rotate -$rotation " .
323 " {$animation_post} " .
324 wfEscapeShellArg( $this->escapeMagickOutput( $params['dstPath'] ) ) . " 2>&1";
325
326 wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
327 wfProfileIn( 'convert' );
328 $retval = 0;
329 $err = wfShellExec( $cmd, $retval, $env );
330 wfProfileOut( 'convert' );
331
332 if ( $retval !== 0 ) {
333 $this->logErrorForExternalProcess( $retval, $err, $cmd );
334 return $this->getMediaTransformError( $params, $err );
335 }
336
337 return false; # No error
338 }
339
340 /**
341 * Transform an image using the Imagick PHP extension
342 *
343 * @param $image File File associated with this thumbnail
344 * @param $params array Array with scaler params
345 *
346 * @return MediaTransformError Error object if error occured, false (=no error) otherwise
347 */
348 protected function transformImageMagickExt( $image, $params ) {
349 global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea;
350
351 try {
352 $im = new Imagick();
353 $im->readImage( $params['srcPath'] );
354
355 if ( $params['mimeType'] == 'image/jpeg' ) {
356 // Sharpening, see bug 6193
357 if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
358 / ( $params['srcWidth'] + $params['srcHeight'] )
359 < $wgSharpenReductionThreshold ) {
360 // Hack, since $wgSharpenParamater is written specifically for the command line convert
361 list( $radius, $sigma ) = explode( 'x', $wgSharpenParameter );
362 $im->sharpenImage( $radius, $sigma );
363 }
364 $im->setCompressionQuality( 80 );
365 } elseif( $params['mimeType'] == 'image/png' ) {
366 $im->setCompressionQuality( 95 );
367 } elseif ( $params['mimeType'] == 'image/gif' ) {
368 if ( $this->getImageArea( $image, $params['srcWidth'],
369 $params['srcHeight'] ) > $wgMaxAnimatedGifArea ) {
370 // Extract initial frame only; we're so big it'll
371 // be a total drag. :P
372 $im->setImageScene( 0 );
373 } elseif ( $this->isAnimatedImage( $image ) ) {
374 // Coalesce is needed to scale animated GIFs properly (bug 1017).
375 $im = $im->coalesceImages();
376 }
377 }
378
379 $rotation = $this->getRotation( $image );
380 list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
381
382 $im->setImageBackgroundColor( new ImagickPixel( 'white' ) );
383
384 // Call Imagick::thumbnailImage on each frame
385 foreach ( $im as $i => $frame ) {
386 if ( !$frame->thumbnailImage( $width, $height, /* fit */ false ) ) {
387 return $this->getMediaTransformError( $params, "Error scaling frame $i" );
388 }
389 }
390 $im->setImageDepth( 8 );
391
392 if ( $rotation ) {
393 if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
394 return $this->getMediaTransformError( $params, "Error rotating $rotation degrees" );
395 }
396 }
397
398 if ( $this->isAnimatedImage( $image ) ) {
399 wfDebug( __METHOD__ . ": Writing animated thumbnail\n" );
400 // This is broken somehow... can't find out how to fix it
401 $result = $im->writeImages( $params['dstPath'], true );
402 } else {
403 $result = $im->writeImage( $params['dstPath'] );
404 }
405 if ( !$result ) {
406 return $this->getMediaTransformError( $params,
407 "Unable to write thumbnail to {$params['dstPath']}" );
408 }
409
410 } catch ( ImagickException $e ) {
411 return $this->getMediaTransformError( $params, $e->getMessage() );
412 }
413
414 return false;
415
416 }
417
418 /**
419 * Transform an image using a custom command
420 *
421 * @param $image File File associated with this thumbnail
422 * @param $params array Array with scaler params
423 *
424 * @return MediaTransformError Error object if error occured, false (=no error) otherwise
425 */
426 protected function transformCustom( $image, $params ) {
427 # Use a custom convert command
428 global $wgCustomConvertCommand;
429
430 # Variables: %s %d %w %h
431 $src = wfEscapeShellArg( $params['srcPath'] );
432 $dst = wfEscapeShellArg( $params['dstPath'] );
433 $cmd = $wgCustomConvertCommand;
434 $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames
435 $cmd = str_replace( '%h', $params['physicalHeight'],
436 str_replace( '%w', $params['physicalWidth'], $cmd ) ); # Size
437 wfDebug( __METHOD__ . ": Running custom convert command $cmd\n" );
438 wfProfileIn( 'convert' );
439 $retval = 0;
440 $err = wfShellExec( $cmd, $retval );
441 wfProfileOut( 'convert' );
442
443 if ( $retval !== 0 ) {
444 $this->logErrorForExternalProcess( $retval, $err, $cmd );
445 return $this->getMediaTransformError( $params, $err );
446 }
447 return false; # No error
448 }
449
450 /**
451 * Log an error that occured in an external process
452 *
453 * @param $retval int
454 * @param $err int
455 * @param $cmd string
456 */
457 protected function logErrorForExternalProcess( $retval, $err, $cmd ) {
458 wfDebugLog( 'thumbnail',
459 sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"',
460 wfHostname(), $retval, trim( $err ), $cmd ) );
461 }
462 /**
463 * Get a MediaTransformError with error 'thumbnail_error'
464 *
465 * @param $params array Parameter array as passed to the transform* functions
466 * @param $errMsg string Error message
467 * @return MediaTransformError
468 */
469 public function getMediaTransformError( $params, $errMsg ) {
470 return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
471 $params['clientHeight'], $errMsg );
472 }
473
474 /**
475 * Transform an image using the built in GD library
476 *
477 * @param $image File File associated with this thumbnail
478 * @param $params array Array with scaler params
479 *
480 * @return MediaTransformError Error object if error occured, false (=no error) otherwise
481 */
482 protected function transformGd( $image, $params ) {
483 # Use PHP's builtin GD library functions.
484 #
485 # First find out what kind of file this is, and select the correct
486 # input routine for this.
487
488 $typemap = array(
489 'image/gif' => array( 'imagecreatefromgif', 'palette', 'imagegif' ),
490 'image/jpeg' => array( 'imagecreatefromjpeg', 'truecolor', array( __CLASS__, 'imageJpegWrapper' ) ),
491 'image/png' => array( 'imagecreatefrompng', 'bits', 'imagepng' ),
492 'image/vnd.wap.wbmp' => array( 'imagecreatefromwbmp', 'palette', 'imagewbmp' ),
493 'image/xbm' => array( 'imagecreatefromxbm', 'palette', 'imagexbm' ),
494 );
495 if ( !isset( $typemap[$params['mimeType']] ) ) {
496 $err = 'Image type not supported';
497 wfDebug( "$err\n" );
498 $errMsg = wfMsg ( 'thumbnail_image-type' );
499 return $this->getMediaTransformError( $params, $errMsg );
500 }
501 list( $loader, $colorStyle, $saveType ) = $typemap[$params['mimeType']];
502
503 if ( !function_exists( $loader ) ) {
504 $err = "Incomplete GD library configuration: missing function $loader";
505 wfDebug( "$err\n" );
506 $errMsg = wfMsg ( 'thumbnail_gd-library', $loader );
507 return $this->getMediaTransformError( $params, $errMsg );
508 }
509
510 if ( !file_exists( $params['srcPath'] ) ) {
511 $err = "File seems to be missing: {$params['srcPath']}";
512 wfDebug( "$err\n" );
513 $errMsg = wfMsg ( 'thumbnail_image-missing', $params['srcPath'] );
514 return $this->getMediaTransformError( $params, $errMsg );
515 }
516
517 $src_image = call_user_func( $loader, $params['srcPath'] );
518
519 $rotation = function_exists( 'imagerotate' ) ? $this->getRotation( $image ) : 0;
520 list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
521 $dst_image = imagecreatetruecolor( $width, $height );
522
523 // Initialise the destination image to transparent instead of
524 // the default solid black, to support PNG and GIF transparency nicely
525 $background = imagecolorallocate( $dst_image, 0, 0, 0 );
526 imagecolortransparent( $dst_image, $background );
527 imagealphablending( $dst_image, false );
528
529 if ( $colorStyle == 'palette' ) {
530 // Don't resample for paletted GIF images.
531 // It may just uglify them, and completely breaks transparency.
532 imagecopyresized( $dst_image, $src_image,
533 0, 0, 0, 0,
534 $width, $height,
535 imagesx( $src_image ), imagesy( $src_image ) );
536 } else {
537 imagecopyresampled( $dst_image, $src_image,
538 0, 0, 0, 0,
539 $width, $height,
540 imagesx( $src_image ), imagesy( $src_image ) );
541 }
542
543 if ( $rotation % 360 != 0 && $rotation % 90 == 0 ) {
544 $rot_image = imagerotate( $dst_image, $rotation, 0 );
545 imagedestroy( $dst_image );
546 $dst_image = $rot_image;
547 }
548
549 imagesavealpha( $dst_image, true );
550
551 call_user_func( $saveType, $dst_image, $params['dstPath'] );
552 imagedestroy( $dst_image );
553 imagedestroy( $src_image );
554
555 return false; # No error
556 }
557
558 /**
559 * Escape a string for ImageMagick's property input (e.g. -set -comment)
560 * See InterpretImageProperties() in magick/property.c
561 */
562 function escapeMagickProperty( $s ) {
563 // Double the backslashes
564 $s = str_replace( '\\', '\\\\', $s );
565 // Double the percents
566 $s = str_replace( '%', '%%', $s );
567 // Escape initial - or @
568 if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
569 $s = '\\' . $s;
570 }
571 return $s;
572 }
573
574 /**
575 * Escape a string for ImageMagick's input filenames. See ExpandFilenames()
576 * and GetPathComponent() in magick/utility.c.
577 *
578 * This won't work with an initial ~ or @, so input files should be prefixed
579 * with the directory name.
580 *
581 * Glob character unescaping is broken in ImageMagick before 6.6.1-5, but
582 * it's broken in a way that doesn't involve trying to convert every file
583 * in a directory, so we're better off escaping and waiting for the bugfix
584 * to filter down to users.
585 *
586 * @param $path string The file path
587 * @param $scene string The scene specification, or false if there is none
588 */
589 function escapeMagickInput( $path, $scene = false ) {
590 # Die on initial metacharacters (caller should prepend path)
591 $firstChar = substr( $path, 0, 1 );
592 if ( $firstChar === '~' || $firstChar === '@' ) {
593 throw new MWException( __METHOD__ . ': cannot escape this path name' );
594 }
595
596 # Escape glob chars
597 $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
598
599 return $this->escapeMagickPath( $path, $scene );
600 }
601
602 /**
603 * Escape a string for ImageMagick's output filename. See
604 * InterpretImageFilename() in magick/image.c.
605 */
606 function escapeMagickOutput( $path, $scene = false ) {
607 $path = str_replace( '%', '%%', $path );
608 return $this->escapeMagickPath( $path, $scene );
609 }
610
611 /**
612 * Armour a string against ImageMagick's GetPathComponent(). This is a
613 * helper function for escapeMagickInput() and escapeMagickOutput().
614 *
615 * @param $path string The file path
616 * @param $scene string The scene specification, or false if there is none
617 */
618 protected function escapeMagickPath( $path, $scene = false ) {
619 # Die on format specifiers (other than drive letters). The regex is
620 # meant to match all the formats you get from "convert -list format"
621 if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
622 if ( wfIsWindows() && is_dir( $m[0] ) ) {
623 // OK, it's a drive letter
624 // ImageMagick has a similar exception, see IsMagickConflict()
625 } else {
626 throw new MWException( __METHOD__ . ': unexpected colon character in path name' );
627 }
628 }
629
630 # If there are square brackets, add a do-nothing scene specification
631 # to force a literal interpretation
632 if ( $scene === false ) {
633 if ( strpos( $path, '[' ) !== false ) {
634 $path .= '[0--1]';
635 }
636 } else {
637 $path .= "[$scene]";
638 }
639 return $path;
640 }
641
642 /**
643 * Retrieve the version of the installed ImageMagick
644 * You can use PHPs version_compare() to use this value
645 * Value is cached for one hour.
646 * @return String representing the IM version.
647 */
648 protected function getMagickVersion() {
649 global $wgMemc;
650
651 $cache = $wgMemc->get( "imagemagick-version" );
652 if ( !$cache ) {
653 global $wgImageMagickConvertCommand;
654 $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . ' -version';
655 wfDebug( __METHOD__ . ": Running convert -version\n" );
656 $retval = '';
657 $return = wfShellExec( $cmd, $retval );
658 $x = preg_match( '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches );
659 if ( $x != 1 ) {
660 wfDebug( __METHOD__ . ": ImageMagick version check failed\n" );
661 return null;
662 }
663 $wgMemc->set( "imagemagick-version", $matches[1], 3600 );
664 return $matches[1];
665 }
666 return $cache;
667 }
668
669 static function imageJpegWrapper( $dst_image, $thumbPath ) {
670 imageinterlace( $dst_image );
671 imagejpeg( $dst_image, $thumbPath, 95 );
672 }
673
674 /**
675 * On supporting image formats, try to read out the low-level orientation
676 * of the file and return the angle that the file needs to be rotated to
677 * be viewed.
678 *
679 * This information is only useful when manipulating the original file;
680 * the width and height we normally work with is logical, and will match
681 * any produced output views.
682 *
683 * The base BitmapHandler doesn't understand any metadata formats, so this
684 * is left up to child classes to implement.
685 *
686 * @param $file File
687 * @return int 0, 90, 180 or 270
688 */
689 public function getRotation( $file ) {
690 return 0;
691 }
692
693 /**
694 * Returns whether the current scaler supports rotation (im and gd do)
695 *
696 * @return bool
697 */
698 public static function canRotate() {
699 $scaler = self::getScalerType( null, false );
700 switch ( $scaler ) {
701 case 'im':
702 # ImageMagick supports autorotation
703 return true;
704 case 'imext':
705 # Imagick::rotateImage
706 return true;
707 case 'gd':
708 # GD's imagerotate function is used to rotate images, but not
709 # all precompiled PHP versions have that function
710 return function_exists( 'imagerotate' );
711 default:
712 # Other scalers don't support rotation
713 return false;
714 }
715 }
716
717 /**
718 * Rerurns whether the file needs to be rendered. Returns true if the
719 * file requires rotation and we are able to rotate it.
720 *
721 * @param $file File
722 * @return bool
723 */
724 public function mustRender( $file ) {
725 return self::canRotate() && $this->getRotation( $file ) != 0;
726 }
727 }