MediaWiki  REL1_21
Bitmap.php
Go to the documentation of this file.
00001 <?php
00029 class BitmapHandler extends ImageHandler {
00037         function normaliseParams( $image, &$params ) {
00038                 if ( !parent::normaliseParams( $image, $params ) ) {
00039                         return false;
00040                 }
00041 
00042                 # Obtain the source, pre-rotation dimensions
00043                 $srcWidth = $image->getWidth( $params['page'] );
00044                 $srcHeight = $image->getHeight( $params['page'] );
00045 
00046                 # Don't make an image bigger than the source
00047                 if ( $params['physicalWidth'] >= $srcWidth ) {
00048                         $params['physicalWidth'] = $srcWidth;
00049                         $params['physicalHeight'] = $srcHeight;
00050 
00051                         # Skip scaling limit checks if no scaling is required
00052                         # due to requested size being bigger than source.
00053                         if ( !$image->mustRender() ) {
00054                                 return true;
00055                         }
00056                 }
00057 
00058                 # Check if the file is smaller than the maximum image area for thumbnailing
00059                 $checkImageAreaHookResult = null;
00060                 wfRunHooks( 'BitmapHandlerCheckImageArea', array( $image, &$params, &$checkImageAreaHookResult ) );
00061                 if ( is_null( $checkImageAreaHookResult ) ) {
00062                         global $wgMaxImageArea;
00063 
00064                         if ( $srcWidth * $srcHeight > $wgMaxImageArea &&
00065                                         !( $image->getMimeType() == 'image/jpeg' &&
00066                                                 self::getScalerType( false, false ) == 'im' ) ) {
00067                                 # Only ImageMagick can efficiently downsize jpg images without loading
00068                                 # the entire file in memory
00069                                 return false;
00070                         }
00071                 } else {
00072                         return $checkImageAreaHookResult;
00073                 }
00074 
00075                 return true;
00076         }
00077 
00090         public function extractPreRotationDimensions( $params, $rotation ) {
00091                 if ( $rotation == 90 || $rotation == 270 ) {
00092                         # We'll resize before rotation, so swap the dimensions again
00093                         $width = $params['physicalHeight'];
00094                         $height = $params['physicalWidth'];
00095                 } else {
00096                         $width = $params['physicalWidth'];
00097                         $height = $params['physicalHeight'];
00098                 }
00099                 return array( $width, $height );
00100         }
00101 
00109         function getImageArea( $image ) {
00110                 return $image->getWidth() * $image->getHeight();
00111         }
00112 
00121         function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
00122                 if ( !$this->normaliseParams( $image, $params ) ) {
00123                         return new TransformParameterError( $params );
00124                 }
00125                 # Create a parameter array to pass to the scaler
00126                 $scalerParams = array(
00127                         # The size to which the image will be resized
00128                         'physicalWidth' => $params['physicalWidth'],
00129                         'physicalHeight' => $params['physicalHeight'],
00130                         'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}",
00131                         # The size of the image on the page
00132                         'clientWidth' => $params['width'],
00133                         'clientHeight' => $params['height'],
00134                         # Comment as will be added to the EXIF of the thumbnail
00135                         'comment' => isset( $params['descriptionUrl'] ) ?
00136                                 "File source: {$params['descriptionUrl']}" : '',
00137                         # Properties of the original image
00138                         'srcWidth' => $image->getWidth(),
00139                         'srcHeight' => $image->getHeight(),
00140                         'mimeType' => $image->getMimeType(),
00141                         'dstPath' => $dstPath,
00142                         'dstUrl' => $dstUrl,
00143                 );
00144 
00145                 # Determine scaler type
00146                 $scaler = self::getScalerType( $dstPath );
00147 
00148                 wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} thumbnail at $dstPath using scaler $scaler\n" );
00149 
00150                 if ( !$image->mustRender() &&
00151                                 $scalerParams['physicalWidth'] == $scalerParams['srcWidth']
00152                                 && $scalerParams['physicalHeight'] == $scalerParams['srcHeight'] ) {
00153 
00154                         # normaliseParams (or the user) wants us to return the unscaled image
00155                         wfDebug( __METHOD__ . ": returning unscaled image\n" );
00156                         return $this->getClientScalingThumbnailImage( $image, $scalerParams );
00157                 }
00158 
00159                 if ( $scaler == 'client' ) {
00160                         # Client-side image scaling, use the source URL
00161                         # Using the destination URL in a TRANSFORM_LATER request would be incorrect
00162                         return $this->getClientScalingThumbnailImage( $image, $scalerParams );
00163                 }
00164 
00165                 if ( $flags & self::TRANSFORM_LATER ) {
00166                         wfDebug( __METHOD__ . ": Transforming later per flags.\n" );
00167                         $params = array(
00168                                 'width' => $scalerParams['clientWidth'],
00169                                 'height' => $scalerParams['clientHeight']
00170                         );
00171                         return new ThumbnailImage( $image, $dstUrl, false, $params );
00172                 }
00173 
00174                 # Try to make a target path for the thumbnail
00175                 if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
00176                         wfDebug( __METHOD__ . ": Unable to create thumbnail destination directory, falling back to client scaling\n" );
00177                         return $this->getClientScalingThumbnailImage( $image, $scalerParams );
00178                 }
00179 
00180                 # Transform functions and binaries need a FS source file
00181                 $scalerParams['srcPath'] = $image->getLocalRefPath();
00182 
00183                 # Try a hook
00184                 $mto = null;
00185                 wfRunHooks( 'BitmapHandlerTransform', array( $this, $image, &$scalerParams, &$mto ) );
00186                 if ( !is_null( $mto ) ) {
00187                         wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto\n" );
00188                         $scaler = 'hookaborted';
00189                 }
00190 
00191                 switch ( $scaler ) {
00192                         case 'hookaborted':
00193                                 # Handled by the hook above
00194                                 $err = $mto->isError() ? $mto : false;
00195                                 break;
00196                         case 'im':
00197                                 $err = $this->transformImageMagick( $image, $scalerParams );
00198                                 break;
00199                         case 'custom':
00200                                 $err = $this->transformCustom( $image, $scalerParams );
00201                                 break;
00202                         case 'imext':
00203                                 $err = $this->transformImageMagickExt( $image, $scalerParams );
00204                                 break;
00205                         case 'gd':
00206                         default:
00207                                 $err = $this->transformGd( $image, $scalerParams );
00208                                 break;
00209                 }
00210 
00211                 # Remove the file if a zero-byte thumbnail was created, or if there was an error
00212                 $removed = $this->removeBadFile( $dstPath, (bool)$err );
00213                 if ( $err ) {
00214                         # transform returned MediaTransforError
00215                         return $err;
00216                 } elseif ( $removed ) {
00217                         # Thumbnail was zero-byte and had to be removed
00218                         return new MediaTransformError( 'thumbnail_error',
00219                                 $scalerParams['clientWidth'], $scalerParams['clientHeight'] );
00220                 } elseif ( $mto ) {
00221                         return $mto;
00222                 } else {
00223                         $params = array(
00224                                 'width' => $scalerParams['clientWidth'],
00225                                 'height' => $scalerParams['clientHeight']
00226                         );
00227                         return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
00228                 }
00229         }
00230 
00237         protected static function getScalerType( $dstPath, $checkDstPath = true ) {
00238                 global $wgUseImageResize, $wgUseImageMagick, $wgCustomConvertCommand;
00239 
00240                 if ( !$dstPath && $checkDstPath ) {
00241                         # No output path available, client side scaling only
00242                         $scaler = 'client';
00243                 } elseif ( !$wgUseImageResize ) {
00244                         $scaler = 'client';
00245                 } elseif ( $wgUseImageMagick ) {
00246                         $scaler = 'im';
00247                 } elseif ( $wgCustomConvertCommand ) {
00248                         $scaler = 'custom';
00249                 } elseif ( function_exists( 'imagecreatetruecolor' ) ) {
00250                         $scaler = 'gd';
00251                 } elseif ( class_exists( 'Imagick' ) ) {
00252                         $scaler = 'imext';
00253                 } else {
00254                         $scaler = 'client';
00255                 }
00256                 return $scaler;
00257         }
00258 
00269         protected function getClientScalingThumbnailImage( $image, $scalerParams ) {
00270                 $params = array(
00271                         'width' => $scalerParams['clientWidth'],
00272                         'height' => $scalerParams['clientHeight']
00273                 );
00274                 return new ThumbnailImage( $image, $image->getURL(), null, $params );
00275         }
00276 
00285         protected function transformImageMagick( $image, $params ) {
00286                 # use ImageMagick
00287                 global $wgSharpenReductionThreshold, $wgSharpenParameter,
00288                         $wgMaxAnimatedGifArea,
00289                         $wgImageMagickTempDir, $wgImageMagickConvertCommand;
00290 
00291                 $quality = array();
00292                 $sharpen = array();
00293                 $scene = false;
00294                 $animation_pre = array();
00295                 $animation_post = array();
00296                 $decoderHint = array();
00297                 if ( $params['mimeType'] == 'image/jpeg' ) {
00298                         $quality = array( '-quality', '80' ); // 80%
00299                         # Sharpening, see bug 6193
00300                         if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
00301                                         / ( $params['srcWidth'] + $params['srcHeight'] )
00302                                         < $wgSharpenReductionThreshold ) {
00303                                 $sharpen = array( '-sharpen', $wgSharpenParameter );
00304                         }
00305                         if ( version_compare( $this->getMagickVersion(), "6.5.6" ) >= 0 ) {
00306                                 // JPEG decoder hint to reduce memory, available since IM 6.5.6-2
00307                                 $decoderHint = array( '-define', "jpeg:size={$params['physicalDimensions']}" );
00308                         }
00309 
00310                 } elseif ( $params['mimeType'] == 'image/png' ) {
00311                         $quality = array( '-quality', '95' ); // zlib 9, adaptive filtering
00312 
00313                 } elseif ( $params['mimeType'] == 'image/gif' ) {
00314                         if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
00315                                 // Extract initial frame only; we're so big it'll
00316                                 // be a total drag. :P
00317                                 $scene = 0;
00318 
00319                         } elseif ( $this->isAnimatedImage( $image ) ) {
00320                                 // Coalesce is needed to scale animated GIFs properly (bug 1017).
00321                                 $animation_pre = array( '-coalesce' );
00322                                 // We optimize the output, but -optimize is broken,
00323                                 // use optimizeTransparency instead (bug 11822)
00324                                 if ( version_compare( $this->getMagickVersion(), "6.3.5" ) >= 0 ) {
00325                                         $animation_post = array( '-fuzz', '5%', '-layers', 'optimizeTransparency' );
00326                                 }
00327                         }
00328                 } elseif ( $params['mimeType'] == 'image/x-xcf' ) {
00329                         $animation_post = array( '-layers', 'merge' );
00330                 }
00331 
00332                 // Use one thread only, to avoid deadlock bugs on OOM
00333                 $env = array( 'OMP_NUM_THREADS' => 1 );
00334                 if ( strval( $wgImageMagickTempDir ) !== '' ) {
00335                         $env['MAGICK_TMPDIR'] = $wgImageMagickTempDir;
00336                 }
00337 
00338                 $rotation = $this->getRotation( $image );
00339                 list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
00340 
00341                 $cmd = call_user_func_array( 'wfEscapeShellArg', array_merge(
00342                         array( $wgImageMagickConvertCommand ),
00343                         $quality,
00344                         // Specify white background color, will be used for transparent images
00345                         // in Internet Explorer/Windows instead of default black.
00346                         array( '-background', 'white' ),
00347                         $decoderHint,
00348                         array( $this->escapeMagickInput( $params['srcPath'], $scene ) ),
00349                         $animation_pre,
00350                         // For the -thumbnail option a "!" is needed to force exact size,
00351                         // or ImageMagick may decide your ratio is wrong and slice off
00352                         // a pixel.
00353                         array( '-thumbnail', "{$width}x{$height}!" ),
00354                         // Add the source url as a comment to the thumb, but don't add the flag if there's no comment
00355                         ( $params['comment'] !== ''
00356                                 ? array( '-set', 'comment', $this->escapeMagickProperty( $params['comment'] ) )
00357                                 : array() ),
00358                         array( '-depth', 8 ),
00359                         $sharpen,
00360                         array( '-rotate', "-$rotation" ),
00361                         $animation_post,
00362                         array( $this->escapeMagickOutput( $params['dstPath'] ) ) ) ) . " 2>&1";
00363 
00364                 wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
00365                 wfProfileIn( 'convert' );
00366                 $retval = 0;
00367                 $err = wfShellExec( $cmd, $retval, $env );
00368                 wfProfileOut( 'convert' );
00369 
00370                 if ( $retval !== 0 ) {
00371                         $this->logErrorForExternalProcess( $retval, $err, $cmd );
00372                         return $this->getMediaTransformError( $params, $err );
00373                 }
00374 
00375                 return false; # No error
00376         }
00377 
00386         protected function transformImageMagickExt( $image, $params ) {
00387                 global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea;
00388 
00389                 try {
00390                         $im = new Imagick();
00391                         $im->readImage( $params['srcPath'] );
00392 
00393                         if ( $params['mimeType'] == 'image/jpeg' ) {
00394                                 // Sharpening, see bug 6193
00395                                 if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
00396                                                 / ( $params['srcWidth'] + $params['srcHeight'] )
00397                                                 < $wgSharpenReductionThreshold ) {
00398                                         // Hack, since $wgSharpenParamater is written specifically for the command line convert
00399                                         list( $radius, $sigma ) = explode( 'x', $wgSharpenParameter );
00400                                         $im->sharpenImage( $radius, $sigma );
00401                                 }
00402                                 $im->setCompressionQuality( 80 );
00403                         } elseif( $params['mimeType'] == 'image/png' ) {
00404                                 $im->setCompressionQuality( 95 );
00405                         } elseif ( $params['mimeType'] == 'image/gif' ) {
00406                                 if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
00407                                         // Extract initial frame only; we're so big it'll
00408                                         // be a total drag. :P
00409                                         $im->setImageScene( 0 );
00410                                 } elseif ( $this->isAnimatedImage( $image ) ) {
00411                                         // Coalesce is needed to scale animated GIFs properly (bug 1017).
00412                                         $im = $im->coalesceImages();
00413                                 }
00414                         }
00415 
00416                         $rotation = $this->getRotation( $image );
00417                         list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
00418 
00419                         $im->setImageBackgroundColor( new ImagickPixel( 'white' ) );
00420 
00421                         // Call Imagick::thumbnailImage on each frame
00422                         foreach ( $im as $i => $frame ) {
00423                                 if ( !$frame->thumbnailImage( $width, $height, /* fit */ false ) ) {
00424                                         return $this->getMediaTransformError( $params, "Error scaling frame $i" );
00425                                 }
00426                         }
00427                         $im->setImageDepth( 8 );
00428 
00429                         if ( $rotation ) {
00430                                 if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
00431                                         return $this->getMediaTransformError( $params, "Error rotating $rotation degrees" );
00432                                 }
00433                         }
00434 
00435                         if ( $this->isAnimatedImage( $image ) ) {
00436                                 wfDebug( __METHOD__ . ": Writing animated thumbnail\n" );
00437                                 // This is broken somehow... can't find out how to fix it
00438                                 $result = $im->writeImages( $params['dstPath'], true );
00439                         } else {
00440                                 $result = $im->writeImage( $params['dstPath'] );
00441                         }
00442                         if ( !$result ) {
00443                                 return $this->getMediaTransformError( $params,
00444                                         "Unable to write thumbnail to {$params['dstPath']}" );
00445                         }
00446 
00447                 } catch ( ImagickException $e ) {
00448                         return $this->getMediaTransformError( $params, $e->getMessage() );
00449                 }
00450 
00451                 return false;
00452 
00453         }
00454 
00463         protected function transformCustom( $image, $params ) {
00464                 # Use a custom convert command
00465                 global $wgCustomConvertCommand;
00466 
00467                 # Variables: %s %d %w %h
00468                 $src = wfEscapeShellArg( $params['srcPath'] );
00469                 $dst = wfEscapeShellArg( $params['dstPath'] );
00470                 $cmd = $wgCustomConvertCommand;
00471                 $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames
00472                 $cmd = str_replace( '%h', wfEscapeShellArg( $params['physicalHeight'] ),
00473                         str_replace( '%w', wfEscapeShellArg( $params['physicalWidth'] ), $cmd ) ); # Size
00474                 wfDebug( __METHOD__ . ": Running custom convert command $cmd\n" );
00475                 wfProfileIn( 'convert' );
00476                 $retval = 0;
00477                 $err = wfShellExec( $cmd, $retval );
00478                 wfProfileOut( 'convert' );
00479 
00480                 if ( $retval !== 0 ) {
00481                         $this->logErrorForExternalProcess( $retval, $err, $cmd );
00482                         return $this->getMediaTransformError( $params, $err );
00483                 }
00484                 return false; # No error
00485         }
00486 
00494         protected function logErrorForExternalProcess( $retval, $err, $cmd ) {
00495                 wfDebugLog( 'thumbnail',
00496                         sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"',
00497                                         wfHostname(), $retval, trim( $err ), $cmd ) );
00498         }
00506         public function getMediaTransformError( $params, $errMsg ) {
00507                 return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
00508                                         $params['clientHeight'], $errMsg );
00509         }
00510 
00519         protected function transformGd( $image, $params ) {
00520                 # Use PHP's builtin GD library functions.
00521                 #
00522                 # First find out what kind of file this is, and select the correct
00523                 # input routine for this.
00524 
00525                 $typemap = array(
00526                         'image/gif'          => array( 'imagecreatefromgif',  'palette',   'imagegif'  ),
00527                         'image/jpeg'         => array( 'imagecreatefromjpeg', 'truecolor', array( __CLASS__, 'imageJpegWrapper' ) ),
00528                         'image/png'          => array( 'imagecreatefrompng',  'bits',      'imagepng'  ),
00529                         'image/vnd.wap.wbmp' => array( 'imagecreatefromwbmp', 'palette',   'imagewbmp'  ),
00530                         'image/xbm'          => array( 'imagecreatefromxbm',  'palette',   'imagexbm'  ),
00531                 );
00532                 if ( !isset( $typemap[$params['mimeType']] ) ) {
00533                         $err = 'Image type not supported';
00534                         wfDebug( "$err\n" );
00535                         $errMsg = wfMessage( 'thumbnail_image-type' )->text();
00536                         return $this->getMediaTransformError( $params, $errMsg );
00537                 }
00538                 list( $loader, $colorStyle, $saveType ) = $typemap[$params['mimeType']];
00539 
00540                 if ( !function_exists( $loader ) ) {
00541                         $err = "Incomplete GD library configuration: missing function $loader";
00542                         wfDebug( "$err\n" );
00543                         $errMsg = wfMessage( 'thumbnail_gd-library', $loader )->text();
00544                         return $this->getMediaTransformError( $params, $errMsg );
00545                 }
00546 
00547                 if ( !file_exists( $params['srcPath'] ) ) {
00548                         $err = "File seems to be missing: {$params['srcPath']}";
00549                         wfDebug( "$err\n" );
00550                         $errMsg = wfMessage( 'thumbnail_image-missing', $params['srcPath'] )->text();
00551                         return $this->getMediaTransformError( $params, $errMsg );
00552                 }
00553 
00554                 $src_image = call_user_func( $loader, $params['srcPath'] );
00555 
00556                 $rotation = function_exists( 'imagerotate' ) ? $this->getRotation( $image ) : 0;
00557                 list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
00558                 $dst_image = imagecreatetruecolor( $width, $height );
00559 
00560                 // Initialise the destination image to transparent instead of
00561                 // the default solid black, to support PNG and GIF transparency nicely
00562                 $background = imagecolorallocate( $dst_image, 0, 0, 0 );
00563                 imagecolortransparent( $dst_image, $background );
00564                 imagealphablending( $dst_image, false );
00565 
00566                 if ( $colorStyle == 'palette' ) {
00567                         // Don't resample for paletted GIF images.
00568                         // It may just uglify them, and completely breaks transparency.
00569                         imagecopyresized( $dst_image, $src_image,
00570                                 0, 0, 0, 0,
00571                                 $width, $height,
00572                                 imagesx( $src_image ), imagesy( $src_image ) );
00573                 } else {
00574                         imagecopyresampled( $dst_image, $src_image,
00575                                 0, 0, 0, 0,
00576                                 $width, $height,
00577                                 imagesx( $src_image ), imagesy( $src_image ) );
00578                 }
00579 
00580                 if ( $rotation % 360 != 0 && $rotation % 90 == 0 ) {
00581                         $rot_image = imagerotate( $dst_image, $rotation, 0 );
00582                         imagedestroy( $dst_image );
00583                         $dst_image = $rot_image;
00584                 }
00585 
00586                 imagesavealpha( $dst_image, true );
00587 
00588                 call_user_func( $saveType, $dst_image, $params['dstPath'] );
00589                 imagedestroy( $dst_image );
00590                 imagedestroy( $src_image );
00591 
00592                 return false; # No error
00593         }
00594 
00600         function escapeMagickProperty( $s ) {
00601                 // Double the backslashes
00602                 $s = str_replace( '\\', '\\\\', $s );
00603                 // Double the percents
00604                 $s = str_replace( '%', '%%', $s );
00605                 // Escape initial - or @
00606                 if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
00607                         $s = '\\' . $s;
00608                 }
00609                 return $s;
00610         }
00611 
00629         function escapeMagickInput( $path, $scene = false ) {
00630                 # Die on initial metacharacters (caller should prepend path)
00631                 $firstChar = substr( $path, 0, 1 );
00632                 if ( $firstChar === '~' || $firstChar === '@' ) {
00633                         throw new MWException( __METHOD__ . ': cannot escape this path name' );
00634                 }
00635 
00636                 # Escape glob chars
00637                 $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
00638 
00639                 return $this->escapeMagickPath( $path, $scene );
00640         }
00641 
00647         function escapeMagickOutput( $path, $scene = false ) {
00648                 $path = str_replace( '%', '%%', $path );
00649                 return $this->escapeMagickPath( $path, $scene );
00650         }
00651 
00661         protected function escapeMagickPath( $path, $scene = false ) {
00662                 # Die on format specifiers (other than drive letters). The regex is
00663                 # meant to match all the formats you get from "convert -list format"
00664                 if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
00665                         if ( wfIsWindows() && is_dir( $m[0] ) ) {
00666                                 // OK, it's a drive letter
00667                                 // ImageMagick has a similar exception, see IsMagickConflict()
00668                         } else {
00669                                 throw new MWException( __METHOD__ . ': unexpected colon character in path name' );
00670                         }
00671                 }
00672 
00673                 # If there are square brackets, add a do-nothing scene specification
00674                 # to force a literal interpretation
00675                 if ( $scene === false ) {
00676                         if ( strpos( $path, '[' ) !== false ) {
00677                                 $path .= '[0--1]';
00678                         }
00679                 } else {
00680                         $path .= "[$scene]";
00681                 }
00682                 return $path;
00683         }
00684 
00691         protected function getMagickVersion() {
00692                 global $wgMemc;
00693 
00694                 $cache = $wgMemc->get( "imagemagick-version" );
00695                 if ( !$cache ) {
00696                         global $wgImageMagickConvertCommand;
00697                         $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . ' -version';
00698                         wfDebug( __METHOD__ . ": Running convert -version\n" );
00699                         $retval = '';
00700                         $return = wfShellExec( $cmd, $retval );
00701                         $x = preg_match( '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches );
00702                         if ( $x != 1 ) {
00703                                 wfDebug( __METHOD__ . ": ImageMagick version check failed\n" );
00704                                 return null;
00705                         }
00706                         $wgMemc->set( "imagemagick-version", $matches[1], 3600 );
00707                         return $matches[1];
00708                 }
00709                 return $cache;
00710         }
00711 
00712         static function imageJpegWrapper( $dst_image, $thumbPath ) {
00713                 imageinterlace( $dst_image );
00714                 imagejpeg( $dst_image, $thumbPath, 95 );
00715         }
00716 
00732         public function getRotation( $file ) {
00733                 return 0;
00734         }
00735 
00741         public static function canRotate() {
00742                 $scaler = self::getScalerType( null, false );
00743                 switch ( $scaler ) {
00744                         case 'im':
00745                                 # ImageMagick supports autorotation
00746                                 return true;
00747                         case 'imext':
00748                                 # Imagick::rotateImage
00749                                 return true;
00750                         case 'gd':
00751                                 # GD's imagerotate function is used to rotate images, but not
00752                                 # all precompiled PHP versions have that function
00753                                 return function_exists( 'imagerotate' );
00754                         default:
00755                                 # Other scalers don't support rotation
00756                                 return false;
00757                 }
00758         }
00759 
00767         public function rotate( $file, $params ) {
00768                 global $wgImageMagickConvertCommand;
00769 
00770                 $rotation = ( $params[ 'rotation' ] + $this->getRotation( $file ) ) % 360;
00771                 $scene = false;
00772 
00773                 $scaler = self::getScalerType( null, false );
00774                 switch ( $scaler ) {
00775                         case 'im':
00776                                 $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . " " .
00777                                         wfEscapeShellArg( $this->escapeMagickInput( $params[ 'srcPath' ], $scene ) ) .
00778                                         " -rotate " . wfEscapeShellArg( "-$rotation" ) . " " .
00779                                         wfEscapeShellArg( $this->escapeMagickOutput( $params[ 'dstPath' ] ) ) . " 2>&1";
00780                                 wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
00781                                 wfProfileIn( 'convert' );
00782                                 $retval = 0;
00783                                 $err = wfShellExec( $cmd, $retval, $env );
00784                                 wfProfileOut( 'convert' );
00785                                 if ( $retval !== 0 ) {
00786                                         $this->logErrorForExternalProcess( $retval, $err, $cmd );
00787                                         return new MediaTransformError( 'thumbnail_error', 0, 0, $err );
00788                                 }
00789                                 return false;
00790                         case 'imext':
00791                                 $im = new Imagick();
00792                                 $im->readImage( $params['srcPath'] );
00793                                 if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
00794                                         return new MediaTransformError( 'thumbnail_error', 0, 0,
00795                                                 "Error rotating $rotation degrees" );
00796                                 }
00797                                 $result = $im->writeImage( $params['dstPath'] );
00798                                 if ( !$result ) {
00799                                         return new MediaTransformError( 'thumbnail_error', 0, 0,
00800                                                 "Unable to write image to {$params['dstPath']}" );
00801                                 }
00802                                 return false;
00803                         default:
00804                                 return new MediaTransformError( 'thumbnail_error', 0, 0,
00805                                         "$scaler rotation not implemented" );
00806                 }
00807         }
00808 
00816         public function mustRender( $file ) {
00817                 return self::canRotate() && $this->getRotation( $file ) != 0;
00818         }
00819 }