* @ingroup Media
*/
class BitmapHandler extends ImageHandler {
+
+ /**
+ * @param $image File
+ * @param $params
+ * @return bool
+ */
function normaliseParams( $image, &$params ) {
global $wgMaxImageArea;
if ( !parent::normaliseParams( $image, $params ) ) {
$mimeType = $image->getMimeType();
$srcWidth = $image->getWidth( $params['page'] );
$srcHeight = $image->getHeight( $params['page'] );
+
+ if ( self::canRotate() ) {
+ $rotation = $this->getRotation( $image );
+ if ( $rotation == 90 || $rotation == 270 ) {
+ wfDebug( __METHOD__ . ": Swapping width and height because the file will be rotated $rotation degrees\n" );
+
+ $width = $params['width'];
+ $params['width'] = $params['height'];
+ $params['height'] = $width;
+ }
+ }
# Don't make an image bigger than the source
$params['physicalWidth'] = $params['width'];
return $width * $height;
}
+ /**
+ * @param $image File
+ * @param $dstPath
+ * @param $dstUrl
+ * @param $params
+ * @param int $flags
+ * @return MediaTransformError|ThumbnailImage|TransformParameterError
+ */
function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
- global $wgUseImageMagick;
- global $wgCustomConvertCommand, $wgUseImageResize;
-
if ( !$this->normaliseParams( $image, $params ) ) {
return new TransformParameterError( $params );
}
# The size to which the image will be resized
'physicalWidth' => $params['physicalWidth'],
'physicalHeight' => $params['physicalHeight'],
- 'physicalSize' => "{$params['physicalWidth']}x{$params['physicalHeight']}",
+ 'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}",
# The size of the image on the page
'clientWidth' => $params['width'],
'clientHeight' => $params['height'],
'dstPath' => $dstPath,
);
- wfDebug( __METHOD__ . ": creating {$scalerParams['physicalSize']} thumbnail at $dstPath\n" );
+ wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} thumbnail at $dstPath\n" );
if ( !$image->mustRender() &&
$scalerParams['physicalWidth'] == $scalerParams['srcWidth']
}
# Determine scaler type
- if ( !$dstPath ) {
- # No output path available, client side scaling only
- $scaler = 'client';
- } elseif ( !$wgUseImageResize ) {
- $scaler = 'client';
- } elseif ( $wgUseImageMagick ) {
- $scaler = 'im';
- } elseif ( $wgCustomConvertCommand ) {
- $scaler = 'custom';
- } elseif ( function_exists( 'imagecreatetruecolor' ) ) {
- $scaler = 'gd';
- } else {
- $scaler = 'client';
- }
+ $scaler = self::getScalerType( $dstPath );
wfDebug( __METHOD__ . ": scaler $scaler\n" );
if ( $scaler == 'client' ) {
case 'custom':
$err = $this->transformCustom( $image, $scalerParams );
break;
+ case 'imext':
+ $err = $this->transformImageMagickExt( $image, $scalerParams );
+ break;
case 'gd':
default:
$err = $this->transformGd( $image, $scalerParams );
$scalerParams['clientHeight'], $dstPath );
}
}
+
+ /**
+ * Returns which scaler type should be used. Creates parent directories
+ * for $dstPath and returns 'client' on error
+ *
+ * @return string client,im,custom,gd
+ */
+ protected static function getScalerType( $dstPath, $checkDstPath = true ) {
+ global $wgUseImageResize, $wgUseImageMagick, $wgCustomConvertCommand;
+
+ if ( !$dstPath && $checkDstPath ) {
+ # No output path available, client side scaling only
+ $scaler = 'client';
+ } elseif ( !$wgUseImageResize ) {
+ $scaler = 'client';
+ } elseif ( $wgUseImageMagick ) {
+ $scaler = 'im';
+ } elseif ( $wgCustomConvertCommand ) {
+ $scaler = 'custom';
+ } elseif ( function_exists( 'imagecreatetruecolor' ) ) {
+ $scaler = 'gd';
+ } elseif ( class_exists( 'Imagick' ) ) {
+ $scaler = 'imext';
+ } else {
+ $scaler = 'client';
+ }
+
+ if ( $scaler != 'client' && $dstPath ) {
+ if ( !wfMkdirParents( dirname( $dstPath ) ) ) {
+ # Unable to create a path for the thumbnail
+ return 'client';
+ }
+ }
+ return $scaler;
+ }
/**
* Get a ThumbnailImage that respresents an image that will be scaled
return new ThumbnailImage( $image, $image->getURL(),
$params['clientWidth'], $params['clientHeight'], $params['srcPath'] );
}
-
+
/**
* Transform an image using ImageMagick
*
$sharpen = "-sharpen " . wfEscapeShellArg( $wgSharpenParameter );
}
// JPEG decoder hint to reduce memory, available since IM 6.5.6-2
- $decoderHint = "-define jpeg:size={$params['physicalSize']}";
+ $decoderHint = "-define jpeg:size={$params['physicalDimensions']}";
} elseif ( $params['mimeType'] == 'image/png' ) {
$quality = "-quality 95"; // zlib 9, adaptive filtering
// For the -thumbnail option a "!" is needed to force exact size,
// or ImageMagick may decide your ratio is wrong and slice off
// a pixel.
- " -thumbnail " . wfEscapeShellArg( "{$params['physicalSize']}!" ) .
- // Add the source url as a comment to the thumb.
- " -set comment " . wfEscapeShellArg( $this->escapeMagickProperty( $params['comment'] ) ) .
- " -depth 8 $sharpen" .
+ " -thumbnail " . wfEscapeShellArg( "{$params['physicalDimensions']}!" ) .
+ // Add the source url as a comment to the thumb, but don't add the flag if there's no comment
+ ( $params['comment'] !== ''
+ ? " -set comment " . wfEscapeShellArg( $this->escapeMagickProperty( $params['comment'] ) )
+ : '' ) .
+ " -depth 8 $sharpen -auto-orient" .
" {$animation_post} " .
wfEscapeShellArg( $this->escapeMagickOutput( $params['dstPath'] ) ) . " 2>&1";
return false; # No error
}
+
+ /**
+ * Transform an image using the Imagick PHP extension
+ *
+ * @param $image File File associated with this thumbnail
+ * @param $params array Array with scaler params
+ *
+ * @return MediaTransformError Error object if error occured, false (=no error) otherwise
+ */
+ protected function transformImageMagickExt( $image, $params ) {
+ global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea;
+
+ try {
+ $im = new Imagick();
+ $im->readImage( $params['srcPath'] );
+
+ if ( $params['mimeType'] == 'image/jpeg' ) {
+ // Sharpening, see bug 6193
+ if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
+ / ( $params['srcWidth'] + $params['srcHeight'] )
+ < $wgSharpenReductionThreshold ) {
+ // Hack, since $wgSharpenParamater is written specifically for the command line convert
+ list( $radius, $sigma ) = explode( 'x', $wgSharpenParameter );
+ $im->sharpenImage( $radius, $sigma );
+ }
+ $im->setCompressionQuality( 80 );
+ } elseif( $params['mimeType'] == 'image/png' ) {
+ $im->setCompressionQuality( 95 );
+ } elseif ( $params['mimeType'] == 'image/gif' ) {
+ if ( $this->getImageArea( $image, $params['srcWidth'],
+ $params['srcHeight'] ) > $wgMaxAnimatedGifArea ) {
+ // Extract initial frame only; we're so big it'll
+ // be a total drag. :P
+ $im->setImageScene( 0 );
+ } elseif ( $this->isAnimatedImage( $image ) ) {
+ // Coalesce is needed to scale animated GIFs properly (bug 1017).
+ $im = $im->coalesceImages();
+ }
+ }
+
+ $rotation = $this->getRotation( $image );
+ if ( $rotation == 90 || $rotation == 270 ) {
+ // We'll resize before rotation, so swap the dimensions again
+ $width = $params['physicalHeight'];
+ $height = $params['physicalWidth'];
+ } else {
+ $width = $params['physicalWidth'];
+ $height = $params['physicalHeight'];
+ }
+
+ $im->setImageBackgroundColor( new ImagickPixel( 'white' ) );
+
+ // Call Imagick::thumbnailImage on each frame
+ foreach ( $im as $i => $frame ) {
+ if ( !$frame->thumbnailImage( $width, $height, /* fit */ false ) ) {
+ return $this->getMediaTransformError( $params, "Error scaling frame $i" );
+ }
+ }
+ $im->setImageDepth( 8 );
+
+ if ( $rotation ) {
+ if ( !$im->rotateImage( new ImagickPixel( 'white' ), $rotation ) ) {
+ return $this->getMediaTransformError( $params, "Error rotating $rotation degrees" );
+ }
+ }
+
+ if ( $this->isAnimatedImage( $image ) ) {
+ wfDebug( __METHOD__ . ": Writing animated thumbnail\n" );
+ // This is broken somehow... can't find out how to fix it
+ $result = $im->writeImages( $params['dstPath'], true );
+ } else {
+ $result = $im->writeImage( $params['dstPath'] );
+ }
+ if ( !$result ) {
+ return $this->getMediaTransformError( $params,
+ "Unable to write thumbnail to {$params['dstPath']}" );
+ }
+
+ } catch ( ImagickException $e ) {
+ return $this->getMediaTransformError( $params, $e->getMessage() );
+ }
+
+ return false;
+
+ }
/**
* Transform an image using a custom command
wfHostname(), $retval, trim( $err ), $cmd ) );
}
/**
- *
+ * Get a MediaTransformError with error 'thumbnail_error'
+ *
+ * @param $params array Parameter array as passed to the transform* functions
+ * @param $errMsg string Error message
+ * @return MediaTransformError
*/
protected function getMediaTransformError( $params, $errMsg ) {
return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
}
$src_image = call_user_func( $loader, $params['srcPath'] );
- $dst_image = imagecreatetruecolor( $params['physicalWidth'],
- $params['physicalHeight'] );
+
+ $rotation = function_exists( 'imagerotate' ) ? $this->getRotation( $image ) : 0;
+ if ( $rotation == 90 || $rotation == 270 ) {
+ # We'll resize before rotation, so swap the dimensions again
+ $width = $params['physicalHeight'];
+ $height = $params['physicalWidth'];
+ } else {
+ $width = $params['physicalWidth'];
+ $height = $params['physicalHeight'];
+ }
+ $dst_image = imagecreatetruecolor( $width, $height );
// Initialise the destination image to transparent instead of
// the default solid black, to support PNG and GIF transparency nicely
// It may just uglify them, and completely breaks transparency.
imagecopyresized( $dst_image, $src_image,
0, 0, 0, 0,
- $params['physicalWidth'], $params['physicalHeight'],
+ $width, $height,
imagesx( $src_image ), imagesy( $src_image ) );
} else {
imagecopyresampled( $dst_image, $src_image,
0, 0, 0, 0,
- $params['physicalWidth'], $params['physicalHeight'],
+ $width, $height,
imagesx( $src_image ), imagesy( $src_image ) );
}
-
+
+ if ( $rotation % 360 != 0 && $rotation % 90 == 0 ) {
+ $rot_image = imagerotate( $dst_image, $rotation, 0 );
+ imagedestroy( $dst_image );
+ $dst_image = $rot_image;
+ }
+
imagesavealpha( $dst_image, true );
call_user_func( $saveType, $dst_image, $params['dstPath'] );
return $fields;
}
+ /**
+ * @param $image File
+ * @return array|bool
+ */
function formatMetadata( $image ) {
$result = array(
'visible' => array(),
}
return $result;
}
+
+ /**
+ * Try to read out the orientation of the file and return the angle that
+ * the file needs to be rotated to be viewed
+ *
+ * @param $file File
+ * @return int 0, 90, 180 or 270
+ */
+ public function getRotation( $file ) {
+ $data = $file->getMetadata();
+ if ( !$data ) {
+ return 0;
+ }
+ $data = unserialize( $data );
+ if ( isset( $data['Orientation'] ) ) {
+ # See http://sylvana.net/jpegcrop/exif_orientation.html
+ switch ( $data['Orientation'] ) {
+ case 8:
+ return 90;
+ case 3:
+ return 180;
+ case 6:
+ return 270;
+ default:
+ return 0;
+ }
+ }
+ return 0;
+ }
+ /**
+ * Returns whether the current scaler supports rotation (im and gd do)
+ *
+ * @return bool
+ */
+ public static function canRotate() {
+ $scaler = self::getScalerType( null, false );
+ switch ( $scaler ) {
+ case 'im':
+ # ImageMagick supports autorotation
+ return true;
+ case 'imext':
+ # Imagick::rotateImage
+ return true;
+ case 'gd':
+ # GD's imagerotate function is used to rotate images, but not
+ # all precompiled PHP versions have that function
+ return function_exists( 'imagerotate' );
+ default:
+ # Other scalers don't support rotation
+ return false;
+ }
+ }
+
+ /**
+ * Rerurns whether the file needs to be rendered. Returns true if the
+ * file requires rotation and we are able to rotate it.
+ *
+ * @param $file File
+ * @return bool
+ */
+ public function mustRender( $file ) {
+ return self::canRotate() && $this->getRotation( $file ) != 0;
+ }
}