From: btongminh Date: Sun, 17 Nov 2013 16:43:27 +0000 (+0100) Subject: Basic support for WebP X-Git-Tag: 1.31.0-rc.0~10964 X-Git-Url: http://git.cyclocoop.org/%7B%24www_url%7Dadmin/compta/operations/?a=commitdiff_plain;h=9c8f333eb87f128933dd754f4c7e4b26fa1cf514;p=lhc%2Fweb%2Fwiklou.git Basic support for WebP Adds basic image size detection for WebP and support in the MediaHandler. Currently renders WebP files as PNGs, because that handles transparency. Bug: T50519 Change-Id: I3c00653a8a034efc3f6b60fe62b7ac2e5391f921 --- diff --git a/.gitattributes b/.gitattributes index 69d7b1baab..09f86a3280 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ *.sh eol=lf *.icc binary +*.webp binary diff --git a/RELEASE-NOTES-1.26 b/RELEASE-NOTES-1.26 index c2228ae81d..983601e840 100644 --- a/RELEASE-NOTES-1.26 +++ b/RELEASE-NOTES-1.26 @@ -30,6 +30,10 @@ production. * (T68699) The expiration of the UserID and Token login cookies ($wgExtendedLoginCookieExpiration) can be configured independently of the expiration of all other cookies ($wgCookieExpiration). +* (bug 50519) Support for generating JPEG/PNG thumbnails from WebP images added + if ImageMagick is used as image scaler ($wgUseImageMagick = true). Uploading + of WebP images still disabled by default. Add $wgFileExtensions[] = + 'webp'; to LocalSettings.php to enable uploading of WebP images. ==== External libraries ==== * Update es5-shim from v4.0.0 to v4.1.5. diff --git a/autoload.php b/autoload.php index 504eaf22e2..917b022ea5 100644 --- a/autoload.php +++ b/autoload.php @@ -1040,6 +1040,7 @@ $wgAutoloadLocalClasses = array( 'RevisionList' => __DIR__ . '/includes/RevisionList.php', 'RevisionListBase' => __DIR__ . '/includes/RevisionList.php', 'RevisiondeleteAction' => __DIR__ . '/includes/actions/RevisiondeleteAction.php', + 'RiffExtractor' => __DIR__ . '/includes/libs/RiffExtractor.php', 'RightsLogFormatter' => __DIR__ . '/includes/logging/RightsLogFormatter.php', 'RollbackAction' => __DIR__ . '/includes/actions/RollbackAction.php', 'RollbackEdits' => __DIR__ . '/maintenance/rollbackEdits.php', @@ -1342,6 +1343,7 @@ $wgAutoloadLocalClasses = array( 'WebInstallerUpgrade' => __DIR__ . '/includes/installer/WebInstallerPage.php', 'WebInstallerUpgradeDoc' => __DIR__ . '/includes/installer/WebInstallerPage.php', 'WebInstallerWelcome' => __DIR__ . '/includes/installer/WebInstallerPage.php', + 'WebPHandler' => __DIR__ . '/includes/media/WebP.php', 'WebRequest' => __DIR__ . '/includes/WebRequest.php', 'WebRequestUpload' => __DIR__ . '/includes/WebRequest.php', 'WebResponse' => __DIR__ . '/includes/WebResponse.php', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 0e6ff165cc..c0fd345afe 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -884,6 +884,7 @@ $wgMediaHandlers = array( 'image/png' => 'PNGHandler', 'image/gif' => 'GIFHandler', 'image/tiff' => 'TiffHandler', + 'image/webp' => 'WebPHandler', 'image/x-ms-bmp' => 'BmpHandler', 'image/x-bmp' => 'BmpHandler', 'image/x-xcf' => 'XCFHandler', diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php index 3b065255a4..2b240c3b02 100644 --- a/includes/MimeMagic.php +++ b/includes/MimeMagic.php @@ -695,7 +695,7 @@ class MimeMagic { } /* Look for WebP */ - if ( strncmp( $head, "RIFF", 4 ) == 0 && strncmp( substr( $head, 8, 8 ), "WEBPVP8 ", 8 ) == 0 ) { + if ( strncmp( $head, "RIFF", 4 ) == 0 && strncmp( substr( $head, 8, 7 ), "WEBPVP8", 7 ) == 0 ) { wfDebug( __METHOD__ . ": recognized file as image/webp\n" ); return "image/webp"; } diff --git a/includes/libs/RiffExtractor.php b/includes/libs/RiffExtractor.php new file mode 100644 index 0000000000..f987c59d21 --- /dev/null +++ b/includes/libs/RiffExtractor.php @@ -0,0 +1,100 @@ + self::extractUInt32( $fileSize ), + 'fourCC' => $fourCC, + 'chunks' => array(), + ); + $numberOfChunks = 0; + + // Find out the chunks + while ( !feof( $file ) && !( $numberOfChunks >= $maxChunks && $maxChunks >= 0 ) ) { + $chunkStart = ftell( $file ); + + $chunkFourCC = fread( $file, 4 ); + if ( !$chunkFourCC || strlen( $chunkFourCC ) != 4 ) { + return $info; + } + + $chunkSize = fread( $file, 4 ); + if ( !$chunkSize || strlen( $chunkSize ) != 4 ) { + return $info; + } + $intChunkSize = self::extractUInt32( $chunkSize ); + + // Add chunk info to the info structure + $info['chunks'][] = array( + 'fourCC' => $chunkFourCC, + 'start' => $chunkStart, + 'size' => $intChunkSize + ); + + // Uneven chunks have padding bytes + $padding = $intChunkSize % 2; + // Seek to the next chunk + fseek( $file, $intChunkSize + $padding, SEEK_CUR ); + + } + + return $info; + } + + /** + * Extract a little-endian uint32 from a 4 byte string + * @param string $string 4-byte string + * @return int + */ + public static function extractUInt32( $string ) { + $unpacked = unpack( 'V', $string ); + return $unpacked[1]; + } +}; diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php index 5af7fbe1ae..4be20b243c 100644 --- a/includes/media/Bitmap.php +++ b/includes/media/Bitmap.php @@ -93,9 +93,8 @@ 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' ) { + } elseif ( $params['mimeType'] == 'image/png' || $params['mimeType'] == 'image/webp' ) { $quality = array( '-quality', '95' ); // zlib 9, adaptive filtering - } elseif ( $params['mimeType'] == 'image/gif' ) { if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) { // Extract initial frame only; we're so big it'll diff --git a/includes/media/WebP.php b/includes/media/WebP.php new file mode 100644 index 0000000000..704db41cfa --- /dev/null +++ b/includes/media/WebP.php @@ -0,0 +1,306 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Media + */ + +/** + * Handler for Google's WebP format + * + * @ingroup Media + */ +class WebPHandler extends BitmapHandler { + const BROKEN_FILE = '0'; // value to store in img_metadata if error extracting metadata. + /** + * @var int Minimum chunk header size to be able to read all header types + */ + const MINIMUM_CHUNK_HEADER_LENGTH = 18; + /** + * @var int version of the metadata stored in db records + */ + const _MW_WEBP_VERSION = 1; + + const VP8X_ICC = 32; + const VP8X_ALPHA = 16; + const VP8X_EXIF = 8; + const VP8X_XMP = 4; + const VP8X_ANIM = 2; + + function getMetadata( $image, $filename ) { + $parsedWebPData = self::extractMetadata( $filename ); + if ( !$parsedWebPData ) { + return self::BROKEN_FILE; + } + + $parsedWebPData['metadata']['_MW_WEBP_VERSION'] = self::_MW_WEBP_VERSION; + return serialize( $parsedWebPData ); + } + + function getMetadataType( $image ) { + return 'parsed-webp'; + } + + function isMetadataValid( $image, $metadata ) { + if ( $metadata === self::BROKEN_FILE ) { + // Do not repetitivly regenerate metadata on broken file. + return self::METADATA_GOOD; + } + + wfSuppressWarnings(); + $data = unserialize( $metadata ); + wfRestoreWarnings(); + + if ( !$data || !is_array( $data ) ) { + wfDebug( __METHOD__ . " invalid WebP metadata\n" ); + + return self::METADATA_BAD; + } + + if ( !isset( $data['metadata']['_MW_WEBP_VERSION'] ) + || $data['metadata']['_MW_WEBP_VERSION'] != self::_MW_WEBP_VERSION + ) { + wfDebug( __METHOD__ . " old but compatible WebP metadata\n" ); + + return self::METADATA_COMPATIBLE; + } + return self::METADATA_GOOD; + } + + /** + * Extracts the image size and WebP type from a file + * + * @param string $chunks Chunks as extracted by RiffExtractor + * @return array|bool Header data array with entries 'compression', 'width' and 'height', + * where 'compression' can be 'lossy', 'lossless', 'animated' or 'unknown'. False if + * file is not a valid WebP file. + */ + public static function extractMetadata( $filename ) { + wfDebugLog( 'WebP', __METHOD__ . ": Extracting metadata from $filename\n" ); + + $info = RiffExtractor::findChunksFromFile( $filename, 100 ); + if ( $info === false ) { + wfDebugLog( 'WebP', __METHOD__ . ": Not a valid RIFF file\n" ); + return false; + } + + if ( $info['fourCC'] != 'WEBP' ) { + wfDebugLog( 'WebP', __METHOD__ . ': FourCC was not WEBP: ' . + bin2hex( $info['fourCC'] ) . " \n" ); + return false; + } + + $metadata = self::extractMetadataFromChunks( $info['chunks'], $filename ); + if ( !$metadata ) { + wfDebugLog( 'WebP', __METHOD__ . ": No VP8 chunks found\n" ); + return false; + } + + return $metadata; + } + + /** + * Extracts the image size and WebP type from a file based on the chunk list + * @param array $chunks Chunks as extracted by RiffExtractor + * @return array Header data array with entries 'compression', 'width' and 'height', where + * 'compression' can be 'lossy', 'lossless', 'animated' or 'unknown' + */ + public static function extractMetadataFromChunks( $chunks, $filename ) { + $vp8Info = array(); + + foreach ( $chunks as $chunk ) { + if ( !in_array( $chunk['fourCC'], array( 'VP8 ', 'VP8L', 'VP8X' ) ) ) { + // Not a chunk containing interesting metadata + continue; + } + + $chunkHeader = file_get_contents( $filename, false, null, + $chunk['start'], self::MINIMUM_CHUNK_HEADER_LENGTH ); + wfDebugLog( 'WebP', __METHOD__ . ": {$chunk['fourCC']}\n" ); + + switch ( $chunk['fourCC'] ) { + case 'VP8 ': + return array_merge( $vp8Info, + self::decodeLossyChunkHeader( $chunkHeader ) ); + case 'VP8L': + return array_merge( $vp8Info, + self::decodeLosslessChunkHeader( $chunkHeader ) ); + case 'VP8X': + $vp8Info = array_merge( $vp8Info, + self::decodeExtendedChunkHeader( $chunkHeader ) ); + // Continue looking for other chunks to improve the metadata + break; + } + } + return $vp8Info; + } + + /** + * Decodes a lossy chunk header + * @param string $header Header string + * @return boolean|array See WebPHandler::decodeHeader + */ + protected static function decodeLossyChunkHeader( $header ) { + // Bytes 0-3 are 'VP8 ' + // Bytes 4-7 are the VP8 stream size + // Bytes 8-10 are the frame tag + // Bytes 11-13 are 0x9D 0x01 0x2A called the sync code + $syncCode = substr( $header, 11, 3 ); + if ( $syncCode != "\x9D\x01\x2A" ) { + wfDebugLog( 'WebP', __METHOD__ . ': Invalid sync code: ' . + bin2hex( $syncCode ) . "\n" ); + return array(); + } + // Bytes 14-17 are image size + $imageSize = unpack( 'v2', substr( $header, 14, 4 ) ); + // Image sizes are 14 bit, 2 MSB are scaling parameters which are ignored here + return array( + 'compression' => 'lossy', + 'width' => $imageSize[1] & 0x3FFF, + 'height' => $imageSize[2] & 0x3FFF + ); + } + + /** + * Decodes a lossless chunk header + * @param string $header Header string + * @return boolean|array See WebPHandler::decodeHeader + */ + public static function decodeLosslessChunkHeader( $header ) { + // Bytes 0-3 are 'VP8L' + // Bytes 4-7 are chunk stream size + // Byte 8 is 0x2F called the signature + if ( $header{8} != "\x2F" ) { + wfDebugLog( 'WebP', __METHOD__ . ': Invalid signature: ' . + bin2hex( $header{8} ) . "\n" ); + return array(); + } + // Bytes 9-12 contain the image size + // Bits 0-13 are width-1; bits 15-27 are height-1 + $imageSize = unpack( 'C4', substr( $header, 9, 4 ) ); + return array( + 'compression' => 'lossless', + 'width' => ( $imageSize[1] | ( ( $imageSize[2] & 0x3F ) << 8 ) ) + 1, + 'height' => ( ( ( $imageSize[2] & 0xC0 ) >> 6 ) | + ( $imageSize[3] << 2 ) | ( ( $imageSize[4] & 0x03 ) << 10 ) ) + 1 + ); + } + + /** + * Decodes an extended chunk header + * @param string $header Header string + * @return boolean|array See WebPHandler::decodeHeader + */ + public static function decodeExtendedChunkHeader( $header ) { + // Bytes 0-3 are 'VP8X' + // Byte 4-7 are chunk length + // Byte 8-11 are a flag bytes + $flags = unpack( 'c', substr( $header, 8, 1 ) ); + + // Byte 12-17 are image size (24 bits) + $width = unpack( 'V', substr( $header, 12, 3 ) . "\x00" ); + $height = unpack( 'V', substr( $header, 15, 3 ) . "\x00" ); + + return array( + 'compression' => 'unknown', + 'animated' => ($flags[1] & self::VP8X_ANIM) == self::VP8X_ANIM, + 'transparency' => ($flags[1] & self::VP8X_ALPHA) == self::VP8X_ALPHA, + 'width' => ( $width[1] & 0xFFFFFF ) + 1, + 'height' => ( $height[1] & 0xFFFFFF ) + 1 + ); + } + + function getImageSize( $file, $path, $metadata = false ) { + if ( $file === null ) { + $metadata = self::getMetadata( $file, $path ); + } + if ( $metadata === false ) { + $metadata = $file->getMetadata(); + } + + wfSuppressWarnings(); + $metadata = unserialize( $metadata ); + wfRestoreWarnings(); + + if ( $metadata == false ) { + return false; + } + return array( $metadata['width'], $metadata['height'] ); + } + + /** + * @param $file + * @return bool True, not all browsers support WebP + */ + function mustRender( $file ) { + return true; + } + + /** + * @param $file + * @return bool False if we are unable to render this image + */ + function canRender( $file ) { + if ( self::isAnimatedImage( $file ) ) { + return false; + } + return true; + } + + /** + * @param File $image + * @return bool + */ + function isAnimatedImage( $image ) { + $ser = $image->getMetadata(); + if ( $ser ) { + $metadata = unserialize( $ser ); + if ( isset($metadata['animated']) && $metadata['animated'] === true ) { + return true; + } + } + + return false; + } + + function canAnimateThumbnail( $file ) { + return false; + } + + /** + * Render files as PNG + * + * @param $ext + * @param $mime + * @param $params + * @return array + */ + function getThumbType( $ext, $mime, $params = null ) { + return array( 'png', 'image/png' ); + } + + /** + * Must use "im" for XCF + * + * @return string + */ + protected function getScalerType( $dstPath, $checkDstPath = true ) { + return 'im'; + } +} diff --git a/tests/phpunit/data/media/2_webp_a.webp b/tests/phpunit/data/media/2_webp_a.webp new file mode 100644 index 0000000000..8764f066b9 Binary files /dev/null and b/tests/phpunit/data/media/2_webp_a.webp differ diff --git a/tests/phpunit/data/media/2_webp_ll.webp b/tests/phpunit/data/media/2_webp_ll.webp new file mode 100644 index 0000000000..5794bbf27d Binary files /dev/null and b/tests/phpunit/data/media/2_webp_ll.webp differ diff --git a/tests/phpunit/data/media/webp_animated.webp b/tests/phpunit/data/media/webp_animated.webp new file mode 100644 index 0000000000..25c6a4dd6c Binary files /dev/null and b/tests/phpunit/data/media/webp_animated.webp differ diff --git a/tests/phpunit/includes/media/WebPTest.php b/tests/phpunit/includes/media/WebPTest.php new file mode 100644 index 0000000000..d36710a34f --- /dev/null +++ b/tests/phpunit/includes/media/WebPTest.php @@ -0,0 +1,127 @@ +tempFileName = tempnam( wfTempDir(), 'WEBP' ); + } + public function tearDown() { + parent::tearDown(); + unlink( $this->tempFileName ); + } + /** + * @dataProvider provideTestExtractMetaData + */ + public function testExtractMetaData( $header, $expectedResult ) { + // Put header into file + file_put_contents( $this->tempFileName, $header ); + + $this->assertEquals( $expectedResult, WebPHandler::extractMetadata( $this->tempFileName ) ); + } + public function provideTestExtractMetaData() { + return array( + // Files from https://developers.google.com/speed/webp/gallery2 + array( "\x52\x49\x46\x46\x90\x68\x01\x00\x57\x45\x42\x50\x56\x50\x38\x4C\x83\x68\x01\x00\x2F\x8F\x01\x4B\x10\x8D\x38\x6C\xDB\x46\x92\xE0\xE0\x82\x7B\x6C", + array( 'compression' => 'lossless', 'width' => 400, 'height' => 301 ) ), + array( "\x52\x49\x46\x46\x64\x5B\x00\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x8F\x01\x00\x2C\x01\x00\x41\x4C\x50\x48\xE5\x0E", + array( 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 400, 'height' => 301) ), + array( "\x52\x49\x46\x46\xA8\x72\x00\x00\x57\x45\x42\x50\x56\x50\x38\x4C\x9B\x72\x00\x00\x2F\x81\x81\x62\x10\x8D\x40\x8C\x24\x39\x6E\x73\x73\x38\x01\x96", + array( 'compression' => 'lossless', 'width' => 386, 'height' => 395 ) ), + array( "\x52\x49\x46\x46\xE0\x42\x00\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x81\x01\x00\x8A\x01\x00\x41\x4C\x50\x48\x56\x10", + array( 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 386, 'height' => 395 ) ), + array( "\x52\x49\x46\x46\x70\x61\x02\x00\x57\x45\x42\x50\x56\x50\x38\x4C\x63\x61\x02\x00\x2F\x1F\xC3\x95\x10\x8D\xC8\x72\xDB\xC8\x92\x24\xD8\x91\xD9\x91", + array( 'compression' => 'lossless', 'width' => 800, 'height' => 600 ) ), + array( "\x52\x49\x46\x46\x1C\x1D\x01\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x1F\x03\x00\x57\x02\x00\x41\x4C\x50\x48\x25\x8B", + array( 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 800, 'height' => 600 ) ), + array( "\x52\x49\x46\x46\xFA\xC5\x00\x00\x57\x45\x42\x50\x56\x50\x38\x4C\xEE\xC5\x00\x00\x2F\xA4\x81\x28\x10\x8D\x40\x68\x24\xC9\x91\xA4\xAE\xF3\x97\x75", + array( 'compression' => 'lossless', 'width' => 421, 'height' => 163 ) ), + array( "\x52\x49\x46\x46\xF6\x5D\x00\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\xA4\x01\x00\xA2\x00\x00\x41\x4C\x50\x48\x38\x1A", + array( 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 421, 'height' => 163 ) ), + array( "\x52\x49\x46\x46\xC4\x96\x01\x00\x57\x45\x42\x50\x56\x50\x38\x4C\xB8\x96\x01\x00\x2F\x2B\xC1\x4A\x10\x11\x87\x6D\xDB\x48\x12\xFC\x60\xB0\x83\x24", + array( 'compression' => 'lossless', 'width' => 300, 'height' => 300 ) ), + array( "\x52\x49\x46\x46\x0A\x11\x01\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x2B\x01\x00\x2B\x01\x00\x41\x4C\x50\x48\x67\x6E", + array( 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 300, 'height' => 300 ) ), + + // Lossy files from https://developers.google.com/speed/webp/gallery1 + array( "\x52\x49\x46\x46\x68\x76\x00\x00\x57\x45\x42\x50\x56\x50\x38\x20\x5C\x76\x00\x00\xD2\xBE\x01\x9D\x01\x2A\x26\x02\x70\x01\x3E\xD5\x4E\x97\x43\xA2", + array( 'compression' => 'lossy', 'width' => 550, 'height' => 368 ) ), + array( "\x52\x49\x46\x46\xB0\xEC\x00\x00\x57\x45\x42\x50\x56\x50\x38\x20\xA4\xEC\x00\x00\xB2\x4B\x02\x9D\x01\x2A\x26\x02\x94\x01\x3E\xD1\x50\x96\x46\x26", + array( 'compression' => 'lossy', 'width' => 550, 'height' => 404 ) ), + array( "\x52\x49\x46\x46\x7A\x19\x03\x00\x57\x45\x42\x50\x56\x50\x38\x20\x6E\x19\x03\x00\xB2\xF8\x09\x9D\x01\x2A\x00\x05\xD0\x02\x3E\xAD\x46\x99\x4A\xA5", + array( 'compression' => 'lossy', 'width' => 1280, 'height' => 720 ) ), + array( "\x52\x49\x46\x46\x44\xB3\x02\x00\x57\x45\x42\x50\x56\x50\x38\x20\x38\xB3\x02\x00\x52\x57\x06\x9D\x01\x2A\x00\x04\x04\x03\x3E\xA5\x44\x96\x49\x26", + array( 'compression' => 'lossy', 'width' => 1024, 'height' => 772) ), + array( "\x52\x49\x46\x46\x02\x43\x01\x00\x57\x45\x42\x50\x56\x50\x38\x20\xF6\x42\x01\x00\x12\xC0\x05\x9D\x01\x2A\x00\x04\xF0\x02\x3E\x79\x34\x93\x47\xA4", + array( 'compression' => 'lossy', 'width' => 1024, 'height' => 752) ), + + // Animated file from https://groups.google.com/a/chromium.org/d/topic/blink-dev/Y8tRC4mdQz8/discussion + array( "\x52\x49\x46\x46\xD0\x0B\x02\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x12\x00\x00\x00\x3F\x01\x00\x3F\x01\x00\x41\x4E", + array( 'compression' => 'unknown', 'animated' => true, 'transparency' => true, 'width' => 320, 'height' => 320 ) ), + + // Error cases + array( '', false ), + array( ' ', false ), + array( 'RIFF ', false ), + array( 'RIFF1234WEBP ', false ), + array( 'RIFF1234WEBPVP8 ', false ), + array( 'RIFF1234WEBPVP8L ', false ), + ); + } + + /** + * @dataProvider provideTestWithFileExtractMetaData + */ + public function testWithFileExtractMetaData( $filename, $expectedResult ) { + $this->assertEquals( $expectedResult, WebPHandler::extractMetadata( $filename ) ); + } + public function provideTestWithFileExtractMetaData() { + return array( + array( __DIR__ . '/../../data/media/2_webp_ll.webp', + array( 'compression' => 'lossless', 'width' => 386, 'height' => 395 ) ), + array( __DIR__ . '/../../data/media/2_webp_a.webp', + array( 'compression' => 'lossy', 'animated' => false, 'transparency' => true, 'width' => 386, 'height' => 395 ) ), + ); + } + + /** + * @dataProvider provideTestGetImageSize + */ + public function testGetImageSize( $path, $expectedResult ) { + $handler = new WebPHandler(); + $this->assertEquals( $expectedResult, $handler->getImageSize( null, $path ) ); + } + public function provideTestGetImageSize() { + return array( + // Public domain files from https://developers.google.com/speed/webp/gallery2 + array( __DIR__ . '/../../data/media/2_webp_a.webp', array( 386, 395 ) ), + array( __DIR__ . '/../../data/media/2_webp_ll.webp', array( 386, 395 ) ), + array( __DIR__ . '/../../data/media/webp_animated.webp', array( 300, 225 ) ), + + // Error cases + array( __FILE__, false ), + ); + } + + /** + * Tests the WebP MIME detection. This should really be a separate test, but sticking it + * here for now. + * + * @dataProvider provideTestGetMimeType + */ + public function testGuessMimeType( $path ) { + $mime = MimeMagic::singleton(); + $this->assertEquals( 'image/webp', $mime->guessMimeType( $path, false ) ); + } + public function provideTestGetMimeType() { + return array( + // Public domain files from https://developers.google.com/speed/webp/gallery2 + array( __DIR__ . '/../../data/media/2_webp_a.webp' ), + array( __DIR__ . '/../../data/media/2_webp_ll.webp' ), + array( __DIR__ . '/../../data/media/webp_animated.webp' ), + ); + } +} + +/* Python code to extract a header and convert to PHP format: + * print '"%s"' % ''.join( '\\x%02X' % ord(c) for c in urllib.urlopen(url).read(36) ) + */