Merge "Don't check namespace in SpecialWantedtemplates"
[lhc/web/wiklou.git] / includes / media / WebP.php
1 <?php
2 /**
3 * Handler for Google's WebP format <https://developers.google.com/speed/webp/>
4 *
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.
9 *
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.
14 *
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
19 *
20 * @file
21 * @ingroup Media
22 */
23
24 /**
25 * Handler for Google's WebP format <https://developers.google.com/speed/webp/>
26 *
27 * @ingroup Media
28 */
29 class WebPHandler extends BitmapHandler {
30 const BROKEN_FILE = '0'; // value to store in img_metadata if error extracting metadata.
31 /**
32 * @var int Minimum chunk header size to be able to read all header types
33 */
34 const MINIMUM_CHUNK_HEADER_LENGTH = 18;
35 /**
36 * @var int version of the metadata stored in db records
37 */
38 const _MW_WEBP_VERSION = 1;
39
40 const VP8X_ICC = 32;
41 const VP8X_ALPHA = 16;
42 const VP8X_EXIF = 8;
43 const VP8X_XMP = 4;
44 const VP8X_ANIM = 2;
45
46 public function getMetadata( $image, $filename ) {
47 $parsedWebPData = self::extractMetadata( $filename );
48 if ( !$parsedWebPData ) {
49 return self::BROKEN_FILE;
50 }
51
52 $parsedWebPData['metadata']['_MW_WEBP_VERSION'] = self::_MW_WEBP_VERSION;
53 return serialize( $parsedWebPData );
54 }
55
56 public function getMetadataType( $image ) {
57 return 'parsed-webp';
58 }
59
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;
64 }
65
66 wfSuppressWarnings();
67 $data = unserialize( $metadata );
68 wfRestoreWarnings();
69
70 if ( !$data || !is_array( $data ) ) {
71 wfDebug( __METHOD__ . " invalid WebP metadata\n" );
72
73 return self::METADATA_BAD;
74 }
75
76 if ( !isset( $data['metadata']['_MW_WEBP_VERSION'] )
77 || $data['metadata']['_MW_WEBP_VERSION'] != self::_MW_WEBP_VERSION
78 ) {
79 wfDebug( __METHOD__ . " old but compatible WebP metadata\n" );
80
81 return self::METADATA_COMPATIBLE;
82 }
83 return self::METADATA_GOOD;
84 }
85
86 /**
87 * Extracts the image size and WebP type from a file
88 *
89 * @param string $chunks Chunks as extracted by RiffExtractor
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.
93 */
94 public static function extractMetadata( $filename ) {
95 wfDebugLog( 'WebP', __METHOD__ . ": Extracting metadata from $filename\n" );
96
97 $info = RiffExtractor::findChunksFromFile( $filename, 100 );
98 if ( $info === false ) {
99 wfDebugLog( 'WebP', __METHOD__ . ": Not a valid RIFF file\n" );
100 return false;
101 }
102
103 if ( $info['fourCC'] != 'WEBP' ) {
104 wfDebugLog( 'WebP', __METHOD__ . ': FourCC was not WEBP: ' .
105 bin2hex( $info['fourCC'] ) . " \n" );
106 return false;
107 }
108
109 $metadata = self::extractMetadataFromChunks( $info['chunks'], $filename );
110 if ( !$metadata ) {
111 wfDebugLog( 'WebP', __METHOD__ . ": No VP8 chunks found\n" );
112 return false;
113 }
114
115 return $metadata;
116 }
117
118 /**
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 * @return array Header data array with entries 'compression', 'width' and 'height', where
122 * 'compression' can be 'lossy', 'lossless', 'animated' or 'unknown'
123 */
124 public static function extractMetadataFromChunks( $chunks, $filename ) {
125 $vp8Info = array();
126
127 foreach ( $chunks as $chunk ) {
128 if ( !in_array( $chunk['fourCC'], array( 'VP8 ', 'VP8L', 'VP8X' ) ) ) {
129 // Not a chunk containing interesting metadata
130 continue;
131 }
132
133 $chunkHeader = file_get_contents( $filename, false, null,
134 $chunk['start'], self::MINIMUM_CHUNK_HEADER_LENGTH );
135 wfDebugLog( 'WebP', __METHOD__ . ": {$chunk['fourCC']}\n" );
136
137 switch ( $chunk['fourCC'] ) {
138 case 'VP8 ':
139 return array_merge( $vp8Info,
140 self::decodeLossyChunkHeader( $chunkHeader ) );
141 case 'VP8L':
142 return array_merge( $vp8Info,
143 self::decodeLosslessChunkHeader( $chunkHeader ) );
144 case 'VP8X':
145 $vp8Info = array_merge( $vp8Info,
146 self::decodeExtendedChunkHeader( $chunkHeader ) );
147 // Continue looking for other chunks to improve the metadata
148 break;
149 }
150 }
151 return $vp8Info;
152 }
153
154 /**
155 * Decodes a lossy chunk header
156 * @param string $header Header string
157 * @return boolean|array See WebPHandler::decodeHeader
158 */
159 protected static function decodeLossyChunkHeader( $header ) {
160 // Bytes 0-3 are 'VP8 '
161 // Bytes 4-7 are the VP8 stream size
162 // Bytes 8-10 are the frame tag
163 // Bytes 11-13 are 0x9D 0x01 0x2A called the sync code
164 $syncCode = substr( $header, 11, 3 );
165 if ( $syncCode != "\x9D\x01\x2A" ) {
166 wfDebugLog( 'WebP', __METHOD__ . ': Invalid sync code: ' .
167 bin2hex( $syncCode ) . "\n" );
168 return array();
169 }
170 // Bytes 14-17 are image size
171 $imageSize = unpack( 'v2', substr( $header, 14, 4 ) );
172 // Image sizes are 14 bit, 2 MSB are scaling parameters which are ignored here
173 return array(
174 'compression' => 'lossy',
175 'width' => $imageSize[1] & 0x3FFF,
176 'height' => $imageSize[2] & 0x3FFF
177 );
178 }
179
180 /**
181 * Decodes a lossless chunk header
182 * @param string $header Header string
183 * @return boolean|array See WebPHandler::decodeHeader
184 */
185 public static function decodeLosslessChunkHeader( $header ) {
186 // Bytes 0-3 are 'VP8L'
187 // Bytes 4-7 are chunk stream size
188 // Byte 8 is 0x2F called the signature
189 if ( $header{8} != "\x2F" ) {
190 wfDebugLog( 'WebP', __METHOD__ . ': Invalid signature: ' .
191 bin2hex( $header{8} ) . "\n" );
192 return array();
193 }
194 // Bytes 9-12 contain the image size
195 // Bits 0-13 are width-1; bits 15-27 are height-1
196 $imageSize = unpack( 'C4', substr( $header, 9, 4 ) );
197 return array(
198 'compression' => 'lossless',
199 'width' => ( $imageSize[1] | ( ( $imageSize[2] & 0x3F ) << 8 ) ) + 1,
200 'height' => ( ( ( $imageSize[2] & 0xC0 ) >> 6 ) |
201 ( $imageSize[3] << 2 ) | ( ( $imageSize[4] & 0x03 ) << 10 ) ) + 1
202 );
203 }
204
205 /**
206 * Decodes an extended chunk header
207 * @param string $header Header string
208 * @return boolean|array See WebPHandler::decodeHeader
209 */
210 public static function decodeExtendedChunkHeader( $header ) {
211 // Bytes 0-3 are 'VP8X'
212 // Byte 4-7 are chunk length
213 // Byte 8-11 are a flag bytes
214 $flags = unpack( 'c', substr( $header, 8, 1 ) );
215
216 // Byte 12-17 are image size (24 bits)
217 $width = unpack( 'V', substr( $header, 12, 3 ) . "\x00" );
218 $height = unpack( 'V', substr( $header, 15, 3 ) . "\x00" );
219
220 return array(
221 'compression' => 'unknown',
222 'animated' => ( $flags[1] & self::VP8X_ANIM ) == self::VP8X_ANIM,
223 'transparency' => ( $flags[1] & self::VP8X_ALPHA ) == self::VP8X_ALPHA,
224 'width' => ( $width[1] & 0xFFFFFF ) + 1,
225 'height' => ( $height[1] & 0xFFFFFF ) + 1
226 );
227 }
228
229 public function getImageSize( $file, $path, $metadata = false ) {
230 if ( $file === null ) {
231 $metadata = self::getMetadata( $file, $path );
232 }
233 if ( $metadata === false ) {
234 $metadata = $file->getMetadata();
235 }
236
237 wfSuppressWarnings();
238 $metadata = unserialize( $metadata );
239 wfRestoreWarnings();
240
241 if ( $metadata == false ) {
242 return false;
243 }
244 return array( $metadata['width'], $metadata['height'] );
245 }
246
247 /**
248 * @param $file
249 * @return bool True, not all browsers support WebP
250 */
251 public function mustRender( $file ) {
252 return true;
253 }
254
255 /**
256 * @param $file
257 * @return bool False if we are unable to render this image
258 */
259 public function canRender( $file ) {
260 if ( self::isAnimatedImage( $file ) ) {
261 return false;
262 }
263 return true;
264 }
265
266 /**
267 * @param File $image
268 * @return bool
269 */
270 public function isAnimatedImage( $image ) {
271 $ser = $image->getMetadata();
272 if ( $ser ) {
273 $metadata = unserialize( $ser );
274 if ( isset( $metadata['animated'] ) && $metadata['animated'] === true ) {
275 return true;
276 }
277 }
278
279 return false;
280 }
281
282 public function canAnimateThumbnail( $file ) {
283 return false;
284 }
285
286 /**
287 * Render files as PNG
288 *
289 * @param $ext
290 * @param $mime
291 * @param $params
292 * @return array
293 */
294 public function getThumbType( $ext, $mime, $params = null ) {
295 return array( 'png', 'image/png' );
296 }
297
298 /**
299 * Must use "im" for XCF
300 *
301 * @return string
302 */
303 protected function getScalerType( $dstPath, $checkDstPath = true ) {
304 return 'im';
305 }
306 }