This commit is contained in:
AJ Slater 2026-05-16 18:28:10 +10:00 committed by GitHub
commit 920bcc55cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 59 additions and 1 deletions

View File

@ -4,7 +4,7 @@ from pathlib import Path
import pytest
from PIL import Image
from PIL import Image, WebPImagePlugin
from .helper import assert_image_equal, hopper
@ -26,3 +26,21 @@ def test_write_lossless_rgb(tmp_path: Path) -> None:
image.load()
assert_image_equal(image, hopper(RGB_MODE))
def test_is_lossless(tmp_path: Path) -> None:
lossless_file = tmp_path / "lossless.webp"
hopper(RGB_MODE).save(lossless_file, lossless=True)
with Image.open(lossless_file) as image:
assert isinstance(image, WebPImagePlugin.WebPImageFile)
assert image.is_lossless is True
lossy_file = tmp_path / "lossy.webp"
hopper(RGB_MODE).save(lossy_file, lossless=False)
with Image.open(lossy_file) as image:
assert isinstance(image, WebPImagePlugin.WebPImageFile)
assert image.is_lossless is False
with Image.open("Tests/images/hopper.webp") as image:
assert isinstance(image, WebPImagePlugin.WebPImageFile)
assert image.is_lossless is False

View File

@ -1375,6 +1375,13 @@ WebP
Pillow reads and writes WebP files. Requires libwebp v0.5.0 or later.
The :py:class:`~PIL.WebPImagePlugin.WebPImageFile` class also exposes the
following attribute:
**is_lossless**
``True`` if every coded frame in the file uses VP8L (lossless)
compression, ``False`` otherwise.
.. _webp-saving:
Saving

View File

@ -22,6 +22,38 @@ _VP8_MODES_BY_IDENTIFIER = {
}
def _is_lossless(data: bytes) -> bool:
# A WebP file is considered lossless when every coded frame uses the
# VP8L bitstream. See https://developers.google.com/speed/webp/docs/riff_container
chunk = data[12:16]
if chunk == b"VP8L":
return True
if chunk != b"VP8X":
return False
# Extended file format: walk the sub-chunks looking for any lossy frame.
pos = 12
found_frame = False
end = len(data)
while pos + 8 <= end:
fourcc = data[pos : pos + 4]
size = int.from_bytes(data[pos + 4 : pos + 8], "little")
pos += 8
if fourcc == b"VP8 ":
return False
if fourcc == b"VP8L":
found_frame = True
elif fourcc == b"ANMF" and pos + 20 <= end:
# ANMF: 16 bytes of frame info, then one VP8 / VP8L sub-chunk
sub = data[pos + 16 : pos + 20]
if sub == b"VP8 ":
return False
if sub == b"VP8L":
found_frame = True
pos += size + (size & 1) # RIFF chunks are padded to an even length
return found_frame
def _accept(prefix: bytes) -> bool | str:
is_riff_file_format = prefix.startswith(b"RIFF")
is_webp_file = prefix[8:12] == b"WEBP"
@ -51,6 +83,7 @@ class WebPImageFile(ImageFile.ImageFile):
# Use the newer AnimDecoder API to parse the (possibly) animated file,
# and access muxed chunks like ICC/EXIF/XMP.
self.is_lossless = _is_lossless(s)
self._decoder = _webp.WebPAnimDecoder(s)
# Get info from decoder