From 695a93dd3353678ae4a9038528a33a1ac03eba38 Mon Sep 17 00:00:00 2001 From: Andrew H Date: Thu, 14 Jan 2016 02:13:11 +0000 Subject: [PATCH] Add support for image interlacing of Bitmap type images Add 'interlace' parameter and $wgMaxInterlacingSizes global. Bug: T120032 Change-Id: I40dee74060026513f1c2be8c22dfe41a0b4a18df --- includes/DefaultSettings.php | 6 ++ includes/media/Bitmap.php | 71 ++++++++++++++++++- .../media/TransformationalImageHandler.php | 1 + .../includes/media/BitmapScalingTest.php | 25 +++++-- 4 files changed, 93 insertions(+), 10 deletions(-) diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 684e392699..f831cfabc7 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -960,6 +960,12 @@ $wgUseImageMagick = false; */ $wgImageMagickConvertCommand = '/usr/bin/convert'; +/** + * Array of max pixel areas for interlacing per MIME type + * @since 1.27 + */ +$wgMaxInterlacingAreas = array(); + /** * Sharpening parameter to ImageMagick */ diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php index faf40b3029..9ac5e6b562 100644 --- a/includes/media/Bitmap.php +++ b/includes/media/Bitmap.php @@ -59,6 +59,43 @@ class BitmapHandler extends TransformationalImageHandler { return $scaler; } + function makeParamString( $params ) { + $res = parent::makeParamString( $params ); + if ( isset( $params['interlace'] ) && $params['interlace'] ) { + return "interlaced-{$res}"; + } else { + return $res; + } + } + + function parseParamString( $str ) { + $remainder = preg_replace( '/^interlaced-/', '', $str ); + $params = parent::parseParamString( $remainder ); + if ( $params === false ) { + return false; + } + $params['interlace'] = $str !== $remainder; + return $params; + } + + /** + * @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; + } + /** * Transform an image using ImageMagick * @@ -70,7 +107,7 @@ class BitmapHandler extends TransformationalImageHandler { protected function transformImageMagick( $image, $params ) { # use ImageMagick global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea, - $wgImageMagickTempDir, $wgImageMagickConvertCommand; + $wgImageMagickTempDir, $wgImageMagickConvertCommand, $wgMaxInterlacingAreas; $quality = array(); $sharpen = array(); @@ -78,9 +115,13 @@ class BitmapHandler extends TransformationalImageHandler { $animation_pre = array(); $animation_post = array(); $decoderHint = array(); + if ( $params['mimeType'] == 'image/jpeg' ) { $qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null; $quality = array( '-quality', $qualityVal ?: '80' ); // 80% + if ( $params['interlace'] ) { + $animation_post = array( '-interlace', 'JPEG' ); + } # Sharpening, see bug 6193 if ( ( $params['physicalWidth'] + $params['physicalHeight'] ) / ( $params['srcWidth'] + $params['srcHeight'] ) @@ -92,7 +133,12 @@ class BitmapHandler extends TransformationalImageHandler { // JPEG decoder hint to reduce memory, available since IM 6.5.6-2 $decoderHint = array( '-define', "jpeg:size={$params['physicalDimensions']}" ); } - } elseif ( $params['mimeType'] == 'image/png' || $params['mimeType'] == 'image/webp' ) { + } elseif ( $params['mimeType'] == 'image/png' ) { + $quality = array( '-quality', '95' ); // zlib 9, adaptive filtering + if ( $params['interlace'] ) { + $animation_post = array( '-interlace', 'PNG' ); + } + } elseif ( $params['mimeType'] == 'image/webp' ) { $quality = array( '-quality', '95' ); // zlib 9, adaptive filtering } elseif ( $params['mimeType'] == 'image/gif' ) { if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) { @@ -108,6 +154,11 @@ class BitmapHandler extends TransformationalImageHandler { $animation_post = array( '-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 @@ -191,7 +242,8 @@ class BitmapHandler extends TransformationalImageHandler { * @return MediaTransformError Error object if error occurred, false (=no error) otherwise */ protected function transformImageMagickExt( $image, $params ) { - global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea; + global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea, + $wgMaxInterlacingAreas; try { $im = new Imagick(); @@ -209,8 +261,14 @@ class BitmapHandler extends TransformationalImageHandler { } $qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null; $im->setCompressionQuality( $qualityVal ?: 80 ); + if ( $params['interlace'] ) { + $im->setInterlaceScheme( Imagick::INTERLACE_JPEG ); + } } 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 @@ -220,6 +278,13 @@ class BitmapHandler extends TransformationalImageHandler { // Coalesce is needed to scale animated GIFs properly (bug 1017). $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 ); diff --git a/includes/media/TransformationalImageHandler.php b/includes/media/TransformationalImageHandler.php index f72df19bd3..597ac26577 100644 --- a/includes/media/TransformationalImageHandler.php +++ b/includes/media/TransformationalImageHandler.php @@ -126,6 +126,7 @@ abstract class TransformationalImageHandler extends ImageHandler { 'mimeType' => $image->getMimeType(), 'dstPath' => $dstPath, 'dstUrl' => $dstUrl, + 'interlace' => isset( $params['interlace'] ) ? $params['interlace'] : false, ); if ( isset( $params['quality'] ) && $params['quality'] === 'low' ) { diff --git a/tests/phpunit/includes/media/BitmapScalingTest.php b/tests/phpunit/includes/media/BitmapScalingTest.php index e4415ece3b..d355e17b1a 100644 --- a/tests/phpunit/includes/media/BitmapScalingTest.php +++ b/tests/phpunit/includes/media/BitmapScalingTest.php @@ -34,7 +34,7 @@ class BitmapScalingTest extends MediaWikiTestCase { array( 'width' => 512, 'height' => 384, 'physicalWidth' => 512, 'physicalHeight' => 384, - 'page' => 1, + 'page' => 1, 'interlace' => false, ), array( 'width' => 512 ), 'Resizing with width set', @@ -44,7 +44,7 @@ class BitmapScalingTest extends MediaWikiTestCase { array( 'width' => 512, 'height' => 384, 'physicalWidth' => 512, 'physicalHeight' => 384, - 'page' => 1, + 'page' => 1, 'interlace' => false, ), array( 'width' => 512, 'height' => 768 ), 'Resizing with height set too high', @@ -54,7 +54,7 @@ class BitmapScalingTest extends MediaWikiTestCase { array( 'width' => 512, 'height' => 384, 'physicalWidth' => 512, 'physicalHeight' => 384, - 'page' => 1, + 'page' => 1, 'interlace' => false, ), array( 'width' => 1024, 'height' => 384 ), 'Resizing with height set', @@ -66,7 +66,7 @@ class BitmapScalingTest extends MediaWikiTestCase { array( 'width' => 5, 'height' => 1, 'physicalWidth' => 5, 'physicalHeight' => 1, - 'page' => 1, + 'page' => 1, 'interlace' => false, ), array( 'width' => 5 ), 'Very wide image', @@ -77,7 +77,7 @@ class BitmapScalingTest extends MediaWikiTestCase { array( 'width' => 1, 'height' => 10, 'physicalWidth' => 1, 'physicalHeight' => 10, - 'page' => 1, + 'page' => 1, 'interlace' => false, ), array( 'width' => 1 ), 'Very high image', @@ -87,7 +87,7 @@ class BitmapScalingTest extends MediaWikiTestCase { array( 'width' => 1, 'height' => 5, 'physicalWidth' => 1, 'physicalHeight' => 10, - 'page' => 1, + 'page' => 1, 'interlace' => false, ), array( 'width' => 10, 'height' => 5 ), 'Very high image with height set', @@ -98,11 +98,22 @@ class BitmapScalingTest extends MediaWikiTestCase { array( 'width' => 5000, 'height' => 5000, 'physicalWidth' => 4000, 'physicalHeight' => 4000, - 'page' => 1, + 'page' => 1, 'interlace' => false, ), array( 'width' => 5000 ), 'Bigger than max image size but doesn\'t need scaling', ), + /* Max interlace image area */ + array( + array( 4000, 4000 ), + array( + 'width' => 5000, 'height' => 5000, + 'physicalWidth' => 4000, 'physicalHeight' => 4000, + 'page' => 1, 'interlace' => false, + ), + array( 'width' => 5000, 'interlace' => true ), + 'Interlace bigger than max interlace area', + ), ); } -- 2.20.1