00d6dc28af14f457fa68a9219862b39cceab771c
[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 -auto-orient" .
322 " {$animation_post} " .
323 wfEscapeShellArg( $this->escapeMagickOutput( $params['dstPath'] ) ) . " 2>&1";
324
325 wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
326 wfProfileIn( 'convert' );
327 $retval = 0;
328 $err = wfShellExec( $cmd, $retval, $env );
329 wfProfileOut( 'convert' );
330
331 if ( $retval !== 0 ) {
332 $this->logErrorForExternalProcess( $retval, $err, $cmd );
333 return $this->getMediaTransformError( $params, $err );
334 }
335
336 return false; # No error
337 }
338
339 /**
340 * Transform an image using the Imagick PHP extension
341 *
342 * @param $image File File associated with this thumbnail
343 * @param $params array Array with scaler params
344 *
345 * @return MediaTransformError Error object if error occured, false (=no error) otherwise
346 */
347 protected function transformImageMagickExt( $image, $params ) {
348 global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea;
349
350 try {
351 $im = new Imagick();
352 $im->readImage( $params['srcPath'] );
353
354 if ( $params['mimeType'] == 'image/jpeg' ) {
355 // Sharpening, see bug 6193
356 if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
357 / ( $params['srcWidth'] + $params['srcHeight'] )
358 < $wgSharpenReductionThreshold ) {
359 // Hack, since $wgSharpenParamater is written specifically for the command line convert
360 list( $radius, $sigma ) = explode( 'x', $wgSharpenParameter );
361 $im->sharpenImage( $radius, $sigma );
362 }
363 $im->setCompressionQuality( 80 );
364 } elseif( $params['mimeType'] == 'image/png' ) {
365 $im->setCompressionQuality( 95 );
366 } elseif ( $params['mimeType'] == 'image/gif' ) {
367 if ( $this->getImageArea( $image, $params['srcWidth'],
368 $params['srcHeight'] ) > $wgMaxAnimatedGifArea ) {
369 // Extract initial frame only; we're so big it'll
370 // be a total drag. :P
371 $im->setImageScene( 0 );
372 } elseif ( $this->isAnimatedImage( $image ) ) {
373 // Coalesce is needed to scale animated GIFs properly (bug 1017).
374 $im = $im->coalesceImages();
375 }
376 }
377
378 $rotation = $this->getRotation( $image );
379 list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
380
381 $im->setImageBackgroundColor( new ImagickPixel( 'white' ) );
382
383 // Call Imagick::thumbnailImage on each frame
384 foreach ( $im as $i => $frame ) {
385 if ( !$frame->thumbnailImage( $width, $height, /* fit */ false ) ) {
386 return $this->getMediaTransformError( $params, "Error scaling frame $i" );
387 }
388 }
389 $im->setImageDepth( 8 );
390
391 if ( $rotation ) {
392 if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
393 return $this->getMediaTransformError( $params, "Error rotating $rotation degrees" );
394 }
395 }
396
397 if ( $this->isAnimatedImage( $image ) ) {
398 wfDebug( __METHOD__ . ": Writing animated thumbnail\n" );
399 // This is broken somehow... can't find out how to fix it
400 $result = $im->writeImages( $params['dstPath'], true );
401 } else {
402 $result = $im->writeImage( $params['dstPath'] );
403 }
404 if ( !$result ) {
405 return $this->getMediaTransformError( $params,
406 "Unable to write thumbnail to {$params['dstPath']}" );
407 }
408
409 } catch ( ImagickException $e ) {
410 return $this->getMediaTransformError( $params, $e->getMessage() );
411 }
412
413 return false;
414
415 }
416
417 /**
418 * Transform an image using a custom command
419 *
420 * @param $image File File associated with this thumbnail
421 * @param $params array Array with scaler params
422 *
423 * @return MediaTransformError Error object if error occured, false (=no error) otherwise
424 */
425 protected function transformCustom( $image, $params ) {
426 # Use a custom convert command
427 global $wgCustomConvertCommand;
428
429 # Variables: %s %d %w %h
430 $src = wfEscapeShellArg( $params['srcPath'] );
431 $dst = wfEscapeShellArg( $params['dstPath'] );
432 $cmd = $wgCustomConvertCommand;
433 $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames
434 $cmd = str_replace( '%h', $params['physicalHeight'],
435 str_replace( '%w', $params['physicalWidth'], $cmd ) ); # Size
436 wfDebug( __METHOD__ . ": Running custom convert command $cmd\n" );
437 wfProfileIn( 'convert' );
438 $retval = 0;
439 $err = wfShellExec( $cmd, $retval );
440 wfProfileOut( 'convert' );
441
442 if ( $retval !== 0 ) {
443 $this->logErrorForExternalProcess( $retval, $err, $cmd );
444 return $this->getMediaTransformError( $params, $err );
445 }
446 return false; # No error
447 }
448
449 /**
450 * Log an error that occured in an external process
451 *
452 * @param $retval int
453 * @param $err int
454 * @param $cmd string
455 */
456 protected function logErrorForExternalProcess( $retval, $err, $cmd ) {
457 wfDebugLog( 'thumbnail',
458 sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"',
459 wfHostname(), $retval, trim( $err ), $cmd ) );
460 }
461 /**
462 * Get a MediaTransformError with error 'thumbnail_error'
463 *
464 * @param $params array Parameter array as passed to the transform* functions
465 * @param $errMsg string Error message
466 * @return MediaTransformError
467 */
468 public function getMediaTransformError( $params, $errMsg ) {
469 return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
470 $params['clientHeight'], $errMsg );
471 }
472
473 /**
474 * Transform an image using the built in GD library
475 *
476 * @param $image File File associated with this thumbnail
477 * @param $params array Array with scaler params
478 *
479 * @return MediaTransformError Error object if error occured, false (=no error) otherwise
480 */
481 protected function transformGd( $image, $params ) {
482 # Use PHP's builtin GD library functions.
483 #
484 # First find out what kind of file this is, and select the correct
485 # input routine for this.
486
487 $typemap = array(
488 'image/gif' => array( 'imagecreatefromgif', 'palette', 'imagegif' ),
489 'image/jpeg' => array( 'imagecreatefromjpeg', 'truecolor', array( __CLASS__, 'imageJpegWrapper' ) ),
490 'image/png' => array( 'imagecreatefrompng', 'bits', 'imagepng' ),
491 'image/vnd.wap.wbmp' => array( 'imagecreatefromwbmp', 'palette', 'imagewbmp' ),
492 'image/xbm' => array( 'imagecreatefromxbm', 'palette', 'imagexbm' ),
493 );
494 if ( !isset( $typemap[$params['mimeType']] ) ) {
495 $err = 'Image type not supported';
496 wfDebug( "$err\n" );
497 $errMsg = wfMsg ( 'thumbnail_image-type' );
498 return $this->getMediaTransformError( $params, $errMsg );
499 }
500 list( $loader, $colorStyle, $saveType ) = $typemap[$params['mimeType']];
501
502 if ( !function_exists( $loader ) ) {
503 $err = "Incomplete GD library configuration: missing function $loader";
504 wfDebug( "$err\n" );
505 $errMsg = wfMsg ( 'thumbnail_gd-library', $loader );
506 return $this->getMediaTransformError( $params, $errMsg );
507 }
508
509 if ( !file_exists( $params['srcPath'] ) ) {
510 $err = "File seems to be missing: {$params['srcPath']}";
511 wfDebug( "$err\n" );
512 $errMsg = wfMsg ( 'thumbnail_image-missing', $params['srcPath'] );
513 return $this->getMediaTransformError( $params, $errMsg );
514 }
515
516 $src_image = call_user_func( $loader, $params['srcPath'] );
517
518 $rotation = function_exists( 'imagerotate' ) ? $this->getRotation( $image ) : 0;
519 list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
520 $dst_image = imagecreatetruecolor( $width, $height );
521
522 // Initialise the destination image to transparent instead of
523 // the default solid black, to support PNG and GIF transparency nicely
524 $background = imagecolorallocate( $dst_image, 0, 0, 0 );
525 imagecolortransparent( $dst_image, $background );
526 imagealphablending( $dst_image, false );
527
528 if ( $colorStyle == 'palette' ) {
529 // Don't resample for paletted GIF images.
530 // It may just uglify them, and completely breaks transparency.
531 imagecopyresized( $dst_image, $src_image,
532 0, 0, 0, 0,
533 $width, $height,
534 imagesx( $src_image ), imagesy( $src_image ) );
535 } else {
536 imagecopyresampled( $dst_image, $src_image,
537 0, 0, 0, 0,
538 $width, $height,
539 imagesx( $src_image ), imagesy( $src_image ) );
540 }
541
542 if ( $rotation % 360 != 0 && $rotation % 90 == 0 ) {
543 $rot_image = imagerotate( $dst_image, $rotation, 0 );
544 imagedestroy( $dst_image );
545 $dst_image = $rot_image;
546 }
547
548 imagesavealpha( $dst_image, true );
549
550 call_user_func( $saveType, $dst_image, $params['dstPath'] );
551 imagedestroy( $dst_image );
552 imagedestroy( $src_image );
553
554 return false; # No error
555 }
556
557 /**
558 * Escape a string for ImageMagick's property input (e.g. -set -comment)
559 * See InterpretImageProperties() in magick/property.c
560 */
561 function escapeMagickProperty( $s ) {
562 // Double the backslashes
563 $s = str_replace( '\\', '\\\\', $s );
564 // Double the percents
565 $s = str_replace( '%', '%%', $s );
566 // Escape initial - or @
567 if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
568 $s = '\\' . $s;
569 }
570 return $s;
571 }
572
573 /**
574 * Escape a string for ImageMagick's input filenames. See ExpandFilenames()
575 * and GetPathComponent() in magick/utility.c.
576 *
577 * This won't work with an initial ~ or @, so input files should be prefixed
578 * with the directory name.
579 *
580 * Glob character unescaping is broken in ImageMagick before 6.6.1-5, but
581 * it's broken in a way that doesn't involve trying to convert every file
582 * in a directory, so we're better off escaping and waiting for the bugfix
583 * to filter down to users.
584 *
585 * @param $path string The file path
586 * @param $scene string The scene specification, or false if there is none
587 */
588 function escapeMagickInput( $path, $scene = false ) {
589 # Die on initial metacharacters (caller should prepend path)
590 $firstChar = substr( $path, 0, 1 );
591 if ( $firstChar === '~' || $firstChar === '@' ) {
592 throw new MWException( __METHOD__ . ': cannot escape this path name' );
593 }
594
595 # Escape glob chars
596 $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
597
598 return $this->escapeMagickPath( $path, $scene );
599 }
600
601 /**
602 * Escape a string for ImageMagick's output filename. See
603 * InterpretImageFilename() in magick/image.c.
604 */
605 function escapeMagickOutput( $path, $scene = false ) {
606 $path = str_replace( '%', '%%', $path );
607 return $this->escapeMagickPath( $path, $scene );
608 }
609
610 /**
611 * Armour a string against ImageMagick's GetPathComponent(). This is a
612 * helper function for escapeMagickInput() and escapeMagickOutput().
613 *
614 * @param $path string The file path
615 * @param $scene string The scene specification, or false if there is none
616 */
617 protected function escapeMagickPath( $path, $scene = false ) {
618 # Die on format specifiers (other than drive letters). The regex is
619 # meant to match all the formats you get from "convert -list format"
620 if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
621 if ( wfIsWindows() && is_dir( $m[0] ) ) {
622 // OK, it's a drive letter
623 // ImageMagick has a similar exception, see IsMagickConflict()
624 } else {
625 throw new MWException( __METHOD__ . ': unexpected colon character in path name' );
626 }
627 }
628
629 # If there are square brackets, add a do-nothing scene specification
630 # to force a literal interpretation
631 if ( $scene === false ) {
632 if ( strpos( $path, '[' ) !== false ) {
633 $path .= '[0--1]';
634 }
635 } else {
636 $path .= "[$scene]";
637 }
638 return $path;
639 }
640
641 /**
642 * Retrieve the version of the installed ImageMagick
643 * You can use PHPs version_compare() to use this value
644 * Value is cached for one hour.
645 * @return String representing the IM version.
646 */
647 protected function getMagickVersion() {
648 global $wgMemc;
649
650 $cache = $wgMemc->get( "imagemagick-version" );
651 if ( !$cache ) {
652 global $wgImageMagickConvertCommand;
653 $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . ' -version';
654 wfDebug( __METHOD__ . ": Running convert -version\n" );
655 $retval = '';
656 $return = wfShellExec( $cmd, $retval );
657 $x = preg_match( '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches );
658 if ( $x != 1 ) {
659 wfDebug( __METHOD__ . ": ImageMagick version check failed\n" );
660 return null;
661 }
662 $wgMemc->set( "imagemagick-version", $matches[1], 3600 );
663 return $matches[1];
664 }
665 return $cache;
666 }
667
668 static function imageJpegWrapper( $dst_image, $thumbPath ) {
669 imageinterlace( $dst_image );
670 imagejpeg( $dst_image, $thumbPath, 95 );
671 }
672
673 /**
674 * On supporting image formats, try to read out the low-level orientation
675 * of the file and return the angle that the file needs to be rotated to
676 * be viewed.
677 *
678 * This information is only useful when manipulating the original file;
679 * the width and height we normally work with is logical, and will match
680 * any produced output views.
681 *
682 * The base BitmapHandler doesn't understand any metadata formats, so this
683 * is left up to child classes to implement.
684 *
685 * @param $file File
686 * @return int 0, 90, 180 or 270
687 */
688 public function getRotation( $file ) {
689 return 0;
690 }
691
692 /**
693 * Returns whether the current scaler supports rotation (im and gd do)
694 *
695 * @return bool
696 */
697 public static function canRotate() {
698 $scaler = self::getScalerType( null, false );
699 switch ( $scaler ) {
700 case 'im':
701 # ImageMagick supports autorotation
702 return true;
703 case 'imext':
704 # Imagick::rotateImage
705 return true;
706 case 'gd':
707 # GD's imagerotate function is used to rotate images, but not
708 # all precompiled PHP versions have that function
709 return function_exists( 'imagerotate' );
710 default:
711 # Other scalers don't support rotation
712 return false;
713 }
714 }
715
716 /**
717 * Rerurns whether the file needs to be rendered. Returns true if the
718 * file requires rotation and we are able to rotate it.
719 *
720 * @param $file File
721 * @return bool
722 */
723 public function mustRender( $file ) {
724 return self::canRotate() && $this->getRotation( $file ) != 0;
725 }
726 }