2023-12-21 14:13:31 +03:00
|
|
|
from __future__ import annotations
|
2024-01-20 14:23:03 +03:00
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
from pathlib import Path
|
|
|
|
|
2020-01-05 00:07:59 +03:00
|
|
|
import pytest
|
2022-01-23 00:56:14 +03:00
|
|
|
from packaging.version import parse as parse_version
|
2020-08-07 13:28:33 +03:00
|
|
|
|
2022-01-23 00:56:14 +03:00
|
|
|
from PIL import Image, features
|
2017-09-27 06:27:40 +03:00
|
|
|
|
2020-01-30 17:56:07 +03:00
|
|
|
from .helper import (
|
|
|
|
assert_image_equal,
|
|
|
|
assert_image_similar,
|
|
|
|
is_big_endian,
|
2020-02-18 01:03:32 +03:00
|
|
|
skip_unless_feature,
|
2020-01-30 17:56:07 +03:00
|
|
|
)
|
2019-07-06 23:40:53 +03:00
|
|
|
|
2020-02-19 20:26:52 +03:00
|
|
|
pytestmark = [
|
|
|
|
skip_unless_feature("webp"),
|
|
|
|
skip_unless_feature("webp_anim"),
|
|
|
|
]
|
2017-09-27 06:27:40 +03:00
|
|
|
|
2019-11-25 23:03:23 +03:00
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_n_frames() -> None:
|
2020-02-18 16:50:34 +03:00
|
|
|
"""Ensure that WebP format sets n_frames and is_animated attributes correctly."""
|
|
|
|
|
|
|
|
with Image.open("Tests/images/hopper.webp") as im:
|
|
|
|
assert im.n_frames == 1
|
|
|
|
assert not im.is_animated
|
|
|
|
|
|
|
|
with Image.open("Tests/images/iss634.webp") as im:
|
|
|
|
assert im.n_frames == 42
|
|
|
|
assert im.is_animated
|
|
|
|
|
2019-11-25 23:03:23 +03:00
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_write_animation_L(tmp_path: Path) -> None:
|
2020-02-18 16:50:34 +03:00
|
|
|
"""
|
|
|
|
Convert an animated GIF to animated WebP, then compare the frame count, and first
|
|
|
|
and last frames to ensure they're visually similar.
|
|
|
|
"""
|
|
|
|
|
|
|
|
with Image.open("Tests/images/iss634.gif") as orig:
|
|
|
|
assert orig.n_frames > 1
|
|
|
|
|
|
|
|
temp_file = str(tmp_path / "temp.webp")
|
|
|
|
orig.save(temp_file, save_all=True)
|
2019-11-25 23:03:23 +03:00
|
|
|
with Image.open(temp_file) as im:
|
2020-02-18 16:50:34 +03:00
|
|
|
assert im.n_frames == orig.n_frames
|
|
|
|
|
|
|
|
# Compare first and last frames to the original animated GIF
|
|
|
|
orig.load()
|
|
|
|
im.load()
|
2021-03-16 16:24:57 +03:00
|
|
|
assert_image_similar(im, orig.convert("RGBA"), 32.9)
|
2022-01-23 00:56:14 +03:00
|
|
|
|
|
|
|
if is_big_endian():
|
2024-05-29 15:51:02 +03:00
|
|
|
version = features.version_module("webp")
|
|
|
|
assert version is not None
|
|
|
|
if parse_version(version) < parse_version("1.2.2"):
|
2022-01-23 23:47:44 +03:00
|
|
|
pytest.skip("Fails with libwebp earlier than 1.2.2")
|
2020-02-18 16:50:34 +03:00
|
|
|
orig.seek(orig.n_frames - 1)
|
|
|
|
im.seek(im.n_frames - 1)
|
|
|
|
orig.load()
|
|
|
|
im.load()
|
2021-03-16 16:24:57 +03:00
|
|
|
assert_image_similar(im, orig.convert("RGBA"), 32.9)
|
2020-02-18 16:50:34 +03:00
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_write_animation_RGB(tmp_path: Path) -> None:
|
2020-02-18 16:50:34 +03:00
|
|
|
"""
|
|
|
|
Write an animated WebP from RGB frames, and ensure the frames
|
|
|
|
are visually similar to the originals.
|
|
|
|
"""
|
|
|
|
|
2024-06-05 15:27:23 +03:00
|
|
|
def check(temp_file: str) -> None:
|
2020-02-18 16:50:34 +03:00
|
|
|
with Image.open(temp_file) as im:
|
|
|
|
assert im.n_frames == 2
|
|
|
|
|
|
|
|
# Compare first frame to original
|
|
|
|
im.load()
|
|
|
|
assert_image_equal(im, frame1.convert("RGBA"))
|
|
|
|
|
|
|
|
# Compare second frame to original
|
2022-01-23 00:56:14 +03:00
|
|
|
if is_big_endian():
|
2024-05-29 15:51:02 +03:00
|
|
|
version = features.version_module("webp")
|
|
|
|
assert version is not None
|
|
|
|
if parse_version(version) < parse_version("1.2.2"):
|
2022-01-23 23:47:44 +03:00
|
|
|
pytest.skip("Fails with libwebp earlier than 1.2.2")
|
2020-02-18 16:50:34 +03:00
|
|
|
im.seek(1)
|
|
|
|
im.load()
|
|
|
|
assert_image_equal(im, frame2.convert("RGBA"))
|
|
|
|
|
|
|
|
with Image.open("Tests/images/anim_frame1.webp") as frame1:
|
|
|
|
with Image.open("Tests/images/anim_frame2.webp") as frame2:
|
|
|
|
temp_file1 = str(tmp_path / "temp.webp")
|
|
|
|
frame1.copy().save(
|
|
|
|
temp_file1, save_all=True, append_images=[frame2], lossless=True
|
|
|
|
)
|
|
|
|
check(temp_file1)
|
|
|
|
|
|
|
|
# Tests appending using a generator
|
2022-04-10 22:17:35 +03:00
|
|
|
def im_generator(ims):
|
2020-02-18 16:50:34 +03:00
|
|
|
yield from ims
|
|
|
|
|
|
|
|
temp_file2 = str(tmp_path / "temp_generator.webp")
|
|
|
|
frame1.copy().save(
|
|
|
|
temp_file2,
|
|
|
|
save_all=True,
|
2022-04-10 22:17:35 +03:00
|
|
|
append_images=im_generator([frame2]),
|
2020-02-18 16:50:34 +03:00
|
|
|
lossless=True,
|
|
|
|
)
|
|
|
|
check(temp_file2)
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_timestamp_and_duration(tmp_path: Path) -> None:
|
2020-02-18 16:50:34 +03:00
|
|
|
"""
|
|
|
|
Try passing a list of durations, and make sure the encoded
|
|
|
|
timestamps and durations are correct.
|
|
|
|
"""
|
|
|
|
|
|
|
|
durations = [0, 10, 20, 30, 40]
|
|
|
|
temp_file = str(tmp_path / "temp.webp")
|
|
|
|
with Image.open("Tests/images/anim_frame1.webp") as frame1:
|
|
|
|
with Image.open("Tests/images/anim_frame2.webp") as frame2:
|
|
|
|
frame1.save(
|
|
|
|
temp_file,
|
|
|
|
save_all=True,
|
|
|
|
append_images=[frame2, frame1, frame2, frame1],
|
|
|
|
duration=durations,
|
|
|
|
)
|
|
|
|
|
|
|
|
with Image.open(temp_file) as im:
|
|
|
|
assert im.n_frames == 5
|
|
|
|
assert im.is_animated
|
|
|
|
|
|
|
|
# Check that timestamps and durations match original values specified
|
|
|
|
ts = 0
|
|
|
|
for frame in range(im.n_frames):
|
|
|
|
im.seek(frame)
|
|
|
|
im.load()
|
|
|
|
assert im.info["duration"] == durations[frame]
|
|
|
|
assert im.info["timestamp"] == ts
|
|
|
|
ts += durations[frame]
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_float_duration(tmp_path: Path) -> None:
|
2023-03-07 10:07:46 +03:00
|
|
|
temp_file = str(tmp_path / "temp.webp")
|
|
|
|
with Image.open("Tests/images/iss634.apng") as im:
|
|
|
|
assert im.info["duration"] == 70.0
|
|
|
|
|
|
|
|
im.save(temp_file, save_all=True)
|
|
|
|
|
|
|
|
with Image.open(temp_file) as reloaded:
|
|
|
|
reloaded.load()
|
|
|
|
assert reloaded.info["duration"] == 70
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_seeking(tmp_path: Path) -> None:
|
2020-02-18 16:50:34 +03:00
|
|
|
"""
|
|
|
|
Create an animated WebP file, and then try seeking through frames in reverse-order,
|
|
|
|
verifying the timestamps and durations are correct.
|
|
|
|
"""
|
|
|
|
|
|
|
|
dur = 33
|
|
|
|
temp_file = str(tmp_path / "temp.webp")
|
|
|
|
with Image.open("Tests/images/anim_frame1.webp") as frame1:
|
|
|
|
with Image.open("Tests/images/anim_frame2.webp") as frame2:
|
|
|
|
frame1.save(
|
|
|
|
temp_file,
|
|
|
|
save_all=True,
|
|
|
|
append_images=[frame2, frame1, frame2, frame1],
|
|
|
|
duration=dur,
|
|
|
|
)
|
|
|
|
|
|
|
|
with Image.open(temp_file) as im:
|
|
|
|
assert im.n_frames == 5
|
|
|
|
assert im.is_animated
|
|
|
|
|
|
|
|
# Traverse frames in reverse, checking timestamps and durations
|
|
|
|
ts = dur * (im.n_frames - 1)
|
|
|
|
for frame in reversed(range(im.n_frames)):
|
|
|
|
im.seek(frame)
|
|
|
|
im.load()
|
|
|
|
assert im.info["duration"] == dur
|
|
|
|
assert im.info["timestamp"] == ts
|
|
|
|
ts -= dur
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_seek_errors() -> None:
|
2020-02-18 16:50:34 +03:00
|
|
|
with Image.open("Tests/images/iss634.webp") as im:
|
|
|
|
with pytest.raises(EOFError):
|
|
|
|
im.seek(-1)
|
|
|
|
|
|
|
|
with pytest.raises(EOFError):
|
|
|
|
im.seek(42)
|
2024-03-13 10:55:26 +03:00
|
|
|
|
|
|
|
|
|
|
|
def test_alpha_quality(tmp_path: Path) -> None:
|
|
|
|
with Image.open("Tests/images/transparent.png") as im:
|
|
|
|
first_frame = Image.new("L", im.size)
|
|
|
|
|
|
|
|
out = str(tmp_path / "temp.webp")
|
|
|
|
first_frame.save(out, save_all=True, append_images=[im])
|
|
|
|
|
|
|
|
out_quality = str(tmp_path / "quality.webp")
|
|
|
|
first_frame.save(
|
|
|
|
out_quality, save_all=True, append_images=[im], alpha_quality=50
|
|
|
|
)
|
|
|
|
with Image.open(out) as reloaded:
|
|
|
|
reloaded.seek(1)
|
|
|
|
with Image.open(out_quality) as reloaded_quality:
|
|
|
|
reloaded_quality.seek(1)
|
|
|
|
assert reloaded.tobytes() != reloaded_quality.tobytes()
|