diff options
Diffstat (limited to 'includes/media/WebP.php')
-rw-r--r-- | includes/media/WebP.php | 306 |
1 files changed, 306 insertions, 0 deletions
diff --git a/includes/media/WebP.php b/includes/media/WebP.php new file mode 100644 index 00000000..ff4dcee2 --- /dev/null +++ b/includes/media/WebP.php @@ -0,0 +1,306 @@ +<?php +/** + * Handler for Google's WebP format <https://developers.google.com/speed/webp/> + * + * 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 <https://developers.google.com/speed/webp/> + * + * @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; + + public 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 ); + } + + public function getMetadataType( $image ) { + return 'parsed-webp'; + } + + public 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 + ); + } + + public 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 + */ + public function mustRender( $file ) { + return true; + } + + /** + * @param $file + * @return bool False if we are unable to render this image + */ + public function canRender( $file ) { + if ( self::isAnimatedImage( $file ) ) { + return false; + } + return true; + } + + /** + * @param File $image + * @return bool + */ + public function isAnimatedImage( $image ) { + $ser = $image->getMetadata(); + if ( $ser ) { + $metadata = unserialize( $ser ); + if ( isset( $metadata['animated'] ) && $metadata['animated'] === true ) { + return true; + } + } + + return false; + } + + public function canAnimateThumbnail( $file ) { + return false; + } + + /** + * Render files as PNG + * + * @param $ext + * @param $mime + * @param $params + * @return array + */ + public 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'; + } +} |