MediaWiki
REL1_22
|
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 }