MediaWiki  REL1_22
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 
00110     function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
00111         if ( !$this->normaliseParams( $image, $params ) ) {
00112             return new TransformParameterError( $params );
00113         }
00114         # Create a parameter array to pass to the scaler
00115         $scalerParams = array(
00116             # The size to which the image will be resized
00117             'physicalWidth' => $params['physicalWidth'],
00118             'physicalHeight' => $params['physicalHeight'],
00119             'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}",
00120             # The size of the image on the page
00121             'clientWidth' => $params['width'],
00122             'clientHeight' => $params['height'],
00123             # Comment as will be added to the Exif of the thumbnail
00124             'comment' => isset( $params['descriptionUrl'] ) ?
00125                 "File source: {$params['descriptionUrl']}" : '',
00126             # Properties of the original image
00127             'srcWidth' => $image->getWidth(),
00128             'srcHeight' => $image->getHeight(),
00129             'mimeType' => $image->getMimeType(),
00130             'dstPath' => $dstPath,
00131             'dstUrl' => $dstUrl,
00132         );
00133 
00134         # Determine scaler type
00135         $scaler = self::getScalerType( $dstPath );
00136 
00137         wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} thumbnail at $dstPath using scaler $scaler\n" );
00138 
00139         if ( !$image->mustRender() &&
00140                 $scalerParams['physicalWidth'] == $scalerParams['srcWidth']
00141                 && $scalerParams['physicalHeight'] == $scalerParams['srcHeight'] ) {
00142 
00143             # normaliseParams (or the user) wants us to return the unscaled image
00144             wfDebug( __METHOD__ . ": returning unscaled image\n" );
00145             return $this->getClientScalingThumbnailImage( $image, $scalerParams );
00146         }
00147 
00148         if ( $scaler == 'client' ) {
00149             # Client-side image scaling, use the source URL
00150             # Using the destination URL in a TRANSFORM_LATER request would be incorrect
00151             return $this->getClientScalingThumbnailImage( $image, $scalerParams );
00152         }
00153 
00154         if ( $flags & self::TRANSFORM_LATER ) {
00155             wfDebug( __METHOD__ . ": Transforming later per flags.\n" );
00156             $params = array(
00157                 'width' => $scalerParams['clientWidth'],
00158                 'height' => $scalerParams['clientHeight']
00159             );
00160             return new ThumbnailImage( $image, $dstUrl, false, $params );
00161         }
00162 
00163         # Try to make a target path for the thumbnail
00164         if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
00165             wfDebug( __METHOD__ . ": Unable to create thumbnail destination directory, falling back to client scaling\n" );
00166             return $this->getClientScalingThumbnailImage( $image, $scalerParams );
00167         }
00168 
00169         # Transform functions and binaries need a FS source file
00170         $scalerParams['srcPath'] = $image->getLocalRefPath();
00171 
00172         # Try a hook
00173         $mto = null;
00174         wfRunHooks( 'BitmapHandlerTransform', array( $this, $image, &$scalerParams, &$mto ) );
00175         if ( !is_null( $mto ) ) {
00176             wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto\n" );
00177             $scaler = 'hookaborted';
00178         }
00179 
00180         switch ( $scaler ) {
00181             case 'hookaborted':
00182                 # Handled by the hook above
00183                 $err = $mto->isError() ? $mto : false;
00184                 break;
00185             case 'im':
00186                 $err = $this->transformImageMagick( $image, $scalerParams );
00187                 break;
00188             case 'custom':
00189                 $err = $this->transformCustom( $image, $scalerParams );
00190                 break;
00191             case 'imext':
00192                 $err = $this->transformImageMagickExt( $image, $scalerParams );
00193                 break;
00194             case 'gd':
00195             default:
00196                 $err = $this->transformGd( $image, $scalerParams );
00197                 break;
00198         }
00199 
00200         # Remove the file if a zero-byte thumbnail was created, or if there was an error
00201         $removed = $this->removeBadFile( $dstPath, (bool)$err );
00202         if ( $err ) {
00203             # transform returned MediaTransforError
00204             return $err;
00205         } elseif ( $removed ) {
00206             # Thumbnail was zero-byte and had to be removed
00207             return new MediaTransformError( 'thumbnail_error',
00208                 $scalerParams['clientWidth'], $scalerParams['clientHeight'] );
00209         } elseif ( $mto ) {
00210             return $mto;
00211         } else {
00212             $params = array(
00213                 'width' => $scalerParams['clientWidth'],
00214                 'height' => $scalerParams['clientHeight']
00215             );
00216             return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
00217         }
00218     }
00219 
00226     protected static function getScalerType( $dstPath, $checkDstPath = true ) {
00227         global $wgUseImageResize, $wgUseImageMagick, $wgCustomConvertCommand;
00228 
00229         if ( !$dstPath && $checkDstPath ) {
00230             # No output path available, client side scaling only
00231             $scaler = 'client';
00232         } elseif ( !$wgUseImageResize ) {
00233             $scaler = 'client';
00234         } elseif ( $wgUseImageMagick ) {
00235             $scaler = 'im';
00236         } elseif ( $wgCustomConvertCommand ) {
00237             $scaler = 'custom';
00238         } elseif ( function_exists( 'imagecreatetruecolor' ) ) {
00239             $scaler = 'gd';
00240         } elseif ( class_exists( 'Imagick' ) ) {
00241             $scaler = 'imext';
00242         } else {
00243             $scaler = 'client';
00244         }
00245         return $scaler;
00246     }
00247 
00258     protected function getClientScalingThumbnailImage( $image, $scalerParams ) {
00259         $params = array(
00260             'width' => $scalerParams['clientWidth'],
00261             'height' => $scalerParams['clientHeight']
00262         );
00263         return new ThumbnailImage( $image, $image->getURL(), null, $params );
00264     }
00265 
00274     protected function transformImageMagick( $image, $params ) {
00275         # use ImageMagick
00276         global $wgSharpenReductionThreshold, $wgSharpenParameter,
00277             $wgMaxAnimatedGifArea,
00278             $wgImageMagickTempDir, $wgImageMagickConvertCommand;
00279 
00280         $quality = array();
00281         $sharpen = array();
00282         $scene = false;
00283         $animation_pre = array();
00284         $animation_post = array();
00285         $decoderHint = array();
00286         if ( $params['mimeType'] == 'image/jpeg' ) {
00287             $quality = array( '-quality', '80' ); // 80%
00288             # Sharpening, see bug 6193
00289             if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
00290                     / ( $params['srcWidth'] + $params['srcHeight'] )
00291                     < $wgSharpenReductionThreshold ) {
00292                 $sharpen = array( '-sharpen', $wgSharpenParameter );
00293             }
00294             if ( version_compare( $this->getMagickVersion(), "6.5.6" ) >= 0 ) {
00295                 // JPEG decoder hint to reduce memory, available since IM 6.5.6-2
00296                 $decoderHint = array( '-define', "jpeg:size={$params['physicalDimensions']}" );
00297             }
00298 
00299         } elseif ( $params['mimeType'] == 'image/png' ) {
00300             $quality = array( '-quality', '95' ); // zlib 9, adaptive filtering
00301 
00302         } elseif ( $params['mimeType'] == 'image/gif' ) {
00303             if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
00304                 // Extract initial frame only; we're so big it'll
00305                 // be a total drag. :P
00306                 $scene = 0;
00307 
00308             } elseif ( $this->isAnimatedImage( $image ) ) {
00309                 // Coalesce is needed to scale animated GIFs properly (bug 1017).
00310                 $animation_pre = array( '-coalesce' );
00311                 // We optimize the output, but -optimize is broken,
00312                 // use optimizeTransparency instead (bug 11822)
00313                 if ( version_compare( $this->getMagickVersion(), "6.3.5" ) >= 0 ) {
00314                     $animation_post = array( '-fuzz', '5%', '-layers', 'optimizeTransparency' );
00315                 }
00316             }
00317         } elseif ( $params['mimeType'] == 'image/x-xcf' ) {
00318             $animation_post = array( '-layers', 'merge' );
00319         }
00320 
00321         // Use one thread only, to avoid deadlock bugs on OOM
00322         $env = array( 'OMP_NUM_THREADS' => 1 );
00323         if ( strval( $wgImageMagickTempDir ) !== '' ) {
00324             $env['MAGICK_TMPDIR'] = $wgImageMagickTempDir;
00325         }
00326 
00327         $rotation = $this->getRotation( $image );
00328         list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
00329 
00330         $cmd = call_user_func_array( 'wfEscapeShellArg', array_merge(
00331             array( $wgImageMagickConvertCommand ),
00332             $quality,
00333             // Specify white background color, will be used for transparent images
00334             // in Internet Explorer/Windows instead of default black.
00335             array( '-background', 'white' ),
00336             $decoderHint,
00337             array( $this->escapeMagickInput( $params['srcPath'], $scene ) ),
00338             $animation_pre,
00339             // For the -thumbnail option a "!" is needed to force exact size,
00340             // or ImageMagick may decide your ratio is wrong and slice off
00341             // a pixel.
00342             array( '-thumbnail', "{$width}x{$height}!" ),
00343             // Add the source url as a comment to the thumb, but don't add the flag if there's no comment
00344             ( $params['comment'] !== ''
00345                 ? array( '-set', 'comment', $this->escapeMagickProperty( $params['comment'] ) )
00346                 : array() ),
00347             array( '-depth', 8 ),
00348             $sharpen,
00349             array( '-rotate', "-$rotation" ),
00350             $animation_post,
00351             array( $this->escapeMagickOutput( $params['dstPath'] ) ) ) );
00352 
00353         wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
00354         wfProfileIn( 'convert' );
00355         $retval = 0;
00356         $err = wfShellExecWithStderr( $cmd, $retval, $env );
00357         wfProfileOut( 'convert' );
00358 
00359         if ( $retval !== 0 ) {
00360             $this->logErrorForExternalProcess( $retval, $err, $cmd );
00361             return $this->getMediaTransformError( $params, $err );
00362         }
00363 
00364         return false; # No error
00365     }
00366 
00375     protected function transformImageMagickExt( $image, $params ) {
00376         global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea;
00377 
00378         try {
00379             $im = new Imagick();
00380             $im->readImage( $params['srcPath'] );
00381 
00382             if ( $params['mimeType'] == 'image/jpeg' ) {
00383                 // Sharpening, see bug 6193
00384                 if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
00385                         / ( $params['srcWidth'] + $params['srcHeight'] )
00386                         < $wgSharpenReductionThreshold ) {
00387                     // Hack, since $wgSharpenParamater is written specifically for the command line convert
00388                     list( $radius, $sigma ) = explode( 'x', $wgSharpenParameter );
00389                     $im->sharpenImage( $radius, $sigma );
00390                 }
00391                 $im->setCompressionQuality( 80 );
00392             } elseif ( $params['mimeType'] == 'image/png' ) {
00393                 $im->setCompressionQuality( 95 );
00394             } elseif ( $params['mimeType'] == 'image/gif' ) {
00395                 if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
00396                     // Extract initial frame only; we're so big it'll
00397                     // be a total drag. :P
00398                     $im->setImageScene( 0 );
00399                 } elseif ( $this->isAnimatedImage( $image ) ) {
00400                     // Coalesce is needed to scale animated GIFs properly (bug 1017).
00401                     $im = $im->coalesceImages();
00402                 }
00403             }
00404 
00405             $rotation = $this->getRotation( $image );
00406             list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
00407 
00408             $im->setImageBackgroundColor( new ImagickPixel( 'white' ) );
00409 
00410             // Call Imagick::thumbnailImage on each frame
00411             foreach ( $im as $i => $frame ) {
00412                 if ( !$frame->thumbnailImage( $width, $height, /* fit */ false ) ) {
00413                     return $this->getMediaTransformError( $params, "Error scaling frame $i" );
00414                 }
00415             }
00416             $im->setImageDepth( 8 );
00417 
00418             if ( $rotation ) {
00419                 if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
00420                     return $this->getMediaTransformError( $params, "Error rotating $rotation degrees" );
00421                 }
00422             }
00423 
00424             if ( $this->isAnimatedImage( $image ) ) {
00425                 wfDebug( __METHOD__ . ": Writing animated thumbnail\n" );
00426                 // This is broken somehow... can't find out how to fix it
00427                 $result = $im->writeImages( $params['dstPath'], true );
00428             } else {
00429                 $result = $im->writeImage( $params['dstPath'] );
00430             }
00431             if ( !$result ) {
00432                 return $this->getMediaTransformError( $params,
00433                     "Unable to write thumbnail to {$params['dstPath']}" );
00434             }
00435 
00436         } catch ( ImagickException $e ) {
00437             return $this->getMediaTransformError( $params, $e->getMessage() );
00438         }
00439 
00440         return false;
00441 
00442     }
00443 
00452     protected function transformCustom( $image, $params ) {
00453         # Use a custom convert command
00454         global $wgCustomConvertCommand;
00455 
00456         # Variables: %s %d %w %h
00457         $src = wfEscapeShellArg( $params['srcPath'] );
00458         $dst = wfEscapeShellArg( $params['dstPath'] );
00459         $cmd = $wgCustomConvertCommand;
00460         $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames
00461         $cmd = str_replace( '%h', wfEscapeShellArg( $params['physicalHeight'] ),
00462             str_replace( '%w', wfEscapeShellArg( $params['physicalWidth'] ), $cmd ) ); # Size
00463         wfDebug( __METHOD__ . ": Running custom convert command $cmd\n" );
00464         wfProfileIn( 'convert' );
00465         $retval = 0;
00466         $err = wfShellExecWithStderr( $cmd, $retval );
00467         wfProfileOut( 'convert' );
00468 
00469         if ( $retval !== 0 ) {
00470             $this->logErrorForExternalProcess( $retval, $err, $cmd );
00471             return $this->getMediaTransformError( $params, $err );
00472         }
00473         return false; # No error
00474     }
00475 
00483     protected function logErrorForExternalProcess( $retval, $err, $cmd ) {
00484         wfDebugLog( 'thumbnail',
00485             sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"',
00486                     wfHostname(), $retval, trim( $err ), $cmd ) );
00487     }
00495     public function getMediaTransformError( $params, $errMsg ) {
00496         return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
00497                     $params['clientHeight'], $errMsg );
00498     }
00499 
00508     protected function transformGd( $image, $params ) {
00509         # Use PHP's builtin GD library functions.
00510         #
00511         # First find out what kind of file this is, and select the correct
00512         # input routine for this.
00513 
00514         $typemap = array(
00515             'image/gif'          => array( 'imagecreatefromgif',  'palette',   'imagegif'  ),
00516             'image/jpeg'         => array( 'imagecreatefromjpeg', 'truecolor', array( __CLASS__, 'imageJpegWrapper' ) ),
00517             'image/png'          => array( 'imagecreatefrompng',  'bits',      'imagepng'  ),
00518             'image/vnd.wap.wbmp' => array( 'imagecreatefromwbmp', 'palette',   'imagewbmp'  ),
00519             'image/xbm'          => array( 'imagecreatefromxbm',  'palette',   'imagexbm'  ),
00520         );
00521         if ( !isset( $typemap[$params['mimeType']] ) ) {
00522             $err = 'Image type not supported';
00523             wfDebug( "$err\n" );
00524             $errMsg = wfMessage( 'thumbnail_image-type' )->text();
00525             return $this->getMediaTransformError( $params, $errMsg );
00526         }
00527         list( $loader, $colorStyle, $saveType ) = $typemap[$params['mimeType']];
00528 
00529         if ( !function_exists( $loader ) ) {
00530             $err = "Incomplete GD library configuration: missing function $loader";
00531             wfDebug( "$err\n" );
00532             $errMsg = wfMessage( 'thumbnail_gd-library', $loader )->text();
00533             return $this->getMediaTransformError( $params, $errMsg );
00534         }
00535 
00536         if ( !file_exists( $params['srcPath'] ) ) {
00537             $err = "File seems to be missing: {$params['srcPath']}";
00538             wfDebug( "$err\n" );
00539             $errMsg = wfMessage( 'thumbnail_image-missing', $params['srcPath'] )->text();
00540             return $this->getMediaTransformError( $params, $errMsg );
00541         }
00542 
00543         $src_image = call_user_func( $loader, $params['srcPath'] );
00544 
00545         $rotation = function_exists( 'imagerotate' ) ? $this->getRotation( $image ) : 0;
00546         list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
00547         $dst_image = imagecreatetruecolor( $width, $height );
00548 
00549         // Initialise the destination image to transparent instead of
00550         // the default solid black, to support PNG and GIF transparency nicely
00551         $background = imagecolorallocate( $dst_image, 0, 0, 0 );
00552         imagecolortransparent( $dst_image, $background );
00553         imagealphablending( $dst_image, false );
00554 
00555         if ( $colorStyle == 'palette' ) {
00556             // Don't resample for paletted GIF images.
00557             // It may just uglify them, and completely breaks transparency.
00558             imagecopyresized( $dst_image, $src_image,
00559                 0, 0, 0, 0,
00560                 $width, $height,
00561                 imagesx( $src_image ), imagesy( $src_image ) );
00562         } else {
00563             imagecopyresampled( $dst_image, $src_image,
00564                 0, 0, 0, 0,
00565                 $width, $height,
00566                 imagesx( $src_image ), imagesy( $src_image ) );
00567         }
00568 
00569         if ( $rotation % 360 != 0 && $rotation % 90 == 0 ) {
00570             $rot_image = imagerotate( $dst_image, $rotation, 0 );
00571             imagedestroy( $dst_image );
00572             $dst_image = $rot_image;
00573         }
00574 
00575         imagesavealpha( $dst_image, true );
00576 
00577         call_user_func( $saveType, $dst_image, $params['dstPath'] );
00578         imagedestroy( $dst_image );
00579         imagedestroy( $src_image );
00580 
00581         return false; # No error
00582     }
00583 
00589     function escapeMagickProperty( $s ) {
00590         // Double the backslashes
00591         $s = str_replace( '\\', '\\\\', $s );
00592         // Double the percents
00593         $s = str_replace( '%', '%%', $s );
00594         // Escape initial - or @
00595         if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
00596             $s = '\\' . $s;
00597         }
00598         return $s;
00599     }
00600 
00618     function escapeMagickInput( $path, $scene = false ) {
00619         # Die on initial metacharacters (caller should prepend path)
00620         $firstChar = substr( $path, 0, 1 );
00621         if ( $firstChar === '~' || $firstChar === '@' ) {
00622             throw new MWException( __METHOD__ . ': cannot escape this path name' );
00623         }
00624 
00625         # Escape glob chars
00626         $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
00627 
00628         return $this->escapeMagickPath( $path, $scene );
00629     }
00630 
00636     function escapeMagickOutput( $path, $scene = false ) {
00637         $path = str_replace( '%', '%%', $path );
00638         return $this->escapeMagickPath( $path, $scene );
00639     }
00640 
00650     protected function escapeMagickPath( $path, $scene = false ) {
00651         # Die on format specifiers (other than drive letters). The regex is
00652         # meant to match all the formats you get from "convert -list format"
00653         if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
00654             if ( wfIsWindows() && is_dir( $m[0] ) ) {
00655                 // OK, it's a drive letter
00656                 // ImageMagick has a similar exception, see IsMagickConflict()
00657             } else {
00658                 throw new MWException( __METHOD__ . ': unexpected colon character in path name' );
00659             }
00660         }
00661 
00662         # If there are square brackets, add a do-nothing scene specification
00663         # to force a literal interpretation
00664         if ( $scene === false ) {
00665             if ( strpos( $path, '[' ) !== false ) {
00666                 $path .= '[0--1]';
00667             }
00668         } else {
00669             $path .= "[$scene]";
00670         }
00671         return $path;
00672     }
00673 
00680     protected function getMagickVersion() {
00681         global $wgMemc;
00682 
00683         $cache = $wgMemc->get( "imagemagick-version" );
00684         if ( !$cache ) {
00685             global $wgImageMagickConvertCommand;
00686             $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . ' -version';
00687             wfDebug( __METHOD__ . ": Running convert -version\n" );
00688             $retval = '';
00689             $return = wfShellExec( $cmd, $retval );
00690             $x = preg_match( '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches );
00691             if ( $x != 1 ) {
00692                 wfDebug( __METHOD__ . ": ImageMagick version check failed\n" );
00693                 return null;
00694             }
00695             $wgMemc->set( "imagemagick-version", $matches[1], 3600 );
00696             return $matches[1];
00697         }
00698         return $cache;
00699     }
00700 
00701     static function imageJpegWrapper( $dst_image, $thumbPath ) {
00702         imageinterlace( $dst_image );
00703         imagejpeg( $dst_image, $thumbPath, 95 );
00704     }
00705 
00706 
00712     public static function canRotate() {
00713         $scaler = self::getScalerType( null, false );
00714         switch ( $scaler ) {
00715             case 'im':
00716                 # ImageMagick supports autorotation
00717                 return true;
00718             case 'imext':
00719                 # Imagick::rotateImage
00720                 return true;
00721             case 'gd':
00722                 # GD's imagerotate function is used to rotate images, but not
00723                 # all precompiled PHP versions have that function
00724                 return function_exists( 'imagerotate' );
00725             default:
00726                 # Other scalers don't support rotation
00727                 return false;
00728         }
00729     }
00730 
00738     public function rotate( $file, $params ) {
00739         global $wgImageMagickConvertCommand;
00740 
00741         $rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
00742         $scene = false;
00743 
00744         $scaler = self::getScalerType( null, false );
00745         switch ( $scaler ) {
00746             case 'im':
00747                 $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . " " .
00748                     wfEscapeShellArg( $this->escapeMagickInput( $params['srcPath'], $scene ) ) .
00749                     " -rotate " . wfEscapeShellArg( "-$rotation" ) . " " .
00750                     wfEscapeShellArg( $this->escapeMagickOutput( $params['dstPath'] ) );
00751                 wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
00752                 wfProfileIn( 'convert' );
00753                 $retval = 0;
00754                 $err = wfShellExecWithStderr( $cmd, $retval, $env );
00755                 wfProfileOut( 'convert' );
00756                 if ( $retval !== 0 ) {
00757                     $this->logErrorForExternalProcess( $retval, $err, $cmd );
00758                     return new MediaTransformError( 'thumbnail_error', 0, 0, $err );
00759                 }
00760                 return false;
00761             case 'imext':
00762                 $im = new Imagick();
00763                 $im->readImage( $params['srcPath'] );
00764                 if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
00765                     return new MediaTransformError( 'thumbnail_error', 0, 0,
00766                         "Error rotating $rotation degrees" );
00767                 }
00768                 $result = $im->writeImage( $params['dstPath'] );
00769                 if ( !$result ) {
00770                     return new MediaTransformError( 'thumbnail_error', 0, 0,
00771                         "Unable to write image to {$params['dstPath']}" );
00772                 }
00773                 return false;
00774             default:
00775                 return new MediaTransformError( 'thumbnail_error', 0, 0,
00776                     "$scaler rotation not implemented" );
00777         }
00778     }
00779 
00787     public function mustRender( $file ) {
00788         return self::canRotate() && $this->getRotation( $file ) != 0;
00789     }
00790 }