17use Wikimedia\XMPReader\Reader as XMPReader;
28 private const BROKEN_FILE =
'0';
32 private const MINIMUM_CHUNK_HEADER_LENGTH = 18;
36 private const MAX_METADATA_CHUNK_SIZE = 1024 * 1024 * 2;
40 private const _MW_WEBP_VERSION = 3;
42 private const VP8X_ICC = 32;
43 private const VP8X_ALPHA = 16;
44 private const VP8X_EXIF = 8;
45 private const VP8X_XMP = 4;
46 private const VP8X_ANIM = 2;
51 private const MIN_ANIM_CHUNK_SIZE = 6;
55 private const MIN_ANMF_CHUNK_SIZE = 16;
60 if ( !$parsedWebPData ) {
61 return [
'metadata' => [
'_error' => self::BROKEN_FILE ] ];
64 $parsedWebPData[
'metadata'][
'_MW_WEBP_VERSION'] = self::_MW_WEBP_VERSION;
66 'width' => $parsedWebPData[
'width'],
67 'height' => $parsedWebPData[
'height'],
68 'bits' => $parsedWebPData[
'bits'],
69 'metadata' => $parsedWebPData
81 $data = $image->getMetadataArray();
82 if ( $data === [
'_error' => self::BROKEN_FILE ] ) {
87 if ( !$data || isset( $data[
'_error'] ) ) {
88 wfDebug( __METHOD__ .
" invalid WebP metadata" );
93 if ( !isset( $data[
'metadata'][
'_MW_WEBP_VERSION'] )
94 || $data[
'metadata'][
'_MW_WEBP_VERSION'] != self::_MW_WEBP_VERSION
96 wfDebug( __METHOD__ .
" old but compatible WebP metadata" );
112 wfDebugLog(
'WebP', __METHOD__ .
": Extracting metadata from $filename" );
115 if ( $info ===
false ) {
116 wfDebugLog(
'WebP', __METHOD__ .
": Not a valid RIFF file" );
120 if ( $info[
'fourCC'] !==
'WEBP' ) {
121 wfDebugLog(
'WebP', __METHOD__ .
': FourCC was not WEBP: ' .
122 bin2hex( $info[
'fourCC'] ) );
127 wfDebugLog(
'WebP', __METHOD__ .
": No VP8 chunks found" );
149 foreach ( $chunks as $chunk ) {
151 if ( !in_array( $chunk[
'fourCC'], [
'VP8 ',
'VP8L',
'VP8X',
'ANIM',
'ANMF',
'EXIF',
'XMP ',
"XMP\0" ] ) ) {
156 $chunkHeader = file_get_contents( $filename,
false,
null,
157 $chunk[
'start'], self::MINIMUM_CHUNK_HEADER_LENGTH );
158 wfDebugLog(
'WebP', __METHOD__ .
": {$chunk['fourCC']}" );
160 switch ( $chunk[
'fourCC'] ) {
162 $vp8Info = array_merge( $vp8Info,
163 self::decodeLossyChunkHeader( $chunkHeader ) );
166 $vp8Info = array_merge( $vp8Info,
167 self::decodeLosslessChunkHeader( $chunkHeader ) );
170 $vp8Info = array_merge( $vp8Info,
171 self::decodeExtendedChunkHeader( $chunkHeader ) );
175 $animChunk = self::extractChunk( $chunk, $filename );
176 if ( $animChunk && strlen( $animChunk ) >= self::MIN_ANIM_CHUNK_SIZE ) {
177 $loopCount = unpack(
'v', substr( $animChunk, 4, 2 ) )[1];
181 if ( $chunk[
'size'] >= self::MIN_ANMF_CHUNK_SIZE ) {
182 $anmfChunk = file_get_contents( $filename,
false,
null,
183 $chunk[
'start'] + 8, self::MIN_ANMF_CHUNK_SIZE );
184 if ( $anmfChunk !==
false && strlen( $anmfChunk ) >= self::MIN_ANMF_CHUNK_SIZE ) {
185 $duration += unpack(
'V', substr( $anmfChunk, 12, 3 ) .
"\x00" )[1];
192 $exifData ??= self::extractChunk( $chunk, $filename );
196 $xmpData ??= self::extractChunk( $chunk, $filename );
200 if ( $frameCount > 1 ) {
201 $vp8Info[
'frameCount'] = $frameCount;
202 $vp8Info[
'duration'] = $duration / 1000.0;
203 $vp8Info[
'looped'] = $loopCount === 0 || $loopCount > 1;
204 $vp8Info[
'animated'] =
true;
207 $vp8Info[
'bits'] = 8;
209 $vp8Info = array_merge( $vp8Info,
210 self::decodeMediaMetadata( $exifData, $xmpData, $filename ) );
222 private static function decodeMediaMetadata( $exifData, $xmpData, $filename ) {
223 if ( $exifData ===
null && $xmpData ===
null ) {
227 $bitmapMetadataHandler =
new BitmapMetadataHandler;
229 if ( $xmpData && XMPReader::isSupported() ) {
230 $xmpReader =
new XMPReader( LoggerFactory::getInstance(
'XMP' ), $filename );
231 $xmpReader->parse( $xmpData );
232 $res = $xmpReader->getResults();
233 foreach ( $res as $type => $array ) {
234 $bitmapMetadataHandler->addMetadata( $array, $type );
242 if ( str_starts_with( $exifData,
"Exif\x00\x00" ) ) {
243 $exifData = substr( $exifData, 6 );
246 getTempFSFileFactory()->
247 newTempFSFile(
'webp-exif_',
'tiff' );
249 $exifDataFile = $tmpFile->getPath();
250 file_put_contents( $exifDataFile, $exifData );
252 $bitmapMetadataHandler->getExif( $exifDataFile, $byteOrder );
254 return [
'media-metadata' => $bitmapMetadataHandler->getMetadataArray() ];
262 private static function extractChunk( $chunk, $filename ) {
263 if ( $chunk[
'size'] > self::MAX_METADATA_CHUNK_SIZE || $chunk[
'size'] < 1 ) {
268 return file_get_contents( $filename,
false,
null, $chunk[
'start'] + 8, $chunk[
'size'] );
281 $syncCode = substr( $header, 11, 3 );
282 if ( $syncCode !==
"\x9D\x01\x2A" ) {
283 wfDebugLog(
'WebP', __METHOD__ .
': Invalid sync code: ' .
284 bin2hex( $syncCode ) );
288 $imageSize = unpack(
'v2', substr( $header, 14, 4 ) );
291 'compression' =>
'lossy',
292 'width' => $imageSize[1] & 0x3FFF,
293 'height' => $imageSize[2] & 0x3FFF
306 if ( $header[8] !==
"\x2F" ) {
307 wfDebugLog(
'WebP', __METHOD__ .
': Invalid signature: ' .
308 bin2hex( $header[8] ) );
313 $imageSize = unpack(
'C4', substr( $header, 9, 4 ) );
315 'compression' =>
'lossless',
316 'width' => ( $imageSize[1] | ( ( $imageSize[2] & 0x3F ) << 8 ) ) + 1,
317 'height' => ( ( ( $imageSize[2] & 0xC0 ) >> 6 ) |
318 ( $imageSize[3] << 2 ) | ( ( $imageSize[4] & 0x0F ) << 10 ) ) + 1
331 $flags = unpack(
'c', substr( $header, 8, 1 ) );
334 $width = unpack(
'V', substr( $header, 12, 3 ) .
"\x00" );
335 $height = unpack(
'V', substr( $header, 15, 3 ) .
"\x00" );
338 'compression' =>
'unknown',
339 'animated' => ( $flags[1] & self::VP8X_ANIM ) === self::VP8X_ANIM,
340 'transparency' => ( $flags[1] & self::VP8X_ALPHA ) === self::VP8X_ALPHA,
341 'width' => ( $width[1] & 0xFFFFFF ) + 1,
342 'height' => ( $height[1] & 0xFFFFFF ) + 1
359 $metadata = $image->getMetadataArray();
360 return !empty( $metadata[
'animated'] );
368 $metadata = $image->getMetadataArray();
369 if ( isset( $metadata[
'frameCount'] ) && $metadata[
'frameCount'] > 0 ) {
370 return $image->getWidth() * $image->getHeight() * $metadata[
'frameCount'];
372 return $image->getWidth() * $image->getHeight();
379 if ( $thumbExt !==
'webp' ) {
386 return $this->
getImageArea( $file ) <= $maxAnimatedWebPArea;
397 return function_exists(
'gd_info' ) && ( gd_info()[
'WebP Support'] ?? false );
403 return $meta[
'media-metadata'] ?? [];
422 $original = parent::getLongDesc( $image );
424 $metadata = $image->getMetadataArray();
426 if ( !$metadata || isset( $metadata[
'_error'] ) || empty( $metadata[
'frameCount'] ) ) {
434 if ( !empty( $metadata[
'looped'] ) ) {
435 $info[] =
wfMessage(
'file-info-webp-looped' )->inLanguage( $lang )->parse();
438 if ( $metadata[
'frameCount'] > 1 ) {
439 $info[] =
wfMessage(
'file-info-webp-frames' )->numParams( $metadata[
'frameCount'] )
440 ->inLanguage( $lang )->parse();
443 if ( !empty( $metadata[
'duration'] ) ) {
444 $info[] = htmlspecialchars( $lang->formatTimePeriod( $metadata[
'duration'] ), ENT_QUOTES );
447 return $lang->commaList( $info );
457 return (
float)( $file->getMetadataArray()[
'duration'] ?? 0.0 );
462class_alias( WebPHandler::class,
'WebPHandler' );
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.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
A class containing constants representing the names of configuration variables.
const MaxAnimatedWebPArea
Name constant for the MaxAnimatedWebPArea setting, for use with Config::get()
const WebPThumbnailType
Name constant for the WebPThumbnailType setting, for use with Config::get()