Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Capsule for WebP saving #8386

Merged
merged 11 commits into from
Sep 26, 2024
32 changes: 17 additions & 15 deletions Tests/test_file_webp.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def test_read_rgb(self) -> None:
def _roundtrip(
self, tmp_path: Path, mode: str, epsilon: float, args: dict[str, Any] = {}
) -> None:
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"

hopper(mode).save(temp_file, **args)
with Image.open(temp_file) as image:
Expand Down Expand Up @@ -116,7 +116,7 @@ def test_write_method(self, tmp_path: Path) -> None:
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()

def test_save_all(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
im = Image.new("RGB", (1, 1))
im2 = Image.new("RGB", (1, 1), "#f00")
im.save(temp_file, save_all=True, append_images=[im2])
Expand All @@ -127,6 +127,11 @@ def test_save_all(self, tmp_path: Path) -> None:
reloaded.seek(1)
assert_image_similar(im2, reloaded, 1)

def test_unsupported_image_mode(self) -> None:
im = Image.new("1", (1, 1))
with pytest.raises(ValueError):
_webp.WebPEncode(im.getim(), False, 0, 0, "", 4, 0, b"", "")

def test_icc_profile(self, tmp_path: Path) -> None:
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
self._roundtrip(
Expand All @@ -151,18 +156,16 @@ def test_write_unsupported_mode_P(self, tmp_path: Path) -> None:

@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_message(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
im = Image.new("RGB", (15000, 15000))
with pytest.raises(ValueError) as e:
im.save(temp_file, method=0)
im.save(tmp_path / "temp.webp", method=0)
assert str(e.value) == "encoding error 6"

@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
im = Image.new("L", (16384, 16384))
with pytest.raises(ValueError) as e:
im.save(temp_file)
im.save(tmp_path / "temp.webp")
assert (
str(e.value)
== "encoding error 5: Image size exceeds WebP limit of 16383 pixels"
Expand All @@ -187,9 +190,8 @@ def test_WebPAnimDecoder_with_invalid_args(self) -> None:
def test_no_resource_warning(self, tmp_path: Path) -> None:
file_path = "Tests/images/hopper.webp"
with Image.open(file_path) as image:
temp_file = str(tmp_path / "temp.webp")
with warnings.catch_warnings():
image.save(temp_file)
image.save(tmp_path / "temp.webp")

def test_file_pointer_could_be_reused(self) -> None:
file_path = "Tests/images/hopper.webp"
Expand All @@ -204,27 +206,27 @@ def test_file_pointer_could_be_reused(self) -> None:
def test_invalid_background(
self, background: int | tuple[int, ...], tmp_path: Path
) -> None:
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
im = hopper()
with pytest.raises(OSError):
im.save(temp_file, save_all=True, append_images=[im], background=background)

def test_background_from_gif(self, tmp_path: Path) -> None:
out_webp = tmp_path / "temp.webp"

# Save L mode GIF with background
with Image.open("Tests/images/no_palette_with_background.gif") as im:
out_webp = str(tmp_path / "temp.webp")
im.save(out_webp, save_all=True)

# Save P mode GIF with background
with Image.open("Tests/images/chi.gif") as im:
original_value = im.convert("RGB").getpixel((1, 1))

# Save as WEBP
out_webp = str(tmp_path / "temp.webp")
im.save(out_webp, save_all=True)

# Save as GIF
out_gif = str(tmp_path / "temp.gif")
out_gif = tmp_path / "temp.gif"
with Image.open(out_webp) as im:
im.save(out_gif)

Expand All @@ -234,18 +236,18 @@ def test_background_from_gif(self, tmp_path: Path) -> None:
assert difference < 5

def test_duration(self, tmp_path: Path) -> None:
out_webp = tmp_path / "temp.webp"

with Image.open("Tests/images/dispose_bgnd.gif") as im:
assert im.info["duration"] == 1000

out_webp = str(tmp_path / "temp.webp")
im.save(out_webp, save_all=True)

with Image.open(out_webp) as reloaded:
reloaded.load()
assert reloaded.info["duration"] == 1000

def test_roundtrip_rgba_palette(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
im = Image.new("RGBA", (1, 1)).convert("P")
assert im.mode == "P"
assert im.palette is not None
Expand Down
45 changes: 13 additions & 32 deletions src/PIL/WebPImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@
SUPPORTED = False


_VALID_WEBP_MODES = {"RGBX": True, "RGBA": True, "RGB": True}

_VALID_WEBP_LEGACY_MODES = {"RGB": True, "RGBA": True}

_VP8_MODES_BY_IDENTIFIER = {
b"VP8 ": "RGB",
b"VP8X": "RGBA",
Expand Down Expand Up @@ -153,6 +149,13 @@ def tell(self) -> int:
return self.__logical_frame


def _convert_frame(im: Image.Image) -> Image.Image:
# Make sure image mode is supported
if im.mode not in ("RGBX", "RGBA", "RGB"):
im = im.convert("RGBA" if im.has_transparency_data else "RGB")
return im


def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
encoderinfo = im.encoderinfo.copy()
append_images = list(encoderinfo.get("append_images", []))
Expand Down Expand Up @@ -243,31 +246,13 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:

for idx in range(nfr):
ims.seek(idx)
ims.load()

# Make sure image mode is supported
frame = ims
rawmode = ims.mode
if ims.mode not in _VALID_WEBP_MODES:
alpha = (
"A" in ims.mode
or "a" in ims.mode
or (ims.mode == "P" and "A" in ims.im.getpalettemode())
)
rawmode = "RGBA" if alpha else "RGB"
frame = ims.convert(rawmode)

if rawmode == "RGB":
# For faster conversion, use RGBX
rawmode = "RGBX"

frame = _convert_frame(ims)

# Append the frame to the animation encoder
enc.add(
frame.tobytes("raw", rawmode),
frame.getim(),
round(timestamp),
frame.size[0],
frame.size[1],
rawmode,
lossless,
quality,
alpha_quality,
Expand All @@ -285,7 +270,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
im.seek(cur_idx)

# Force encoder to flush frames
enc.add(None, round(timestamp), 0, 0, "", lossless, quality, alpha_quality, 0)
enc.add(None, round(timestamp), lossless, quality, alpha_quality, 0)

# Get the final output from the encoder
data = enc.assemble(icc_profile, exif, xmp)
Expand All @@ -310,17 +295,13 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
method = im.encoderinfo.get("method", 4)
exact = 1 if im.encoderinfo.get("exact") else 0

if im.mode not in _VALID_WEBP_LEGACY_MODES:
im = im.convert("RGBA" if im.has_transparency_data else "RGB")
im = _convert_frame(im)

data = _webp.WebPEncode(
im.tobytes(),
im.size[0],
im.size[1],
im.getim(),
lossless,
float(quality),
float(alpha_quality),
im.mode,
icc_profile,
method,
exact,
Expand Down
Loading
Loading