Add support for image interlacing of Bitmap type images
authorAndrew H <crazy4sb@gmail.com>
Thu, 14 Jan 2016 02:13:11 +0000 (02:13 +0000)
committerAndrew H <crazy4sb@gmail.com>
Sat, 16 Jan 2016 05:48:27 +0000 (05:48 +0000)
Add 'interlace' parameter and $wgMaxInterlacingSizes global.

Bug: T120032
Change-Id: I40dee74060026513f1c2be8c22dfe41a0b4a18df

includes/DefaultSettings.php
includes/media/Bitmap.php
includes/media/TransformationalImageHandler.php
tests/phpunit/includes/media/BitmapScalingTest.php

index 684e392..f831cfa 100644 (file)
@@ -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
  */
index faf40b3..9ac5e6b 100644 (file)
@@ -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 );
index f72df19..597ac26 100644 (file)
@@ -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' ) {
index e4415ec..d355e17 100644 (file)
@@ -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',
+                       ),
                );
        }