3 * Handler for Google's WebP format <https://developers.google.com/speed/webp/>
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
25 * Handler for Google's WebP format <https://developers.google.com/speed/webp/>
29 class WebPHandler
extends BitmapHandler
{
30 const BROKEN_FILE
= '0'; // value to store in img_metadata if error extracting metadata.
32 * @var int Minimum chunk header size to be able to read all header types
34 const MINIMUM_CHUNK_HEADER_LENGTH
= 18;
36 * @var int version of the metadata stored in db records
38 const _MW_WEBP_VERSION
= 1;
41 const VP8X_ALPHA
= 16;
46 public function getMetadata( $image, $filename ) {
47 $parsedWebPData = self
::extractMetadata( $filename );
48 if ( !$parsedWebPData ) {
49 return self
::BROKEN_FILE
;
52 $parsedWebPData['metadata']['_MW_WEBP_VERSION'] = self
::_MW_WEBP_VERSION
;
53 return serialize( $parsedWebPData );
56 public function getMetadataType( $image ) {
60 public function isMetadataValid( $image, $metadata ) {
61 if ( $metadata === self
::BROKEN_FILE
) {
62 // Do not repetitivly regenerate metadata on broken file.
63 return self
::METADATA_GOOD
;
66 MediaWiki\
suppressWarnings();
67 $data = unserialize( $metadata );
68 MediaWiki\restoreWarnings
();
70 if ( !$data ||
!is_array( $data ) ) {
71 wfDebug( __METHOD__
. " invalid WebP metadata\n" );
73 return self
::METADATA_BAD
;
76 if ( !isset( $data['metadata']['_MW_WEBP_VERSION'] )
77 ||
$data['metadata']['_MW_WEBP_VERSION'] != self
::_MW_WEBP_VERSION
79 wfDebug( __METHOD__
. " old but compatible WebP metadata\n" );
81 return self
::METADATA_COMPATIBLE
;
83 return self
::METADATA_GOOD
;
87 * Extracts the image size and WebP type from a file
89 * @param string $filename
90 * @return array|bool Header data array with entries 'compression', 'width' and 'height',
91 * where 'compression' can be 'lossy', 'lossless', 'animated' or 'unknown'. False if
92 * file is not a valid WebP file.
94 public static function extractMetadata( $filename ) {
95 wfDebugLog( 'WebP', __METHOD__
. ": Extracting metadata from $filename\n" );
97 $info = RiffExtractor
::findChunksFromFile( $filename, 100 );
98 if ( $info === false ) {
99 wfDebugLog( 'WebP', __METHOD__
. ": Not a valid RIFF file\n" );
103 if ( $info['fourCC'] != 'WEBP' ) {
104 wfDebugLog( 'WebP', __METHOD__
. ': FourCC was not WEBP: ' .
105 bin2hex( $info['fourCC'] ) . " \n" );
109 $metadata = self
::extractMetadataFromChunks( $info['chunks'], $filename );
111 wfDebugLog( 'WebP', __METHOD__
. ": No VP8 chunks found\n" );
119 * Extracts the image size and WebP type from a file based on the chunk list
120 * @param array $chunks Chunks as extracted by RiffExtractor
121 * @param string $filename
122 * @return array Header data array with entries 'compression', 'width' and 'height', where
123 * 'compression' can be 'lossy', 'lossless', 'animated' or 'unknown'
125 public static function extractMetadataFromChunks( $chunks, $filename ) {
128 foreach ( $chunks as $chunk ) {
129 if ( !in_array( $chunk['fourCC'], [ 'VP8 ', 'VP8L', 'VP8X' ] ) ) {
130 // Not a chunk containing interesting metadata
134 $chunkHeader = file_get_contents( $filename, false, null,
135 $chunk['start'], self
::MINIMUM_CHUNK_HEADER_LENGTH
);
136 wfDebugLog( 'WebP', __METHOD__
. ": {$chunk['fourCC']}\n" );
138 switch ( $chunk['fourCC'] ) {
140 return array_merge( $vp8Info,
141 self
::decodeLossyChunkHeader( $chunkHeader ) );
143 return array_merge( $vp8Info,
144 self
::decodeLosslessChunkHeader( $chunkHeader ) );
146 $vp8Info = array_merge( $vp8Info,
147 self
::decodeExtendedChunkHeader( $chunkHeader ) );
148 // Continue looking for other chunks to improve the metadata
156 * Decodes a lossy chunk header
157 * @param string $header Header string
158 * @return bool|array See WebPHandler::decodeHeader
160 protected static function decodeLossyChunkHeader( $header ) {
161 // Bytes 0-3 are 'VP8 '
162 // Bytes 4-7 are the VP8 stream size
163 // Bytes 8-10 are the frame tag
164 // Bytes 11-13 are 0x9D 0x01 0x2A called the sync code
165 $syncCode = substr( $header, 11, 3 );
166 if ( $syncCode != "\x9D\x01\x2A" ) {
167 wfDebugLog( 'WebP', __METHOD__
. ': Invalid sync code: ' .
168 bin2hex( $syncCode ) . "\n" );
171 // Bytes 14-17 are image size
172 $imageSize = unpack( 'v2', substr( $header, 14, 4 ) );
173 // Image sizes are 14 bit, 2 MSB are scaling parameters which are ignored here
175 'compression' => 'lossy',
176 'width' => $imageSize[1] & 0x3FFF,
177 'height' => $imageSize[2] & 0x3FFF
182 * Decodes a lossless chunk header
183 * @param string $header Header string
184 * @return bool|array See WebPHandler::decodeHeader
186 public static function decodeLosslessChunkHeader( $header ) {
187 // Bytes 0-3 are 'VP8L'
188 // Bytes 4-7 are chunk stream size
189 // Byte 8 is 0x2F called the signature
190 if ( $header{8} != "\x2F" ) {
191 wfDebugLog( 'WebP', __METHOD__
. ': Invalid signature: ' .
192 bin2hex( $header{8} ) . "\n" );
195 // Bytes 9-12 contain the image size
196 // Bits 0-13 are width-1; bits 15-27 are height-1
197 $imageSize = unpack( 'C4', substr( $header, 9, 4 ) );
199 'compression' => 'lossless',
200 'width' => ( $imageSize[1] |
( ( $imageSize[2] & 0x3F ) << 8 ) ) +
1,
201 'height' => ( ( ( $imageSize[2] & 0xC0 ) >> 6 ) |
202 ( $imageSize[3] << 2 ) |
( ( $imageSize[4] & 0x03 ) << 10 ) ) +
1
207 * Decodes an extended chunk header
208 * @param string $header Header string
209 * @return bool|array See WebPHandler::decodeHeader
211 public static function decodeExtendedChunkHeader( $header ) {
212 // Bytes 0-3 are 'VP8X'
213 // Byte 4-7 are chunk length
214 // Byte 8-11 are a flag bytes
215 $flags = unpack( 'c', substr( $header, 8, 1 ) );
217 // Byte 12-17 are image size (24 bits)
218 $width = unpack( 'V', substr( $header, 12, 3 ) . "\x00" );
219 $height = unpack( 'V', substr( $header, 15, 3 ) . "\x00" );
222 'compression' => 'unknown',
223 'animated' => ( $flags[1] & self
::VP8X_ANIM
) == self
::VP8X_ANIM
,
224 'transparency' => ( $flags[1] & self
::VP8X_ALPHA
) == self
::VP8X_ALPHA
,
225 'width' => ( $width[1] & 0xFFFFFF ) +
1,
226 'height' => ( $height[1] & 0xFFFFFF ) +
1
230 public function getImageSize( $file, $path, $metadata = false ) {
231 if ( $file === null ) {
232 $metadata = self
::getMetadata( $file, $path );
234 if ( $metadata === false && $file instanceof File
) {
235 $metadata = $file->getMetadata();
238 MediaWiki\
suppressWarnings();
239 $metadata = unserialize( $metadata );
240 MediaWiki\restoreWarnings
();
242 if ( $metadata == false ) {
245 return [ $metadata['width'], $metadata['height'] ];
250 * @return bool True, not all browsers support WebP
252 public function mustRender( $file ) {
258 * @return bool False if we are unable to render this image
260 public function canRender( $file ) {
261 if ( self
::isAnimatedImage( $file ) ) {
271 public function isAnimatedImage( $image ) {
272 $ser = $image->getMetadata();
274 $metadata = unserialize( $ser );
275 if ( isset( $metadata['animated'] ) && $metadata['animated'] === true ) {
283 public function canAnimateThumbnail( $file ) {
288 * Render files as PNG
291 * @param string $mime
292 * @param array|null $params
295 public function getThumbType( $ext, $mime, $params = null ) {
296 return [ 'png', 'image/png' ];
300 * Must use "im" for XCF
304 protected function getScalerType( $dstPath, $checkDstPath = true ) {