19use UnexpectedValueException;
43 if ( !$dstPath && $checkDstPath ) {
44 # No output path available, client side scaling only
46 } elseif ( !$useImageResize ) {
48 } elseif ( $useImageMagick ) {
50 } elseif ( $customConvertCommand ) {
52 } elseif ( $this->
hasGDSupport() && function_exists(
'imagecreatetruecolor' ) ) {
54 } elseif ( class_exists(
'Imagick' ) ) {
78 $res = parent::makeParamString( $params );
79 if ( $res && isset( $params[
'interlace'] ) && $params[
'interlace'] ) {
80 return "interlaced-$res";
90 $remainder = preg_replace(
'/^interlaced-/',
'', $str );
91 $params = parent::parseParamString( $remainder );
92 if ( $params ===
false ) {
95 $params[
'interlace'] = $str !== $remainder;
104 if ( $name ===
'interlace' ) {
105 return $value ===
false || $value ===
true;
107 return parent::validateParam( $name, $value );
119 if ( !parent::normaliseParams( $image, $params ) ) {
122 $mimeType = $image->getMimeType();
123 $interlace = isset( $params[
'interlace'] ) && $params[
'interlace']
124 && isset( $maxInterlacingAreas[$mimeType] )
125 && $this->
getImageArea( $image ) <= $maxInterlacingAreas[$mimeType];
126 $params[
'interlace'] = $interlace;
137 switch ( $pixelFormat ) {
139 return [
'1x1',
'1x1',
'1x1' ];
141 return [
'2x1',
'1x1',
'1x1' ];
143 return [
'2x2',
'1x1',
'1x1' ];
145 throw new UnexpectedValueException(
'Invalid pixel format for JPEG output' );
172 $animation_post = [];
176 if ( $params[
'mimeType'] ===
'image/jpeg' ) {
177 $qualityVal = isset( $params[
'quality'] ) ? (string)$params[
'quality'] :
null;
178 $quality = [
'-quality', $qualityVal ?: (string)$jpegQuality ];
179 if ( $params[
'interlace'] ) {
180 $animation_post = [
'-interlace',
'JPEG' ];
182 # Sharpening, see T8193
183 if ( ( $params[
'physicalWidth'] + $params[
'physicalHeight'] )
184 / ( $params[
'srcWidth'] + $params[
'srcHeight'] )
185 < $sharpenReductionThreshold
187 $sharpen = [
'-sharpen', $sharpenParameter ];
191 $decoderHint = [
'-define',
"jpeg:size={$params['physicalDimensions']}" ];
193 if ( $jpegPixelFormat ) {
195 $subsampling = [
'-sampling-factor', implode(
',', $factors ) ];
197 } elseif ( $params[
'mimeType'] ===
'image/png' ) {
198 $quality = [
'-quality',
'95' ];
199 if ( $params[
'interlace'] ) {
200 $animation_post = [
'-interlace',
'PNG' ];
202 } elseif ( $params[
'mimeType'] ===
'image/webp' ) {
203 $quality = [
'-quality',
'95' ];
204 } elseif ( $params[
'mimeType'] ===
'image/gif' ) {
205 if ( $this->
getImageArea( $image ) > $maxAnimatedGifArea ) {
211 $animation_pre = [
'-coalesce' ];
215 $animation_post = [
'-fuzz',
'5%',
'-layers',
'optimizeTransparency' ];
220 $animation_post[] =
'-interlace';
221 $animation_post[] =
'GIF';
223 } elseif ( $params[
'mimeType'] ===
'image/x-xcf' ) {
231 '-background',
'transparent',
233 '-background',
'white',
238 $env = [
'OMP_NUM_THREADS' =>
'1' ];
239 if ( (
string)$imageMagickTempDir !==
'' ) {
240 $env[
'MAGICK_TMPDIR'] = (string)$imageMagickTempDir;
243 $rotation = isset( $params[
'disableRotation'] ) ? 0 : $this->
getRotation( $image );
245 $mirroring = isset( $params[
'disableRotation'] ) ? null : $this->
getMirrored( $image );
246 $mirrored = match ( $mirroring ) {
247 'horizontal' => [
'-flop' ],
248 'vertical' => [
'-flip' ],
252 $cmd = Shell::escape( ...array_merge(
253 [ $imageMagickConvertCommand ],
257 [
'-background',
'white' ],
264 [
'-thumbnail',
"{$width}x{$height}!" ],
266 ( $params[
'comment'] !==
''
270 [
'+set',
'Thumb::URI' ],
273 [
'-rotate',
"-$rotation" ],
279 wfDebug( __METHOD__ .
": running ImageMagick: $cmd" );
280 $shell = Shell::command()->unsafeCommand( $cmd )->environment( $env )->execute();
281 $retval = $shell->getExitCode();
282 $err = $shell->getStderr();
284 if ( $retval !== 0 ) {
290 return false; # No error
310 $im->readImage( $params[
'srcPath'] );
312 if ( $params[
'mimeType'] ===
'image/jpeg' ) {
314 if ( ( $params[
'physicalWidth'] + $params[
'physicalHeight'] )
315 / ( $params[
'srcWidth'] + $params[
'srcHeight'] )
316 < $sharpenReductionThreshold
319 [ $radius, $sigma ] = explode(
'x', $sharpenParameter, 2 );
320 $im->sharpenImage( (
float)$radius, (
float)$sigma );
322 $qualityVal = isset( $params[
'quality'] ) ? (int)$params[
'quality'] :
null;
323 $im->setCompressionQuality( $qualityVal ?: $jpegQuality );
324 if ( $params[
'interlace'] ) {
325 $im->setInterlaceScheme( Imagick::INTERLACE_JPEG );
327 if ( $jpegPixelFormat ) {
329 $im->setSamplingFactors( $factors );
331 } elseif ( $params[
'mimeType'] ===
'image/png' ) {
332 $im->setCompressionQuality( 95 );
333 if ( $params[
'interlace'] ) {
334 $im->setInterlaceScheme( Imagick::INTERLACE_PNG );
336 } elseif ( $params[
'mimeType'] ===
'image/gif' ) {
337 if ( $this->
getImageArea( $image ) > $maxAnimatedGifArea ) {
340 $im->setImageScene( 0 );
343 $im = $im->coalesceImages();
346 if ( $params[
'interlace'] ) {
347 $im->setInterlaceScheme( Imagick::INTERLACE_GIF );
351 $rotation = isset( $params[
'disableRotation'] ) ? 0 : $this->
getRotation( $image );
354 $im->setImageBackgroundColor(
new ImagickPixel(
'white' ) );
357 foreach ( $im as $i => $frame ) {
358 if ( !$frame->thumbnailImage( $width, $height,
false ) ) {
362 $im->setImageDepth( 8 );
364 if ( $rotation && !$im->rotateImage(
new ImagickPixel(
'white' ), 360 - $rotation ) ) {
368 if ( !isset( $params[
'disableRotation'] ) ) {
380 wfDebug( __METHOD__ .
": Writing animated thumbnail" );
382 $result = $im->writeImages( $params[
'dstPath'],
true );
384 $result = $im->writeImage( $params[
'dstPath'] );
388 "Unable to write thumbnail to {$params['dstPath']}" );
390 }
catch ( ImagickException $e ) {
412 $cmd = strtr( $customConvertCommand, [
413 '%s' => Shell::escape( $params[
'srcPath'] ),
414 '%d' => Shell::escape( $params[
'dstPath'] ),
415 '%w' => Shell::escape( $params[
'physicalWidth'] ),
416 '%h' => Shell::escape( $params[
'physicalHeight'] ),
418 wfDebug( __METHOD__ .
": Running custom convert command $cmd" );
419 $shell = Shell::command()->unsafeCommand( $cmd )->execute();
420 $retval = $shell->getExitCode();
421 $err = $shell->getStderr();
423 if ( $retval !== 0 ) {
429 return false; # No error
441 # Use PHP's builtin GD library functions.
442 # First find out what kind of file this is, and select the correct
443 # input routine for this.
446 'image/gif' => [
'imagecreatefromgif',
'palette',
false, imagegif( ... ) ],
447 'image/jpeg' => [
'imagecreatefromjpeg',
'truecolor',
true,
449 'image/png' => [
'imagecreatefrompng',
'bits',
false, imagepng( ... ) ],
450 'image/vnd.wap.wbmp' => [
'imagecreatefromwbmp',
'palette',
false, imagewbmp( ... ) ],
451 'image/xbm' => [
'imagecreatefromxbm',
'palette',
false, imagexbm( ... ) ],
454 if ( !isset( $typemap[$params[
'mimeType']] ) ) {
455 $err =
'Image type not supported';
457 $errMsg =
wfMessage(
'thumbnail_image-type' )->text();
461 [ $loader, $colorStyle, $useQuality, $saveType ] = $typemap[$params[
'mimeType']];
463 if ( !function_exists( $loader ) ) {
464 $err =
"Incomplete GD library configuration: missing function $loader";
466 $errMsg =
wfMessage(
'thumbnail_gd-library', $loader )->text();
471 if ( !file_exists( $params[
'srcPath'] ) ) {
472 $err =
"File seems to be missing: {$params['srcPath']}";
474 $errMsg =
wfMessage(
'thumbnail_image-missing', $params[
'srcPath'] )->text();
479 if ( filesize( $params[
'srcPath'] ) === 0 ) {
480 $err =
"Image file size seems to be zero.";
482 $errMsg =
wfMessage(
'thumbnail_image-size-zero', $params[
'srcPath'] )->text();
487 $src_image = $loader( $params[
'srcPath'] );
489 $rotation = function_exists(
'imagerotate' ) && !isset( $params[
'disableRotation'] ) ?
493 $dst_image = imagecreatetruecolor( $width, $height );
497 $background = imagecolorallocate( $dst_image, 0, 0, 0 );
498 imagecolortransparent( $dst_image, $background );
499 imagealphablending( $dst_image,
false );
501 if ( $colorStyle ===
'palette' ) {
504 imagecopyresized( $dst_image, $src_image,
507 imagesx( $src_image ), imagesy( $src_image ) );
509 imagecopyresampled( $dst_image, $src_image,
512 imagesx( $src_image ), imagesy( $src_image ) );
515 if ( $rotation % 360 !== 0 ) {
516 $dst_image = imagerotate( $dst_image, $rotation, 0 );
519 if ( !isset( $params[
'disableRotation'] ) ) {
522 imageflip( $dst_image, IMG_FLIP_HORIZONTAL );
525 imageflip( $dst_image, IMG_FLIP_VERTICAL );
530 imagesavealpha( $dst_image,
true );
532 $funcParams = [ $dst_image, $params[
'dstPath'] ];
533 if ( $useQuality && isset( $params[
'quality'] ) ) {
534 $funcParams[] = $params[
'quality'];
537 $saveType( ...$funcParams );
539 return false; # No error
554 imageinterlace( $dst_image );
555 imagejpeg( $dst_image, $thumbPath, $quality ?? $jpegQuality );
568 # ImageMagick supports autorotation
571 # Imagick::rotateImage
574 # GD's imagerotate function is used to rotate images, but not
575 # all precompiled PHP versions have that function
576 return function_exists(
'imagerotate' );
578 # Other scalers don't support rotation
592 if ( $enableAutoRotation ===
null ) {
597 return $enableAutoRotation;
608 public function rotate( $file, $params ) {
612 $rotation = ( $params[
'rotation'] + $this->
getRotation( $file ) ) % 360;
618 $cmd = Shell::escape( $imageMagickConvertCommand ) .
" " .
620 " -rotate " . Shell::escape(
"-$rotation" ) .
" " .
622 wfDebug( __METHOD__ .
": running ImageMagick: $cmd" );
623 $shell = Shell::command()->unsafeCommand( $cmd )->execute();
624 $err = $shell->getStderr();
625 $retval = $shell->getExitCode();
626 if ( $retval !== 0 ) {
635 $im->readImage( $params[
'srcPath'] );
636 if ( !$im->rotateImage(
new ImagickPixel(
'white' ), 360 - $rotation ) ) {
638 "Error rotating $rotation degrees" );
640 $result = $im->writeImage( $params[
'dstPath'] );
643 "Unable to write image to {$params['dstPath']}" );
649 "$scaler rotation not implemented" );
655class_alias( BitmapHandler::class,
'BitmapHandler' );
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
A class containing constants representing the names of configuration variables.
const JpegPixelFormat
Name constant for the JpegPixelFormat setting, for use with Config::get()
const UseImageMagick
Name constant for the UseImageMagick setting, for use with Config::get()
const ImageMagickTempDir
Name constant for the ImageMagickTempDir setting, for use with Config::get()
const ImageMagickConvertCommand
Name constant for the ImageMagickConvertCommand setting, for use with Config::get()
const UseImageResize
Name constant for the UseImageResize setting, for use with Config::get()
const MaxInterlacingAreas
Name constant for the MaxInterlacingAreas setting, for use with Config::get()
const CustomConvertCommand
Name constant for the CustomConvertCommand setting, for use with Config::get()
const JpegQuality
Name constant for the JpegQuality setting, for use with Config::get()
const EnableAutoRotation
Name constant for the EnableAutoRotation setting, for use with Config::get()
const SharpenParameter
Name constant for the SharpenParameter setting, for use with Config::get()
const SharpenReductionThreshold
Name constant for the SharpenReductionThreshold setting, for use with Config::get()
const MaxAnimatedGifArea
Name constant for the MaxAnimatedGifArea setting, for use with Config::get()