yoga/yoga/image/encoders/webp.py

121 lines
3.2 KiB
Python

import io
import struct
def little_endian_unint32_bytes_to_python_int(bytes_):
return struct.unpack("<L", bytes_)[0]
def get_riff_structure(data):
if data[0:4] != b"RIFF":
raise ValueError("Invalid RIFF: Not a RIFF file")
result = {
"formtype": data[8:12].decode(),
"size": little_endian_unint32_bytes_to_python_int(data[4:8]),
"chunks": [],
}
if result["size"] + 8 != len(data):
raise ValueError("Invalid RIFF: Truncated data")
offset = 12 # RIFF header length
while offset < len(data):
chunk = {
"type": data[offset : offset + 4].decode(),
"data_offset": offset + 8,
"size": little_endian_unint32_bytes_to_python_int(
data[offset + 4 : offset + 8]
),
}
result["chunks"].append(chunk)
offset += 8 + chunk["size"] + chunk["size"] % 2
return result
def get_vp8x_info(data):
# fmt: off
# RRILEXAR
VP8X_FLAG_ICC = 0b00100000 # noqa: E221
VP8X_FLAG_ALPHA = 0b00010000 # noqa: E221
VP8X_FLAG_EXIF = 0b00001000 # noqa: E221
VP8X_FLAG_XMP = 0b00000100 # noqa: E221
VP8X_FLAG_ANIM = 0b00000010 # noqa: E221
# fmt: on
if len(data) != 10:
ValueError("Invalid VP8X data")
return {
"has_icc": bool(data[0] & VP8X_FLAG_ICC),
"has_alpha": bool(data[0] & VP8X_FLAG_ALPHA),
"has_exif": bool(data[0] & VP8X_FLAG_EXIF),
"has_xmp": bool(data[0] & VP8X_FLAG_XMP),
"has_anim": bool(data[0] & VP8X_FLAG_ANIM),
"canvas_width": little_endian_unint32_bytes_to_python_int(
data[4:7] + b"\x00"
)
+ 1,
"canvas_height": little_endian_unint32_bytes_to_python_int(
data[7:10] + b"\x00"
)
+ 1,
}
def is_riff(file_bytes):
"""Whether or not the given bytes represent a RIFF file.
:params bytes file_bytes: The bytes of the file to check.
:rtype: bool
:return: ``True`` if the bytes represent a RIFF file, ``False`` else.
"""
return file_bytes.startswith(b"RIFF")
def is_lossy_webp(file_bytes):
"""Whether or not the given bytes represent a lossy WEBP file.
:params bytes file_bytes: The bytes of the file to check.
:rtype: bool
:return: ``True`` if the bytes represent a lossy WEBP file, ``False`` else.
"""
if not is_riff(file_bytes):
return False
riff = get_riff_structure(file_bytes)
if riff["formtype"] == "WEBP":
chunks = [chunk["type"] for chunk in riff["chunks"]]
if "VP8 " in chunks:
return True
return False
def optimize_lossy_webp(image, quality):
"""Encode image to lossy WEBP using Pillow.
:param PIL.Image image: The image to encode.
:param float quality: The output WEBP quality (from ``0.00``to ``1.00``).
:returns: The encoded image's bytes.
"""
if not 0.00 <= quality <= 1.00:
raise ValueError("WEBP quality value must be between 0.00 and 1.00")
image_io = io.BytesIO()
image.save(
image_io,
format="WEBP",
lossless=False,
quality=int(quality * 100),
method=6,
)
image_io.seek(0)
return image_io.read()