MediaWiki 1.44.0
WebPHandler.php
Go to the documentation of this file.
1<?php
27use Wikimedia\XMPReader\Reader as XMPReader;
28
38 private const BROKEN_FILE = '0';
42 private const MINIMUM_CHUNK_HEADER_LENGTH = 18;
46 private const MAX_METADATA_CHUNK_SIZE = 1024 * 1024 * 2;
50 private const _MW_WEBP_VERSION = 2;
51
52 private const VP8X_ICC = 32;
53 private const VP8X_ALPHA = 16;
54 private const VP8X_EXIF = 8;
55 private const VP8X_XMP = 4;
56 private const VP8X_ANIM = 2;
57
58 public function getSizeAndMetadata( $state, $filename ) {
59 $parsedWebPData = self::extractMetadata( $filename );
60 if ( !$parsedWebPData ) {
61 return [ 'metadata' => [ '_error' => self::BROKEN_FILE ] ];
62 }
63
64 $parsedWebPData['metadata']['_MW_WEBP_VERSION'] = self::_MW_WEBP_VERSION;
65 $info = [
66 'width' => $parsedWebPData['width'],
67 'height' => $parsedWebPData['height'],
68 'metadata' => $parsedWebPData
69 ];
70 return $info;
71 }
72
73 public function getMetadataType( $image ) {
74 return 'parsed-webp';
75 }
76
77 public function isFileMetadataValid( $image ) {
78 $data = $image->getMetadataArray();
79 if ( $data === [ '_error' => self::BROKEN_FILE ] ) {
80 // Do not repetitivly regenerate metadata on broken file.
82 }
83
84 if ( !$data || !isset( $data['_error'] ) ) {
85 wfDebug( __METHOD__ . " invalid WebP metadata" );
86
87 return self::METADATA_BAD;
88 }
89
90 if ( !isset( $data['metadata']['_MW_WEBP_VERSION'] )
91 || $data['metadata']['_MW_WEBP_VERSION'] != self::_MW_WEBP_VERSION
92 ) {
93 wfDebug( __METHOD__ . " old but compatible WebP metadata" );
94
96 }
98 }
99
108 public static function extractMetadata( $filename ) {
109 wfDebugLog( 'WebP', __METHOD__ . ": Extracting metadata from $filename" );
110
111 $info = RiffExtractor::findChunksFromFile( $filename, 100 );
112 if ( $info === false ) {
113 wfDebugLog( 'WebP', __METHOD__ . ": Not a valid RIFF file" );
114 return false;
115 }
116
117 if ( $info['fourCC'] !== 'WEBP' ) {
118 wfDebugLog( 'WebP', __METHOD__ . ': FourCC was not WEBP: ' .
119 bin2hex( $info['fourCC'] ) );
120 return false;
121 }
122 $metadata = self::extractMetadataFromChunks( $info['chunks'], $filename );
123 if ( !$metadata ) {
124 wfDebugLog( 'WebP', __METHOD__ . ": No VP8 chunks found" );
125 return false;
126 }
127
128 return $metadata;
129 }
130
138 public static function extractMetadataFromChunks( $chunks, $filename ) {
139 $vp8Info = [];
140 $exifData = null;
141 $xmpData = null;
142
143 foreach ( $chunks as $chunk ) {
144 // Note, spec says it should be 'XMP ' but some real life files use "XMP\0"
145 if ( !in_array( $chunk['fourCC'], [ 'VP8 ', 'VP8L', 'VP8X', 'EXIF', 'XMP ', "XMP\0" ] ) ) {
146 // Not a chunk containing interesting metadata
147 continue;
148 }
149
150 $chunkHeader = file_get_contents( $filename, false, null,
151 $chunk['start'], self::MINIMUM_CHUNK_HEADER_LENGTH );
152 wfDebugLog( 'WebP', __METHOD__ . ": {$chunk['fourCC']}" );
153
154 switch ( $chunk['fourCC'] ) {
155 case 'VP8 ':
156 $vp8Info = array_merge( $vp8Info,
157 self::decodeLossyChunkHeader( $chunkHeader ) );
158 break;
159 case 'VP8L':
160 $vp8Info = array_merge( $vp8Info,
161 self::decodeLosslessChunkHeader( $chunkHeader ) );
162 break;
163 case 'VP8X':
164 $vp8Info = array_merge( $vp8Info,
165 self::decodeExtendedChunkHeader( $chunkHeader ) );
166 // Continue looking for other chunks to improve the metadata
167 break;
168 case 'EXIF':
169 // Spec says ignore all but first one
170 $exifData ??= self::extractChunk( $chunk, $filename );
171 break;
172 case 'XMP ':
173 case "XMP\0":
174 $xmpData ??= self::extractChunk( $chunk, $filename );
175 break;
176 }
177 }
178 $vp8Info = array_merge( $vp8Info,
179 self::decodeMediaMetadata( $exifData, $xmpData, $filename ) );
180 return $vp8Info;
181 }
182
191 private static function decodeMediaMetadata( $exifData, $xmpData, $filename ) {
192 if ( $exifData === null && $xmpData === null ) {
193 // Nothing to do
194 return [];
195 }
196 $bitmapMetadataHandler = new BitmapMetadataHandler;
197
198 if ( $xmpData && XMPReader::isSupported() ) {
199 $xmpReader = new XMPReader( LoggerFactory::getInstance( 'XMP' ), $filename );
200 $xmpReader->parse( $xmpData );
201 $res = $xmpReader->getResults();
202 foreach ( $res as $type => $array ) {
203 $bitmapMetadataHandler->addMetadata( $array, $type );
204 }
205 }
206
207 if ( $exifData ) {
208 // The Exif section of a webp file is basically a tiff file without an image.
209 // Some files start with an Exif\0\0. This is wrong according to standard and
210 // will prevent us from reading file, so remove for compatibility.
211 if ( substr( $exifData, 0, 6 ) === "Exif\x00\x00" ) {
212 $exifData = substr( $exifData, 6 );
213 }
214 $tmpFile = MediaWikiServices::getInstance()->
215 getTempFSFileFactory()->
216 newTempFSFile( 'webp-exif_', 'tiff' );
217
218 $exifDataFile = $tmpFile->getPath();
219 file_put_contents( $exifDataFile, $exifData );
220 $byteOrder = BitmapMetadataHandler::getTiffByteOrder( $exifDataFile );
221 $bitmapMetadataHandler->getExif( $exifDataFile, $byteOrder );
222 }
223 return [ 'media-metadata' => $bitmapMetadataHandler->getMetadataArray() ];
224 }
225
231 private static function extractChunk( $chunk, $filename ) {
232 if ( $chunk['size'] > self::MAX_METADATA_CHUNK_SIZE || $chunk['size'] < 1 ) {
233 return null;
234 }
235
236 // Skip first 8 bytes as that is the fourCC header followed by size of chunk.
237 return file_get_contents( $filename, false, null, $chunk['start'] + 8, $chunk['size'] );
238 }
239
245 protected static function decodeLossyChunkHeader( $header ) {
246 // Bytes 0-3 are 'VP8 '
247 // Bytes 4-7 are the VP8 stream size
248 // Bytes 8-10 are the frame tag
249 // Bytes 11-13 are 0x9D 0x01 0x2A called the sync code
250 $syncCode = substr( $header, 11, 3 );
251 if ( $syncCode !== "\x9D\x01\x2A" ) {
252 wfDebugLog( 'WebP', __METHOD__ . ': Invalid sync code: ' .
253 bin2hex( $syncCode ) );
254 return [];
255 }
256 // Bytes 14-17 are image size
257 $imageSize = unpack( 'v2', substr( $header, 14, 4 ) );
258 // Image sizes are 14 bit, 2 MSB are scaling parameters which are ignored here
259 return [
260 'compression' => 'lossy',
261 'width' => $imageSize[1] & 0x3FFF,
262 'height' => $imageSize[2] & 0x3FFF
263 ];
264 }
265
271 public static function decodeLosslessChunkHeader( $header ) {
272 // Bytes 0-3 are 'VP8L'
273 // Bytes 4-7 are chunk stream size
274 // Byte 8 is 0x2F called the signature
275 if ( $header[8] !== "\x2F" ) {
276 wfDebugLog( 'WebP', __METHOD__ . ': Invalid signature: ' .
277 bin2hex( $header[8] ) );
278 return [];
279 }
280 // Bytes 9-12 contain the image size
281 // Bits 0-13 are width-1; bits 15-27 are height-1
282 $imageSize = unpack( 'C4', substr( $header, 9, 4 ) );
283 return [
284 'compression' => 'lossless',
285 'width' => ( $imageSize[1] | ( ( $imageSize[2] & 0x3F ) << 8 ) ) + 1,
286 'height' => ( ( ( $imageSize[2] & 0xC0 ) >> 6 ) |
287 ( $imageSize[3] << 2 ) | ( ( $imageSize[4] & 0x03 ) << 10 ) ) + 1
288 ];
289 }
290
296 public static function decodeExtendedChunkHeader( $header ) {
297 // Bytes 0-3 are 'VP8X'
298 // Byte 4-7 are chunk length
299 // Byte 8-11 are a flag bytes
300 $flags = unpack( 'c', substr( $header, 8, 1 ) );
301
302 // Byte 12-17 are image size (24 bits)
303 $width = unpack( 'V', substr( $header, 12, 3 ) . "\x00" );
304 $height = unpack( 'V', substr( $header, 15, 3 ) . "\x00" );
305
306 return [
307 'compression' => 'unknown',
308 'animated' => ( $flags[1] & self::VP8X_ANIM ) === self::VP8X_ANIM,
309 'transparency' => ( $flags[1] & self::VP8X_ALPHA ) === self::VP8X_ALPHA,
310 'width' => ( $width[1] & 0xFFFFFF ) + 1,
311 'height' => ( $height[1] & 0xFFFFFF ) + 1
312 ];
313 }
314
319 public function mustRender( $file ) {
320 return true;
321 }
322
327 public function canRender( $file ) {
328 if ( $this->isAnimatedImage( $file ) ) {
329 return false;
330 }
331 return true;
332 }
333
338 public function isAnimatedImage( $image ) {
339 $metadata = $image->getMetadataArray();
340 if ( isset( $metadata['animated'] ) && $metadata['animated'] === true ) {
341 return true;
342 }
343
344 return false;
345 }
346
347 public function canAnimateThumbnail( $file ) {
348 return false;
349 }
350
359 public function getThumbType( $ext, $mime, $params = null ) {
360 return [ 'png', 'image/png' ];
361 }
362
363 protected function hasGDSupport() {
364 return function_exists( 'gd_info' ) && ( gd_info()['WebP Support'] ?? false );
365 }
366
367 public function getCommonMetaArray( File $image ) {
368 $meta = $image->getMetadataArray();
369 return $meta['media-metadata'] ?? [];
370 }
371
372 public function formatMetadata( $image, $context = false ) {
373 $meta = $this->getCommonMetaArray( $image );
374 if ( !$meta ) {
375 return false;
376 }
377
378 return $this->formatMetadataHelper( $meta, $context );
379 }
380}
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
Generic handler for bitmap images.
Class to deal with reconciling and extracting metadata from bitmap images.
static getTiffByteOrder( $filename)
Read the first 2 bytes of a tiff file to figure out Little Endian or Big Endian.
const METADATA_COMPATIBLE
formatMetadataHelper( $metadataArray, $context=false)
sorts the visible/invisible field.
const METADATA_GOOD
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:93
getMetadataArray()
Get the unserialized handler-specific metadata STUB.
Definition File.php:791
Create PSR-3 logger objects.
Service locator for MediaWiki core services.
static findChunksFromFile( $filename, $maxChunks=-1)
Handler for Google's WebP format https://developers.google.com/speed/webp/
mustRender( $file)
getCommonMetaArray(File $image)
Get an array of standard (FormatMetadata type) metadata values.
canAnimateThumbnail( $file)
If the material is animated, we can animate the thumbnail.
isFileMetadataValid( $image)
Check if the metadata is valid for this handler.
static extractMetadataFromChunks( $chunks, $filename)
Extracts the image size and WebP type from a file based on the chunk list.
isAnimatedImage( $image)
static decodeLossyChunkHeader( $header)
Decodes a lossy chunk header.
getSizeAndMetadata( $state, $filename)
Get image size information and metadata array.
canRender( $file)
formatMetadata( $image, $context=false)
Get an array structure that looks like this:
getThumbType( $ext, $mime, $params=null)
Render files as PNG.
static decodeLosslessChunkHeader( $header)
Decodes a lossless chunk header.
getMetadataType( $image)
Get a string describing the type of metadata, for display purposes.
hasGDSupport()
Whether the php-gd extension supports this type of file.
static extractMetadata( $filename)
Extracts the image size and WebP type from a file.
static decodeExtendedChunkHeader( $header)
Decodes an extended chunk header.
$header