From: Gilles Dubuc Date: Mon, 15 Jun 2015 16:23:46 +0000 (+0200) Subject: TinyRGB support for JPG thumbnails X-Git-Tag: 1.31.0-rc.0~11022 X-Git-Url: http://git.cyclocoop.org/%24action?a=commitdiff_plain;h=584a23931854e256eb219483cf5cba8ecc411ee8;p=lhc%2Fweb%2Fwiklou.git TinyRGB support for JPG thumbnails TinyRGB is an ICC profile released by Facebook under CC0. It is designed to be fully compatible with sRGB. It offers the vast advantages of being much smaller than sRGB, as well as being free as in freedom (the sRGB profile found in the majority of JPGs is copyrighted). This change aims to provide the ability to swap sRGB for TinyRGB at the time thumbnails are generated. JPGs that use another ICC profile than sRGB or no profile at all are unaffected. Bug: T100976 Change-Id: I2ae35ddad4e8a82db8b9541974367dc76c884e7a --- diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 39f739da56..0aba961fb9 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -970,6 +970,14 @@ $wgJpegTran = '/usr/bin/jpegtran'; */ $wgExiv2Command = '/usr/bin/exiv2'; + +/** + * Path to exiftool binary. Used for lossless ICC profile swapping. + * + * @since 1.26 + */ +$wgExiftool = '/usr/bin/exiftool'; + /** * Scalable Vector Graphics (SVG) may be uploaded as images. * Since SVG support is not yet standard in browsers, it is @@ -1322,6 +1330,14 @@ $wgUploadThumbnailRenderHttpCustomHost = false; */ $wgUploadThumbnailRenderHttpCustomDomain = false; +/** + * When this variable is true and JPGs use the sRGB ICC profile, swaps it for the more lightweight + * (and free) TinyRGB profile when generating thumbnails. + * + * @since 1.26 + */ +$wgUseTinyRGBForJPGThumbnails = false; + /** * Default parameters for the "" tag */ diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php index 09d3807815..5af7fbe1ae 100644 --- a/includes/media/Bitmap.php +++ b/includes/media/Bitmap.php @@ -70,7 +70,8 @@ class BitmapHandler extends TransformationalImageHandler { protected function transformImageMagick( $image, $params ) { # use ImageMagick global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea, - $wgImageMagickTempDir, $wgImageMagickConvertCommand; + $wgImageMagickTempDir, $wgImageMagickConvertCommand, $wgResourceBasePath, + $wgUseTinyRGBForJPGThumbnails; $quality = array(); $sharpen = array(); diff --git a/includes/media/ExifBitmap.php b/includes/media/ExifBitmap.php index bf6f7fcebf..5ba5c68c00 100644 --- a/includes/media/ExifBitmap.php +++ b/includes/media/ExifBitmap.php @@ -30,6 +30,7 @@ class ExifBitmapHandler extends BitmapHandler { const BROKEN_FILE = '-1'; // error extracting metadata const OLD_BROKEN_FILE = '0'; // outdated error extracting metadata. + const SRGB_ICC_PROFILE_NAME = 'IEC 61966-2.1 Default RGB colour space - sRGB'; function convertMetadataVersion( $metadata, $version = 1 ) { // basically flattens arrays. @@ -243,4 +244,73 @@ class ExifBitmapHandler extends BitmapHandler { return 0; } + + protected function transformImageMagick( $image, $params ) { + global $wgUseTinyRGBForJPGThumbnails; + + $ret = parent::transformImageMagick( $image, $params ); + + if ( $ret ) { + return $ret; + } + + if ( $params['mimeType'] === 'image/jpeg' && $wgUseTinyRGBForJPGThumbnails ) { + // T100976 If the profile embedded in the JPG is sRGB, swap it for the smaller + // (and free) TinyRGB + + $this->swapICCProfile( + $params['dstPath'], + self::SRGB_ICC_PROFILE_NAME, + realpath( __DIR__ ) . '/tinyrgb.icc' + ); + } + + return false; + } + + /** + * Swaps an embedded ICC profile for another, if found. Depends on exiftool, no-op if not installed. + * @param string $filepath File to be manipulated (will be overwritten) + * @param string $oldProfileString Exact name of color profile to look for (the one that will be replaced) + * @param string $profileFilepath ICC profile file to apply to the file + * @since 1.26 + * @return bool + */ + public function swapICCProfile( $filepath, $oldProfileString, $profileFilepath ) { + global $wgExiftool; + + if ( !$wgExiftool || !is_executable( $wgExiftool ) ) { + return false; + } + + $cmd = wfEscapeShellArg( $wgExiftool, + '-DeviceModelDesc', + '-S', + '-T', + $filepath + ); + + $output = wfShellExecWithStderr( $cmd, $retval ); + + if ( $retval !== 0 || strcasecmp( trim( $output ), $oldProfileString ) !== 0 ) { + // We can't establish that this file has the expected ICC profile, don't process it + return false; + } + + $cmd = wfEscapeShellArg( $wgExiftool, + '-overwrite_original', + '-icc_profile<=' . $profileFilepath, + $filepath + ); + + $output = wfShellExecWithStderr( $cmd, $retval ); + + if ( $retval !== 0 ) { + $this->logErrorForExternalProcess( $retval, $output, $cmd ); + + return false; + } + + return true; + } } diff --git a/includes/media/Jpeg.php b/includes/media/Jpeg.php index 5463922b14..040ff96e1c 100644 --- a/includes/media/Jpeg.php +++ b/includes/media/Jpeg.php @@ -137,7 +137,7 @@ class JpegHandler extends ExifBitmapHandler { $rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360; - if ( $wgJpegTran && is_file( $wgJpegTran ) ) { + if ( $wgJpegTran && is_executable( $wgJpegTran ) ) { $cmd = wfEscapeShellArg( $wgJpegTran ) . " -rotate " . wfEscapeShellArg( $rotation ) . " -outfile " . wfEscapeShellArg( $params['dstPath'] ) . diff --git a/includes/media/tinyrgb.icc b/includes/media/tinyrgb.icc new file mode 100644 index 0000000000..eab973f57d Binary files /dev/null and b/includes/media/tinyrgb.icc differ diff --git a/maintenance/dictionary/mediawiki.dic b/maintenance/dictionary/mediawiki.dic index ad19a1ced8..f19c64fa0d 100644 --- a/maintenance/dictionary/mediawiki.dic +++ b/maintenance/dictionary/mediawiki.dic @@ -1434,6 +1434,7 @@ excludepage excludeuser executables exempt +exiftool existingwiki exists exiv diff --git a/tests/phpunit/data/media/srgb.jpg b/tests/phpunit/data/media/srgb.jpg new file mode 100644 index 0000000000..b965dc4f6d Binary files /dev/null and b/tests/phpunit/data/media/srgb.jpg differ diff --git a/tests/phpunit/data/media/tinyrgb.icc b/tests/phpunit/data/media/tinyrgb.icc new file mode 100644 index 0000000000..eab973f57d Binary files /dev/null and b/tests/phpunit/data/media/tinyrgb.icc differ diff --git a/tests/phpunit/data/media/tinyrgb.jpg b/tests/phpunit/data/media/tinyrgb.jpg new file mode 100644 index 0000000000..12a8e09f90 Binary files /dev/null and b/tests/phpunit/data/media/tinyrgb.jpg differ diff --git a/tests/phpunit/includes/media/ExifBitmapTest.php b/tests/phpunit/includes/media/ExifBitmapTest.php index 41330f4182..adbc97757c 100644 --- a/tests/phpunit/includes/media/ExifBitmapTest.php +++ b/tests/phpunit/includes/media/ExifBitmapTest.php @@ -3,7 +3,7 @@ /** * @group Media */ -class ExifBitmapTest extends MediaWikiTestCase { +class ExifBitmapTest extends MediaWikiMediaTestCase { /** * @var ExifBitmapHandler @@ -143,4 +143,41 @@ class ExifBitmapTest extends MediaWikiTestCase { $res = $this->handler->convertMetadataVersion( $metadata, 1 ); $this->assertEquals( $expected, $res ); } + + /** + * @dataProvider provideSwappingICCProfile + * @covers BitmapHandler::swapICCProfile + */ + public function testSwappingICCProfile( $sourceFilename, $controlFilename, $newProfileFilename, $oldProfileName ) { + global $wgExiftool; + + if ( !$wgExiftool || !is_file( $wgExiftool ) ) { + $this->markTestSkipped( "Exiftool not installed, cannot test ICC profile swapping" ); + } + + $this->setMwGlobals( 'wgUseTinyRGBForJPGThumbnails', true ); + + $sourceFilepath = $this->filePath . $sourceFilename; + $controlFilepath = $this->filePath . $controlFilename; + $profileFilepath = $this->filePath . $newProfileFilename; + $filepath = $this->getNewTempFile(); + + copy( $sourceFilepath, $filepath ); + + $file = $this->dataFile( $sourceFilename, 'image/jpeg' ); + $this->handler->swapICCProfile( $filepath, $oldProfileName, $profileFilepath ); + + $this->assertEquals( sha1( file_get_contents( $filepath ) ), sha1( file_get_contents( $controlFilepath ) ) ); + } + + public function provideSwappingICCProfile() { + return array( + // File with sRGB should end up with TinyRGB + array( 'srgb.jpg', 'tinyrgb.jpg', 'tinyrgb.icc', 'IEC 61966-2.1 Default RGB colour space - sRGB' ), + // File with TinyRGB should be left unchanged + array( 'tinyrgb.jpg', 'tinyrgb.jpg', 'tinyrgb.icc', 'IEC 61966-2.1 Default RGB colour space - sRGB' ), + // File with no profile should be left unchanged + array( 'test.jpg', 'test.jpg', 'tinyrgb.icc', 'IEC 61966-2.1 Default RGB colour space - sRGB' ) + ); + } }