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