<?php
/**
- * Generic handler for bitmap images
+ * Generic handler for bitmap images.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Media
*
* @ingroup Media
*/
-class BitmapHandler extends ImageHandler {
- function normaliseParams( $image, &$params ) {
- global $wgMaxImageArea;
- if ( !parent::normaliseParams( $image, $params ) ) {
- return false;
- }
-
- $mimeType = $image->getMimeType();
- $srcWidth = $image->getWidth( $params['page'] );
- $srcHeight = $image->getHeight( $params['page'] );
-
- # Don't make an image bigger than the source
- $params['physicalWidth'] = $params['width'];
- $params['physicalHeight'] = $params['height'];
-
- if ( $params['physicalWidth'] >= $srcWidth ) {
- $params['physicalWidth'] = $srcWidth;
- $params['physicalHeight'] = $srcHeight;
- # Skip scaling limit checks if no scaling is required
- if ( !$image->mustRender() )
- return true;
- }
-
- # Don't thumbnail an image so big that it will fill hard drives and send servers into swap
- # JPEG has the handy property of allowing thumbnailing without full decompression, so we make
- # an exception for it.
- # FIXME: This actually only applies to ImageMagick
- if ( $mimeType !== 'image/jpeg' &&
- $srcWidth * $srcHeight > $wgMaxImageArea )
- {
- return false;
- }
-
- return true;
- }
-
-
- // Function that returns the number of pixels to be thumbnailed.
- // Intended for animated GIFs to multiply by the number of frames.
- function getImageArea( $image, $width, $height ) {
- return $width * $height;
- }
-
- function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
- global $wgUseImageMagick;
- global $wgCustomConvertCommand, $wgUseImageResize;
+class BitmapHandler extends TransformationalImageHandler {
- if ( !$this->normaliseParams( $image, $params ) ) {
- return new TransformParameterError( $params );
- }
- # Create a parameter array to pass to the scaler
- $scalerParams = array(
- # The size to which the image will be resized
- 'physicalWidth' => $params['physicalWidth'],
- 'physicalHeight' => $params['physicalHeight'],
- 'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}",
- # The size of the image on the page
- 'clientWidth' => $params['width'],
- 'clientHeight' => $params['height'],
- # Comment as will be added to the EXIF of the thumbnail
- 'comment' => isset( $params['descriptionUrl'] ) ?
- "File source: {$params['descriptionUrl']}" : '',
- # Properties of the original image
- 'srcWidth' => $image->getWidth(),
- 'srcHeight' => $image->getHeight(),
- 'mimeType' => $image->getMimeType(),
- 'srcPath' => $image->getPath(),
- 'dstPath' => $dstPath,
- );
-
- wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} thumbnail at $dstPath\n" );
-
- if ( !$image->mustRender() &&
- $scalerParams['physicalWidth'] == $scalerParams['srcWidth']
- && $scalerParams['physicalHeight'] == $scalerParams['srcHeight'] ) {
-
- # normaliseParams (or the user) wants us to return the unscaled image
- wfDebug( __METHOD__ . ": returning unscaled image\n" );
- return $this->getClientScalingThumbnailImage( $image, $scalerParams );
- }
+ /**
+ * Returns which scaler type should be used. Creates parent directories
+ * for $dstPath and returns 'client' on error
+ *
+ * @param string $dstPath
+ * @param bool $checkDstPath
+ * @return string|Callable One of client, im, custom, gd, imext or an array( object, method )
+ */
+ protected function getScalerType( $dstPath, $checkDstPath = true ) {
+ global $wgUseImageResize, $wgUseImageMagick, $wgCustomConvertCommand;
- # Determine scaler type
- if ( !$dstPath ) {
+ if ( !$dstPath && $checkDstPath ) {
# No output path available, client side scaling only
$scaler = 'client';
} elseif ( !$wgUseImageResize ) {
$scaler = 'custom';
} elseif ( function_exists( 'imagecreatetruecolor' ) ) {
$scaler = 'gd';
+ } elseif ( class_exists( 'Imagick' ) ) {
+ $scaler = 'imext';
} else {
$scaler = 'client';
}
- wfDebug( __METHOD__ . ": scaler $scaler\n" );
- if ( $scaler == 'client' ) {
- # Client-side image scaling, use the source URL
- # Using the destination URL in a TRANSFORM_LATER request would be incorrect
- return $this->getClientScalingThumbnailImage( $image, $scalerParams );
- }
+ return $scaler;
+ }
- if ( $flags & self::TRANSFORM_LATER ) {
- wfDebug( __METHOD__ . ": Transforming later per flags.\n" );
- return new ThumbnailImage( $image, $dstUrl, $scalerParams['clientWidth'],
- $scalerParams['clientHeight'], $dstPath );
+ public function makeParamString( $params ) {
+ $res = parent::makeParamString( $params );
+ if ( isset( $params['interlace'] ) && $params['interlace'] ) {
+ return "interlaced-{$res}";
+ } else {
+ return $res;
}
+ }
- # Try to make a target path for the thumbnail
- if ( !wfMkdirParents( dirname( $dstPath ) ) ) {
- wfDebug( __METHOD__ . ": Unable to create thumbnail destination directory, falling back to client scaling\n" );
- return $this->getClientScalingThumbnailImage( $image, $scalerParams );
+ public function parseParamString( $str ) {
+ $remainder = preg_replace( '/^interlaced-/', '', $str );
+ $params = parent::parseParamString( $remainder );
+ if ( $params === false ) {
+ return false;
}
+ $params['interlace'] = $str !== $remainder;
+ return $params;
+ }
- switch ( $scaler ) {
- case 'im':
- $err = $this->transformImageMagick( $image, $scalerParams );
- break;
- case 'custom':
- $err = $this->transformCustom( $image, $scalerParams );
- break;
- case 'gd':
- default:
- $err = $this->transformGd( $image, $scalerParams );
- break;
+ public function validateParam( $name, $value ) {
+ if ( $name === 'interlace' ) {
+ return $value === false || $value === true;
+ } else {
+ return parent::validateParam( $name, $value );
}
+ }
- # Remove the file if a zero-byte thumbnail was created, or if there was an error
- $removed = $this->removeBadFile( $dstPath, (bool)$err );
- if ( $err ) {
- # transform returned MediaTransforError
- return $err;
- } elseif ( $removed ) {
- # Thumbnail was zero-byte and had to be removed
- return new MediaTransformError( 'thumbnail_error',
- $scalerParams['clientWidth'], $scalerParams['clientHeight'] );
- } else {
- return new ThumbnailImage( $image, $dstUrl, $scalerParams['clientWidth'],
- $scalerParams['clientHeight'], $dstPath );
+ /**
+ * @param File $image
+ * @param array &$params
+ * @return bool
+ */
+ function normaliseParams( $image, &$params ) {
+ global $wgMaxInterlacingAreas;
+ if ( !parent::normaliseParams( $image, $params ) ) {
+ return false;
}
+ $mimeType = $image->getMimeType();
+ $interlace = isset( $params['interlace'] ) && $params['interlace']
+ && isset( $wgMaxInterlacingAreas[$mimeType] )
+ && $this->getImageArea( $image ) <= $wgMaxInterlacingAreas[$mimeType];
+ $params['interlace'] = $interlace;
+ return true;
}
/**
- * Get a ThumbnailImage that respresents an image that will be scaled
- * client side
+ * Get ImageMagick subsampling factors for the target JPEG pixel format.
*
- * @param $image File File associated with this thumbnail
- * @param $params array Array with scaler params
- * @return ThumbnailImage
+ * @param string $pixelFormat one of 'yuv444', 'yuv422', 'yuv420'
+ * @return array of string keys
*/
- protected function getClientScalingThumbnailImage( $image, $params ) {
- return new ThumbnailImage( $image, $image->getURL(),
- $params['clientWidth'], $params['clientHeight'], $params['srcPath'] );
+ protected function imageMagickSubsampling( $pixelFormat ) {
+ switch ( $pixelFormat ) {
+ case 'yuv444':
+ return [ '1x1', '1x1', '1x1' ];
+ case 'yuv422':
+ return [ '2x1', '1x1', '1x1' ];
+ case 'yuv420':
+ return [ '2x2', '1x1', '1x1' ];
+ default:
+ throw new MWException( 'Invalid pixel format for JPEG output' );
+ }
}
/**
* Transform an image using ImageMagick
*
- * @param $image File File associated with this thumbnail
- * @param $params array Array with scaler params
+ * @param File $image File associated with this thumbnail
+ * @param array $params Array with scaler params
*
- * @return MediaTransformError Error object if error occured, false (=no error) otherwise
+ * @return MediaTransformError|bool Error object if error occurred, false (=no error) otherwise
*/
protected function transformImageMagick( $image, $params ) {
# use ImageMagick
- global $wgSharpenReductionThreshold, $wgSharpenParameter,
- $wgMaxAnimatedGifArea,
- $wgImageMagickTempDir, $wgImageMagickConvertCommand;
+ global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea,
+ $wgImageMagickTempDir, $wgImageMagickConvertCommand, $wgJpegPixelFormat;
- $quality = '';
- $sharpen = '';
+ $quality = [];
+ $sharpen = [];
$scene = false;
- $animation_pre = '';
- $animation_post = '';
- $decoderHint = '';
+ $animation_pre = [];
+ $animation_post = [];
+ $decoderHint = [];
+ $subsampling = [];
+
if ( $params['mimeType'] == 'image/jpeg' ) {
- $quality = "-quality 80"; // 80%
- # Sharpening, see bug 6193
+ $qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null;
+ $quality = [ '-quality', $qualityVal ?: '80' ]; // 80%
+ if ( $params['interlace'] ) {
+ $animation_post = [ '-interlace', 'JPEG' ];
+ }
+ # Sharpening, see T8193
if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
- / ( $params['srcWidth'] + $params['srcHeight'] )
- < $wgSharpenReductionThreshold ) {
- $sharpen = "-sharpen " . wfEscapeShellArg( $wgSharpenParameter );
+ / ( $params['srcWidth'] + $params['srcHeight'] )
+ < $wgSharpenReductionThreshold
+ ) {
+ $sharpen = [ '-sharpen', $wgSharpenParameter ];
+ }
+ if ( version_compare( $this->getMagickVersion(), "6.5.6" ) >= 0 ) {
+ // JPEG decoder hint to reduce memory, available since IM 6.5.6-2
+ $decoderHint = [ '-define', "jpeg:size={$params['physicalDimensions']}" ];
+ }
+ if ( $wgJpegPixelFormat ) {
+ $factors = $this->imageMagickSubsampling( $wgJpegPixelFormat );
+ $subsampling = [ '-sampling-factor', implode( ',', $factors ) ];
}
- // JPEG decoder hint to reduce memory, available since IM 6.5.6-2
- $decoderHint = "-define jpeg:size={$params['physicalDimensions']}";
-
} elseif ( $params['mimeType'] == 'image/png' ) {
- $quality = "-quality 95"; // zlib 9, adaptive filtering
-
+ $quality = [ '-quality', '95' ]; // zlib 9, adaptive filtering
+ if ( $params['interlace'] ) {
+ $animation_post = [ '-interlace', 'PNG' ];
+ }
+ } elseif ( $params['mimeType'] == 'image/webp' ) {
+ $quality = [ '-quality', '95' ]; // zlib 9, adaptive filtering
} elseif ( $params['mimeType'] == 'image/gif' ) {
- if ( $this->getImageArea( $image, $params['srcWidth'],
- $params['srcHeight'] ) > $wgMaxAnimatedGifArea ) {
+ if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
// Extract initial frame only; we're so big it'll
// be a total drag. :P
$scene = 0;
-
} elseif ( $this->isAnimatedImage( $image ) ) {
- // Coalesce is needed to scale animated GIFs properly (bug 1017).
- $animation_pre = '-coalesce';
+ // Coalesce is needed to scale animated GIFs properly (T3017).
+ $animation_pre = [ '-coalesce' ];
// We optimize the output, but -optimize is broken,
- // use optimizeTransparency instead (bug 11822)
+ // use optimizeTransparency instead (T13822)
if ( version_compare( $this->getMagickVersion(), "6.3.5" ) >= 0 ) {
- $animation_post = '-fuzz 5% -layers optimizeTransparency +map';
+ $animation_post = [ '-fuzz', '5%', '-layers', 'optimizeTransparency' ];
}
}
+ if ( $params['interlace'] && version_compare( $this->getMagickVersion(), "6.3.4" ) >= 0
+ && !$this->isAnimatedImage( $image ) ) { // interlacing animated GIFs is a bad idea
+ $animation_post[] = '-interlace';
+ $animation_post[] = 'GIF';
+ }
+ } elseif ( $params['mimeType'] == 'image/x-xcf' ) {
+ // Before merging layers, we need to set the background
+ // to be transparent to preserve alpha, as -layers merge
+ // merges all layers on to a canvas filled with the
+ // background colour. After merging we reset the background
+ // to be white for the default background colour setting
+ // in the PNG image (which is used in old IE)
+ $animation_pre = [
+ '-background', 'transparent',
+ '-layers', 'merge',
+ '-background', 'white',
+ ];
+ MediaWiki\suppressWarnings();
+ $xcfMeta = unserialize( $image->getMetadata() );
+ MediaWiki\restoreWarnings();
+ if ( $xcfMeta
+ && isset( $xcfMeta['colorType'] )
+ && $xcfMeta['colorType'] === 'greyscale-alpha'
+ && version_compare( $this->getMagickVersion(), "6.8.9-3" ) < 0
+ ) {
+ // T68323 - Greyscale images not rendered properly.
+ // So only take the "red" channel.
+ $channelOnly = [ '-channel', 'R', '-separate' ];
+ $animation_pre = array_merge( $animation_pre, $channelOnly );
+ }
}
// Use one thread only, to avoid deadlock bugs on OOM
- $env = array( 'OMP_NUM_THREADS' => 1 );
+ $env = [ 'OMP_NUM_THREADS' => 1 ];
if ( strval( $wgImageMagickTempDir ) !== '' ) {
$env['MAGICK_TMPDIR'] = $wgImageMagickTempDir;
}
- $cmd =
- wfEscapeShellArg( $wgImageMagickConvertCommand ) .
+ $rotation = isset( $params['disableRotation'] ) ? 0 : $this->getRotation( $image );
+ list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
+
+ $cmd = call_user_func_array( 'wfEscapeShellArg', array_merge(
+ [ $wgImageMagickConvertCommand ],
+ $quality,
// Specify white background color, will be used for transparent images
// in Internet Explorer/Windows instead of default black.
- " {$quality} -background white" .
- " {$decoderHint} " .
- wfEscapeShellArg( $this->escapeMagickInput( $params['srcPath'], $scene ) ) .
- " {$animation_pre}" .
+ [ '-background', 'white' ],
+ $decoderHint,
+ [ $this->escapeMagickInput( $params['srcPath'], $scene ) ],
+ $animation_pre,
// 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['physicalDimensions']}!" ) .
+ [ '-thumbnail', "{$width}x{$height}!" ],
// 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" .
- " {$animation_post} " .
- wfEscapeShellArg( $this->escapeMagickOutput( $params['dstPath'] ) ) . " 2>&1";
+ ? [ '-set', 'comment', $this->escapeMagickProperty( $params['comment'] ) ]
+ : [] ),
+ // T108616: Avoid exposure of local file path
+ [ '+set', 'Thumb::URI' ],
+ [ '-depth', 8 ],
+ $sharpen,
+ [ '-rotate', "-$rotation" ],
+ $subsampling,
+ $animation_post,
+ [ $this->escapeMagickOutput( $params['dstPath'] ) ] ) );
wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
- wfProfileIn( 'convert' );
$retval = 0;
- $err = wfShellExec( $cmd, $retval, $env );
- wfProfileOut( 'convert' );
+ $err = wfShellExecWithStderr( $cmd, $retval, $env );
if ( $retval !== 0 ) {
$this->logErrorForExternalProcess( $retval, $err, $cmd );
- return $this->getMediaTransformError( $params, $err );
+
+ return $this->getMediaTransformError( $params, "$err\nError code: $retval" );
}
return false; # No error
}
+ /**
+ * Transform an image using the Imagick PHP extension
+ *
+ * @param File $image File associated with this thumbnail
+ * @param array $params Array with scaler params
+ *
+ * @return MediaTransformError Error|bool object if error occurred, false (=no error) otherwise
+ */
+ protected function transformImageMagickExt( $image, $params ) {
+ global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea,
+ $wgJpegPixelFormat;
+
+ try {
+ $im = new Imagick();
+ $im->readImage( $params['srcPath'] );
+
+ if ( $params['mimeType'] == 'image/jpeg' ) {
+ // Sharpening, see T8193
+ if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
+ / ( $params['srcWidth'] + $params['srcHeight'] )
+ < $wgSharpenReductionThreshold
+ ) {
+ // Hack, since $wgSharpenParameter is written specifically for the command line convert
+ list( $radius, $sigma ) = explode( 'x', $wgSharpenParameter );
+ $im->sharpenImage( $radius, $sigma );
+ }
+ $qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null;
+ $im->setCompressionQuality( $qualityVal ?: 80 );
+ if ( $params['interlace'] ) {
+ $im->setInterlaceScheme( Imagick::INTERLACE_JPEG );
+ }
+ if ( $wgJpegPixelFormat ) {
+ $factors = $this->imageMagickSubsampling( $wgJpegPixelFormat );
+ $im->setSamplingFactors( $factors );
+ }
+ } elseif ( $params['mimeType'] == 'image/png' ) {
+ $im->setCompressionQuality( 95 );
+ if ( $params['interlace'] ) {
+ $im->setInterlaceScheme( Imagick::INTERLACE_PNG );
+ }
+ } elseif ( $params['mimeType'] == 'image/gif' ) {
+ if ( $this->getImageArea( $image ) > $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 (T3017).
+ $im = $im->coalesceImages();
+ }
+ // GIF interlacing is only available since 6.3.4
+ $v = Imagick::getVersion();
+ preg_match( '/ImageMagick ([0-9]+\.[0-9]+\.[0-9]+)/', $v['versionString'], $v );
+
+ if ( $params['interlace'] && version_compare( $v[1], '6.3.4' ) >= 0 ) {
+ $im->setInterlaceScheme( Imagick::INTERLACE_GIF );
+ }
+ }
+
+ $rotation = isset( $params['disableRotation'] ) ? 0 : $this->getRotation( $image );
+ list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
+
+ $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' ), 360 - $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
*
- * @param $image File File associated with this thumbnail
- * @param $params array Array with scaler params
+ * @param File $image File associated with this thumbnail
+ * @param array $params Array with scaler params
*
- * @return MediaTransformError Error object if error occured, false (=no error) otherwise
+ * @return MediaTransformError Error|bool object if error occurred, false (=no error) otherwise
*/
protected function transformCustom( $image, $params ) {
# Use a custom convert command
$dst = wfEscapeShellArg( $params['dstPath'] );
$cmd = $wgCustomConvertCommand;
$cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames
- $cmd = str_replace( '%h', $params['physicalHeight'],
- str_replace( '%w', $params['physicalWidth'], $cmd ) ); # Size
+ $cmd = str_replace( '%h', wfEscapeShellArg( $params['physicalHeight'] ),
+ str_replace( '%w', wfEscapeShellArg( $params['physicalWidth'] ), $cmd ) ); # Size
wfDebug( __METHOD__ . ": Running custom convert command $cmd\n" );
- wfProfileIn( 'convert' );
$retval = 0;
- $err = wfShellExec( $cmd, $retval );
- wfProfileOut( 'convert' );
+ $err = wfShellExecWithStderr( $cmd, $retval );
if ( $retval !== 0 ) {
$this->logErrorForExternalProcess( $retval, $err, $cmd );
+
return $this->getMediaTransformError( $params, $err );
}
- return false; # No error
- }
- /**
- * Log an error that occured in an external process
- *
- * @param $retval int
- * @param $err int
- * @param $cmd string
- */
- protected function logErrorForExternalProcess( $retval, $err, $cmd ) {
- wfDebugLog( 'thumbnail',
- sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"',
- 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'],
- $params['clientHeight'], $errMsg );
+ return false; # No error
}
/**
* Transform an image using the built in GD library
*
- * @param $image File File associated with this thumbnail
- * @param $params array Array with scaler params
+ * @param File $image File associated with this thumbnail
+ * @param array $params Array with scaler params
*
- * @return MediaTransformError Error object if error occured, false (=no error) otherwise
+ * @return MediaTransformError|bool Error object if error occurred, false (=no error) otherwise
*/
protected function transformGd( $image, $params ) {
# Use PHP's builtin GD library functions.
- #
# First find out what kind of file this is, and select the correct
# input routine for this.
- $typemap = array(
- 'image/gif' => array( 'imagecreatefromgif', 'palette', 'imagegif' ),
- 'image/jpeg' => array( 'imagecreatefromjpeg', 'truecolor', array( __CLASS__, 'imageJpegWrapper' ) ),
- 'image/png' => array( 'imagecreatefrompng', 'bits', 'imagepng' ),
- 'image/vnd.wap.wbmp' => array( 'imagecreatefromwbmp', 'palette', 'imagewbmp' ),
- 'image/xbm' => array( 'imagecreatefromxbm', 'palette', 'imagexbm' ),
- );
+ $typemap = [
+ 'image/gif' => [ 'imagecreatefromgif', 'palette', false, 'imagegif' ],
+ 'image/jpeg' => [ 'imagecreatefromjpeg', 'truecolor', true,
+ [ __CLASS__, 'imageJpegWrapper' ] ],
+ 'image/png' => [ 'imagecreatefrompng', 'bits', false, 'imagepng' ],
+ 'image/vnd.wap.wbmp' => [ 'imagecreatefromwbmp', 'palette', false, 'imagewbmp' ],
+ 'image/xbm' => [ 'imagecreatefromxbm', 'palette', false, 'imagexbm' ],
+ ];
+
if ( !isset( $typemap[$params['mimeType']] ) ) {
$err = 'Image type not supported';
wfDebug( "$err\n" );
- $errMsg = wfMsg ( 'thumbnail_image-type' );
+ $errMsg = wfMessage( 'thumbnail_image-type' )->text();
+
return $this->getMediaTransformError( $params, $errMsg );
}
- list( $loader, $colorStyle, $saveType ) = $typemap[$params['mimeType']];
+ list( $loader, $colorStyle, $useQuality, $saveType ) = $typemap[$params['mimeType']];
if ( !function_exists( $loader ) ) {
$err = "Incomplete GD library configuration: missing function $loader";
wfDebug( "$err\n" );
- $errMsg = wfMsg ( 'thumbnail_gd-library', $loader );
+ $errMsg = wfMessage( 'thumbnail_gd-library', $loader )->text();
+
return $this->getMediaTransformError( $params, $errMsg );
}
if ( !file_exists( $params['srcPath'] ) ) {
$err = "File seems to be missing: {$params['srcPath']}";
wfDebug( "$err\n" );
- $errMsg = wfMsg ( 'thumbnail_image-missing', $params['srcPath'] );
+ $errMsg = wfMessage( 'thumbnail_image-missing', $params['srcPath'] )->text();
+
return $this->getMediaTransformError( $params, $errMsg );
}
$src_image = call_user_func( $loader, $params['srcPath'] );
- $dst_image = imagecreatetruecolor( $params['physicalWidth'],
- $params['physicalHeight'] );
+
+ $rotation = function_exists( 'imagerotate' ) && !isset( $params['disableRotation'] ) ?
+ $this->getRotation( $image ) :
+ 0;
+ list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
+ $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'] );
+ $funcParams = [ $dst_image, $params['dstPath'] ];
+ if ( $useQuality && isset( $params['quality'] ) ) {
+ $funcParams[] = $params['quality'];
+ }
+ call_user_func_array( $saveType, $funcParams );
+
imagedestroy( $dst_image );
imagedestroy( $src_image );
}
/**
- * Escape a string for ImageMagick's property input (e.g. -set -comment)
- * See InterpretImageProperties() in magick/property.c
+ * Callback for transformGd when transforming jpeg images.
*/
- function escapeMagickProperty( $s ) {
- // Double the backslashes
- $s = str_replace( '\\', '\\\\', $s );
- // Double the percents
- $s = str_replace( '%', '%%', $s );
- // Escape initial - or @
- if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
- $s = '\\' . $s;
- }
- return $s;
+ // FIXME: transformImageMagick() & transformImageMagickExt() uses JPEG quality 80, here it's 95?
+ static function imageJpegWrapper( $dst_image, $thumbPath, $quality = 95 ) {
+ imageinterlace( $dst_image );
+ imagejpeg( $dst_image, $thumbPath, $quality );
}
/**
- * Escape a string for ImageMagick's input filenames. See ExpandFilenames()
- * and GetPathComponent() in magick/utility.c.
- *
- * This won't work with an initial ~ or @, so input files should be prefixed
- * with the directory name.
- *
- * Glob character unescaping is broken in ImageMagick before 6.6.1-5, but
- * it's broken in a way that doesn't involve trying to convert every file
- * in a directory, so we're better off escaping and waiting for the bugfix
- * to filter down to users.
+ * Returns whether the current scaler supports rotation (im and gd do)
*
- * @param $path string The file path
- * @param $scene string The scene specification, or false if there is none
+ * @return bool
*/
- function escapeMagickInput( $path, $scene = false ) {
- # Die on initial metacharacters (caller should prepend path)
- $firstChar = substr( $path, 0, 1 );
- if ( $firstChar === '~' || $firstChar === '@' ) {
- throw new MWException( __METHOD__ . ': cannot escape this path name' );
+ public function canRotate() {
+ $scaler = $this->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;
}
-
- # Escape glob chars
- $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
-
- return $this->escapeMagickPath( $path, $scene );
}
/**
- * Escape a string for ImageMagick's output filename. See
- * InterpretImageFilename() in magick/image.c.
+ * @see $wgEnableAutoRotation
+ * @return bool Whether auto rotation is enabled
*/
- function escapeMagickOutput( $path, $scene = false ) {
- $path = str_replace( '%', '%%', $path );
- return $this->escapeMagickPath( $path, $scene );
- }
+ public function autoRotateEnabled() {
+ global $wgEnableAutoRotation;
- /**
- * Armour a string against ImageMagick's GetPathComponent(). This is a
- * helper function for escapeMagickInput() and escapeMagickOutput().
- *
- * @param $path string The file path
- * @param $scene string The scene specification, or false if there is none
- */
- protected function escapeMagickPath( $path, $scene = false ) {
- # Die on format specifiers (other than drive letters). The regex is
- # meant to match all the formats you get from "convert -list format"
- if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
- if ( wfIsWindows() && is_dir( $m[0] ) ) {
- // OK, it's a drive letter
- // ImageMagick has a similar exception, see IsMagickConflict()
- } else {
- throw new MWException( __METHOD__ . ': unexpected colon character in path name' );
- }
+ if ( $wgEnableAutoRotation === null ) {
+ // Only enable auto-rotation when we actually can
+ return $this->canRotate();
}
- # If there are square brackets, add a do-nothing scene specification
- # to force a literal interpretation
- if ( $scene === false ) {
- if ( strpos( $path, '[' ) !== false ) {
- $path .= '[0--1]';
- }
- } else {
- $path .= "[$scene]";
- }
- return $path;
+ return $wgEnableAutoRotation;
}
/**
- * Retrieve the version of the installed ImageMagick
- * You can use PHPs version_compare() to use this value
- * Value is cached for one hour.
- * @return String representing the IM version.
+ * @param File $file
+ * @param array $params Rotate parameters.
+ * 'rotation' clockwise rotation in degrees, allowed are multiples of 90
+ * @since 1.21
+ * @return bool|MediaTransformError
*/
- protected function getMagickVersion() {
- global $wgMemc;
-
- $cache = $wgMemc->get( "imagemagick-version" );
- if ( !$cache ) {
- global $wgImageMagickConvertCommand;
- $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . ' -version';
- wfDebug( __METHOD__ . ": Running convert -version\n" );
- $retval = '';
- $return = wfShellExec( $cmd, $retval );
- $x = preg_match( '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches );
- if ( $x != 1 ) {
- wfDebug( __METHOD__ . ": ImageMagick version check failed\n" );
- return null;
- }
- $wgMemc->set( "imagemagick-version", $matches[1], 3600 );
- return $matches[1];
- }
- return $cache;
- }
-
- static function imageJpegWrapper( $dst_image, $thumbPath ) {
- imageinterlace( $dst_image );
- imagejpeg( $dst_image, $thumbPath, 95 );
- }
+ public function rotate( $file, $params ) {
+ global $wgImageMagickConvertCommand;
+ $rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
+ $scene = false;
- function getMetadata( $image, $filename ) {
- global $wgShowEXIF;
- if ( $wgShowEXIF && file_exists( $filename ) ) {
- $exif = new Exif( $filename );
- $data = $exif->getFilteredData();
- if ( $data ) {
- $data['MEDIAWIKI_EXIF_VERSION'] = Exif::version();
- return serialize( $data );
- } else {
- return '0';
- }
- } else {
- return '';
- }
- }
-
- function getMetadataType( $image ) {
- return 'exif';
- }
-
- function isMetadataValid( $image, $metadata ) {
- global $wgShowEXIF;
- if ( !$wgShowEXIF ) {
- # Metadata disabled and so an empty field is expected
- return true;
- }
- if ( $metadata === '0' ) {
- # Special value indicating that there is no EXIF data in the file
- return true;
- }
- wfSuppressWarnings();
- $exif = unserialize( $metadata );
- wfRestoreWarnings();
- if ( !isset( $exif['MEDIAWIKI_EXIF_VERSION'] ) ||
- $exif['MEDIAWIKI_EXIF_VERSION'] != Exif::version() )
- {
- # Wrong version
- wfDebug( __METHOD__ . ": wrong version\n" );
- return false;
- }
- return true;
- }
+ $scaler = $this->getScalerType( null, false );
+ switch ( $scaler ) {
+ case 'im':
+ $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . " " .
+ wfEscapeShellArg( $this->escapeMagickInput( $params['srcPath'], $scene ) ) .
+ " -rotate " . wfEscapeShellArg( "-$rotation" ) . " " .
+ wfEscapeShellArg( $this->escapeMagickOutput( $params['dstPath'] ) );
+ wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
+ $retval = 0;
+ $err = wfShellExecWithStderr( $cmd, $retval );
+ if ( $retval !== 0 ) {
+ $this->logErrorForExternalProcess( $retval, $err, $cmd );
+
+ return new MediaTransformError( 'thumbnail_error', 0, 0, $err );
+ }
- /**
- * Get a list of EXIF metadata items which should be displayed when
- * the metadata table is collapsed.
- *
- * @return array of strings
- * @access private
- */
- function visibleMetadataFields() {
- $fields = array();
- $lines = explode( "\n", wfMsgForContent( 'metadata-fields' ) );
- foreach ( $lines as $line ) {
- $matches = array();
- if ( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) {
- $fields[] = $matches[1];
- }
- }
- $fields = array_map( 'strtolower', $fields );
- return $fields;
- }
+ return false;
+ case 'imext':
+ $im = new Imagick();
+ $im->readImage( $params['srcPath'] );
+ if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
+ return new MediaTransformError( 'thumbnail_error', 0, 0,
+ "Error rotating $rotation degrees" );
+ }
+ $result = $im->writeImage( $params['dstPath'] );
+ if ( !$result ) {
+ return new MediaTransformError( 'thumbnail_error', 0, 0,
+ "Unable to write image to {$params['dstPath']}" );
+ }
- function formatMetadata( $image ) {
- $result = array(
- 'visible' => array(),
- 'collapsed' => array()
- );
- $metadata = $image->getMetadata();
- if ( !$metadata ) {
- return false;
- }
- $exif = unserialize( $metadata );
- if ( !$exif ) {
- return false;
- }
- unset( $exif['MEDIAWIKI_EXIF_VERSION'] );
- $format = new FormatExif( $exif );
-
- $formatted = $format->getFormattedData();
- // Sort fields into visible and collapsed
- $visibleFields = $this->visibleMetadataFields();
- foreach ( $formatted as $name => $value ) {
- $tag = strtolower( $name );
- self::addMeta( $result,
- in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
- 'exif',
- $tag,
- $value
- );
+ return false;
+ default:
+ return new MediaTransformError( 'thumbnail_error', 0, 0,
+ "$scaler rotation not implemented" );
}
- return $result;
}
}