WebPHandler.php 8.9 KB

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