Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4099772
WebPHandler.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
11 KB
Referenced Files
None
Subscribers
None
WebPHandler.php
View Options
<?php
/**
* Handler for Google's WebP format <https://developers.google.com/speed/webp/>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Media
*/
use
MediaWiki\Logger\LoggerFactory
;
use
MediaWiki\MediaWikiServices
;
use
Wikimedia\XMPReader\Reader
as
XMPReader
;
/**
* Handler for Google's WebP format <https://developers.google.com/speed/webp/>
*
* @ingroup Media
*/
class
WebPHandler
extends
BitmapHandler
{
/**
* Value to store in img_metadata if there was an error extracting metadata
*/
private
const
BROKEN_FILE
=
'0'
;
/**
* Minimum chunk header size to be able to read all header types
*/
private
const
MINIMUM_CHUNK_HEADER_LENGTH
=
18
;
/**
* Max size of metadata chunk to extract
*/
private
const
MAX_METADATA_CHUNK_SIZE
=
1024
*
1024
*
2
;
/**
* Version of the metadata stored in db records
*/
private
const
_MW_WEBP_VERSION
=
2
;
private
const
VP8X_ICC
=
32
;
private
const
VP8X_ALPHA
=
16
;
private
const
VP8X_EXIF
=
8
;
private
const
VP8X_XMP
=
4
;
private
const
VP8X_ANIM
=
2
;
public
function
getSizeAndMetadata
(
$state
,
$filename
)
{
$parsedWebPData
=
self
::
extractMetadata
(
$filename
);
if
(
!
$parsedWebPData
)
{
return
[
'metadata'
=>
[
'_error'
=>
self
::
BROKEN_FILE
]
];
}
$parsedWebPData
[
'metadata'
][
'_MW_WEBP_VERSION'
]
=
self
::
_MW_WEBP_VERSION
;
$info
=
[
'width'
=>
$parsedWebPData
[
'width'
],
'height'
=>
$parsedWebPData
[
'height'
],
'metadata'
=>
$parsedWebPData
];
return
$info
;
}
public
function
getMetadataType
(
$image
)
{
return
'parsed-webp'
;
}
public
function
isFileMetadataValid
(
$image
)
{
$data
=
$image
->
getMetadataArray
();
if
(
$data
===
[
'_error'
=>
self
::
BROKEN_FILE
]
)
{
// Do not repetitivly regenerate metadata on broken file.
return
self
::
METADATA_GOOD
;
}
if
(
!
$data
||
!
isset
(
$data
[
'_error'
]
)
)
{
wfDebug
(
__METHOD__
.
" invalid WebP metadata"
);
return
self
::
METADATA_BAD
;
}
if
(
!
isset
(
$data
[
'metadata'
][
'_MW_WEBP_VERSION'
]
)
||
$data
[
'metadata'
][
'_MW_WEBP_VERSION'
]
!=
self
::
_MW_WEBP_VERSION
)
{
wfDebug
(
__METHOD__
.
" old but compatible WebP metadata"
);
return
self
::
METADATA_COMPATIBLE
;
}
return
self
::
METADATA_GOOD
;
}
/**
* Extracts the image size and WebP type from a file
*
* @param string $filename
* @return array|false Header data array with entries 'compression', 'width' and 'height',
* where 'compression' can be 'lossy', 'lossless', 'animated' or 'unknown'. False if
* file is not a valid WebP file.
*/
public
static
function
extractMetadata
(
$filename
)
{
wfDebugLog
(
'WebP'
,
__METHOD__
.
": Extracting metadata from $filename"
);
$info
=
RiffExtractor
::
findChunksFromFile
(
$filename
,
100
);
if
(
$info
===
false
)
{
wfDebugLog
(
'WebP'
,
__METHOD__
.
": Not a valid RIFF file"
);
return
false
;
}
if
(
$info
[
'fourCC'
]
!==
'WEBP'
)
{
wfDebugLog
(
'WebP'
,
__METHOD__
.
': FourCC was not WEBP: '
.
bin2hex
(
$info
[
'fourCC'
]
)
);
return
false
;
}
$metadata
=
self
::
extractMetadataFromChunks
(
$info
[
'chunks'
],
$filename
);
if
(
!
$metadata
)
{
wfDebugLog
(
'WebP'
,
__METHOD__
.
": No VP8 chunks found"
);
return
false
;
}
return
$metadata
;
}
/**
* Extracts the image size and WebP type from a file based on the chunk list
* @param array[] $chunks Chunks as extracted by RiffExtractor
* @param string $filename
* @return array Header data array with entries 'compression', 'width' and 'height', where
* 'compression' can be 'lossy', 'lossless', 'animated' or 'unknown'
*/
public
static
function
extractMetadataFromChunks
(
$chunks
,
$filename
)
{
$vp8Info
=
[];
$exifData
=
null
;
$xmpData
=
null
;
foreach
(
$chunks
as
$chunk
)
{
// Note, spec says it should be 'XMP ' but some real life files use "XMP\0"
if
(
!
in_array
(
$chunk
[
'fourCC'
],
[
'VP8 '
,
'VP8L'
,
'VP8X'
,
'EXIF'
,
'XMP '
,
"XMP
\0
"
]
)
)
{
// Not a chunk containing interesting metadata
continue
;
}
$chunkHeader
=
file_get_contents
(
$filename
,
false
,
null
,
$chunk
[
'start'
],
self
::
MINIMUM_CHUNK_HEADER_LENGTH
);
wfDebugLog
(
'WebP'
,
__METHOD__
.
": {$chunk['fourCC']}"
);
switch
(
$chunk
[
'fourCC'
]
)
{
case
'VP8 '
:
$vp8Info
=
array_merge
(
$vp8Info
,
self
::
decodeLossyChunkHeader
(
$chunkHeader
)
);
break
;
case
'VP8L'
:
$vp8Info
=
array_merge
(
$vp8Info
,
self
::
decodeLosslessChunkHeader
(
$chunkHeader
)
);
break
;
case
'VP8X'
:
$vp8Info
=
array_merge
(
$vp8Info
,
self
::
decodeExtendedChunkHeader
(
$chunkHeader
)
);
// Continue looking for other chunks to improve the metadata
break
;
case
'EXIF'
:
// Spec says ignore all but first one
$exifData
??=
self
::
extractChunk
(
$chunk
,
$filename
);
break
;
case
'XMP '
:
case
"XMP
\0
"
:
$xmpData
??=
self
::
extractChunk
(
$chunk
,
$filename
);
break
;
}
}
$vp8Info
=
array_merge
(
$vp8Info
,
self
::
decodeMediaMetadata
(
$exifData
,
$xmpData
,
$filename
)
);
return
$vp8Info
;
}
/**
* Decode metadata about the file (XMP & Exif).
*
* @param string|null $exifData Binary exif data from file
* @param string|null $xmpData XMP data from file
* @param string|null $filename (Used for logging only)
* @return array
*/
private
static
function
decodeMediaMetadata
(
$exifData
,
$xmpData
,
$filename
)
{
if
(
$exifData
===
null
&&
$xmpData
===
null
)
{
// Nothing to do
return
[];
}
$bitmapMetadataHandler
=
new
BitmapMetadataHandler
;
if
(
$xmpData
&&
XMPReader
::
isSupported
()
)
{
$xmpReader
=
new
XMPReader
(
LoggerFactory
::
getInstance
(
'XMP'
),
$filename
);
$xmpReader
->
parse
(
$xmpData
);
$res
=
$xmpReader
->
getResults
();
foreach
(
$res
as
$type
=>
$array
)
{
$bitmapMetadataHandler
->
addMetadata
(
$array
,
$type
);
}
}
if
(
$exifData
)
{
// The Exif section of a webp file is basically a tiff file without an image.
// Some files start with an Exif\0\0. This is wrong according to standard and
// will prevent us from reading file, so remove for compatibility.
if
(
substr
(
$exifData
,
0
,
6
)
===
"Exif
\x
00
\x
00"
)
{
$exifData
=
substr
(
$exifData
,
6
);
}
$tmpFile
=
MediaWikiServices
::
getInstance
()->
getTempFSFileFactory
()->
newTempFSFile
(
'webp-exif_'
,
'tiff'
);
$exifDataFile
=
$tmpFile
->
getPath
();
file_put_contents
(
$exifDataFile
,
$exifData
);
$byteOrder
=
BitmapMetadataHandler
::
getTiffByteOrder
(
$exifDataFile
);
$bitmapMetadataHandler
->
getExif
(
$exifDataFile
,
$byteOrder
);
}
return
[
'media-metadata'
=>
$bitmapMetadataHandler
->
getMetadataArray
()
];
}
/**
* @param array $chunk Information about chunk
* @param string $filename
* @return null|string Contents of chunk (excluding fourcc, size and padding)
*/
private
static
function
extractChunk
(
$chunk
,
$filename
)
{
if
(
$chunk
[
'size'
]
>
self
::
MAX_METADATA_CHUNK_SIZE
||
$chunk
[
'size'
]
<
1
)
{
return
null
;
}
// Skip first 8 bytes as that is the fourCC header followed by size of chunk.
return
file_get_contents
(
$filename
,
false
,
null
,
$chunk
[
'start'
]
+
8
,
$chunk
[
'size'
]
);
}
/**
* Decodes a lossy chunk header
* @param string $header First few bytes of the header, expected to be at least 18 bytes long
* @return bool|array See WebPHandler::decodeHeader
*/
protected
static
function
decodeLossyChunkHeader
(
$header
)
{
// Bytes 0-3 are 'VP8 '
// Bytes 4-7 are the VP8 stream size
// Bytes 8-10 are the frame tag
// Bytes 11-13 are 0x9D 0x01 0x2A called the sync code
$syncCode
=
substr
(
$header
,
11
,
3
);
if
(
$syncCode
!==
"
\x
9D
\x
01
\x
2A"
)
{
wfDebugLog
(
'WebP'
,
__METHOD__
.
': Invalid sync code: '
.
bin2hex
(
$syncCode
)
);
return
[];
}
// Bytes 14-17 are image size
$imageSize
=
unpack
(
'v2'
,
substr
(
$header
,
14
,
4
)
);
// Image sizes are 14 bit, 2 MSB are scaling parameters which are ignored here
return
[
'compression'
=>
'lossy'
,
'width'
=>
$imageSize
[
1
]
&
0x3FFF
,
'height'
=>
$imageSize
[
2
]
&
0x3FFF
];
}
/**
* Decodes a lossless chunk header
* @param string $header First few bytes of the header, expected to be at least 13 bytes long
* @return bool|array See WebPHandler::decodeHeader
*/
public
static
function
decodeLosslessChunkHeader
(
$header
)
{
// Bytes 0-3 are 'VP8L'
// Bytes 4-7 are chunk stream size
// Byte 8 is 0x2F called the signature
if
(
$header
[
8
]
!==
"
\x
2F"
)
{
wfDebugLog
(
'WebP'
,
__METHOD__
.
': Invalid signature: '
.
bin2hex
(
$header
[
8
]
)
);
return
[];
}
// Bytes 9-12 contain the image size
// Bits 0-13 are width-1; bits 15-27 are height-1
$imageSize
=
unpack
(
'C4'
,
substr
(
$header
,
9
,
4
)
);
return
[
'compression'
=>
'lossless'
,
'width'
=>
(
$imageSize
[
1
]
|
(
(
$imageSize
[
2
]
&
0x3F
)
<<
8
)
)
+
1
,
'height'
=>
(
(
(
$imageSize
[
2
]
&
0xC0
)
>>
6
)
|
(
$imageSize
[
3
]
<<
2
)
|
(
(
$imageSize
[
4
]
&
0x03
)
<<
10
)
)
+
1
];
}
/**
* Decodes an extended chunk header
* @param string $header First few bytes of the header, expected to be at least 18 bytes long
* @return bool|array See WebPHandler::decodeHeader
*/
public
static
function
decodeExtendedChunkHeader
(
$header
)
{
// Bytes 0-3 are 'VP8X'
// Byte 4-7 are chunk length
// Byte 8-11 are a flag bytes
$flags
=
unpack
(
'c'
,
substr
(
$header
,
8
,
1
)
);
// Byte 12-17 are image size (24 bits)
$width
=
unpack
(
'V'
,
substr
(
$header
,
12
,
3
)
.
"
\x
00"
);
$height
=
unpack
(
'V'
,
substr
(
$header
,
15
,
3
)
.
"
\x
00"
);
return
[
'compression'
=>
'unknown'
,
'animated'
=>
(
$flags
[
1
]
&
self
::
VP8X_ANIM
)
===
self
::
VP8X_ANIM
,
'transparency'
=>
(
$flags
[
1
]
&
self
::
VP8X_ALPHA
)
===
self
::
VP8X_ALPHA
,
'width'
=>
(
$width
[
1
]
&
0xFFFFFF
)
+
1
,
'height'
=>
(
$height
[
1
]
&
0xFFFFFF
)
+
1
];
}
/**
* @param File $file
* @return bool True, not all browsers support WebP
*/
public
function
mustRender
(
$file
)
{
return
true
;
}
/**
* @param File $file
* @return bool False if we are unable to render this image
*/
public
function
canRender
(
$file
)
{
if
(
$this
->
isAnimatedImage
(
$file
)
)
{
return
false
;
}
return
true
;
}
/**
* @param File $image
* @return bool
*/
public
function
isAnimatedImage
(
$image
)
{
$metadata
=
$image
->
getMetadataArray
();
if
(
isset
(
$metadata
[
'animated'
]
)
&&
$metadata
[
'animated'
]
===
true
)
{
return
true
;
}
return
false
;
}
public
function
canAnimateThumbnail
(
$file
)
{
return
false
;
}
/**
* Render files as PNG
*
* @param string $ext
* @param string $mime
* @param array|null $params
* @return array
*/
public
function
getThumbType
(
$ext
,
$mime
,
$params
=
null
)
{
return
[
'png'
,
'image/png'
];
}
protected
function
hasGDSupport
()
{
return
function_exists
(
'gd_info'
)
&&
(
gd_info
()[
'WebP Support'
]
??
false
);
}
public
function
getCommonMetaArray
(
File
$image
)
{
$meta
=
$image
->
getMetadataArray
();
return
$meta
[
'media-metadata'
]
??
[];
}
public
function
formatMetadata
(
$image
,
$context
=
false
)
{
$meta
=
$this
->
getCommonMetaArray
(
$image
);
if
(
!
$meta
)
{
return
false
;
}
return
$this
->
formatMetadataHelper
(
$meta
,
$context
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Tue, Aug 18, 16:04 (3 w, 5 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
8f/13/397e7ae6ecbe9dd7abc1dd9349d0
Default Alt Text
WebPHandler.php (11 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment