TinyRGB support for JPG thumbnails
authorGilles Dubuc <gdubuc@wikimedia.org>
Mon, 15 Jun 2015 16:23:46 +0000 (18:23 +0200)
committerOri.livneh <ori@wikimedia.org>
Mon, 22 Jun 2015 16:17:54 +0000 (16:17 +0000)
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

includes/DefaultSettings.php
includes/media/Bitmap.php
includes/media/ExifBitmap.php
includes/media/Jpeg.php
includes/media/tinyrgb.icc [new file with mode: 0644]
maintenance/dictionary/mediawiki.dic
tests/phpunit/data/media/srgb.jpg [new file with mode: 0644]
tests/phpunit/data/media/tinyrgb.icc [new file with mode: 0644]
tests/phpunit/data/media/tinyrgb.jpg [new file with mode: 0644]
tests/phpunit/includes/media/ExifBitmapTest.php

index 39f739d..0aba961 100644 (file)
@@ -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 "<gallery>" tag
  */
index 09d3807..5af7fbe 100644 (file)
@@ -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();
index bf6f7fc..5ba5c68 100644 (file)
@@ -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;
+       }
 }
index 5463922..040ff96 100644 (file)
@@ -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 (file)
index 0000000..eab973f
Binary files /dev/null and b/includes/media/tinyrgb.icc differ
index ad19a1c..f19c64f 100644 (file)
@@ -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 (file)
index 0000000..b965dc4
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 (file)
index 0000000..eab973f
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 (file)
index 0000000..12a8e09
Binary files /dev/null and b/tests/phpunit/data/media/tinyrgb.jpg differ
index 41330f4..adbc977 100644 (file)
@@ -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' )
+               );
+       }
 }