Remove some unused variables
[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 'dstUrl' => $dstUrl,
105 );
106
107 wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} thumbnail at $dstPath\n" );
108
109 if ( !$image->mustRender() &&
110 $scalerParams['physicalWidth'] == $scalerParams['srcWidth']
111 && $scalerParams['physicalHeight'] == $scalerParams['srcHeight'] ) {
112
113 # normaliseParams (or the user) wants us to return the unscaled image
114 wfDebug( __METHOD__ . ": returning unscaled image\n" );
115 return $this->getClientScalingThumbnailImage( $image, $scalerParams );
116 }
117
118 # Determine scaler type
119 $scaler = self::getScalerType( $dstPath );
120 wfDebug( __METHOD__ . ": scaler $scaler\n" );
121
122 if ( $scaler == 'client' ) {
123 # Client-side image scaling, use the source URL
124 # Using the destination URL in a TRANSFORM_LATER request would be incorrect
125 return $this->getClientScalingThumbnailImage( $image, $scalerParams );
126 }
127
128 if ( $flags & self::TRANSFORM_LATER ) {
129 wfDebug( __METHOD__ . ": Transforming later per flags.\n" );
130 return new ThumbnailImage( $image, $dstUrl, $scalerParams['clientWidth'],
131 $scalerParams['clientHeight'], $dstPath );
132 }
133
134 # Try to make a target path for the thumbnail
135 if ( !wfMkdirParents( dirname( $dstPath ) ) ) {
136 wfDebug( __METHOD__ . ": Unable to create thumbnail destination directory, falling back to client scaling\n" );
137 return $this->getClientScalingThumbnailImage( $image, $scalerParams );
138 }
139
140 # Try a hook
141 $mto = null;
142 wfRunHooks( 'BitmapHandlerTransform', array( $this, $image, &$scalerParams, &$mto ) );
143 if ( !is_null( $mto ) ) {
144 wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto\n" );
145 $scaler = 'hookaborted';
146 }
147
148 switch ( $scaler ) {
149 case 'hookaborted':
150 # Handled by the hook above
151 $err = $mto->isError() ? $mto : false;
152 break;
153 case 'im':
154 $err = $this->transformImageMagick( $image, $scalerParams );
155 break;
156 case 'custom':
157 $err = $this->transformCustom( $image, $scalerParams );
158 break;
159 case 'imext':
160 $err = $this->transformImageMagickExt( $image, $scalerParams );
161 break;
162 case 'gd':
163 default:
164 $err = $this->transformGd( $image, $scalerParams );
165 break;
166 }
167
168 # Remove the file if a zero-byte thumbnail was created, or if there was an error
169 $removed = $this->removeBadFile( $dstPath, (bool)$err );
170 if ( $err ) {
171 # transform returned MediaTransforError
172 return $err;
173 } elseif ( $removed ) {
174 # Thumbnail was zero-byte and had to be removed
175 return new MediaTransformError( 'thumbnail_error',
176 $scalerParams['clientWidth'], $scalerParams['clientHeight'] );
177 } elseif ( $mto ) {
178 return $mto;
179 } else {
180 return new ThumbnailImage( $image, $dstUrl, $scalerParams['clientWidth'],
181 $scalerParams['clientHeight'], $dstPath );
182 }
183 }
184
185 /**
186 * Returns which scaler type should be used. Creates parent directories
187 * for $dstPath and returns 'client' on error
188 *
189 * @return string client,im,custom,gd
190 */
191 protected static function getScalerType( $dstPath, $checkDstPath = true ) {
192 global $wgUseImageResize, $wgUseImageMagick, $wgCustomConvertCommand;
193
194 if ( !$dstPath && $checkDstPath ) {
195 # No output path available, client side scaling only
196 $scaler = 'client';
197 } elseif ( !$wgUseImageResize ) {
198 $scaler = 'client';
199 } elseif ( $wgUseImageMagick ) {
200 $scaler = 'im';
201 } elseif ( $wgCustomConvertCommand ) {
202 $scaler = 'custom';
203 } elseif ( function_exists( 'imagecreatetruecolor' ) ) {
204 $scaler = 'gd';
205 } elseif ( class_exists( 'Imagick' ) ) {
206 $scaler = 'imext';
207 } else {
208 $scaler = 'client';
209 }
210
211 if ( $scaler != 'client' && $dstPath ) {
212 if ( !wfMkdirParents( dirname( $dstPath ) ) ) {
213 # Unable to create a path for the thumbnail
214 return 'client';
215 }
216 }
217 return $scaler;
218 }
219
220 /**
221 * Get a ThumbnailImage that respresents an image that will be scaled
222 * client side
223 *
224 * @param $image File File associated with this thumbnail
225 * @param $params array Array with scaler params
226 * @return ThumbnailImage
227 */
228 protected function getClientScalingThumbnailImage( $image, $params ) {
229 return new ThumbnailImage( $image, $image->getURL(),
230 $params['clientWidth'], $params['clientHeight'], $params['srcPath'] );
231 }
232
233 /**
234 * Transform an image using ImageMagick
235 *
236 * @param $image File File associated with this thumbnail
237 * @param $params array Array with scaler params
238 *
239 * @return MediaTransformError Error object if error occured, false (=no error) otherwise
240 */
241 protected function transformImageMagick( $image, $params ) {
242 # use ImageMagick
243 global $wgSharpenReductionThreshold, $wgSharpenParameter,
244 $wgMaxAnimatedGifArea,
245 $wgImageMagickTempDir, $wgImageMagickConvertCommand;
246
247 $quality = '';
248 $sharpen = '';
249 $scene = false;
250 $animation_pre = '';
251 $animation_post = '';
252 $decoderHint = '';
253 if ( $params['mimeType'] == 'image/jpeg' ) {
254 $quality = "-quality 80"; // 80%
255 # Sharpening, see bug 6193
256 if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
257 / ( $params['srcWidth'] + $params['srcHeight'] )
258 < $wgSharpenReductionThreshold ) {
259 $sharpen = "-sharpen " . wfEscapeShellArg( $wgSharpenParameter );
260 }
261 // JPEG decoder hint to reduce memory, available since IM 6.5.6-2
262 $decoderHint = "-define jpeg:size={$params['physicalDimensions']}";
263
264 } elseif ( $params['mimeType'] == 'image/png' ) {
265 $quality = "-quality 95"; // zlib 9, adaptive filtering
266
267 } elseif ( $params['mimeType'] == 'image/gif' ) {
268 if ( $this->getImageArea( $image, $params['srcWidth'],
269 $params['srcHeight'] ) > $wgMaxAnimatedGifArea ) {
270 // Extract initial frame only; we're so big it'll
271 // be a total drag. :P
272 $scene = 0;
273
274 } elseif ( $this->isAnimatedImage( $image ) ) {
275 // Coalesce is needed to scale animated GIFs properly (bug 1017).
276 $animation_pre = '-coalesce';
277 // We optimize the output, but -optimize is broken,
278 // use optimizeTransparency instead (bug 11822)
279 if ( version_compare( $this->getMagickVersion(), "6.3.5" ) >= 0 ) {
280 $animation_post = '-fuzz 5% -layers optimizeTransparency +map';
281 }
282 }
283 }
284
285 // Use one thread only, to avoid deadlock bugs on OOM
286 $env = array( 'OMP_NUM_THREADS' => 1 );
287 if ( strval( $wgImageMagickTempDir ) !== '' ) {
288 $env['MAGICK_TMPDIR'] = $wgImageMagickTempDir;
289 }
290
291 $cmd =
292 wfEscapeShellArg( $wgImageMagickConvertCommand ) .
293 // Specify white background color, will be used for transparent images
294 // in Internet Explorer/Windows instead of default black.
295 " {$quality} -background white" .
296 " {$decoderHint} " .
297 wfEscapeShellArg( $this->escapeMagickInput( $params['srcPath'], $scene ) ) .
298 " {$animation_pre}" .
299 // For the -thumbnail option a "!" is needed to force exact size,
300 // or ImageMagick may decide your ratio is wrong and slice off
301 // a pixel.
302 " -thumbnail " . wfEscapeShellArg( "{$params['physicalDimensions']}!" ) .
303 // Add the source url as a comment to the thumb, but don't add the flag if there's no comment
304 ( $params['comment'] !== ''
305 ? " -set comment " . wfEscapeShellArg( $this->escapeMagickProperty( $params['comment'] ) )
306 : '' ) .
307 " -depth 8 $sharpen -auto-orient" .
308 " {$animation_post} " .
309 wfEscapeShellArg( $this->escapeMagickOutput( $params['dstPath'] ) ) . " 2>&1";
310
311 wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
312 wfProfileIn( 'convert' );
313 $retval = 0;
314 $err = wfShellExec( $cmd, $retval, $env );
315 wfProfileOut( 'convert' );
316
317 if ( $retval !== 0 ) {
318 $this->logErrorForExternalProcess( $retval, $err, $cmd );
319 return $this->getMediaTransformError( $params, $err );
320 }
321
322 return false; # No error
323 }
324
325 /**
326 * Transform an image using the Imagick PHP extension
327 *
328 * @param $image File File associated with this thumbnail
329 * @param $params array Array with scaler params
330 *
331 * @return MediaTransformError Error object if error occured, false (=no error) otherwise
332 */
333 protected function transformImageMagickExt( $image, $params ) {
334 global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea;
335
336 try {
337 $im = new Imagick();
338 $im->readImage( $params['srcPath'] );
339
340 if ( $params['mimeType'] == 'image/jpeg' ) {
341 // Sharpening, see bug 6193
342 if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
343 / ( $params['srcWidth'] + $params['srcHeight'] )
344 < $wgSharpenReductionThreshold ) {
345 // Hack, since $wgSharpenParamater is written specifically for the command line convert
346 list( $radius, $sigma ) = explode( 'x', $wgSharpenParameter );
347 $im->sharpenImage( $radius, $sigma );
348 }
349 $im->setCompressionQuality( 80 );
350 } elseif( $params['mimeType'] == 'image/png' ) {
351 $im->setCompressionQuality( 95 );
352 } elseif ( $params['mimeType'] == 'image/gif' ) {
353 if ( $this->getImageArea( $image, $params['srcWidth'],
354 $params['srcHeight'] ) > $wgMaxAnimatedGifArea ) {
355 // Extract initial frame only; we're so big it'll
356 // be a total drag. :P
357 $im->setImageScene( 0 );
358 } elseif ( $this->isAnimatedImage( $image ) ) {
359 // Coalesce is needed to scale animated GIFs properly (bug 1017).
360 $im = $im->coalesceImages();
361 }
362 }
363
364 $rotation = $this->getRotation( $image );
365 if ( $rotation == 90 || $rotation == 270 ) {
366 // We'll resize before rotation, so swap the dimensions again
367 $width = $params['physicalHeight'];
368 $height = $params['physicalWidth'];
369 } else {
370 $width = $params['physicalWidth'];
371 $height = $params['physicalHeight'];
372 }
373
374 $im->setImageBackgroundColor( new ImagickPixel( 'white' ) );
375
376 // Call Imagick::thumbnailImage on each frame
377 foreach ( $im as $i => $frame ) {
378 if ( !$frame->thumbnailImage( $width, $height, /* fit */ false ) ) {
379 return $this->getMediaTransformError( $params, "Error scaling frame $i" );
380 }
381 }
382 $im->setImageDepth( 8 );
383
384 if ( $rotation ) {
385 if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
386 return $this->getMediaTransformError( $params, "Error rotating $rotation degrees" );
387 }
388 }
389
390 if ( $this->isAnimatedImage( $image ) ) {
391 wfDebug( __METHOD__ . ": Writing animated thumbnail\n" );
392 // This is broken somehow... can't find out how to fix it
393 $result = $im->writeImages( $params['dstPath'], true );
394 } else {
395 $result = $im->writeImage( $params['dstPath'] );
396 }
397 if ( !$result ) {
398 return $this->getMediaTransformError( $params,
399 "Unable to write thumbnail to {$params['dstPath']}" );
400 }
401
402 } catch ( ImagickException $e ) {
403 return $this->getMediaTransformError( $params, $e->getMessage() );
404 }
405
406 return false;
407
408 }
409
410 /**
411 * Transform an image using a custom command
412 *
413 * @param $image File File associated with this thumbnail
414 * @param $params array Array with scaler params
415 *
416 * @return MediaTransformError Error object if error occured, false (=no error) otherwise
417 */
418 protected function transformCustom( $image, $params ) {
419 # Use a custom convert command
420 global $wgCustomConvertCommand;
421
422 # Variables: %s %d %w %h
423 $src = wfEscapeShellArg( $params['srcPath'] );
424 $dst = wfEscapeShellArg( $params['dstPath'] );
425 $cmd = $wgCustomConvertCommand;
426 $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames
427 $cmd = str_replace( '%h', $params['physicalHeight'],
428 str_replace( '%w', $params['physicalWidth'], $cmd ) ); # Size
429 wfDebug( __METHOD__ . ": Running custom convert command $cmd\n" );
430 wfProfileIn( 'convert' );
431 $retval = 0;
432 $err = wfShellExec( $cmd, $retval );
433 wfProfileOut( 'convert' );
434
435 if ( $retval !== 0 ) {
436 $this->logErrorForExternalProcess( $retval, $err, $cmd );
437 return $this->getMediaTransformError( $params, $err );
438 }
439 return false; # No error
440 }
441
442 /**
443 * Log an error that occured in an external process
444 *
445 * @param $retval int
446 * @param $err int
447 * @param $cmd string
448 */
449 protected function logErrorForExternalProcess( $retval, $err, $cmd ) {
450 wfDebugLog( 'thumbnail',
451 sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"',
452 wfHostname(), $retval, trim( $err ), $cmd ) );
453 }
454 /**
455 * Get a MediaTransformError with error 'thumbnail_error'
456 *
457 * @param $params array Parameter array as passed to the transform* functions
458 * @param $errMsg string Error message
459 * @return MediaTransformError
460 */
461 public function getMediaTransformError( $params, $errMsg ) {
462 return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
463 $params['clientHeight'], $errMsg );
464 }
465
466 /**
467 * Transform an image using the built in GD library
468 *
469 * @param $image File File associated with this thumbnail
470 * @param $params array Array with scaler params
471 *
472 * @return MediaTransformError Error object if error occured, false (=no error) otherwise
473 */
474 protected function transformGd( $image, $params ) {
475 # Use PHP's builtin GD library functions.
476 #
477 # First find out what kind of file this is, and select the correct
478 # input routine for this.
479
480 $typemap = array(
481 'image/gif' => array( 'imagecreatefromgif', 'palette', 'imagegif' ),
482 'image/jpeg' => array( 'imagecreatefromjpeg', 'truecolor', array( __CLASS__, 'imageJpegWrapper' ) ),
483 'image/png' => array( 'imagecreatefrompng', 'bits', 'imagepng' ),
484 'image/vnd.wap.wbmp' => array( 'imagecreatefromwbmp', 'palette', 'imagewbmp' ),
485 'image/xbm' => array( 'imagecreatefromxbm', 'palette', 'imagexbm' ),
486 );
487 if ( !isset( $typemap[$params['mimeType']] ) ) {
488 $err = 'Image type not supported';
489 wfDebug( "$err\n" );
490 $errMsg = wfMsg ( 'thumbnail_image-type' );
491 return $this->getMediaTransformError( $params, $errMsg );
492 }
493 list( $loader, $colorStyle, $saveType ) = $typemap[$params['mimeType']];
494
495 if ( !function_exists( $loader ) ) {
496 $err = "Incomplete GD library configuration: missing function $loader";
497 wfDebug( "$err\n" );
498 $errMsg = wfMsg ( 'thumbnail_gd-library', $loader );
499 return $this->getMediaTransformError( $params, $errMsg );
500 }
501
502 if ( !file_exists( $params['srcPath'] ) ) {
503 $err = "File seems to be missing: {$params['srcPath']}";
504 wfDebug( "$err\n" );
505 $errMsg = wfMsg ( 'thumbnail_image-missing', $params['srcPath'] );
506 return $this->getMediaTransformError( $params, $errMsg );
507 }
508
509 $src_image = call_user_func( $loader, $params['srcPath'] );
510
511 $rotation = function_exists( 'imagerotate' ) ? $this->getRotation( $image ) : 0;
512 if ( $rotation == 90 || $rotation == 270 ) {
513 # We'll resize before rotation, so swap the dimensions again
514 $width = $params['physicalHeight'];
515 $height = $params['physicalWidth'];
516 } else {
517 $width = $params['physicalWidth'];
518 $height = $params['physicalHeight'];
519 }
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 * Its unclear if anything still uses this
675 * as jpeg is now in its own subclass.
676 *
677 * And really each media handler should use a
678 * different getMetadata, as the formats aren't
679 * all that similar and usually have different
680 * metadata needs.
681 *
682 * @deprecated
683 */
684 function getMetadata( $image, $filename ) {
685 wfDeprecated( __METHOD__ );
686 global $wgShowEXIF;
687 if ( $wgShowEXIF && file_exists( $filename ) ) {
688 $exif = new Exif( $filename );
689 $data = $exif->getFilteredData();
690 if ( $data ) {
691 $data['MEDIAWIKI_EXIF_VERSION'] = Exif::version();
692 return serialize( $data );
693 } else {
694 return '0';
695 }
696 } else {
697 return '';
698 }
699 }
700
701 function getMetadataType( $image ) {
702 return 'exif';
703 }
704
705 function isMetadataValid( $image, $metadata ) {
706 global $wgShowEXIF;
707 if ( !$wgShowEXIF ) {
708 # Metadata disabled and so an empty field is expected
709 return true;
710 }
711 if ( $metadata === '0' ) {
712 # Special value indicating that there is no EXIF data in the file
713 return true;
714 }
715 wfSuppressWarnings();
716 $exif = unserialize( $metadata );
717 wfRestoreWarnings();
718 if ( !isset( $exif['MEDIAWIKI_EXIF_VERSION'] ) ||
719 $exif['MEDIAWIKI_EXIF_VERSION'] != Exif::version() )
720 {
721 # Wrong version
722 wfDebug( __METHOD__ . ": wrong version\n" );
723 return false;
724 }
725 return true;
726 }
727
728 /**
729 * Get a list of EXIF metadata items which should be displayed when
730 * the metadata table is collapsed.
731 *
732 * @return array of strings
733 * @access private
734 */
735 function visibleMetadataFields() {
736 $fields = array();
737 $lines = explode( "\n", wfMsgForContent( 'metadata-fields' ) );
738 foreach ( $lines as $line ) {
739 $matches = array();
740 if ( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) {
741 $fields[] = $matches[1];
742 }
743 }
744 $fields = array_map( 'strtolower', $fields );
745 return $fields;
746 }
747
748 /**
749 * @param $image File
750 * @return array|bool
751 */
752 function formatMetadata( $image ) {
753 $result = array(
754 'visible' => array(),
755 'collapsed' => array()
756 );
757 $metadata = $image->getMetadata();
758 if ( !$metadata ) {
759 return false;
760 }
761 $exif = unserialize( $metadata );
762 if ( !$exif ) {
763 return false;
764 }
765 unset( $exif['MEDIAWIKI_EXIF_VERSION'] );
766 $format = new FormatExif( $exif );
767
768 $formatted = $format->getFormattedData();
769 // Sort fields into visible and collapsed
770 $visibleFields = $this->visibleMetadataFields();
771 foreach ( $formatted as $name => $value ) {
772 $tag = strtolower( $name );
773 self::addMeta( $result,
774 in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
775 'exif',
776 $tag,
777 $value
778 );
779 }
780 return $result;
781 }
782
783 /**
784 * Try to read out the orientation of the file and return the angle that
785 * the file needs to be rotated to be viewed
786 *
787 * @param $file File
788 * @return int 0, 90, 180 or 270
789 */
790 public function getRotation( $file ) {
791 $data = $file->getMetadata();
792 if ( !$data ) {
793 return 0;
794 }
795 $data = unserialize( $data );
796 if ( isset( $data['Orientation'] ) ) {
797 # See http://sylvana.net/jpegcrop/exif_orientation.html
798 switch ( $data['Orientation'] ) {
799 case 8:
800 return 90;
801 case 3:
802 return 180;
803 case 6:
804 return 270;
805 default:
806 return 0;
807 }
808 }
809 return 0;
810 }
811 /**
812 * Returns whether the current scaler supports rotation (im and gd do)
813 *
814 * @return bool
815 */
816 public static function canRotate() {
817 $scaler = self::getScalerType( null, false );
818 switch ( $scaler ) {
819 case 'im':
820 # ImageMagick supports autorotation
821 return true;
822 case 'imext':
823 # Imagick::rotateImage
824 return true;
825 case 'gd':
826 # GD's imagerotate function is used to rotate images, but not
827 # all precompiled PHP versions have that function
828 return function_exists( 'imagerotate' );
829 default:
830 # Other scalers don't support rotation
831 return false;
832 }
833 }
834
835 /**
836 * Rerurns whether the file needs to be rendered. Returns true if the
837 * file requires rotation and we are able to rotate it.
838 *
839 * @param $file File
840 * @return bool
841 */
842 public function mustRender( $file ) {
843 return self::canRotate() && $this->getRotation( $file ) != 0;
844 }
845 }