MediaWiki  REL1_24
Bitmap.php
Go to the documentation of this file.
00001 <?php
00029 class BitmapHandler extends TransformationalImageHandler {
00030 
00039     protected function getScalerType( $dstPath, $checkDstPath = true ) {
00040         global $wgUseImageResize, $wgUseImageMagick, $wgCustomConvertCommand;
00041 
00042         if ( !$dstPath && $checkDstPath ) {
00043             # No output path available, client side scaling only
00044             $scaler = 'client';
00045         } elseif ( !$wgUseImageResize ) {
00046             $scaler = 'client';
00047         } elseif ( $wgUseImageMagick ) {
00048             $scaler = 'im';
00049         } elseif ( $wgCustomConvertCommand ) {
00050             $scaler = 'custom';
00051         } elseif ( function_exists( 'imagecreatetruecolor' ) ) {
00052             $scaler = 'gd';
00053         } elseif ( class_exists( 'Imagick' ) ) {
00054             $scaler = 'imext';
00055         } else {
00056             $scaler = 'client';
00057         }
00058 
00059         return $scaler;
00060     }
00061 
00070     protected function transformImageMagick( $image, $params ) {
00071         # use ImageMagick
00072         global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea,
00073             $wgImageMagickTempDir, $wgImageMagickConvertCommand;
00074 
00075         $quality = array();
00076         $sharpen = array();
00077         $scene = false;
00078         $animation_pre = array();
00079         $animation_post = array();
00080         $decoderHint = array();
00081         if ( $params['mimeType'] == 'image/jpeg' ) {
00082             $qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null;
00083             $quality = array( '-quality', $qualityVal ?: '80' ); // 80%
00084             # Sharpening, see bug 6193
00085             if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
00086                 / ( $params['srcWidth'] + $params['srcHeight'] )
00087                 < $wgSharpenReductionThreshold
00088             ) {
00089                 $sharpen = array( '-sharpen', $wgSharpenParameter );
00090             }
00091             if ( version_compare( $this->getMagickVersion(), "6.5.6" ) >= 0 ) {
00092                 // JPEG decoder hint to reduce memory, available since IM 6.5.6-2
00093                 $decoderHint = array( '-define', "jpeg:size={$params['physicalDimensions']}" );
00094             }
00095         } elseif ( $params['mimeType'] == 'image/png' ) {
00096             $quality = array( '-quality', '95' ); // zlib 9, adaptive filtering
00097 
00098         } elseif ( $params['mimeType'] == 'image/gif' ) {
00099             if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
00100                 // Extract initial frame only; we're so big it'll
00101                 // be a total drag. :P
00102                 $scene = 0;
00103             } elseif ( $this->isAnimatedImage( $image ) ) {
00104                 // Coalesce is needed to scale animated GIFs properly (bug 1017).
00105                 $animation_pre = array( '-coalesce' );
00106                 // We optimize the output, but -optimize is broken,
00107                 // use optimizeTransparency instead (bug 11822)
00108                 if ( version_compare( $this->getMagickVersion(), "6.3.5" ) >= 0 ) {
00109                     $animation_post = array( '-fuzz', '5%', '-layers', 'optimizeTransparency' );
00110                 }
00111             }
00112         } elseif ( $params['mimeType'] == 'image/x-xcf' ) {
00113             // Before merging layers, we need to set the background
00114             // to be transparent to preserve alpha, as -layers merge
00115             // merges all layers on to a canvas filled with the
00116             // background colour. After merging we reset the background
00117             // to be white for the default background colour setting
00118             // in the PNG image (which is used in old IE)
00119             $animation_pre = array(
00120                 '-background', 'transparent',
00121                 '-layers', 'merge',
00122                 '-background', 'white',
00123             );
00124             wfSuppressWarnings();
00125             $xcfMeta = unserialize( $image->getMetadata() );
00126             wfRestoreWarnings();
00127             if ( $xcfMeta
00128                 && isset( $xcfMeta['colorType'] )
00129                 && $xcfMeta['colorType'] === 'greyscale-alpha'
00130                 && version_compare( $this->getMagickVersion(), "6.8.9-3" ) < 0
00131             ) {
00132                 // bug 66323 - Greyscale images not rendered properly.
00133                 // So only take the "red" channel.
00134                 $channelOnly = array( '-channel', 'R', '-separate' );
00135                 $animation_pre = array_merge( $animation_pre, $channelOnly );
00136             }
00137         }
00138 
00139         // Use one thread only, to avoid deadlock bugs on OOM
00140         $env = array( 'OMP_NUM_THREADS' => 1 );
00141         if ( strval( $wgImageMagickTempDir ) !== '' ) {
00142             $env['MAGICK_TMPDIR'] = $wgImageMagickTempDir;
00143         }
00144 
00145         $rotation = $this->getRotation( $image );
00146         list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
00147 
00148         $cmd = call_user_func_array( 'wfEscapeShellArg', array_merge(
00149             array( $wgImageMagickConvertCommand ),
00150             $quality,
00151             // Specify white background color, will be used for transparent images
00152             // in Internet Explorer/Windows instead of default black.
00153             array( '-background', 'white' ),
00154             $decoderHint,
00155             array( $this->escapeMagickInput( $params['srcPath'], $scene ) ),
00156             $animation_pre,
00157             // For the -thumbnail option a "!" is needed to force exact size,
00158             // or ImageMagick may decide your ratio is wrong and slice off
00159             // a pixel.
00160             array( '-thumbnail', "{$width}x{$height}!" ),
00161             // Add the source url as a comment to the thumb, but don't add the flag if there's no comment
00162             ( $params['comment'] !== ''
00163                 ? array( '-set', 'comment', $this->escapeMagickProperty( $params['comment'] ) )
00164                 : array() ),
00165             array( '-depth', 8 ),
00166             $sharpen,
00167             array( '-rotate', "-$rotation" ),
00168             $animation_post,
00169             array( $this->escapeMagickOutput( $params['dstPath'] ) ) ) );
00170 
00171         wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
00172         wfProfileIn( 'convert' );
00173         $retval = 0;
00174         $err = wfShellExecWithStderr( $cmd, $retval, $env );
00175         wfProfileOut( 'convert' );
00176 
00177         if ( $retval !== 0 ) {
00178             $this->logErrorForExternalProcess( $retval, $err, $cmd );
00179 
00180             return $this->getMediaTransformError( $params, "$err\nError code: $retval" );
00181         }
00182 
00183         return false; # No error
00184     }
00185 
00194     protected function transformImageMagickExt( $image, $params ) {
00195         global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea;
00196 
00197         try {
00198             $im = new Imagick();
00199             $im->readImage( $params['srcPath'] );
00200 
00201             if ( $params['mimeType'] == 'image/jpeg' ) {
00202                 // Sharpening, see bug 6193
00203                 if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
00204                     / ( $params['srcWidth'] + $params['srcHeight'] )
00205                     < $wgSharpenReductionThreshold
00206                 ) {
00207                     // Hack, since $wgSharpenParamater is written specifically for the command line convert
00208                     list( $radius, $sigma ) = explode( 'x', $wgSharpenParameter );
00209                     $im->sharpenImage( $radius, $sigma );
00210                 }
00211                 $qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null;
00212                 $im->setCompressionQuality( $qualityVal ?: 80 );
00213             } elseif ( $params['mimeType'] == 'image/png' ) {
00214                 $im->setCompressionQuality( 95 );
00215             } elseif ( $params['mimeType'] == 'image/gif' ) {
00216                 if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
00217                     // Extract initial frame only; we're so big it'll
00218                     // be a total drag. :P
00219                     $im->setImageScene( 0 );
00220                 } elseif ( $this->isAnimatedImage( $image ) ) {
00221                     // Coalesce is needed to scale animated GIFs properly (bug 1017).
00222                     $im = $im->coalesceImages();
00223                 }
00224             }
00225 
00226             $rotation = $this->getRotation( $image );
00227             list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
00228 
00229             $im->setImageBackgroundColor( new ImagickPixel( 'white' ) );
00230 
00231             // Call Imagick::thumbnailImage on each frame
00232             foreach ( $im as $i => $frame ) {
00233                 if ( !$frame->thumbnailImage( $width, $height, /* fit */ false ) ) {
00234                     return $this->getMediaTransformError( $params, "Error scaling frame $i" );
00235                 }
00236             }
00237             $im->setImageDepth( 8 );
00238 
00239             if ( $rotation ) {
00240                 if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
00241                     return $this->getMediaTransformError( $params, "Error rotating $rotation degrees" );
00242                 }
00243             }
00244 
00245             if ( $this->isAnimatedImage( $image ) ) {
00246                 wfDebug( __METHOD__ . ": Writing animated thumbnail\n" );
00247                 // This is broken somehow... can't find out how to fix it
00248                 $result = $im->writeImages( $params['dstPath'], true );
00249             } else {
00250                 $result = $im->writeImage( $params['dstPath'] );
00251             }
00252             if ( !$result ) {
00253                 return $this->getMediaTransformError( $params,
00254                     "Unable to write thumbnail to {$params['dstPath']}" );
00255             }
00256         } catch ( ImagickException $e ) {
00257             return $this->getMediaTransformError( $params, $e->getMessage() );
00258         }
00259 
00260         return false;
00261     }
00262 
00271     protected function transformCustom( $image, $params ) {
00272         # Use a custom convert command
00273         global $wgCustomConvertCommand;
00274 
00275         # Variables: %s %d %w %h
00276         $src = wfEscapeShellArg( $params['srcPath'] );
00277         $dst = wfEscapeShellArg( $params['dstPath'] );
00278         $cmd = $wgCustomConvertCommand;
00279         $cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames
00280         $cmd = str_replace( '%h', wfEscapeShellArg( $params['physicalHeight'] ),
00281             str_replace( '%w', wfEscapeShellArg( $params['physicalWidth'] ), $cmd ) ); # Size
00282         wfDebug( __METHOD__ . ": Running custom convert command $cmd\n" );
00283         wfProfileIn( 'convert' );
00284         $retval = 0;
00285         $err = wfShellExecWithStderr( $cmd, $retval );
00286         wfProfileOut( 'convert' );
00287 
00288         if ( $retval !== 0 ) {
00289             $this->logErrorForExternalProcess( $retval, $err, $cmd );
00290 
00291             return $this->getMediaTransformError( $params, $err );
00292         }
00293 
00294         return false; # No error
00295     }
00296 
00305     protected function transformGd( $image, $params ) {
00306         # Use PHP's builtin GD library functions.
00307         #
00308         # First find out what kind of file this is, and select the correct
00309         # input routine for this.
00310 
00311         $typemap = array(
00312             'image/gif' => array( 'imagecreatefromgif', 'palette', false, 'imagegif' ),
00313             'image/jpeg' => array( 'imagecreatefromjpeg', 'truecolor', true,
00314                 array( __CLASS__, 'imageJpegWrapper' ) ),
00315             'image/png' => array( 'imagecreatefrompng', 'bits', false, 'imagepng' ),
00316             'image/vnd.wap.wbmp' => array( 'imagecreatefromwbmp', 'palette', false, 'imagewbmp' ),
00317             'image/xbm' => array( 'imagecreatefromxbm', 'palette', false, 'imagexbm' ),
00318         );
00319 
00320         if ( !isset( $typemap[$params['mimeType']] ) ) {
00321             $err = 'Image type not supported';
00322             wfDebug( "$err\n" );
00323             $errMsg = wfMessage( 'thumbnail_image-type' )->text();
00324 
00325             return $this->getMediaTransformError( $params, $errMsg );
00326         }
00327         list( $loader, $colorStyle, $useQuality, $saveType ) = $typemap[$params['mimeType']];
00328 
00329         if ( !function_exists( $loader ) ) {
00330             $err = "Incomplete GD library configuration: missing function $loader";
00331             wfDebug( "$err\n" );
00332             $errMsg = wfMessage( 'thumbnail_gd-library', $loader )->text();
00333 
00334             return $this->getMediaTransformError( $params, $errMsg );
00335         }
00336 
00337         if ( !file_exists( $params['srcPath'] ) ) {
00338             $err = "File seems to be missing: {$params['srcPath']}";
00339             wfDebug( "$err\n" );
00340             $errMsg = wfMessage( 'thumbnail_image-missing', $params['srcPath'] )->text();
00341 
00342             return $this->getMediaTransformError( $params, $errMsg );
00343         }
00344 
00345         $src_image = call_user_func( $loader, $params['srcPath'] );
00346 
00347         $rotation = function_exists( 'imagerotate' ) ? $this->getRotation( $image ) : 0;
00348         list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
00349         $dst_image = imagecreatetruecolor( $width, $height );
00350 
00351         // Initialise the destination image to transparent instead of
00352         // the default solid black, to support PNG and GIF transparency nicely
00353         $background = imagecolorallocate( $dst_image, 0, 0, 0 );
00354         imagecolortransparent( $dst_image, $background );
00355         imagealphablending( $dst_image, false );
00356 
00357         if ( $colorStyle == 'palette' ) {
00358             // Don't resample for paletted GIF images.
00359             // It may just uglify them, and completely breaks transparency.
00360             imagecopyresized( $dst_image, $src_image,
00361                 0, 0, 0, 0,
00362                 $width, $height,
00363                 imagesx( $src_image ), imagesy( $src_image ) );
00364         } else {
00365             imagecopyresampled( $dst_image, $src_image,
00366                 0, 0, 0, 0,
00367                 $width, $height,
00368                 imagesx( $src_image ), imagesy( $src_image ) );
00369         }
00370 
00371         if ( $rotation % 360 != 0 && $rotation % 90 == 0 ) {
00372             $rot_image = imagerotate( $dst_image, $rotation, 0 );
00373             imagedestroy( $dst_image );
00374             $dst_image = $rot_image;
00375         }
00376 
00377         imagesavealpha( $dst_image, true );
00378 
00379         $funcParams = array( $dst_image, $params['dstPath'] );
00380         if ( $useQuality && isset( $params['quality'] ) ) {
00381             $funcParams[] = $params['quality'];
00382         }
00383         call_user_func_array( $saveType, $funcParams );
00384 
00385         imagedestroy( $dst_image );
00386         imagedestroy( $src_image );
00387 
00388         return false; # No error
00389     }
00390 
00394     // FIXME: transformImageMagick() & transformImageMagickExt() uses JPEG quality 80, here it's 95?
00395     static function imageJpegWrapper( $dst_image, $thumbPath, $quality = 95 ) {
00396         imageinterlace( $dst_image );
00397         imagejpeg( $dst_image, $thumbPath, $quality );
00398     }
00399 
00405     public function canRotate() {
00406         $scaler = $this->getScalerType( null, false );
00407         switch ( $scaler ) {
00408             case 'im':
00409                 # ImageMagick supports autorotation
00410                 return true;
00411             case 'imext':
00412                 # Imagick::rotateImage
00413                 return true;
00414             case 'gd':
00415                 # GD's imagerotate function is used to rotate images, but not
00416                 # all precompiled PHP versions have that function
00417                 return function_exists( 'imagerotate' );
00418             default:
00419                 # Other scalers don't support rotation
00420                 return false;
00421         }
00422     }
00423 
00428     public function autoRotateEnabled() {
00429         global $wgEnableAutoRotation;
00430 
00431         if ( $wgEnableAutoRotation === null ) {
00432             // Only enable auto-rotation when we actually can
00433             return $this->canRotate();
00434         }
00435 
00436         return $wgEnableAutoRotation;
00437     }
00438 
00446     public function rotate( $file, $params ) {
00447         global $wgImageMagickConvertCommand;
00448 
00449         $rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
00450         $scene = false;
00451 
00452         $scaler = $this->getScalerType( null, false );
00453         switch ( $scaler ) {
00454             case 'im':
00455                 $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . " " .
00456                     wfEscapeShellArg( $this->escapeMagickInput( $params['srcPath'], $scene ) ) .
00457                     " -rotate " . wfEscapeShellArg( "-$rotation" ) . " " .
00458                     wfEscapeShellArg( $this->escapeMagickOutput( $params['dstPath'] ) );
00459                 wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
00460                 wfProfileIn( 'convert' );
00461                 $retval = 0;
00462                 $err = wfShellExecWithStderr( $cmd, $retval );
00463                 wfProfileOut( 'convert' );
00464                 if ( $retval !== 0 ) {
00465                     $this->logErrorForExternalProcess( $retval, $err, $cmd );
00466 
00467                     return new MediaTransformError( 'thumbnail_error', 0, 0, $err );
00468                 }
00469 
00470                 return false;
00471             case 'imext':
00472                 $im = new Imagick();
00473                 $im->readImage( $params['srcPath'] );
00474                 if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
00475                     return new MediaTransformError( 'thumbnail_error', 0, 0,
00476                         "Error rotating $rotation degrees" );
00477                 }
00478                 $result = $im->writeImage( $params['dstPath'] );
00479                 if ( !$result ) {
00480                     return new MediaTransformError( 'thumbnail_error', 0, 0,
00481                         "Unable to write image to {$params['dstPath']}" );
00482                 }
00483 
00484                 return false;
00485             default:
00486                 return new MediaTransformError( 'thumbnail_error', 0, 0,
00487                     "$scaler rotation not implemented" );
00488         }
00489     }
00490 }