ImageMagick should not be used if it's not installed. When a custom convert command is configured, it's the site admin's responsibility to make sure it works with Webp and XCF files. The php-imagick extension is just a wrapper for the ImageMagick library, and would also work for Webp and XCF files. Bug: T308386 Change-Id: I9fcf22b0c4b6d5d132e5a44530e3f255075f7fb4
632 lines
21 KiB
PHP
632 lines
21 KiB
PHP
<?php
|
|
/**
|
|
* 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
|
|
*/
|
|
|
|
use MediaWiki\MainConfigNames;
|
|
use MediaWiki\MediaWikiServices;
|
|
use MediaWiki\Shell\Shell;
|
|
|
|
/**
|
|
* Generic handler for bitmap images
|
|
*
|
|
* @stable to extend
|
|
* @ingroup Media
|
|
*/
|
|
class BitmapHandler extends TransformationalImageHandler {
|
|
|
|
/**
|
|
* Returns which scaler type should be used. Creates parent directories
|
|
* for $dstPath and returns 'client' on error
|
|
* @stable to override
|
|
*
|
|
* @param string|null $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 ) {
|
|
$mainConfig = MediaWikiServices::getInstance()->getMainConfig();
|
|
$useImageResize = $mainConfig->get( MainConfigNames::UseImageResize );
|
|
$useImageMagick = $mainConfig->get( MainConfigNames::UseImageMagick );
|
|
$customConvertCommand = $mainConfig->get( MainConfigNames::CustomConvertCommand );
|
|
if ( !$dstPath && $checkDstPath ) {
|
|
# No output path available, client side scaling only
|
|
$scaler = 'client';
|
|
} elseif ( !$useImageResize ) {
|
|
$scaler = 'client';
|
|
} elseif ( $useImageMagick ) {
|
|
$scaler = 'im';
|
|
} elseif ( $customConvertCommand ) {
|
|
$scaler = 'custom';
|
|
} elseif ( $this->hasGDSupport() && function_exists( 'imagecreatetruecolor' ) ) {
|
|
$scaler = 'gd';
|
|
} elseif ( class_exists( 'Imagick' ) ) {
|
|
$scaler = 'imext';
|
|
} else {
|
|
$scaler = 'client';
|
|
}
|
|
|
|
return $scaler;
|
|
}
|
|
|
|
/**
|
|
* Whether the php-gd extension supports this type of file.
|
|
*
|
|
* @stable to override
|
|
* @return bool
|
|
*/
|
|
protected function hasGDSupport() {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
* @stable to override
|
|
*/
|
|
public function makeParamString( $params ) {
|
|
$res = parent::makeParamString( $params );
|
|
if ( isset( $params['interlace'] ) && $params['interlace'] ) {
|
|
return "interlaced-$res";
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
* @stable to override
|
|
*/
|
|
public function parseParamString( $str ) {
|
|
$remainder = preg_replace( '/^interlaced-/', '', $str );
|
|
$params = parent::parseParamString( $remainder );
|
|
if ( $params === false ) {
|
|
return false;
|
|
}
|
|
$params['interlace'] = $str !== $remainder;
|
|
return $params;
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
* @stable to override
|
|
*/
|
|
public function validateParam( $name, $value ) {
|
|
if ( $name === 'interlace' ) {
|
|
return $value === false || $value === true;
|
|
}
|
|
return parent::validateParam( $name, $value );
|
|
}
|
|
|
|
/**
|
|
* @stable to override
|
|
* @param File $image
|
|
* @param array &$params
|
|
* @return bool
|
|
*/
|
|
public function normaliseParams( $image, &$params ) {
|
|
$maxInterlacingAreas = MediaWikiServices::getInstance()->getMainConfig()
|
|
->get( MainConfigNames::MaxInterlacingAreas );
|
|
if ( !parent::normaliseParams( $image, $params ) ) {
|
|
return false;
|
|
}
|
|
$mimeType = $image->getMimeType();
|
|
$interlace = isset( $params['interlace'] ) && $params['interlace']
|
|
&& isset( $maxInterlacingAreas[$mimeType] )
|
|
&& $this->getImageArea( $image ) <= $maxInterlacingAreas[$mimeType];
|
|
$params['interlace'] = $interlace;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get ImageMagick subsampling factors for the target JPEG pixel format.
|
|
*
|
|
* @param string $pixelFormat one of 'yuv444', 'yuv422', 'yuv420'
|
|
* @return string[] List of sampling factors
|
|
*/
|
|
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 UnexpectedValueException( 'Invalid pixel format for JPEG output' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transform an image using ImageMagick
|
|
* @stable to override
|
|
*
|
|
* @param File $image File associated with this thumbnail
|
|
* @param array $params Array with scaler params
|
|
*
|
|
* @return MediaTransformError|false Error object if error occurred, false (=no error) otherwise
|
|
*/
|
|
protected function transformImageMagick( $image, $params ) {
|
|
# use ImageMagick
|
|
$mainConfig = MediaWikiServices::getInstance()->getMainConfig();
|
|
$sharpenReductionThreshold = $mainConfig->get( MainConfigNames::SharpenReductionThreshold );
|
|
$sharpenParameter = $mainConfig->get( MainConfigNames::SharpenParameter );
|
|
$maxAnimatedGifArea = $mainConfig->get( MainConfigNames::MaxAnimatedGifArea );
|
|
$imageMagickTempDir = $mainConfig->get( MainConfigNames::ImageMagickTempDir );
|
|
$imageMagickConvertCommand = $mainConfig->get( MainConfigNames::ImageMagickConvertCommand );
|
|
$jpegPixelFormat = $mainConfig->get( MainConfigNames::JpegPixelFormat );
|
|
$jpegQuality = $mainConfig->get( MainConfigNames::JpegQuality );
|
|
$quality = [];
|
|
$sharpen = [];
|
|
$scene = false;
|
|
$animation_pre = [];
|
|
$animation_post = [];
|
|
$decoderHint = [];
|
|
$subsampling = [];
|
|
|
|
if ( $params['mimeType'] === 'image/jpeg' ) {
|
|
$qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null;
|
|
$quality = [ '-quality', $qualityVal ?: (string)$jpegQuality ]; // 80% by default
|
|
if ( $params['interlace'] ) {
|
|
$animation_post = [ '-interlace', 'JPEG' ];
|
|
}
|
|
# Sharpening, see T8193
|
|
if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
|
|
/ ( $params['srcWidth'] + $params['srcHeight'] )
|
|
< $sharpenReductionThreshold
|
|
) {
|
|
$sharpen = [ '-sharpen', $sharpenParameter ];
|
|
}
|
|
|
|
// JPEG decoder hint to reduce memory, available since IM 6.5.6-2
|
|
$decoderHint = [ '-define', "jpeg:size={$params['physicalDimensions']}" ];
|
|
|
|
if ( $jpegPixelFormat ) {
|
|
$factors = $this->imageMagickSubsampling( $jpegPixelFormat );
|
|
$subsampling = [ '-sampling-factor', implode( ',', $factors ) ];
|
|
}
|
|
} elseif ( $params['mimeType'] === 'image/png' ) {
|
|
$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 ) > $maxAnimatedGifArea ) {
|
|
// 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 (T3017).
|
|
$animation_pre = [ '-coalesce' ];
|
|
|
|
// We optimize the output, but -optimize is broken,
|
|
// use optimizeTransparency instead (T13822). Version >= IM 6.3.5
|
|
$animation_post = [ '-fuzz', '5%', '-layers', 'optimizeTransparency' ];
|
|
}
|
|
if ( $params['interlace'] && !$this->isAnimatedImage( $image ) ) {
|
|
// Version >= IM 6.3.4
|
|
// 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',
|
|
];
|
|
}
|
|
|
|
// Use one thread only, to avoid deadlock bugs on OOM
|
|
$env = [ 'OMP_NUM_THREADS' => 1 ];
|
|
if ( (string)$imageMagickTempDir !== '' ) {
|
|
$env['MAGICK_TMPDIR'] = $imageMagickTempDir;
|
|
}
|
|
|
|
$rotation = isset( $params['disableRotation'] ) ? 0 : $this->getRotation( $image );
|
|
[ $width, $height ] = $this->extractPreRotationDimensions( $params, $rotation );
|
|
|
|
$cmd = Shell::escape( ...array_merge(
|
|
[ $imageMagickConvertCommand ],
|
|
$quality,
|
|
// Specify white background color, will be used for transparent images
|
|
// in Internet Explorer/Windows instead of default black.
|
|
[ '-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', "{$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', $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" );
|
|
$retval = 0;
|
|
$err = wfShellExecWithStderr( $cmd, $retval, $env );
|
|
|
|
if ( $retval !== 0 ) {
|
|
$this->logErrorForExternalProcess( $retval, $err, $cmd );
|
|
|
|
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|false Error object if error occurred, false (=no error) otherwise
|
|
*/
|
|
protected function transformImageMagickExt( $image, $params ) {
|
|
$mainConfig = MediaWikiServices::getInstance()->getMainConfig();
|
|
$sharpenReductionThreshold = $mainConfig->get( MainConfigNames::SharpenReductionThreshold );
|
|
$sharpenParameter = $mainConfig->get( MainConfigNames::SharpenParameter );
|
|
$maxAnimatedGifArea = $mainConfig->get( MainConfigNames::MaxAnimatedGifArea );
|
|
$jpegPixelFormat = $mainConfig->get( MainConfigNames::JpegPixelFormat );
|
|
$jpegQuality = $mainConfig->get( MainConfigNames::JpegQuality );
|
|
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'] )
|
|
< $sharpenReductionThreshold
|
|
) {
|
|
// Hack, since $wgSharpenParameter is written specifically for the command line convert
|
|
[ $radius, $sigma ] = explode( 'x', $sharpenParameter, 2 );
|
|
$im->sharpenImage( (float)$radius, (float)$sigma );
|
|
}
|
|
$qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null;
|
|
$im->setCompressionQuality( $qualityVal ?: $jpegQuality );
|
|
if ( $params['interlace'] ) {
|
|
$im->setInterlaceScheme( Imagick::INTERLACE_JPEG );
|
|
}
|
|
if ( $jpegPixelFormat ) {
|
|
$factors = $this->imageMagickSubsampling( $jpegPixelFormat );
|
|
$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 ) > $maxAnimatedGifArea ) {
|
|
// 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
|
|
if ( $params['interlace'] ) {
|
|
$im->setInterlaceScheme( Imagick::INTERLACE_GIF );
|
|
}
|
|
}
|
|
|
|
$rotation = isset( $params['disableRotation'] ) ? 0 : $this->getRotation( $image );
|
|
[ $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 && !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
|
|
return $this->getMediaTransformError( $params, "Error rotating $rotation degrees" );
|
|
}
|
|
|
|
if ( $this->isAnimatedImage( $image ) ) {
|
|
wfDebug( __METHOD__ . ": Writing animated thumbnail" );
|
|
// 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 File $image File associated with this thumbnail
|
|
* @param array $params Array with scaler params
|
|
*
|
|
* @return MediaTransformError|false Error object if error occurred, false (=no error) otherwise
|
|
*/
|
|
protected function transformCustom( $image, $params ) {
|
|
// Use a custom convert command
|
|
$customConvertCommand = MediaWikiServices::getInstance()->getMainConfig()
|
|
->get( MainConfigNames::CustomConvertCommand );
|
|
|
|
// Find all variables in the original command at once,
|
|
// so that replacement values cannot inject variable placeholders
|
|
$cmd = strtr( $customConvertCommand, [
|
|
'%s' => Shell::escape( $params['srcPath'] ),
|
|
'%d' => Shell::escape( $params['dstPath'] ),
|
|
'%w' => Shell::escape( $params['physicalWidth'] ),
|
|
'%h' => Shell::escape( $params['physicalHeight'] ),
|
|
] );
|
|
wfDebug( __METHOD__ . ": Running custom convert command $cmd" );
|
|
$retval = 0;
|
|
$err = wfShellExecWithStderr( $cmd, $retval );
|
|
|
|
if ( $retval !== 0 ) {
|
|
$this->logErrorForExternalProcess( $retval, $err, $cmd );
|
|
|
|
return $this->getMediaTransformError( $params, $err );
|
|
}
|
|
|
|
return false; # No error
|
|
}
|
|
|
|
/**
|
|
* Transform an image using the built in GD library
|
|
*
|
|
* @param File $image File associated with this thumbnail
|
|
* @param array $params Array with scaler params
|
|
*
|
|
* @return MediaTransformError|false 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 = [
|
|
'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 );
|
|
$errMsg = wfMessage( 'thumbnail_image-type' )->text();
|
|
|
|
return $this->getMediaTransformError( $params, $errMsg );
|
|
}
|
|
[ $loader, $colorStyle, $useQuality, $saveType ] = $typemap[$params['mimeType']];
|
|
|
|
if ( !function_exists( $loader ) ) {
|
|
$err = "Incomplete GD library configuration: missing function $loader";
|
|
wfDebug( $err );
|
|
$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 );
|
|
$errMsg = wfMessage( 'thumbnail_image-missing', $params['srcPath'] )->text();
|
|
|
|
return $this->getMediaTransformError( $params, $errMsg );
|
|
}
|
|
|
|
if ( filesize( $params['srcPath'] ) === 0 ) {
|
|
$err = "Image file size seems to be zero.";
|
|
wfDebug( $err );
|
|
$errMsg = wfMessage( 'thumbnail_image-size-zero', $params['srcPath'] )->text();
|
|
|
|
return $this->getMediaTransformError( $params, $errMsg );
|
|
}
|
|
|
|
$src_image = $loader( $params['srcPath'] );
|
|
|
|
$rotation = function_exists( 'imagerotate' ) && !isset( $params['disableRotation'] ) ?
|
|
$this->getRotation( $image ) :
|
|
0;
|
|
[ $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
|
|
$background = imagecolorallocate( $dst_image, 0, 0, 0 );
|
|
imagecolortransparent( $dst_image, $background );
|
|
imagealphablending( $dst_image, false );
|
|
|
|
if ( $colorStyle === 'palette' ) {
|
|
// Don't resample for paletted GIF images.
|
|
// It may just uglify them, and completely breaks transparency.
|
|
imagecopyresized( $dst_image, $src_image,
|
|
0, 0, 0, 0,
|
|
$width, $height,
|
|
imagesx( $src_image ), imagesy( $src_image ) );
|
|
} else {
|
|
imagecopyresampled( $dst_image, $src_image,
|
|
0, 0, 0, 0,
|
|
$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 );
|
|
|
|
$funcParams = [ $dst_image, $params['dstPath'] ];
|
|
if ( $useQuality && isset( $params['quality'] ) ) {
|
|
$funcParams[] = $params['quality'];
|
|
}
|
|
// @phan-suppress-next-line PhanParamTooFewInternalUnpack,PhanParamTooFewUnpack There are at least 2 args
|
|
$saveType( ...$funcParams );
|
|
|
|
imagedestroy( $dst_image );
|
|
imagedestroy( $src_image );
|
|
|
|
return false; # No error
|
|
}
|
|
|
|
/**
|
|
* Callback for transformGd when transforming jpeg images.
|
|
*
|
|
* @phpcs:ignore MediaWiki.Commenting.FunctionComment.ObjectTypeHintParam
|
|
* @param resource|object $dst_image Image resource of the original image
|
|
* @param string $thumbPath File path to write the thumbnail image to
|
|
* @param int|null $quality Quality of the thumbnail from 1-100,
|
|
* or null to use default quality.
|
|
*/
|
|
public static function imageJpegWrapper( $dst_image, $thumbPath, $quality = null ) {
|
|
$jpegQuality = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::JpegQuality );
|
|
|
|
imageinterlace( $dst_image );
|
|
imagejpeg( $dst_image, $thumbPath, $quality ?? $jpegQuality );
|
|
}
|
|
|
|
/**
|
|
* Returns whether the current scaler supports rotation (im and gd do)
|
|
* @stable to override
|
|
*
|
|
* @return bool
|
|
*/
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @see $wgEnableAutoRotation
|
|
* @stable to override
|
|
* @return bool Whether auto rotation is enabled
|
|
*/
|
|
public function autoRotateEnabled() {
|
|
$enableAutoRotation = MediaWikiServices::getInstance()->getMainConfig()
|
|
->get( MainConfigNames::EnableAutoRotation );
|
|
|
|
if ( $enableAutoRotation === null ) {
|
|
// Only enable auto-rotation when we actually can
|
|
return $this->canRotate();
|
|
}
|
|
|
|
return $enableAutoRotation;
|
|
}
|
|
|
|
/**
|
|
* @stable to override
|
|
* @param File $file
|
|
* @param array $params Rotate parameters.
|
|
* 'rotation' clockwise rotation in degrees, allowed are multiples of 90
|
|
* @since 1.21
|
|
* @return MediaTransformError|false
|
|
*/
|
|
public function rotate( $file, $params ) {
|
|
$imageMagickConvertCommand = MediaWikiServices::getInstance()
|
|
->getMainConfig()->get( MainConfigNames::ImageMagickConvertCommand );
|
|
|
|
$rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
|
|
$scene = false;
|
|
|
|
$scaler = $this->getScalerType( null, false );
|
|
switch ( $scaler ) {
|
|
case 'im':
|
|
$cmd = Shell::escape( $imageMagickConvertCommand ) . " " .
|
|
Shell::escape( $this->escapeMagickInput( $params['srcPath'], $scene ) ) .
|
|
" -rotate " . Shell::escape( "-$rotation" ) . " " .
|
|
Shell::escape( $this->escapeMagickOutput( $params['dstPath'] ) );
|
|
wfDebug( __METHOD__ . ": running ImageMagick: $cmd" );
|
|
$retval = 0;
|
|
$err = wfShellExecWithStderr( $cmd, $retval );
|
|
if ( $retval !== 0 ) {
|
|
$this->logErrorForExternalProcess( $retval, $err, $cmd );
|
|
|
|
return new MediaTransformError( 'thumbnail_error', 0, 0, $err );
|
|
}
|
|
|
|
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']}" );
|
|
}
|
|
|
|
return false;
|
|
default:
|
|
return new MediaTransformError( 'thumbnail_error', 0, 0,
|
|
"$scaler rotation not implemented" );
|
|
}
|
|
}
|
|
}
|