Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion Tests/test_file_avif.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
assert_image_similar_tofile,
hopper,
skip_unless_feature,
skip_unless_feature_version,
)

try:
Expand All @@ -46,7 +47,7 @@ def assert_xmp_orientation(xmp: bytes, expected: int) -> None:
assert int(xmp.split(b'tiff:Orientation="')[1].split(b'"')[0]) == expected


def roundtrip(im: ImageFile.ImageFile, **options: Any) -> ImageFile.ImageFile:
def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile:
out = BytesIO()
im.save(out, "AVIF", **options)
return Image.open(out)
Expand Down Expand Up @@ -128,6 +129,14 @@ def test_read(self) -> None:
image, "Tests/images/avif/hopper_avif_write.png", 11.5
)

@skip_unless_feature_version("avif", "1.3.0")
def test_write_l(self) -> None:
im = hopper("L")
reloaded = roundtrip(im)

assert reloaded.mode == "L"
assert_image_similar(reloaded, im, 1.67)

def test_write_rgb(self, tmp_path: Path) -> None:
"""
Can we write a RGB mode file to avif without error?
Expand Down Expand Up @@ -420,6 +429,14 @@ def test_encoder_subsampling(self, tmp_path: Path, subsampling: str) -> None:
test_file = tmp_path / "temp.avif"
im.save(test_file, subsampling=subsampling)

@skip_unless_feature_version("avif", "1.3.0")
def test_encoding_subsampling_400(self) -> None:
im = hopper()
reloaded = roundtrip(im, subsampling="4:0:0")

assert reloaded.mode == "L"
assert_image_similar(reloaded, im.convert("L"), 1.67)

def test_encoder_subsampling_invalid(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
Expand Down
3 changes: 2 additions & 1 deletion docs/handbook/image-file-formats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
quality, 100 the largest size and best quality.

**subsampling**
If present, sets the subsampling for the encoder. Defaults to ``4:2:0``.
If present, sets the subsampling for the encoder. If absent, and all frames are in
grayscale mode without alpha, ``4:0:0`` is used. Otherwise defaults to ``4:2:0``.
Options include:

* ``4:0:0``
Expand Down
37 changes: 21 additions & 16 deletions src/PIL/AvifImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from io import BytesIO
from typing import IO

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

try:
from . import _avif
Expand Down Expand Up @@ -153,17 +153,23 @@ def _save(
else:
append_images = []

total = 0
grayscale = True
grayscale_modes = {"1", "L", "I", "I;16", "I;16L", "I;16B", "I;16N", "F"}
for ims in [im] + append_images:
total += getattr(ims, "n_frames", 1)
for frame in ImageSequence.Iterator(ims):
if frame.mode not in grayscale_modes:
grayscale = False
break
if not grayscale:
break

quality = info.get("quality", 75)
if not isinstance(quality, int) or quality < 0 or quality > 100:
msg = "Invalid quality setting"
raise ValueError(msg)

duration = info.get("duration", 0)
subsampling = info.get("subsampling", "4:2:0")
subsampling = info.get("subsampling", "4:0:0" if grayscale else "4:2:0")
speed = info.get("speed", 6)
max_threads = info.get("max_threads", _get_default_max_threads())
codec = info.get("codec", "auto")
Expand Down Expand Up @@ -236,21 +242,20 @@ def _save(
frame_idx = 0
frame_duration = 0
cur_idx = im.tell()
is_single_frame = total == 1
is_single_frame = not append_images and not getattr(im, "is_animated", False)
try:
for ims in [im] + append_images:
# Get number of frames in this image
nfr = getattr(ims, "n_frames", 1)

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

for frame in ImageSequence.Iterator(ims):
# Make sure image mode is supported
frame = ims
rawmode = ims.mode
if ims.mode not in {"RGB", "RGBA"}:
rawmode = "RGBA" if ims.has_transparency_data else "RGB"
frame = ims.convert(rawmode)
rawmode = frame.mode
if ims.mode not in {"L", "RGB", "RGBA"}:
if ims.has_transparency_data:
rawmode = "RGBA"
elif ims.mode in grayscale_modes:
rawmode = "L"
else:
rawmode = "RGB"
frame = frame.convert(rawmode)

# Update frame duration
if isinstance(duration, (list, tuple)):
Expand Down
35 changes: 29 additions & 6 deletions src/_avif.c
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,10 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) {

if (strcmp(mode, "RGBA") == 0) {
rgb.format = AVIF_RGB_FORMAT_RGBA;
#if AVIF_VERSION >= 1030000 // 1.3.0
} else if (strcmp(mode, "L") == 0) {
rgb.format = AVIF_RGB_FORMAT_GRAY;
#endif
} else {
rgb.format = AVIF_RGB_FORMAT_RGB;
}
Expand Down Expand Up @@ -706,25 +710,36 @@ _decoder_get_info(AvifDecoderObject *self) {
PyObject *xmp = NULL;
PyObject *ret = NULL;

if (image->xmp.size) {
xmp = PyBytes_FromStringAndSize((const char *)image->xmp.data, image->xmp.size);
char *mode;
if (decoder->alphaPresent) {
mode = "RGBA";
#if AVIF_VERSION >= 1030000 // 1.3.0
} else if (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) {
mode = "L";
#endif
} else {
mode = "RGB";
}

if (image->icc.size) {
icc = PyBytes_FromStringAndSize((const char *)image->icc.data, image->icc.size);
}

if (image->exif.size) {
exif =
PyBytes_FromStringAndSize((const char *)image->exif.data, image->exif.size);
}

if (image->icc.size) {
icc = PyBytes_FromStringAndSize((const char *)image->icc.data, image->icc.size);
if (image->xmp.size) {
xmp = PyBytes_FromStringAndSize((const char *)image->xmp.data, image->xmp.size);
}

ret = Py_BuildValue(
"(II)IsSSIS",
image->width,
image->height,
decoder->imageCount,
decoder->alphaPresent ? "RGBA" : "RGB",
mode,
NULL == icc ? Py_None : icc,
NULL == exif ? Py_None : exif,
irot_imir_to_exif_orientation(image),
Expand Down Expand Up @@ -771,7 +786,15 @@ _decoder_get_frame(AvifDecoderObject *self, PyObject *args) {
avifRGBImageSetDefaults(&rgb, image);

rgb.depth = 8;
rgb.format = decoder->alphaPresent ? AVIF_RGB_FORMAT_RGBA : AVIF_RGB_FORMAT_RGB;
if (decoder->alphaPresent) {
rgb.format = AVIF_RGB_FORMAT_RGBA;
#if AVIF_VERSION >= 1030000 // 1.3.0
} else if (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) {
rgb.format = AVIF_RGB_FORMAT_GRAY;
#endif
} else {
rgb.format = AVIF_RGB_FORMAT_RGB;
}

result = avifRGBImageAllocatePixels(&rgb);
if (result != AVIF_RESULT_OK) {
Expand Down
Loading