MediaWiki
REL1_19
|
00001 <?php 00014 class BitmapHandler extends ImageHandler { 00022 function normaliseParams( $image, &$params ) { 00023 if ( !parent::normaliseParams( $image, $params ) ) { 00024 return false; 00025 } 00026 00027 # Obtain the source, pre-rotation dimensions 00028 $srcWidth = $image->getWidth( $params['page'] ); 00029 $srcHeight = $image->getHeight( $params['page'] ); 00030 00031 # Don't make an image bigger than the source 00032 if ( $params['physicalWidth'] >= $srcWidth ) { 00033 $params['physicalWidth'] = $srcWidth; 00034 $params['physicalHeight'] = $srcHeight; 00035 00036 # Skip scaling limit checks if no scaling is required 00037 # due to requested size being bigger than source. 00038 if ( !$image->mustRender() ) { 00039 return true; 00040 } 00041 } 00042 00043 # Check if the file is smaller than the maximum image area for thumbnailing 00044 $checkImageAreaHookResult = null; 00045 wfRunHooks( 'BitmapHandlerCheckImageArea', array( $image, &$params, &$checkImageAreaHookResult ) ); 00046 if ( is_null( $checkImageAreaHookResult ) ) { 00047 global $wgMaxImageArea; 00048 00049 if ( $srcWidth * $srcHeight > $wgMaxImageArea && 00050 !( $image->getMimeType() == 'image/jpeg' && 00051 self::getScalerType( false, false ) == 'im' ) ) { 00052 # Only ImageMagick can efficiently downsize jpg images without loading 00053 # the entire file in memory 00054 return false; 00055 } 00056 } else { 00057 return $checkImageAreaHookResult; 00058 } 00059 00060 return true; 00061 } 00062 00063 00076 public function extractPreRotationDimensions( $params, $rotation ) { 00077 if ( $rotation == 90 || $rotation == 270 ) { 00078 # We'll resize before rotation, so swap the dimensions again 00079 $width = $params['physicalHeight']; 00080 $height = $params['physicalWidth']; 00081 } else { 00082 $width = $params['physicalWidth']; 00083 $height = $params['physicalHeight']; 00084 } 00085 return array( $width, $height ); 00086 } 00087 00088 00096 function getImageArea( $image ) { 00097 return $image->getWidth() * $image->getHeight(); 00098 } 00099 00108 function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) { 00109 if ( !$this->normaliseParams( $image, $params ) ) { 00110 return new TransformParameterError( $params ); 00111 } 00112 # Create a parameter array to pass to the scaler 00113 $scalerParams = array( 00114 # The size to which the image will be resized 00115 'physicalWidth' => $params['physicalWidth'], 00116 'physicalHeight' => $params['physicalHeight'], 00117 'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}", 00118 # The size of the image on the page 00119 'clientWidth' => $params['width'], 00120 'clientHeight' => $params['height'], 00121 # Comment as will be added to the EXIF of the thumbnail 00122 'comment' => isset( $params['descriptionUrl'] ) ? 00123 "File source: {$params['descriptionUrl']}" : '', 00124 # Properties of the original image 00125 'srcWidth' => $image->getWidth(), 00126 'srcHeight' => $image->getHeight(), 00127 'mimeType' => $image->getMimeType(), 00128 'dstPath' => $dstPath, 00129 'dstUrl' => $dstUrl, 00130 ); 00131 00132 # Determine scaler type 00133 $scaler = self::getScalerType( $dstPath ); 00134 00135 wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} thumbnail at $dstPath using scaler $scaler\n" ); 00136 00137 if ( !$image->mustRender() && 00138 $scalerParams['physicalWidth'] == $scalerParams['srcWidth'] 00139 && $scalerParams['physicalHeight'] == $scalerParams['srcHeight'] ) { 00140 00141 # normaliseParams (or the user) wants us to return the unscaled image 00142 wfDebug( __METHOD__ . ": returning unscaled image\n" ); 00143 return $this->getClientScalingThumbnailImage( $image, $scalerParams ); 00144 } 00145 00146 00147 if ( $scaler == 'client' ) { 00148 # Client-side image scaling, use the source URL 00149 # Using the destination URL in a TRANSFORM_LATER request would be incorrect 00150 return $this->getClientScalingThumbnailImage( $image, $scalerParams ); 00151 } 00152 00153 if ( $flags & self::TRANSFORM_LATER ) { 00154 wfDebug( __METHOD__ . ": Transforming later per flags.\n" ); 00155 return new ThumbnailImage( $image, $dstUrl, $scalerParams['clientWidth'], 00156 $scalerParams['clientHeight'], false ); 00157 } 00158 00159 # Try to make a target path for the thumbnail 00160 if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) { 00161 wfDebug( __METHOD__ . ": Unable to create thumbnail destination directory, falling back to client scaling\n" ); 00162 return $this->getClientScalingThumbnailImage( $image, $scalerParams ); 00163 } 00164 00165 # Transform functions and binaries need a FS source file 00166 $scalerParams['srcPath'] = $image->getLocalRefPath(); 00167 00168 # Try a hook 00169 $mto = null; 00170 wfRunHooks( 'BitmapHandlerTransform', array( $this, $image, &$scalerParams, &$mto ) ); 00171 if ( !is_null( $mto ) ) { 00172 wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto\n" ); 00173 $scaler = 'hookaborted'; 00174 } 00175 00176 switch ( $scaler ) { 00177 case 'hookaborted': 00178 # Handled by the hook above 00179 $err = $mto->isError() ? $mto : false; 00180 break; 00181 case 'im': 00182 $err = $this->transformImageMagick( $image, $scalerParams ); 00183 break; 00184 case 'custom': 00185 $err = $this->transformCustom( $image, $scalerParams ); 00186 break; 00187 case 'imext': 00188 $err = $this->transformImageMagickExt( $image, $scalerParams ); 00189 break; 00190 case 'gd': 00191 default: 00192 $err = $this->transformGd( $image, $scalerParams ); 00193 break; 00194 } 00195 00196 # Remove the file if a zero-byte thumbnail was created, or if there was an error 00197 $removed = $this->removeBadFile( $dstPath, (bool)$err ); 00198 if ( $err ) { 00199 # transform returned MediaTransforError 00200 return $err; 00201 } elseif ( $removed ) { 00202 # Thumbnail was zero-byte and had to be removed 00203 return new MediaTransformError( 'thumbnail_error', 00204 $scalerParams['clientWidth'], $scalerParams['clientHeight'] ); 00205 } elseif ( $mto ) { 00206 return $mto; 00207 } else { 00208 return new ThumbnailImage( $image, $dstUrl, $scalerParams['clientWidth'], 00209 $scalerParams['clientHeight'], $dstPath ); 00210 } 00211 } 00212 00219 protected static function getScalerType( $dstPath, $checkDstPath = true ) { 00220 global $wgUseImageResize, $wgUseImageMagick, $wgCustomConvertCommand; 00221 00222 if ( !$dstPath && $checkDstPath ) { 00223 # No output path available, client side scaling only 00224 $scaler = 'client'; 00225 } elseif ( !$wgUseImageResize ) { 00226 $scaler = 'client'; 00227 } elseif ( $wgUseImageMagick ) { 00228 $scaler = 'im'; 00229 } elseif ( $wgCustomConvertCommand ) { 00230 $scaler = 'custom'; 00231 } elseif ( function_exists( 'imagecreatetruecolor' ) ) { 00232 $scaler = 'gd'; 00233 } elseif ( class_exists( 'Imagick' ) ) { 00234 $scaler = 'imext'; 00235 } else { 00236 $scaler = 'client'; 00237 } 00238 return $scaler; 00239 } 00240 00251 protected function getClientScalingThumbnailImage( $image, $params ) { 00252 return new ThumbnailImage( $image, $image->getURL(), 00253 $params['clientWidth'], $params['clientHeight'], null ); 00254 } 00255 00264 protected function transformImageMagick( $image, $params ) { 00265 # use ImageMagick 00266 global $wgSharpenReductionThreshold, $wgSharpenParameter, 00267 $wgMaxAnimatedGifArea, 00268 $wgImageMagickTempDir, $wgImageMagickConvertCommand; 00269 00270 $quality = array(); 00271 $sharpen = array(); 00272 $scene = false; 00273 $animation_pre = array(); 00274 $animation_post = array(); 00275 $decoderHint = array(); 00276 if ( $params['mimeType'] == 'image/jpeg' ) { 00277 $quality = array( '-quality', '80' ); // 80% 00278 # Sharpening, see bug 6193 00279 if ( ( $params['physicalWidth'] + $params['physicalHeight'] ) 00280 / ( $params['srcWidth'] + $params['srcHeight'] ) 00281 < $wgSharpenReductionThreshold ) { 00282 $sharpen = array( '-sharpen', $wgSharpenParameter ); 00283 } 00284 if ( version_compare( $this->getMagickVersion(), "6.5.6" ) >= 0 ) { 00285 // JPEG decoder hint to reduce memory, available since IM 6.5.6-2 00286 $decoderHint = array( '-define', "jpeg:size={$params['physicalDimensions']}" ); 00287 } 00288 00289 } elseif ( $params['mimeType'] == 'image/png' ) { 00290 $quality = array( '-quality', '95' ); // zlib 9, adaptive filtering 00291 00292 } elseif ( $params['mimeType'] == 'image/gif' ) { 00293 if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) { 00294 // Extract initial frame only; we're so big it'll 00295 // be a total drag. :P 00296 $scene = 0; 00297 00298 } elseif ( $this->isAnimatedImage( $image ) ) { 00299 // Coalesce is needed to scale animated GIFs properly (bug 1017). 00300 $animation_pre = array( '-coalesce' ); 00301 // We optimize the output, but -optimize is broken, 00302 // use optimizeTransparency instead (bug 11822) 00303 if ( version_compare( $this->getMagickVersion(), "6.3.5" ) >= 0 ) { 00304 $animation_post = array( '-fuzz', '5%', '-layers', 'optimizeTransparency' ); 00305 } 00306 } 00307 } elseif ( $params['mimeType'] == 'image/x-xcf' ) { 00308 $animation_post = array( '-layers', 'merge' ); 00309 } 00310 00311 // Use one thread only, to avoid deadlock bugs on OOM 00312 $env = array( 'OMP_NUM_THREADS' => 1 ); 00313 if ( strval( $wgImageMagickTempDir ) !== '' ) { 00314 $env['MAGICK_TMPDIR'] = $wgImageMagickTempDir; 00315 } 00316 00317 $rotation = $this->getRotation( $image ); 00318 list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation ); 00319 00320 $cmd = call_user_func_array( 'wfEscapeShellArg', array_merge( 00321 array( $wgImageMagickConvertCommand ), 00322 $quality, 00323 // Specify white background color, will be used for transparent images 00324 // in Internet Explorer/Windows instead of default black. 00325 array( '-background', 'white' ), 00326 $decoderHint, 00327 array( $this->escapeMagickInput( $params['srcPath'], $scene ) ), 00328 $animation_pre, 00329 // For the -thumbnail option a "!" is needed to force exact size, 00330 // or ImageMagick may decide your ratio is wrong and slice off 00331 // a pixel. 00332 array( '-thumbnail', "{$width}x{$height}!" ), 00333 // Add the source url as a comment to the thumb, but don't add the flag if there's no comment 00334 ( $params['comment'] !== '' 00335 ? array( '-set', 'comment', $this->escapeMagickProperty( $params['comment'] ) ) 00336 : array() ), 00337 array( '-depth', 8 ), 00338 $sharpen, 00339 array( '-rotate', "-$rotation" ), 00340 $animation_post, 00341 array( $this->escapeMagickOutput( $params['dstPath'] ) ) ) ) . " 2>&1"; 00342 00343 wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" ); 00344 wfProfileIn( 'convert' ); 00345 $retval = 0; 00346 $err = wfShellExec( $cmd, $retval, $env ); 00347 wfProfileOut( 'convert' ); 00348 00349 if ( $retval !== 0 ) { 00350 $this->logErrorForExternalProcess( $retval, $err, $cmd ); 00351 return $this->getMediaTransformError( $params, $err ); 00352 } 00353 00354 return false; # No error 00355 } 00356 00365 protected function transformImageMagickExt( $image, $params ) { 00366 global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea; 00367 00368 try { 00369 $im = new Imagick(); 00370 $im->readImage( $params['srcPath'] ); 00371 00372 if ( $params['mimeType'] == 'image/jpeg' ) { 00373 // Sharpening, see bug 6193 00374 if ( ( $params['physicalWidth'] + $params['physicalHeight'] ) 00375 / ( $params['srcWidth'] + $params['srcHeight'] ) 00376 < $wgSharpenReductionThreshold ) { 00377 // Hack, since $wgSharpenParamater is written specifically for the command line convert 00378 list( $radius, $sigma ) = explode( 'x', $wgSharpenParameter ); 00379 $im->sharpenImage( $radius, $sigma ); 00380 } 00381 $im->setCompressionQuality( 80 ); 00382 } elseif( $params['mimeType'] == 'image/png' ) { 00383 $im->setCompressionQuality( 95 ); 00384 } elseif ( $params['mimeType'] == 'image/gif' ) { 00385 if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) { 00386 // Extract initial frame only; we're so big it'll 00387 // be a total drag. :P 00388 $im->setImageScene( 0 ); 00389 } elseif ( $this->isAnimatedImage( $image ) ) { 00390 // Coalesce is needed to scale animated GIFs properly (bug 1017). 00391 $im = $im->coalesceImages(); 00392 } 00393 } 00394 00395 $rotation = $this->getRotation( $image ); 00396 list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation ); 00397 00398 $im->setImageBackgroundColor( new ImagickPixel( 'white' ) ); 00399 00400 // Call Imagick::thumbnailImage on each frame 00401 foreach ( $im as $i => $frame ) { 00402 if ( !$frame->thumbnailImage( $width, $height, /* fit */ false ) ) { 00403 return $this->getMediaTransformError( $params, "Error scaling frame $i" ); 00404 } 00405 } 00406 $im->setImageDepth( 8 ); 00407 00408 if ( $rotation ) { 00409 if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) { 00410 return $this->getMediaTransformError( $params, "Error rotating $rotation degrees" ); 00411 } 00412 } 00413 00414 if ( $this->isAnimatedImage( $image ) ) { 00415 wfDebug( __METHOD__ . ": Writing animated thumbnail\n" ); 00416 // This is broken somehow... can't find out how to fix it 00417 $result = $im->writeImages( $params['dstPath'], true ); 00418 } else { 00419 $result = $im->writeImage( $params['dstPath'] ); 00420 } 00421 if ( !$result ) { 00422 return $this->getMediaTransformError( $params, 00423 "Unable to write thumbnail to {$params['dstPath']}" ); 00424 } 00425 00426 } catch ( ImagickException $e ) { 00427 return $this->getMediaTransformError( $params, $e->getMessage() ); 00428 } 00429 00430 return false; 00431 00432 } 00433 00442 protected function transformCustom( $image, $params ) { 00443 # Use a custom convert command 00444 global $wgCustomConvertCommand; 00445 00446 # Variables: %s %d %w %h 00447 $src = wfEscapeShellArg( $params['srcPath'] ); 00448 $dst = wfEscapeShellArg( $params['dstPath'] ); 00449 $cmd = $wgCustomConvertCommand; 00450 $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames 00451 $cmd = str_replace( '%h', wfEscapeShellArg( $params['physicalHeight'] ), 00452 str_replace( '%w', wfEscapeShellArg( $params['physicalWidth'] ), $cmd ) ); # Size 00453 wfDebug( __METHOD__ . ": Running custom convert command $cmd\n" ); 00454 wfProfileIn( 'convert' ); 00455 $retval = 0; 00456 $err = wfShellExec( $cmd, $retval ); 00457 wfProfileOut( 'convert' ); 00458 00459 if ( $retval !== 0 ) { 00460 $this->logErrorForExternalProcess( $retval, $err, $cmd ); 00461 return $this->getMediaTransformError( $params, $err ); 00462 } 00463 return false; # No error 00464 } 00465 00473 protected function logErrorForExternalProcess( $retval, $err, $cmd ) { 00474 wfDebugLog( 'thumbnail', 00475 sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"', 00476 wfHostname(), $retval, trim( $err ), $cmd ) ); 00477 } 00485 public function getMediaTransformError( $params, $errMsg ) { 00486 return new MediaTransformError( 'thumbnail_error', $params['clientWidth'], 00487 $params['clientHeight'], $errMsg ); 00488 } 00489 00498 protected function transformGd( $image, $params ) { 00499 # Use PHP's builtin GD library functions. 00500 # 00501 # First find out what kind of file this is, and select the correct 00502 # input routine for this. 00503 00504 $typemap = array( 00505 'image/gif' => array( 'imagecreatefromgif', 'palette', 'imagegif' ), 00506 'image/jpeg' => array( 'imagecreatefromjpeg', 'truecolor', array( __CLASS__, 'imageJpegWrapper' ) ), 00507 'image/png' => array( 'imagecreatefrompng', 'bits', 'imagepng' ), 00508 'image/vnd.wap.wbmp' => array( 'imagecreatefromwbmp', 'palette', 'imagewbmp' ), 00509 'image/xbm' => array( 'imagecreatefromxbm', 'palette', 'imagexbm' ), 00510 ); 00511 if ( !isset( $typemap[$params['mimeType']] ) ) { 00512 $err = 'Image type not supported'; 00513 wfDebug( "$err\n" ); 00514 $errMsg = wfMsg( 'thumbnail_image-type' ); 00515 return $this->getMediaTransformError( $params, $errMsg ); 00516 } 00517 list( $loader, $colorStyle, $saveType ) = $typemap[$params['mimeType']]; 00518 00519 if ( !function_exists( $loader ) ) { 00520 $err = "Incomplete GD library configuration: missing function $loader"; 00521 wfDebug( "$err\n" ); 00522 $errMsg = wfMsg( 'thumbnail_gd-library', $loader ); 00523 return $this->getMediaTransformError( $params, $errMsg ); 00524 } 00525 00526 if ( !file_exists( $params['srcPath'] ) ) { 00527 $err = "File seems to be missing: {$params['srcPath']}"; 00528 wfDebug( "$err\n" ); 00529 $errMsg = wfMsg( 'thumbnail_image-missing', $params['srcPath'] ); 00530 return $this->getMediaTransformError( $params, $errMsg ); 00531 } 00532 00533 $src_image = call_user_func( $loader, $params['srcPath'] ); 00534 00535 $rotation = function_exists( 'imagerotate' ) ? $this->getRotation( $image ) : 0; 00536 list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation ); 00537 $dst_image = imagecreatetruecolor( $width, $height ); 00538 00539 // Initialise the destination image to transparent instead of 00540 // the default solid black, to support PNG and GIF transparency nicely 00541 $background = imagecolorallocate( $dst_image, 0, 0, 0 ); 00542 imagecolortransparent( $dst_image, $background ); 00543 imagealphablending( $dst_image, false ); 00544 00545 if ( $colorStyle == 'palette' ) { 00546 // Don't resample for paletted GIF images. 00547 // It may just uglify them, and completely breaks transparency. 00548 imagecopyresized( $dst_image, $src_image, 00549 0, 0, 0, 0, 00550 $width, $height, 00551 imagesx( $src_image ), imagesy( $src_image ) ); 00552 } else { 00553 imagecopyresampled( $dst_image, $src_image, 00554 0, 0, 0, 0, 00555 $width, $height, 00556 imagesx( $src_image ), imagesy( $src_image ) ); 00557 } 00558 00559 if ( $rotation % 360 != 0 && $rotation % 90 == 0 ) { 00560 $rot_image = imagerotate( $dst_image, $rotation, 0 ); 00561 imagedestroy( $dst_image ); 00562 $dst_image = $rot_image; 00563 } 00564 00565 imagesavealpha( $dst_image, true ); 00566 00567 call_user_func( $saveType, $dst_image, $params['dstPath'] ); 00568 imagedestroy( $dst_image ); 00569 imagedestroy( $src_image ); 00570 00571 return false; # No error 00572 } 00573 00578 function escapeMagickProperty( $s ) { 00579 // Double the backslashes 00580 $s = str_replace( '\\', '\\\\', $s ); 00581 // Double the percents 00582 $s = str_replace( '%', '%%', $s ); 00583 // Escape initial - or @ 00584 if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) { 00585 $s = '\\' . $s; 00586 } 00587 return $s; 00588 } 00589 00605 function escapeMagickInput( $path, $scene = false ) { 00606 # Die on initial metacharacters (caller should prepend path) 00607 $firstChar = substr( $path, 0, 1 ); 00608 if ( $firstChar === '~' || $firstChar === '@' ) { 00609 throw new MWException( __METHOD__ . ': cannot escape this path name' ); 00610 } 00611 00612 # Escape glob chars 00613 $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path ); 00614 00615 return $this->escapeMagickPath( $path, $scene ); 00616 } 00617 00622 function escapeMagickOutput( $path, $scene = false ) { 00623 $path = str_replace( '%', '%%', $path ); 00624 return $this->escapeMagickPath( $path, $scene ); 00625 } 00626 00634 protected function escapeMagickPath( $path, $scene = false ) { 00635 # Die on format specifiers (other than drive letters). The regex is 00636 # meant to match all the formats you get from "convert -list format" 00637 if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) { 00638 if ( wfIsWindows() && is_dir( $m[0] ) ) { 00639 // OK, it's a drive letter 00640 // ImageMagick has a similar exception, see IsMagickConflict() 00641 } else { 00642 throw new MWException( __METHOD__ . ': unexpected colon character in path name' ); 00643 } 00644 } 00645 00646 # If there are square brackets, add a do-nothing scene specification 00647 # to force a literal interpretation 00648 if ( $scene === false ) { 00649 if ( strpos( $path, '[' ) !== false ) { 00650 $path .= '[0--1]'; 00651 } 00652 } else { 00653 $path .= "[$scene]"; 00654 } 00655 return $path; 00656 } 00657 00664 protected function getMagickVersion() { 00665 global $wgMemc; 00666 00667 $cache = $wgMemc->get( "imagemagick-version" ); 00668 if ( !$cache ) { 00669 global $wgImageMagickConvertCommand; 00670 $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . ' -version'; 00671 wfDebug( __METHOD__ . ": Running convert -version\n" ); 00672 $retval = ''; 00673 $return = wfShellExec( $cmd, $retval ); 00674 $x = preg_match( '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches ); 00675 if ( $x != 1 ) { 00676 wfDebug( __METHOD__ . ": ImageMagick version check failed\n" ); 00677 return null; 00678 } 00679 $wgMemc->set( "imagemagick-version", $matches[1], 3600 ); 00680 return $matches[1]; 00681 } 00682 return $cache; 00683 } 00684 00685 static function imageJpegWrapper( $dst_image, $thumbPath ) { 00686 imageinterlace( $dst_image ); 00687 imagejpeg( $dst_image, $thumbPath, 95 ); 00688 } 00689 00705 public function getRotation( $file ) { 00706 return 0; 00707 } 00708 00714 public static function canRotate() { 00715 $scaler = self::getScalerType( null, false ); 00716 switch ( $scaler ) { 00717 case 'im': 00718 # ImageMagick supports autorotation 00719 return true; 00720 case 'imext': 00721 # Imagick::rotateImage 00722 return true; 00723 case 'gd': 00724 # GD's imagerotate function is used to rotate images, but not 00725 # all precompiled PHP versions have that function 00726 return function_exists( 'imagerotate' ); 00727 default: 00728 # Other scalers don't support rotation 00729 return false; 00730 } 00731 } 00732 00740 public function mustRender( $file ) { 00741 return self::canRotate() && $this->getRotation( $file ) != 0; 00742 } 00743 }