MediaWiki  master
WebPHandler.php
Go to the documentation of this file.
1 <?php
29 class WebPHandler extends BitmapHandler {
33  private const BROKEN_FILE = '0';
37  private const MINIMUM_CHUNK_HEADER_LENGTH = 18;
41  private const _MW_WEBP_VERSION = 1;
42 
43  private const VP8X_ICC = 32;
44  private const VP8X_ALPHA = 16;
45  private const VP8X_EXIF = 8;
46  private const VP8X_XMP = 4;
47  private const VP8X_ANIM = 2;
48 
49  public function getSizeAndMetadata( $state, $filename ) {
50  $parsedWebPData = self::extractMetadata( $filename );
51  if ( !$parsedWebPData ) {
52  return [ 'metadata' => [ '_error' => self::BROKEN_FILE ] ];
53  }
54 
55  $parsedWebPData['metadata']['_MW_WEBP_VERSION'] = self::_MW_WEBP_VERSION;
56  $info = [
57  'width' => $parsedWebPData['width'],
58  'height' => $parsedWebPData['height'],
59  'metadata' => $parsedWebPData
60  ];
61  return $info;
62  }
63 
64  public function getMetadataType( $image ) {
65  return 'parsed-webp';
66  }
67 
68  public function isFileMetadataValid( $image ) {
69  $data = $image->getMetadataArray();
70  if ( $data === [ '_error' => self::BROKEN_FILE ] ) {
71  // Do not repetitivly regenerate metadata on broken file.
72  return self::METADATA_GOOD;
73  }
74 
75  if ( !$data || !isset( $data['_error'] ) ) {
76  wfDebug( __METHOD__ . " invalid WebP metadata" );
77 
78  return self::METADATA_BAD;
79  }
80 
81  if ( !isset( $data['metadata']['_MW_WEBP_VERSION'] )
82  || $data['metadata']['_MW_WEBP_VERSION'] != self::_MW_WEBP_VERSION
83  ) {
84  wfDebug( __METHOD__ . " old but compatible WebP metadata" );
85 
87  }
88  return self::METADATA_GOOD;
89  }
90 
99  public static function extractMetadata( $filename ) {
100  wfDebugLog( 'WebP', __METHOD__ . ": Extracting metadata from $filename" );
101 
102  $info = RiffExtractor::findChunksFromFile( $filename, 100 );
103  if ( $info === false ) {
104  wfDebugLog( 'WebP', __METHOD__ . ": Not a valid RIFF file" );
105  return false;
106  }
107 
108  if ( $info['fourCC'] !== 'WEBP' ) {
109  wfDebugLog( 'WebP', __METHOD__ . ': FourCC was not WEBP: ' .
110  bin2hex( $info['fourCC'] ) );
111  return false;
112  }
113 
114  $metadata = self::extractMetadataFromChunks( $info['chunks'], $filename );
115  if ( !$metadata ) {
116  wfDebugLog( 'WebP', __METHOD__ . ": No VP8 chunks found" );
117  return false;
118  }
119 
120  return $metadata;
121  }
122 
130  public static function extractMetadataFromChunks( $chunks, $filename ) {
131  $vp8Info = [];
132 
133  foreach ( $chunks as $chunk ) {
134  if ( !in_array( $chunk['fourCC'], [ 'VP8 ', 'VP8L', 'VP8X' ] ) ) {
135  // Not a chunk containing interesting metadata
136  continue;
137  }
138 
139  $chunkHeader = file_get_contents( $filename, false, null,
140  $chunk['start'], self::MINIMUM_CHUNK_HEADER_LENGTH );
141  wfDebugLog( 'WebP', __METHOD__ . ": {$chunk['fourCC']}" );
142 
143  switch ( $chunk['fourCC'] ) {
144  case 'VP8 ':
145  return array_merge( $vp8Info,
146  self::decodeLossyChunkHeader( $chunkHeader ) );
147  case 'VP8L':
148  return array_merge( $vp8Info,
149  self::decodeLosslessChunkHeader( $chunkHeader ) );
150  case 'VP8X':
151  $vp8Info = array_merge( $vp8Info,
152  self::decodeExtendedChunkHeader( $chunkHeader ) );
153  // Continue looking for other chunks to improve the metadata
154  break;
155  }
156  }
157  return $vp8Info;
158  }
159 
165  protected static function decodeLossyChunkHeader( $header ) {
166  // Bytes 0-3 are 'VP8 '
167  // Bytes 4-7 are the VP8 stream size
168  // Bytes 8-10 are the frame tag
169  // Bytes 11-13 are 0x9D 0x01 0x2A called the sync code
170  $syncCode = substr( $header, 11, 3 );
171  if ( $syncCode !== "\x9D\x01\x2A" ) {
172  wfDebugLog( 'WebP', __METHOD__ . ': Invalid sync code: ' .
173  bin2hex( $syncCode ) );
174  return [];
175  }
176  // Bytes 14-17 are image size
177  $imageSize = unpack( 'v2', substr( $header, 14, 4 ) );
178  // Image sizes are 14 bit, 2 MSB are scaling parameters which are ignored here
179  return [
180  'compression' => 'lossy',
181  'width' => $imageSize[1] & 0x3FFF,
182  'height' => $imageSize[2] & 0x3FFF
183  ];
184  }
185 
191  public static function decodeLosslessChunkHeader( $header ) {
192  // Bytes 0-3 are 'VP8L'
193  // Bytes 4-7 are chunk stream size
194  // Byte 8 is 0x2F called the signature
195  if ( $header[8] !== "\x2F" ) {
196  wfDebugLog( 'WebP', __METHOD__ . ': Invalid signature: ' .
197  bin2hex( $header[8] ) );
198  return [];
199  }
200  // Bytes 9-12 contain the image size
201  // Bits 0-13 are width-1; bits 15-27 are height-1
202  $imageSize = unpack( 'C4', substr( $header, 9, 4 ) );
203  return [
204  'compression' => 'lossless',
205  'width' => ( $imageSize[1] | ( ( $imageSize[2] & 0x3F ) << 8 ) ) + 1,
206  'height' => ( ( ( $imageSize[2] & 0xC0 ) >> 6 ) |
207  ( $imageSize[3] << 2 ) | ( ( $imageSize[4] & 0x03 ) << 10 ) ) + 1
208  ];
209  }
210 
216  public static function decodeExtendedChunkHeader( $header ) {
217  // Bytes 0-3 are 'VP8X'
218  // Byte 4-7 are chunk length
219  // Byte 8-11 are a flag bytes
220  $flags = unpack( 'c', substr( $header, 8, 1 ) );
221 
222  // Byte 12-17 are image size (24 bits)
223  $width = unpack( 'V', substr( $header, 12, 3 ) . "\x00" );
224  $height = unpack( 'V', substr( $header, 15, 3 ) . "\x00" );
225 
226  return [
227  'compression' => 'unknown',
228  'animated' => ( $flags[1] & self::VP8X_ANIM ) === self::VP8X_ANIM,
229  'transparency' => ( $flags[1] & self::VP8X_ALPHA ) === self::VP8X_ALPHA,
230  'width' => ( $width[1] & 0xFFFFFF ) + 1,
231  'height' => ( $height[1] & 0xFFFFFF ) + 1
232  ];
233  }
234 
239  public function mustRender( $file ) {
240  return true;
241  }
242 
247  public function canRender( $file ) {
248  if ( $this->isAnimatedImage( $file ) ) {
249  return false;
250  }
251  return true;
252  }
253 
258  public function isAnimatedImage( $image ) {
259  $metadata = $image->getMetadataArray();
260  if ( isset( $metadata['animated'] ) && $metadata['animated'] === true ) {
261  return true;
262  }
263 
264  return false;
265  }
266 
267  public function canAnimateThumbnail( $file ) {
268  return false;
269  }
270 
279  public function getThumbType( $ext, $mime, $params = null ) {
280  return [ 'png', 'image/png' ];
281  }
282 
290  protected function getScalerType( $dstPath, $checkDstPath = true ) {
291  return 'im';
292  }
293 }
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.
const METADATA_COMPATIBLE
const METADATA_BAD
const METADATA_GOOD
static findChunksFromFile( $filename, $maxChunks=-1)
Handler for Google's WebP format https://developers.google.com/speed/webp/
Definition: WebPHandler.php:29
mustRender( $file)
canAnimateThumbnail( $file)
If the material is animated, we can animate the thumbnail.
isFileMetadataValid( $image)
Check if the metadata is valid for this handler.
Definition: WebPHandler.php:68
static extractMetadataFromChunks( $chunks, $filename)
Extracts the image size and WebP type from a file based on the chunk list.
isAnimatedImage( $image)
getScalerType( $dstPath, $checkDstPath=true)
Must use "im" for XCF.
static decodeLossyChunkHeader( $header)
Decodes a lossy chunk header.
getSizeAndMetadata( $state, $filename)
Get image size information and metadata array.
Definition: WebPHandler.php:49
canRender( $file)
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.
Definition: WebPHandler.php:64
static extractMetadata( $filename)
Extracts the image size and WebP type from a file.
Definition: WebPHandler.php:99
static decodeExtendedChunkHeader( $header)
Decodes an extended chunk header.
$mime
Definition: router.php:60
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
if(!is_readable( $file)) $ext
Definition: router.php:48
$header