Skip to content

Commit

Permalink
fixup
Browse files Browse the repository at this point in the history
  • Loading branch information
fdintino committed Aug 14, 2024
1 parent b4f3fb4 commit 66270ff
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 55 deletions.
Binary file added Tests/images/avif/chimera-missing-pixi.avif
Binary file not shown.
70 changes: 45 additions & 25 deletions Tests/test_file_avif.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import pytest

from PIL import AvifImagePlugin, Image, UnidentifiedImageError, features
from PIL import AvifImagePlugin, Image, ImageDraw, UnidentifiedImageError, features

from .helper import (
PillowLeakTestCase,
Expand All @@ -18,6 +18,7 @@
assert_image_similar_tofile,
hopper,
skip_unless_feature,
skip_unless_feature_version,
)

try:
Expand Down Expand Up @@ -72,17 +73,6 @@ def is_docker_qemu():
return "qemu" in init_proc_exe


def skip_unless_avif_version_gte(version):
if not _avif:
reason = "AVIF unavailable"
should_skip = True
else:
version_str = ".".join([str(v) for v in version])
reason = f"{_avif.libavif_version} < {version_str}"
should_skip = _avif.VERSION < version
return pytest.mark.skipif(should_skip, reason=reason)


def has_alpha_premultiplied(im_bytes):
stream = BytesIO(im_bytes)
length = len(im_bytes)
Expand Down Expand Up @@ -240,7 +230,7 @@ def test_background_from_gif(self, tmp_path):
difference = sum(
[abs(original_value[i] - reread_value[i]) for i in range(0, 3)]
)
assert difference < 12
assert difference < 5

def test_save_single_frame(self, tmp_path):
temp_file = str(tmp_path / "temp.avif")
Expand Down Expand Up @@ -449,7 +439,6 @@ def test_encoder_codec_cannot_encode(self, tmp_path):
im.save(test_file, codec="dav1d")

@skip_unless_avif_encoder("aom")
@skip_unless_avif_version_gte((0, 8, 2))
@skip_unless_feature("avif")
def test_encoder_advanced_codec_options(self):
with Image.open(TEST_AVIF_FILE) as im:
Expand All @@ -468,7 +457,6 @@ def test_encoder_advanced_codec_options(self):
assert ctrl_buf.getvalue() != test_buf.getvalue()

@skip_unless_avif_encoder("aom")
@skip_unless_avif_version_gte((0, 8, 2))
@skip_unless_feature("avif")
@pytest.mark.parametrize("val", [{"foo": "bar"}, 1234])
def test_encoder_advanced_codec_options_invalid(self, tmp_path, val):
Expand Down Expand Up @@ -524,6 +512,7 @@ def test_encoder_codec_available_cannot_decode(self):
def test_encoder_codec_available_invalid(self):
assert _avif.encoder_codec_available("foo") is False

@skip_unless_feature_version("avif", "1.0.0")
@pytest.mark.parametrize(
"quality,expected_qminmax",
[
Expand All @@ -535,15 +524,16 @@ def test_encoder_codec_available_invalid(self):
],
)
def test_encoder_quality_qmin_qmax_map(self, tmp_path, quality, expected_qminmax):
MockEncoder = mock.Mock(wraps=_avif.AvifEncoder)
with mock.patch.object(_avif, "AvifEncoder", new=MockEncoder) as mock_encoder:
with Image.open("Tests/images/avif/hopper.avif") as im:
test_file = str(tmp_path / "temp.avif")
if quality is None:
im.save(test_file)
else:
im.save(test_file, quality=quality)
assert mock_encoder.call_args[0][3:5] == expected_qminmax
qmin, qmax = expected_qminmax
with Image.open("Tests/images/avif/hopper.avif") as im:
out_quality = BytesIO()
out_qminmax = BytesIO()
im.save(out_qminmax, "AVIF", qmin=qmin, qmax=qmax)
if quality is None:
im.save(out_quality, "AVIF")
else:
im.save(out_quality, "AVIF", quality=quality)
assert len(out_quality.getvalue()) == len(out_qminmax.getvalue())

def test_encoder_quality_valueerror(self, tmp_path):
with Image.open("Tests/images/avif/hopper.avif") as im:
Expand Down Expand Up @@ -586,6 +576,37 @@ def test_decoder_upsampling_invalid(self):
finally:
AvifImagePlugin.CHROMA_UPSAMPLING = "auto"

def test_p_mode_transparency(self):
im = Image.new("P", size=(64, 64))
draw = ImageDraw.Draw(im)
draw.rectangle(xy=[(0, 0), (32, 32)], fill=255)
draw.rectangle(xy=[(32, 32), (64, 64)], fill=255)

buf_png = BytesIO()
im.save(buf_png, format="PNG", transparency=0)
im_png = Image.open(buf_png)
buf_out = BytesIO()
im_png.save(buf_out, format="AVIF", quality=100)

assert_image_similar(im_png.convert("RGBA"), Image.open(buf_out), 1)

def test_decoder_strict_flags(self):
# This would fail if full avif strictFlags were enabled
with Image.open("Tests/images/avif/chimera-missing-pixi.avif") as im:
assert im.size == (480, 270)

