MediaWiki  REL1_23
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(
00061             'BitmapHandlerCheckImageArea',
00062             array( $image, &$params, &$checkImageAreaHookResult )
00063         );
00064 
00065         if ( is_null( $checkImageAreaHookResult ) ) {
00066             global $wgMaxImageArea;
00067 
00068             if ( $srcWidth * $srcHeight > $wgMaxImageArea
00069                 && !( $image->getMimeType() == 'image/jpeg'
00070                     && self::getScalerType( false, false ) == 'im' )
00071             ) {
00072                 # Only ImageMagick can efficiently downsize jpg images without loading
00073                 # the entire file in memory
00074                 return false;
00075             }
00076         } else {
00077             return $checkImageAreaHookResult;
00078         }
00079 
00080         return true;
00081     }
00082 
00095     public function extractPreRotationDimensions( $params, $rotation ) {
00096         if ( $rotation == 90 || $rotation == 270 ) {
00097             # We'll resize before rotation, so swap the dimensions again
00098             $width = $params['physicalHeight'];
00099             $height = $params['physicalWidth'];
00100         } else {
00101             $width = $params['physicalWidth'];
00102             $height = $params['physicalHeight'];
00103         }
00104 
00105         return array( $width, $height );
00106     }
00107 
00116     function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
00117         if ( !$this->normaliseParams( $image, $params ) ) {
00118             return new TransformParameterError( $params );
00119         }
00120         # Create a parameter array to pass to the scaler
00121         $scalerParams = array(
00122             # The size to which the image will be resized
00123             'physicalWidth' => $params['physicalWidth'],
00124             'physicalHeight' => $params['physicalHeight'],
00125             'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}",
00126             # The size of the image on the page
00127             'clientWidth' => $params['width'],
00128             'clientHeight' => $params['height'],
00129             # Comment as will be added to the Exif of the thumbnail
00130             'comment' => isset( $params['descriptionUrl'] )
00131                 ? "File source: {$params['descriptionUrl']}"
00132                 : '',
00133             # Properties of the original image
00134             'srcWidth' => $image->getWidth(),
00135             'srcHeight' => $image->getHeight(),
00136             'mimeType' => $image->getMimeType(),
00137             'dstPath' => $dstPath,
00138             'dstUrl' => $dstUrl,
00139         );
00140 
00141         # Determine scaler type
00142         $scaler = self::getScalerType( $dstPath );
00143 
00144         wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} " .
00145             "thumbnail at $dstPath using scaler $scaler\n" );
00146 
00147         if ( !$image->mustRender() &&
00148             $scalerParams['physicalWidth'] == $scalerParams['srcWidth']
00149             && $scalerParams['physicalHeight'] == $scalerParams['srcHeight']
00150         ) {
00151 
00152             # normaliseParams (or the user) wants us to return the unscaled image
00153             wfDebug( __METHOD__ . ": returning unscaled image\n" );
00154 
00155             return $this->getClientScalingThumbnailImage( $image, $scalerParams );
00156         }
00157 
00158         if ( $scaler == 'client' ) {
00159             # Client-side image scaling, use the source URL
00160             # Using the destination URL in a TRANSFORM_LATER request would be incorrect
00161             return $this->getClientScalingThumbnailImage( $image, $scalerParams );
00162         }
00163 
00164         if ( $flags & self::TRANSFORM_LATER ) {
00165             wfDebug( __METHOD__ . ": Transforming later per flags.\n" );
00166             $params = array(
00167                 'width' => $scalerParams['clientWidth'],
00168                 'height' => $scalerParams['clientHeight']
00169             );
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 " .
00177                 "directory, falling back to client scaling\n" );
00178 
00179             return $this->getClientScalingThumbnailImage( $image, $scalerParams );
00180         }
00181 
00182         # Transform functions and binaries need a FS source file
00183         $scalerParams['srcPath'] = $image->getLocalRefPath();
00184         if ( $scalerParams['srcPath'] === false ) { // Failed to get local copy
00185             wfDebugLog( 'thumbnail',
00186                 sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
00187                     wfHostname(), $image->getName() ) );
00188 
00189             return new MediaTransformError( 'thumbnail_error',
00190                 $scalerParams['clientWidth'], $scalerParams['clientHeight'],
00191                 wfMessage( 'filemissing' )->text()
00192             );
00193         }
00194 
00195         # Try a hook
00196         $mto = null;
00197         wfRunHooks( 'BitmapHandlerTransform', array( $this, $image, &$scalerParams, &$mto ) );
00198         if ( !is_null( $mto ) ) {
00199             wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto\n" );
00200             $scaler = 'hookaborted';
00201         }
00202 
00203         switch ( $scaler ) {
00204             case 'hookaborted':
00205                 # Handled by the hook above
00206 
00207                 $err = $mto->isError() ? $mto : false;
00208                 break;
00209             case 'im':
00210                 $err = $this->transformImageMagick( $image, $scalerParams );
00211                 break;
00212             case 'custom':
00213                 $err = $this->transformCustom( $image, $scalerParams );
00214                 break;
00215             case 'imext':
00216                 $err = $this->transformImageMagickExt( $image, $scalerParams );
00217                 break;
00218             case 'gd':
00219             default:
00220                 $err = $this->transformGd( $image, $scalerParams );
00221                 break;
00222         }
00223 
00224         # Remove the file if a zero-byte thumbnail was created, or if there was an error
00225         $removed = $this->removeBadFile( $dstPath, (bool)$err );
00226         if ( $err ) {
00227             # transform returned MediaTransforError
00228             return $err;
00229         } elseif ( $removed ) {
00230             # Thumbnail was zero-byte and had to be removed
00231             return new MediaTransformError( 'thumbnail_error',
00232                 $scalerParams['clientWidth'], $scalerParams['clientHeight'],
00233                 wfMessage( 'unknown-error' )->text()
00234             );
00235         } elseif ( $mto ) {
00236             return $mto;
00237         } else {
00238             $params = array(
00239                 'width' => $scalerParams['clientWidth'],
00240                 'height' => $scalerParams['clientHeight']
00241             );
00242 
00243             return new ThumbnailImage( $image, $dstUrl, $dstPath, $params );
00244         }
00245     }
00246 
00255     protected static function getScalerType( $dstPath, $checkDstPath = true ) {
00256         global $wgUseImageResize, $wgUseImageMagick, $wgCustomConvertCommand;
00257 
00258         if ( !$dstPath && $checkDstPath ) {
00259             # No output path available, client side scaling only
00260             $scaler = 'client';
00261         } elseif ( !$wgUseImageResize ) {
00262             $scaler = 'client';
00263         } elseif ( $wgUseImageMagick ) {
00264             $scaler = 'im';
00265         } elseif ( $wgCustomConvertCommand ) {
00266             $scaler = 'custom';
00267         } elseif ( function_exists( 'imagecreatetruecolor' ) ) {
00268             $scaler = 'gd';
00269         } elseif ( class_exists( 'Imagick' ) ) {
00270             $scaler = 'imext';
00271         } else {
00272             $scaler = 'client';
00273         }
00274 
00275         return $scaler;
00276     }
00277 
00288     protected function getClientScalingThumbnailImage( $image, $scalerParams ) {
00289         $params = array(
00290             'width' => $scalerParams['clientWidth'],
00291             'height' => $scalerParams['clientHeight']
00292         );
00293 
00294         return new ThumbnailImage( $image, $image->getURL(), null, $params );
00295     }
00296 
00305     protected function transformImageMagick( $image, $params ) {
00306         # use ImageMagick
00307         global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea,
00308             $wgImageMagickTempDir, $wgImageMagickConvertCommand;
00309 
00310         $quality = array();
00311         $sharpen = array();
00312         $scene = false;
00313         $animation_pre = array();
00314         $animation_post = array();
00315         $decoderHint = array();
00316         if ( $params['mimeType'] == 'image/jpeg' ) {
00317             $quality = array( '-quality', '80' ); // 80%
00318             # Sharpening, see bug 6193
00319             if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
00320                 / ( $params['srcWidth'] + $params['srcHeight'] )
00321                 < $wgSharpenReductionThreshold
00322             ) {
00323                 $sharpen = array( '-sharpen', $wgSharpenParameter );
00324             }
00325             if ( version_compare( $this->getMagickVersion(), "6.5.6" ) >= 0 ) {
00326                 // JPEG decoder hint to reduce memory, available since IM 6.5.6-2
00327                 $decoderHint = array( '-define', "jpeg:size={$params['physicalDimensions']}" );
00328             }
00329         } elseif ( $params['mimeType'] == 'image/png' ) {
00330             $quality = array( '-quality', '95' ); // zlib 9, adaptive filtering
00331 
00332         } elseif ( $params['mimeType'] == 'image/gif' ) {
00333             if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
00334                 // Extract initial frame only; we're so big it'll
00335                 // be a total drag. :P
00336                 $scene = 0;
00337             } elseif ( $this->isAnimatedImage( $image ) ) {
00338                 // Coalesce is needed to scale animated GIFs properly (bug 1017).
00339                 $animation_pre = array( '-coalesce' );
00340                 // We optimize the output, but -optimize is broken,
00341                 // use optimizeTransparency instead (bug 11822)
00342                 if ( version_compare( $this->getMagickVersion(), "6.3.5" ) >= 0 ) {
00343                     $animation_post = array( '-fuzz', '5%', '-layers', 'optimizeTransparency' );
00344                 }
00345             }
00346         } elseif ( $params['mimeType'] == 'image/x-xcf' ) {
00347             $animation_post = array( '-layers', 'merge' );
00348         }
00349 
00350         // Use one thread only, to avoid deadlock bugs on OOM
00351         $env = array( 'OMP_NUM_THREADS' => 1 );
00352         if ( strval( $wgImageMagickTempDir ) !== '' ) {
00353             $env['MAGICK_TMPDIR'] = $wgImageMagickTempDir;
00354         }
00355 
00356         $rotation = $this->getRotation( $image );
00357         list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
00358 
00359         $cmd = call_user_func_array( 'wfEscapeShellArg', array_merge(
00360             array( $wgImageMagickConvertCommand ),
00361             $quality,
00362             // Specify white background color, will be used for transparent images
00363             // in Internet Explorer/Windows instead of default black.
00364             array( '-background', 'white' ),
00365             $decoderHint,
00366             array( $this->escapeMagickInput( $params['srcPath'], $scene ) ),
00367             $animation_pre,
00368             // For the -thumbnail option a "!" is needed to force exact size,
00369             // or ImageMagick may decide your ratio is wrong and slice off
00370             // a pixel.
00371             array( '-thumbnail', "{$width}x{$height}!" ),
00372             // Add the source url as a comment to the thumb, but don't add the flag if there's no comment
00373             ( $params['comment'] !== ''
00374                 ? array( '-set', 'comment', $this->escapeMagickProperty( $params['comment'] ) )
00375                 : array() ),
00376             array( '-depth', 8 ),
00377             $sharpen,
00378             array( '-rotate', "-$rotation" ),
00379             $animation_post,
00380             array( $this->escapeMagickOutput( $params['dstPath'] ) ) ) );
00381 
00382         wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
00383         wfProfileIn( 'convert' );
00384         $retval = 0;
00385         $err = wfShellExecWithStderr( $cmd, $retval, $env );
00386         wfProfileOut( 'convert' );
00387 
00388         if ( $retval !== 0 ) {
00389             $this->logErrorForExternalProcess( $retval, $err, $cmd );
00390 
00391             return $this->getMediaTransformError( $params, "$err\nError code: $retval" );
00392         }
00393 
00394         return false; # No error
00395     }
00396 
00405     protected function transformImageMagickExt( $image, $params ) {
00406         global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea;
00407 
00408         try {
00409             $im = new Imagick();
00410             $im->readImage( $params['srcPath'] );
00411 
00412             if ( $params['mimeType'] == 'image/jpeg' ) {
00413                 // Sharpening, see bug 6193
00414                 if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
00415                     / ( $params['srcWidth'] + $params['srcHeight'] )
00416                     < $wgSharpenReductionThreshold
00417                 ) {
00418                     // Hack, since $wgSharpenParamater is written specifically for the command line convert
00419                     list( $radius, $sigma ) = explode( 'x', $wgSharpenParameter );
00420                     $im->sharpenImage( $radius, $sigma );
00421                 }
00422                 $im->setCompressionQuality( 80 );
00423             } elseif ( $params['mimeType'] == 'image/png' ) {
00424                 $im->setCompressionQuality( 95 );
00425             } elseif ( $params['mimeType'] == 'image/gif' ) {
00426                 if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
00427                     // Extract initial frame only; we're so big it'll
00428                     // be a total drag. :P
00429                     $im->setImageScene( 0 );
00430                 } elseif ( $this->isAnimatedImage( $image ) ) {
00431                     // Coalesce is needed to scale animated GIFs properly (bug 1017).
00432                     $im = $im->coalesceImages();
00433                 }
00434             }
00435 
00436             $rotation = $this->getRotation( $image );
00437             list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
00438 
00439             $im->setImageBackgroundColor( new ImagickPixel( 'white' ) );
00440 
00441             // Call Imagick::thumbnailImage on each frame
00442             foreach ( $im as $i => $frame ) {
00443                 if ( !$frame->thumbnailImage( $width, $height, /* fit */ false ) ) {
00444                     return $this->getMediaTransformError( $params, "Error scaling frame $i" );
00445                 }
00446             }
00447             $im->setImageDepth( 8 );
00448 
00449             if ( $rotation ) {
00450                 if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
00451                     return $this->getMediaTransformError( $params, "Error rotating $rotation degrees" );
00452                 }
00453             }
00454 
00455             if ( $this->isAnimatedImage( $image ) ) {
00456                 wfDebug( __METHOD__ . ": Writing animated thumbnail\n" );
00457                 // This is broken somehow... can't find out how to fix it
00458                 $result = $im->writeImages( $params['dstPath'], true );
00459             } else {
00460                 $result = $im->writeImage( $params['dstPath'] );
00461             }
00462             if ( !$result ) {
00463                 return $this->getMediaTransformError( $params,
00464                     "Unable to write thumbnail to {$params['dstPath']}" );
00465             }
00466         } catch ( ImagickException $e ) {
00467             return $this->getMediaTransformError( $params, $e->getMessage() );
00468         }
00469 
00470         return false;
00471     }
00472 
00481     protected function transformCustom( $image, $params ) {
00482         # Use a custom convert command
00483         global $wgCustomConvertCommand;
00484 
00485         # Variables: %s %d %w %h
00486         $src = wfEscapeShellArg( $params['srcPath'] );
00487         $dst = wfEscapeShellArg( $params['dstPath'] );
00488         $cmd = $wgCustomConvertCommand;
00489         $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames
00490         $cmd = str_replace( '%h', wfEscapeShellArg( $params['physicalHeight'] ),
00491             str_replace( '%w', wfEscapeShellArg( $params['physicalWidth'] ), $cmd ) ); # Size
00492         wfDebug( __METHOD__ . ": Running custom convert command $cmd\n" );
00493         wfProfileIn( 'convert' );
00494         $retval = 0;
00495         $err = wfShellExecWithStderr( $cmd, $retval );
00496         wfProfileOut( 'convert' );
00497 
00498         if ( $retval !== 0 ) {
00499             $this->logErrorForExternalProcess( $retval, $err, $cmd );
00500 
00501             return $this->getMediaTransformError( $params, $err );
00502         }
00503 
00504         return false; # No error
00505     }
00506 
00514     public function getMediaTransformError( $params, $errMsg ) {
00515         return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
00516             $params['clientHeight'], $errMsg );
00517     }
00518 
00527     protected function transformGd( $image, $params ) {
00528         # Use PHP's builtin GD library functions.
00529         #
00530         # First find out what kind of file this is, and select the correct
00531         # input routine for this.
00532 
00533         $typemap = array(
00534             'image/gif' => array( 'imagecreatefromgif', 'palette', 'imagegif' ),
00535             'image/jpeg' => array( 'imagecreatefromjpeg', 'truecolor',
00536                 array( __CLASS__, 'imageJpegWrapper' ) ),
00537             'image/png' => array( 'imagecreatefrompng', 'bits', 'imagepng' ),
00538             'image/vnd.wap.wbmp' => array( 'imagecreatefromwbmp', 'palette', 'imagewbmp' ),
00539             'image/xbm' => array( 'imagecreatefromxbm', 'palette', 'imagexbm' ),
00540         );
00541         if ( !isset( $typemap[$params['mimeType']] ) ) {
00542             $err = 'Image type not supported';
00543             wfDebug( "$err\n" );
00544             $errMsg = wfMessage( 'thumbnail_image-type' )->text();
00545 
00546             return $this->getMediaTransformError( $params, $errMsg );
00547         }
00548         list( $loader, $colorStyle, $saveType ) = $typemap[$params['mimeType']];
00549 
00550         if ( !function_exists( $loader ) ) {
00551             $err = "Incomplete GD library configuration: missing function $loader";
00552             wfDebug( "$err\n" );
00553             $errMsg = wfMessage( 'thumbnail_gd-library', $loader )->text();
00554 
00555             return $this->getMediaTransformError( $params, $errMsg );
00556         }
00557 
00558         if ( !file_exists( $params['srcPath'] ) ) {
00559             $err = "File seems to be missing: {$params['srcPath']}";
00560             wfDebug( "$err\n" );
00561             $errMsg = wfMessage( 'thumbnail_image-missing', $params['srcPath'] )->text();
00562 
00563             return $this->getMediaTransformError( $params, $errMsg );
00564         }
00565 
00566         $src_image = call_user_func( $loader, $params['srcPath'] );
00567 
00568         $rotation = function_exists( 'imagerotate' ) ? $this->getRotation( $image ) : 0;
00569         list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
00570         $dst_image = imagecreatetruecolor( $width, $height );
00571 
00572         // Initialise the destination image to transparent instead of
00573         // the default solid black, to support PNG and GIF transparency nicely
00574         $background = imagecolorallocate( $dst_image, 0, 0, 0 );
00575         imagecolortransparent( $dst_image, $background );
00576         imagealphablending( $dst_image, false );
00577 
00578         if ( $colorStyle == 'palette' ) {
00579             // Don't resample for paletted GIF images.
00580             // It may just uglify them, and completely breaks transparency.
00581             imagecopyresized( $dst_image, $src_image,
00582                 0, 0, 0, 0,
00583                 $width, $height,
00584                 imagesx( $src_image ), imagesy( $src_image ) );
00585         } else {
00586             imagecopyresampled( $dst_image, $src_image,
00587                 0, 0, 0, 0,
00588                 $width, $height,
00589                 imagesx( $src_image ), imagesy( $src_image ) );
00590         }
00591 
00592         if ( $rotation % 360 != 0 && $rotation % 90 == 0 ) {
00593             $rot_image = imagerotate( $dst_image, $rotation, 0 );
00594             imagedestroy( $dst_image );
00595             $dst_image = $rot_image;
00596         }
00597 
00598         imagesavealpha( $dst_image, true );
00599 
00600         call_user_func( $saveType, $dst_image, $params['dstPath'] );
00601         imagedestroy( $dst_image );
00602         imagedestroy( $src_image );
00603 
00604         return false; # No error
00605     }
00606 
00613     function escapeMagickProperty( $s ) {
00614         // Double the backslashes
00615         $s = str_replace( '\\', '\\\\', $s );
00616         // Double the percents
00617         $s = str_replace( '%', '%%', $s );
00618         // Escape initial - or @
00619         if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
00620             $s = '\\' . $s;
00621         }
00622 
00623         return $s;
00624     }
00625 
00643     function escapeMagickInput( $path, $scene = false ) {
00644         # Die on initial metacharacters (caller should prepend path)
00645         $firstChar = substr( $path, 0, 1 );
00646         if ( $firstChar === '~' || $firstChar === '@' ) {
00647             throw new MWException( __METHOD__ . ': cannot escape this path name' );
00648         }
00649 
00650         # Escape glob chars
00651         $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
00652 
00653         return $this->escapeMagickPath( $path, $scene );
00654     }
00655 
00663     function escapeMagickOutput( $path, $scene = false ) {
00664         $path = str_replace( '%', '%%', $path );
00665 
00666         return $this->escapeMagickPath( $path, $scene );
00667     }
00668 
00678     protected function escapeMagickPath( $path, $scene = false ) {
00679         # Die on format specifiers (other than drive letters). The regex is
00680         # meant to match all the formats you get from "convert -list format"
00681         if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
00682             if ( wfIsWindows() && is_dir( $m[0] ) ) {
00683                 // OK, it's a drive letter
00684                 // ImageMagick has a similar exception, see IsMagickConflict()
00685             } else {
00686                 throw new MWException( __METHOD__ . ': unexpected colon character in path name' );
00687             }
00688         }
00689 
00690         # If there are square brackets, add a do-nothing scene specification
00691         # to force a literal interpretation
00692         if ( $scene === false ) {
00693             if ( strpos( $path, '[' ) !== false ) {
00694                 $path .= '[0--1]';
00695             }
00696         } else {
00697             $path .= "[$scene]";
00698         }
00699 
00700         return $path;
00701     }
00702 
00709     protected function getMagickVersion() {
00710         global $wgMemc;
00711 
00712         $cache = $wgMemc->get( "imagemagick-version" );
00713         if ( !$cache ) {
00714             global $wgImageMagickConvertCommand;
00715             $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . ' -version';
00716             wfDebug( __METHOD__ . ": Running convert -version\n" );
00717             $retval = '';
00718             $return = wfShellExec( $cmd, $retval );
00719             $x = preg_match( '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches );
00720             if ( $x != 1 ) {
00721                 wfDebug( __METHOD__ . ": ImageMagick version check failed\n" );
00722 
00723                 return null;
00724             }
00725             $wgMemc->set( "imagemagick-version", $matches[1], 3600 );
00726 
00727             return $matches[1];
00728         }
00729 
00730         return $cache;
00731     }
00732 
00733     static function imageJpegWrapper( $dst_image, $thumbPath ) {
00734         imageinterlace( $dst_image );
00735         imagejpeg( $dst_image, $thumbPath, 95 );
00736     }
00737 
00743     public static function canRotate() {
00744         $scaler = self::getScalerType( null, false );
00745         switch ( $scaler ) {
00746             case 'im':
00747                 # ImageMagick supports autorotation
00748                 return true;
00749             case 'imext':
00750                 # Imagick::rotateImage
00751                 return true;
00752             case 'gd':
00753                 # GD's imagerotate function is used to rotate images, but not
00754                 # all precompiled PHP versions have that function
00755                 return function_exists( 'imagerotate' );
00756             default:
00757                 # Other scalers don't support rotation
00758                 return false;
00759         }
00760     }
00761 
00766     public static function autoRotateEnabled() {
00767         global $wgEnableAutoRotation;
00768 
00769         if ( $wgEnableAutoRotation === null ) {
00770             // Only enable auto-rotation when the bitmap handler can rotate
00771             $wgEnableAutoRotation = BitmapHandler::canRotate();
00772         }
00773 
00774         return $wgEnableAutoRotation;
00775     }
00776 
00784     public function rotate( $file, $params ) {
00785         global $wgImageMagickConvertCommand;
00786 
00787         $rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
00788         $scene = false;
00789 
00790         $scaler = self::getScalerType( null, false );
00791         switch ( $scaler ) {
00792             case 'im':
00793                 $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . " " .
00794                     wfEscapeShellArg( $this->escapeMagickInput( $params['srcPath'], $scene ) ) .
00795                     " -rotate " . wfEscapeShellArg( "-$rotation" ) . " " .
00796                     wfEscapeShellArg( $this->escapeMagickOutput( $params['dstPath'] ) );
00797                 wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
00798                 wfProfileIn( 'convert' );
00799                 $retval = 0;
00800                 $err = wfShellExecWithStderr( $cmd, $retval );
00801                 wfProfileOut( 'convert' );
00802                 if ( $retval !== 0 ) {
00803                     $this->logErrorForExternalProcess( $retval, $err, $cmd );
00804 
00805                     return new MediaTransformError( 'thumbnail_error', 0, 0, $err );
00806                 }
00807 
00808                 return false;
00809             case 'imext':
00810                 $im = new Imagick();
00811                 $im->readImage( $params['srcPath'] );
00812                 if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
00813                     return new MediaTransformError( 'thumbnail_error', 0, 0,
00814                         "Error rotating $rotation degrees" );
00815                 }
00816                 $result = $im->writeImage( $params['dstPath'] );
00817                 if ( !$result ) {
00818                     return new MediaTransformError( 'thumbnail_error', 0, 0,
00819                         "Unable to write image to {$params['dstPath']}" );
00820                 }
00821 
00822                 return false;
00823             default:
00824                 return new MediaTransformError( 'thumbnail_error', 0, 0,
00825                     "$scaler rotation not implemented" );
00826         }
00827     }
00828 
00836     public function mustRender( $file ) {
00837         return self::canRotate() && $this->getRotation( $file ) != 0;
00838     }
00839 }