From: jenkins-bot Date: Thu, 28 Apr 2016 00:02:47 +0000 (+0000) Subject: Merge "Enable 4:2:0 chroma subsampling for JPEG thumbnails" X-Git-Tag: 1.31.0-rc.0~7153 X-Git-Url: http://git.cyclocoop.org/%7B%24www_url%7Dadmin/password.php?a=commitdiff_plain;h=3114b22f29683855ac133d55b085cb12286870d6;hp=cfea4137e91d204c8158e9368d7ce5c339f7f354;p=lhc%2Fweb%2Fwiklou.git Merge "Enable 4:2:0 chroma subsampling for JPEG thumbnails" --- diff --git a/RELEASE-NOTES-1.27 b/RELEASE-NOTES-1.27 index be15bde472..9b77cd1bf4 100644 --- a/RELEASE-NOTES-1.27 +++ b/RELEASE-NOTES-1.27 @@ -190,6 +190,9 @@ HHVM 3.1. Additionally, the following PHP extensions are required: is deprecated. * (T33313) Add a preference for watching uploads by default, also applies to API-based upload tools. +* $wgJpegPixelFormat was added to override chroma subsampling for JPEG image + thumbnails created via ImageMagick. Defaults to 'yuv420', providing bandwidth + savings versus the previous behavior on many files. === External library changes in 1.27 === diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 13f7c4ef88..5b3684b336 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -980,6 +980,27 @@ $wgCustomConvertCommand = false; */ $wgJpegTran = '/usr/bin/jpegtran'; +/** + * At default setting of 'yuv420', JPEG thumbnails will use 4:2:0 chroma + * subsampling to reduce file size, at the cost of possible color fringing + * at sharp edges. + * + * See https://en.wikipedia.org/wiki/Chroma_subsampling + * + * Supported values: + * false - use scaling system's default (same as pre-1.27 behavior) + * 'yuv444' - luma and chroma at same resolution + * 'yuv422' - chroma at 1/2 resolution horizontally, full vertically + * 'yuv420' - chroma at 1/2 resolution in both dimensions + * + * This setting is currently supported only for the ImageMagick backend; + * others may default to 4:2:0 or 4:4:4 or maintaining the source file's + * sampling in the thumbnail. + * + * @since 1.27 + */ +$wgJpegPixelFormat = 'yuv420'; + /** * Some tests and extensions use exiv2 to manipulate the Exif metadata in some * image formats. diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php index 4da41c8de4..ccd345c31f 100644 --- a/includes/media/Bitmap.php +++ b/includes/media/Bitmap.php @@ -104,6 +104,25 @@ class BitmapHandler extends TransformationalImageHandler { return true; } + /** + * Get ImageMagick subsampling factors for the target JPEG pixel format. + * + * @param string $pixelFormat one of 'yuv444', 'yuv422', 'yuv420' + * @return array of string keys + */ + 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 * @@ -115,7 +134,7 @@ class BitmapHandler extends TransformationalImageHandler { protected function transformImageMagick( $image, $params ) { # use ImageMagick global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea, - $wgImageMagickTempDir, $wgImageMagickConvertCommand; + $wgImageMagickTempDir, $wgImageMagickConvertCommand, $wgJpegPixelFormat; $quality = []; $sharpen = []; @@ -123,6 +142,7 @@ class BitmapHandler extends TransformationalImageHandler { $animation_pre = []; $animation_post = []; $decoderHint = []; + $subsampling = []; if ( $params['mimeType'] == 'image/jpeg' ) { $qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null; @@ -141,6 +161,10 @@ class BitmapHandler extends TransformationalImageHandler { // 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 ) ]; + } } elseif ( $params['mimeType'] == 'image/png' ) { $quality = [ '-quality', '95' ]; // zlib 9, adaptive filtering if ( $params['interlace'] ) { @@ -225,6 +249,7 @@ class BitmapHandler extends TransformationalImageHandler { [ '-depth', 8 ], $sharpen, [ '-rotate', "-$rotation" ], + $subsampling, $animation_post, [ $this->escapeMagickOutput( $params['dstPath'] ) ] ) ); @@ -251,7 +276,7 @@ class BitmapHandler extends TransformationalImageHandler { */ protected function transformImageMagickExt( $image, $params ) { global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea, - $wgMaxInterlacingAreas; + $wgMaxInterlacingAreas, $wgJpegPixelFormat; try { $im = new Imagick(); @@ -272,6 +297,10 @@ class BitmapHandler extends TransformationalImageHandler { 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'] ) { diff --git a/tests/phpunit/data/media/yuv420.jpg b/tests/phpunit/data/media/yuv420.jpg new file mode 100644 index 0000000000..e741ca6549 Binary files /dev/null and b/tests/phpunit/data/media/yuv420.jpg differ diff --git a/tests/phpunit/data/media/yuv444.jpg b/tests/phpunit/data/media/yuv444.jpg new file mode 100644 index 0000000000..6bccefa7be Binary files /dev/null and b/tests/phpunit/data/media/yuv444.jpg differ diff --git a/tests/phpunit/includes/media/JpegPixelFormatTest.php b/tests/phpunit/includes/media/JpegPixelFormatTest.php new file mode 100644 index 0000000000..6815a62bd9 --- /dev/null +++ b/tests/phpunit/includes/media/JpegPixelFormatTest.php @@ -0,0 +1,115 @@ +markTestSkipped( "This test is only applicable when using ImageMagick thumbnailing" ); + } + if ( !$wgUseImageResize ) { + $this->markTestSkipped( "This test is only applicable when using thumbnailing" ); + } + + $fmtStr = var_export( $pixelFormat, true ); + $this->setMwGlobals( 'wgJpegPixelFormat', $pixelFormat ); + + $file = $this->dataFile( $sourceFile, 'image/jpeg' ); + + $params = [ + 'width' => 320, + ]; + $thumb = $file->transform( $params, File::RENDER_NOW | File::RENDER_FORCE ); + $this->assertTrue( !$thumb->isError(), "created JPEG thumbnail for pixel format $fmtStr" ); + + $path = $thumb->getLocalCopyPath(); + $this->assertTrue( is_string( $path ), "path returned for JPEG thumbnail for $fmtStr" ); + + $cmd = [ + 'identify', + '-format', + '%[jpeg:sampling-factor]', + $path + ]; + $retval = null; + $output = wfShellExec( $cmd, $retval ); + $this->assertTrue( $retval === 0, "ImageMagick's identify command should return success" ); + + $expected = $samplingFactor; + $actual = trim( $output ); + $this->assertEquals( + $expected, + trim( $output ), + "IM identify expects JPEG chroma subsampling \"$expected\" for $fmtStr" + ); + } + + public static function providePixelFormats() { + return [ + // From 4:4:4 source file + [ + 'yuv444.jpg', + false, + '1x1,1x1,1x1' + ], + [ + 'yuv444.jpg', + 'yuv444', + '1x1,1x1,1x1' + ], + [ + 'yuv444.jpg', + 'yuv422', + '2x1,1x1,1x1' + ], + [ + 'yuv444.jpg', + 'yuv420', + '2x2,1x1,1x1' + ], + // From 4:2:0 source file + [ + 'yuv420.jpg', + false, + '2x2,1x1,1x1' + ], + [ + 'yuv420.jpg', + 'yuv444', + '1x1,1x1,1x1' + ], + [ + 'yuv420.jpg', + 'yuv422', + '2x1,1x1,1x1' + ], + [ + 'yuv420.jpg', + 'yuv420', + '2x2,1x1,1x1' + ] + ]; + } +}