@skip_unless_avif_encoder("aom")
def test_aom_optimizations(self):
im = hopper("RGB")
buf = BytesIO()
im.save(buf, format="AVIF", codec="aom", speed=1)

@skip_unless_avif_encoder("svt")
def test_svt_optimizations(self):
im = hopper("RGB")
buf = BytesIO()
im.save(buf, format="AVIF", codec="svt", speed=1)


@skip_unless_feature("avif")
class TestAvifAnimation:
Expand Down Expand Up @@ -685,7 +706,6 @@ def test_heif_raises_unidentified_image_error(self):
with Image.open("Tests/images/avif/rgba10.heif"):
pass

@skip_unless_avif_version_gte((0, 9, 0))
@pytest.mark.parametrize("alpha_premultipled", [False, True])
def test_alpha_premultiplied_true(self, alpha_premultipled):
im = Image.new("RGBA", (10, 10), (0, 0, 0, 0))
Expand Down
52 changes: 32 additions & 20 deletions src/PIL/AvifImagePlugin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from io import BytesIO

from . import Image, ImageFile
from . import ExifTags, Image, ImageFile

try:
from . import _avif
Expand All @@ -13,6 +13,7 @@
# to Image.open (see https://github.com/python-pillow/Pillow/issues/569)
DECODE_CODEC_CHOICE = "auto"
CHROMA_UPSAMPLING = "auto"
DEFAULT_MAX_THREADS = 0

_VALID_AVIF_MODES = {"RGB", "RGBA"}

Expand Down Expand Up @@ -47,9 +48,12 @@ class AvifImageFile(ImageFile.ImageFile):
__loaded = -1
__frame = 0

def load_seek(self, pos: int) -> None:
pass

def _open(self):
self._decoder = _avif.AvifDecoder(
self.fp.read(), DECODE_CODEC_CHOICE, CHROMA_UPSAMPLING
self.fp.read(), DECODE_CODEC_CHOICE, CHROMA_UPSAMPLING, DEFAULT_MAX_THREADS
)

# Get info from decoder
Expand Down Expand Up @@ -114,28 +118,16 @@ def _save(im, fp, filename, save_all=False):

is_single_frame = total == 1

qmin = info.get("qmin")
qmax = info.get("qmax")

if qmin is None and qmax is None:
# The min and max quantizer settings in libavif range from 0 (best quality)
# to 63 (worst quality). If neither are explicitly specified, we use a 0-100
# quality scale (default 75) and calculate the qmin and qmax from that.
#
# - qmin is 0 for quality >= 64. Below that, qmin has an inverse linear
# relation to quality (i.e., quality 63 = qmin 1, quality 0 => qmin 63)
# - qmax is 0 for quality=100, then qmax increases linearly relative to
# quality decreasing, until it flattens out at quality=37.
quality = info.get("quality", 75)
if not isinstance(quality, int) or quality < 0 or quality > 100:
msg = "Invalid quality setting"
raise ValueError(msg)
qmin = max(0, min(64 - quality, 63))
qmax = max(0, min(100 - quality, 63))
qmin = info.get("qmin", -1)
qmax = info.get("qmax", -1)
quality = info.get("quality", 75)
if not isinstance(quality, int) or quality < 0 or quality > 100:
raise ValueError("Invalid quality setting")

duration = info.get("duration", 0)
subsampling = info.get("subsampling", "4:2:0")
speed = info.get("speed", 6)
max_threads = info.get("max_threads", DEFAULT_MAX_THREADS)
codec = info.get("codec", "auto")
range_ = info.get("range", "full")
tile_rows_log2 = info.get("tile_rows", 0)
Expand All @@ -147,6 +139,20 @@ def _save(im, fp, filename, save_all=False):
exif = info.get("exif", im.info.get("exif"))
if isinstance(exif, Image.Exif):
exif = exif.tobytes()

exif_orientation = 0
if exif:
exif_data = Image.Exif()
try:
exif_data.load(exif)
except SyntaxError:
pass
else:
orientation_tag = next(
k for k, v in ExifTags.TAGS.items() if v == "Orientation"
)
exif_orientation = exif_data.get(orientation_tag) or 0

xmp = info.get("xmp", im.info.get("xmp") or im.info.get("XML:com.adobe.xmp"))

if isinstance(xmp, str):
Expand Down Expand Up @@ -181,6 +187,7 @@ def _save(im, fp, filename, save_all=False):
qmax,
quality,
speed,
max_threads,
codec,
range_,
tile_rows_log2,
Expand All @@ -189,6 +196,7 @@ def _save(im, fp, filename, save_all=False):
autotiling,
icc_profile or b"",
exif or b"",
exif_orientation,
xmp or b"",
advanced,
)
Expand All @@ -214,6 +222,10 @@ def _save(im, fp, filename, save_all=False):
"A" in ims.mode
or "a" in ims.mode
or (ims.mode == "P" and "A" in ims.im.getpalettemode())
or (
ims.mode == "P"
and ims.info.get("transparency", None) is not None
)
)
rawmode = "RGBA" if alpha else "RGB"
frame = ims.convert(rawmode)
Expand Down
Loading

0 comments on commit 66270ff

Please sign in to comment.