Skip to content

Add AVIF plugin (decoder + encoder using libavif) #5201

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

Merged
merged 64 commits into from
Apr 1, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
3878b58
Add AVIF plugin (using libavif)
fdintino Jan 3, 2021
e2add24
Added type hints (#2)
radarhere Oct 16, 2024
e5494a2
Fix PLAT envvar in cibuildwheel container
fdintino Oct 16, 2024
8b8bbba
Update Tests/check_avif_leaks.py
fdintino Oct 18, 2024
58ef692
Simplified code (#3)
radarhere Oct 19, 2024
d6a0a15
Merge branch 'main' into libavif-plugin
radarhere Nov 8, 2024
50b993a
Set default max threads in Python (#4)
radarhere Nov 11, 2024
671e3c8
Removed unused upsampling setting (#5)
radarhere Nov 12, 2024
658cdf3
Merge branch 'main' into libavif-plugin
radarhere Nov 18, 2024
7225cb9
Merge branch 'main' into libavif-plugin
radarhere Nov 24, 2024
3730bf2
Merge branch 'main' into libavif-plugin
radarhere Nov 30, 2024
c40bcbf
Improved error handling
radarhere Dec 2, 2024
d76ae2f
Do not ignore SyntaxError when saving EXIF data (#8)
radarhere Dec 7, 2024
9ad8311
Allow libavif to install rav1e, except on manylinux2014 and aarch64 (#7)
radarhere Dec 7, 2024
de4c6c1
Removed ld64 flag (#6)
radarhere Dec 7, 2024
524d802
fix: set exif orientation from irot/imir when decoding AVIF
fdintino Dec 9, 2024
7b73d77
Merge branch 'main' into libavif-plugin
radarhere Dec 13, 2024
a56acd8
Removed unittest mock (#10)
radarhere Dec 13, 2024
f5dc957
Use cmds_cmake (#9)
radarhere Dec 13, 2024
8d77678
chore(docs): update quality and speed with correct defaults
fdintino Dec 13, 2024
4eaa6b7
Merge branch 'main' into libavif-plugin
radarhere Dec 14, 2024
bdb24f9
Removed `_avif.HAVE_AVIF` and `_avif.VERSION` (#11)
radarhere Dec 15, 2024
ddc8e7e
Use "rav1e" if available as default ("auto") avif encoder
fdintino Dec 15, 2024
b585f9e
Simplified EXIF code (#12)
radarhere Dec 15, 2024
da2e18d
Revert "Use "rav1e" if available as default ("auto") avif encoder"
fdintino Dec 17, 2024
9b6e575
Merge branch 'main' into libavif-plugin
radarhere Dec 18, 2024
3a9a3ab
Reduced epsilons (#13)
radarhere Dec 24, 2024
9328932
Removed avifEncOptions (#14)
radarhere Jan 3, 2025
be02830
Merge branch 'main' into libavif-plugin
radarhere Jan 3, 2025
4135664
Merge branch 'main' into libavif-plugin
radarhere Jan 4, 2025
29c158d
Merge branch 'main' into libavif-plugin
radarhere Jan 8, 2025
4c63ea6
Fixed series of tuples as advanced argument (#15)
radarhere Jan 15, 2025
ce6bf21
Merge branch 'main' into libavif-plugin
radarhere Jan 17, 2025
4b29af4
Skip building libavif on 32-bit Windows (#16)
radarhere Jan 21, 2025
38f0d10
Merge branch 'main' into libavif-plugin
radarhere Jan 25, 2025
1410d23
Removed qmin and qmax (#17)
radarhere Jan 25, 2025
6cbad27
Merge branch 'main' into libavif-plugin
radarhere Feb 1, 2025
19ba2dd
Use rgb.rowBytes in overflow check (#18)
radarhere Feb 3, 2025
4508f37
Use aom LICENSE instead of PATENTS (#19)
radarhere Feb 3, 2025
7de1212
Merge branch 'main' into libavif-plugin
radarhere Feb 7, 2025
e1509ee
Removed memset and ignoreAlpha (#20)
radarhere Feb 9, 2025
0590f08
Handle avifDecoderCreate and avifEncoderCreate errors (#21)
radarhere Feb 12, 2025
5761b44
Merge branch 'main' into libavif-plugin
radarhere Feb 14, 2025
38b9941
Sort formats alphabetically
radarhere Feb 15, 2025
10dfa63
Simplified code
radarhere Feb 15, 2025
9abfdbc
Merge branch 'main' into libavif-plugin
radarhere Mar 3, 2025
d80ac3c
Use default PyTypeObject values
radarhere Feb 8, 2025
b4eec64
Merge branch 'main' into libavif-plugin
radarhere Mar 17, 2025
d552087
Updated libavif to 1.2.1 (#26)
radarhere Mar 17, 2025
79f7339
Updated wording
radarhere Mar 19, 2025
46f4508
Added version comments
radarhere Mar 19, 2025
9bebf37
Sort alphabetically
radarhere Mar 19, 2025
5da2113
Simplified code
radarhere Mar 19, 2025
0732554
Merge branch 'main' into libavif-plugin
radarhere Mar 21, 2025
9ea5e3d
Remove support for libavif < 1.0.0
radarhere Mar 21, 2025
fca6df2
Added get_codec_version() (#29)
radarhere Mar 21, 2025
024a894
Merge branch 'radarhere-avif_1' into libavif-plugin
fdintino Mar 21, 2025
2ba9356
Update rust (#33)
radarhere Mar 25, 2025
fdc68e6
Merge branch 'main' into libavif-plugin
radarhere Mar 31, 2025
9e63868
Continue to build libyuv on macOS
radarhere Mar 31, 2025
eff2680
Added release notes
radarhere Mar 31, 2025
fb096e1
Derive some avif test images from existing Pillow test images
fdintino Mar 31, 2025
1276543
Updated size
radarhere Mar 31, 2025
c8d0408
Continue to build libyuv on Linux
radarhere Mar 31, 2025
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
Prev Previous commit
Next Next commit
fix: set exif orientation from irot/imir when decoding AVIF
  • Loading branch information
fdintino committed Dec 9, 2024
commit 524d802eda37e23bc1f438b205f40e95ba37aeab
Binary file added Tests/images/avif/rot0mir0.avif
Binary file not shown.
Binary file added Tests/images/avif/rot0mir1.avif
Binary file not shown.
Binary file added Tests/images/avif/rot1mir0.avif
Binary file not shown.
Binary file added Tests/images/avif/rot1mir1.avif
Binary file not shown.
Binary file added Tests/images/avif/rot2mir0.avif
Binary file not shown.
Binary file added Tests/images/avif/rot2mir1.avif
Binary file not shown.
Binary file added Tests/images/avif/rot3mir0.avif
Binary file not shown.
Binary file added Tests/images/avif/rot3mir1.avif
Binary file not shown.
50 changes: 46 additions & 4 deletions Tests/test_file_avif.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pathlib import Path
from struct import unpack
from typing import Any
from unittest import mock

import pytest

Expand Down Expand Up @@ -72,7 +73,7 @@
except (FileNotFoundError, PermissionError):
return False
else:
return "qemu" in init_proc_exe

Check warning on line 76 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L76

Added line #L76 was not covered by tests


def has_alpha_premultiplied(im_bytes: bytes) -> bool:
Expand All @@ -83,9 +84,9 @@
size, boxtype = unpack(">L4s", stream.read(8))
if not all(0x20 <= c <= 0x7E for c in boxtype):
# Not ascii
return False

Check warning on line 87 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L87

Added line #L87 was not covered by tests
if size == 1: # 64bit size
(size,) = unpack(">Q", stream.read(8))

Check warning on line 89 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L89

Added line #L89 was not covered by tests
end = start + size
version, _ = unpack(">B3s", stream.read(4))
if boxtype in (b"ftyp", b"hdlr", b"pitm", b"iloc", b"iinf"):
Expand All @@ -105,7 +106,7 @@
stream.read(2 if version == 0 else 4)
else:
return False
return False

Check warning on line 109 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L109

Added line #L109 was not covered by tests


class TestUnsupportedAvif:
Expand Down Expand Up @@ -172,7 +173,7 @@
# the image.
target = hopper(mode)
if mode != "RGB":
target = target.convert("RGB")

Check warning on line 176 in Tests/test_file_avif.py

View check run for this annotation

Codecov / codecov/patch

Tests/test_file_avif.py#L176

Added line #L176 was not covered by tests
assert_image_similar(image, target, epsilon)

def test_write_rgb(self, tmp_path: Path) -> None:
Expand Down Expand Up @@ -329,24 +330,65 @@
exif = im.getexif()
assert exif[274] == 3

@pytest.mark.parametrize("bytes", [True, False])
def test_exif_save(self, tmp_path: Path, bytes: bool) -> None:
@pytest.mark.parametrize("bytes,orientation", [(True, 1), (False, 2)])
def test_exif_save(
self,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
bytes: bool,
orientation: int,
) -> None:
mock_avif_encoder = mock.Mock(wraps=_avif.AvifEncoder)
monkeypatch.setattr(_avif, "AvifEncoder", mock_avif_encoder)
exif = Image.Exif()
exif[274] = 1
exif[274] = orientation
exif_data = exif.tobytes()
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
im.save(test_file, exif=exif_data if bytes else exif)

with Image.open(test_file) as reloaded:
assert reloaded.info["exif"] == exif_data
if orientation == 1:
assert "exif" not in reloaded.info
else:
assert reloaded.info["exif"] == exif_data
mock_avif_encoder.mock_calls[0].args[16:17] == (b"", orientation)

def test_exif_invalid(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = str(tmp_path / "temp.avif")
with pytest.raises(SyntaxError):
im.save(test_file, exif=b"invalid")

@pytest.mark.parametrize(
"rot,mir,exif_orientation",
[
(0, 0, 4),
(0, 1, 2),
(1, 0, 5),
(1, 1, 7),
(2, 0, 2),
(2, 1, 4),
(3, 0, 7),
(3, 1, 5),
],
)
def test_rot_mir_exif(
self, rot: int, mir: int, exif_orientation: int, tmp_path: Path
) -> None:
with Image.open(f"Tests/images/avif/rot{rot}mir{mir}.avif") as im:
exif = im.info["exif"]
test_file = str(tmp_path / "temp.avif")
im.save(test_file, exif=exif)

exif_data = Image.Exif()
exif_data.load(exif)
assert exif_data[274] == exif_orientation
with Image.open(test_file) as reloaded:
exif_data = Image.Exif()
exif_data.load(reloaded.info["exif"])
assert exif_data[274] == exif_orientation

def test_xmp(self) -> None:
with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im:
xmp = im.info["xmp"]
Expand Down
23 changes: 20 additions & 3 deletions src/PIL/AvifImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@

def _get_default_max_threads():
if DEFAULT_MAX_THREADS:
return DEFAULT_MAX_THREADS

Check warning on line 53 in src/PIL/AvifImagePlugin.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/AvifImagePlugin.py#L53

Added line #L53 was not covered by tests
if hasattr(os, "sched_getaffinity"):
return len(os.sched_getaffinity(0))

Check warning on line 55 in src/PIL/AvifImagePlugin.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/AvifImagePlugin.py#L55

Added line #L55 was not covered by tests
else:
return os.cpu_count() or 1

Expand Down Expand Up @@ -86,7 +86,9 @@
)

# Get info from decoder
width, height, n_frames, mode, icc, exif, xmp = self._decoder.get_info()
width, height, n_frames, mode, icc, exif, xmp, exif_orientation = (
self._decoder.get_info()
)
self._size = width, height
self.n_frames = n_frames
self.is_animated = self.n_frames > 1
Expand All @@ -99,6 +101,16 @@
if xmp:
self.info["xmp"] = xmp

if exif_orientation != 1 or exif is not None:
exif_data = Image.Exif()
orig_orientation = 1
if exif is not None:
exif_data.load(exif)
orig_orientation = exif_data.get(ExifTags.Base.Orientation, 1)
if exif_orientation != orig_orientation:
exif_data[ExifTags.Base.Orientation] = exif_orientation
self.info["exif"] = exif_data.tobytes()

def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
Expand Down Expand Up @@ -176,9 +188,14 @@
else:
exif_data = Image.Exif()
exif_data.load(exif)
exif_orientation = exif_data.pop(ExifTags.Base.Orientation, 1)
exif_orientation = exif_data.pop(ExifTags.Base.Orientation, 0)
if exif_orientation != 0:
if len(exif_data):
exif = exif_data.tobytes()

Check warning on line 194 in src/PIL/AvifImagePlugin.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/AvifImagePlugin.py#L194

Added line #L194 was not covered by tests
else:
exif = None
else:
exif_orientation = 1
exif_orientation = 0

xmp = info.get("xmp")

Expand Down
47 changes: 44 additions & 3 deletions src/_avif.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,22 @@
static PyTypeObject AvifDecoder_Type;

static int
normalize_quantize_value(int qvalue) {

Check warning on line 42 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L42

Added line #L42 was not covered by tests
if (qvalue < AVIF_QUANTIZER_BEST_QUALITY) {
return AVIF_QUANTIZER_BEST_QUALITY;

Check warning on line 44 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L44

Added line #L44 was not covered by tests
} else if (qvalue > AVIF_QUANTIZER_WORST_QUALITY) {
return AVIF_QUANTIZER_WORST_QUALITY;

Check warning on line 46 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L46

Added line #L46 was not covered by tests
} else {
return qvalue;

Check warning on line 48 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L48

Added line #L48 was not covered by tests
}
}

static int
normalize_tiles_log2(int value) {
if (value < 0) {
return 0;

Check warning on line 55 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L55

Added line #L55 was not covered by tests
} else if (value > 6) {
return 6;

Check warning on line 57 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L57

Added line #L57 was not covered by tests
} else {
return value;
}
Expand All @@ -71,11 +71,49 @@
case AVIF_RESULT_TRUNCATED_DATA:
case AVIF_RESULT_NO_CONTENT:
return PyExc_SyntaxError;
default:
return PyExc_RuntimeError;

Check warning on line 75 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L74-L75

Added lines #L74 - L75 were not covered by tests
}
}

static uint8_t
irot_imir_to_exif_orientation(const avifImage *image) {
#if AVIF_VERSION_MAJOR >= 1
uint8_t axis = image->imir.axis;
#else
uint8_t axis = image->imir.mode;
#endif
uint8_t angle = image->irot.angle;
int irot = !!(image->transformFlags & AVIF_TRANSFORM_IROT);
int imir = !!(image->transformFlags & AVIF_TRANSFORM_IMIR);
if (irot && angle == 1) {
if (imir) {
return axis ? 7 // 90 degrees anti-clockwise then swap left and right.
: 5; // 90 degrees anti-clockwise then swap top and bottom.
}
return 6; // 90 degrees anti-clockwise.

Check warning on line 94 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L94

Added line #L94 was not covered by tests
}
if (irot && angle == 2) {
if (imir) {
return axis ? 4 // 180 degrees anti-clockwise then swap left and right.
: 2; // 180 degrees anti-clockwise then swap top and bottom.

Check warning on line 99 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L98-L99

Added lines #L98 - L99 were not covered by tests
}
return 3; // 180 degrees anti-clockwise.

Check warning on line 101 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L101

Added line #L101 was not covered by tests
}
if (irot && angle == 3) {
if (imir) {
return axis ? 5 // 270 degrees anti-clockwise then swap left and right.
: 7; // 270 degrees anti-clockwise then swap top and bottom.
}
return 8; // 270 degrees anti-clockwise.

Check warning on line 108 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L108

Added line #L108 was not covered by tests
}
if (imir) {
return axis ? 2 // Swap left and right.
: 4; // Swap top and bottom.
}
return 1; // Default orientation ("top-left", no-op).
}

static void
exif_orientation_to_irot_imir(avifImage *image, int orientation) {
const avifTransformFlags otherFlags =
Expand Down Expand Up @@ -106,16 +144,16 @@
image->imir.mode = 1;
#endif
return;
case 3: // The 0th row is at the visual bottom of the image, and the 0th column

Check warning on line 147 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L147

Added line #L147 was not covered by tests
// is the visual right-hand side.
image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT;
image->irot.angle = 2;

Check warning on line 150 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L149-L150

Added lines #L149 - L150 were not covered by tests
#if AVIF_VERSION_MAJOR >= 1
image->imir.axis = 0; // ignored

Check warning on line 152 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L152

Added line #L152 was not covered by tests
#else
image->imir.mode = 0; // ignored
#endif
return;

Check warning on line 156 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L156

Added line #L156 was not covered by tests
case 4: // The 0th row is at the visual bottom of the image, and the 0th column
// is the visual left-hand side.
image->transformFlags = otherFlags | AVIF_TRANSFORM_IMIR;
Expand All @@ -138,16 +176,16 @@
image->imir.mode = 0;
#endif
return;
case 6: // The 0th row is the visual right-hand side of the image, and the 0th

Check warning on line 179 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L179

Added line #L179 was not covered by tests
// column is the visual top.
image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT;
image->irot.angle = 3;

Check warning on line 182 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L181-L182

Added lines #L181 - L182 were not covered by tests
#if AVIF_VERSION_MAJOR >= 1
image->imir.axis = 0; // ignored

Check warning on line 184 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L184

Added line #L184 was not covered by tests
#else
image->imir.mode = 0; // ignored
#endif
return;

Check warning on line 188 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L188

Added line #L188 was not covered by tests
case 7: // The 0th row is the visual right-hand side of the image, and the 0th
// column is the visual bottom.
image->transformFlags =
Expand All @@ -160,16 +198,16 @@
image->imir.mode = 0;
#endif
return;
case 8: // The 0th row is the visual left-hand side of the image, and the 0th

Check warning on line 201 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L201

Added line #L201 was not covered by tests
// column is the visual bottom.
image->transformFlags = otherFlags | AVIF_TRANSFORM_IROT;
image->irot.angle = 1;

Check warning on line 204 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L203-L204

Added lines #L203 - L204 were not covered by tests
#if AVIF_VERSION_MAJOR >= 1
image->imir.axis = 0; // ignored

Check warning on line 206 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L206

Added line #L206 was not covered by tests
#else
image->imir.mode = 0; // ignored
#endif
return;

Check warning on line 210 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L210

Added line #L210 was not covered by tests
}
}

Expand Down Expand Up @@ -209,34 +247,34 @@
PyObject *keyval, *py_key, *py_val;
char *key, *val;
if (!PyTuple_Check(opts)) {
PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options");
return 1;

Check warning on line 251 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L250-L251

Added lines #L250 - L251 were not covered by tests
}
size = PyTuple_GET_SIZE(opts);

for (i = 0; i < size; i++) {
keyval = PyTuple_GetItem(opts, i);
if (!PyTuple_Check(keyval) || PyTuple_GET_SIZE(keyval) != 2) {
PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options");
return 1;

Check warning on line 259 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L258-L259

Added lines #L258 - L259 were not covered by tests
}
py_key = PyTuple_GetItem(keyval, 0);
py_val = PyTuple_GetItem(keyval, 1);
if (!PyBytes_Check(py_key) || !PyBytes_Check(py_val)) {
PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options");
return 1;

Check warning on line 265 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L264-L265

Added lines #L264 - L265 were not covered by tests
}
key = PyBytes_AsString(py_key);
val = PyBytes_AsString(py_val);

avifResult result = avifEncoderSetCodecSpecificOption(encoder, key, val);
if (result != AVIF_RESULT_OK) {
PyErr_Format(
exc_type_for_avif_result(result),

Check warning on line 273 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L272-L273

Added lines #L272 - L273 were not covered by tests
"Setting advanced codec options failed: %s",
avifResultToString(result)

Check warning on line 275 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L275

Added line #L275 was not covered by tests
);
return 1;

Check warning on line 277 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L277

Added line #L277 was not covered by tests
}
}
return 0;
Expand Down Expand Up @@ -318,15 +356,15 @@
enc_options.qmax = normalize_quantize_value(100 - quality);
#endif
} else {
enc_options.qmin = normalize_quantize_value(qmin);
enc_options.qmax = normalize_quantize_value(qmax);

Check warning on line 360 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L359-L360

Added lines #L359 - L360 were not covered by tests
}
enc_options.quality = quality;

if (speed < AVIF_SPEED_SLOWEST) {
speed = AVIF_SPEED_SLOWEST;

Check warning on line 365 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L365

Added line #L365 was not covered by tests
} else if (speed > AVIF_SPEED_FASTEST) {
speed = AVIF_SPEED_FASTEST;

Check warning on line 367 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L367

Added line #L367 was not covered by tests
}
enc_options.speed = speed;

Expand All @@ -347,8 +385,8 @@

// Validate canvas dimensions
if (width <= 0 || height <= 0) {
PyErr_SetString(PyExc_ValueError, "invalid canvas dimensions");
return NULL;

Check warning on line 389 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L388-L389

Added lines #L388 - L389 were not covered by tests
}

enc_options.tile_rows_log2 = normalize_tiles_log2(tile_rows_log2);
Expand Down Expand Up @@ -378,9 +416,9 @@
encoder->maxThreads = is_aom_encode && max_threads > 64 ? 64 : max_threads;
#if AVIF_VERSION >= 1000000
if (enc_options.qmin != -1 && enc_options.qmax != -1) {
encoder->minQuantizer = enc_options.qmin;
encoder->maxQuantizer = enc_options.qmax;
} else {

Check warning on line 421 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L419-L421

Added lines #L419 - L421 were not covered by tests
encoder->quality = enc_options.quality;
}
#else
Expand All @@ -400,7 +438,7 @@
if (advanced != Py_None) {
#if AVIF_VERSION >= 80200
if (_add_codec_specific_options(encoder, advanced)) {
return NULL;

Check warning on line 441 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L441

Added line #L441 was not covered by tests
}
#else
PyErr_SetString(
Expand Down Expand Up @@ -437,12 +475,12 @@
PyBytes_GET_SIZE(icc_bytes)
);
if (result != AVIF_RESULT_OK) {
PyErr_Format(
exc_type_for_avif_result(result),

Check warning on line 479 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L478-L479

Added lines #L478 - L479 were not covered by tests
"Setting ICC profile failed: %s",
avifResultToString(result)

Check warning on line 481 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L481

Added line #L481 was not covered by tests
);
return NULL;

Check warning on line 483 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L483

Added line #L483 was not covered by tests
}
} else {
image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709;
Expand All @@ -450,21 +488,21 @@
}

if (PyBytes_GET_SIZE(exif_bytes)) {
self->exif_bytes = exif_bytes;
Py_INCREF(exif_bytes);

Check warning on line 492 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L491-L492

Added lines #L491 - L492 were not covered by tests

result = avifImageSetMetadataExif(
image,
(uint8_t *)PyBytes_AS_STRING(exif_bytes),
PyBytes_GET_SIZE(exif_bytes)

Check warning on line 497 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L494-L497

Added lines #L494 - L497 were not covered by tests
);
if (result != AVIF_RESULT_OK) {
PyErr_Format(
exc_type_for_avif_result(result),

Check warning on line 501 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L500-L501

Added lines #L500 - L501 were not covered by tests
"Setting EXIF data failed: %s",
avifResultToString(result)

Check warning on line 503 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L503

Added line #L503 was not covered by tests
);
return NULL;

Check warning on line 505 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L505

Added line #L505 was not covered by tests
}
}
if (PyBytes_GET_SIZE(xmp_bytes)) {
Expand All @@ -477,23 +515,25 @@
PyBytes_GET_SIZE(xmp_bytes)
);
if (result != AVIF_RESULT_OK) {
PyErr_Format(
exc_type_for_avif_result(result),

Check warning on line 519 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L518-L519

Added lines #L518 - L519 were not covered by tests
"Setting XMP data failed: %s",
avifResultToString(result)

Check warning on line 521 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L521

Added line #L521 was not covered by tests
);
return NULL;

Check warning on line 523 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L523

Added line #L523 was not covered by tests
}
}
exif_orientation_to_irot_imir(image, exif_orientation);
if (exif_orientation > 0) {
exif_orientation_to_irot_imir(image, exif_orientation);
}

self->image = image;
self->frame_index = -1;

return (PyObject *)self;
}
PyErr_SetString(PyExc_RuntimeError, "could not create encoder object");
return NULL;

Check warning on line 536 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L535-L536

Added lines #L535 - L536 were not covered by tests
}

PyObject *
Expand Down Expand Up @@ -540,7 +580,7 @@
&mode,
&is_single_frame
)) {
return NULL;

Check warning on line 583 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L583

Added line #L583 was not covered by tests
}

is_first_frame = (self->frame_index == -1);
Expand Down Expand Up @@ -590,25 +630,25 @@

result = avifRGBImageAllocatePixels(&rgb);
if (result != AVIF_RESULT_OK) {
PyErr_Format(
exc_type_for_avif_result(result),

Check warning on line 634 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L633-L634

Added lines #L633 - L634 were not covered by tests
"Pixel allocation failed: %s",
avifResultToString(result)

Check warning on line 636 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L636

Added line #L636 was not covered by tests
);
return NULL;

Check warning on line 638 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L638

Added line #L638 was not covered by tests
}

if (rgb.rowBytes * rgb.height != size) {
PyErr_Format(
PyExc_RuntimeError,

Check warning on line 643 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L642-L643

Added lines #L642 - L643 were not covered by tests
"rgb data is incorrect size: %u * %u (%u) != %u",
rgb.rowBytes,
rgb.height,
rgb.rowBytes * rgb.height,
size

Check warning on line 648 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L645-L648

Added lines #L645 - L648 were not covered by tests
);
ret = NULL;
goto end;

Check warning on line 651 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L650-L651

Added lines #L650 - L651 were not covered by tests
}

// rgb.pixels is safe for writes
Expand All @@ -618,13 +658,13 @@
Py_END_ALLOW_THREADS

if (result != AVIF_RESULT_OK) {
PyErr_Format(
exc_type_for_avif_result(result),

Check warning on line 662 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L661-L662

Added lines #L661 - L662 were not covered by tests
"Conversion to YUV failed: %s",
avifResultToString(result)

Check warning on line 664 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L664

Added line #L664 was not covered by tests
);
ret = NULL;
goto end;

Check warning on line 667 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L666-L667

Added lines #L666 - L667 were not covered by tests
}

uint32_t addImageFlags = AVIF_ADD_IMAGE_FLAG_NONE;
Expand Down Expand Up @@ -672,13 +712,13 @@
Py_END_ALLOW_THREADS

if (result != AVIF_RESULT_OK) {
PyErr_Format(
exc_type_for_avif_result(result),

Check warning on line 716 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L715-L716

Added lines #L715 - L716 were not covered by tests
"Failed to finish encoding: %s",
avifResultToString(result)

Check warning on line 718 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L718

Added line #L718 was not covered by tests
);
avifRWDataFree(&raw);
return NULL;

Check warning on line 721 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L720-L721

Added lines #L720 - L721 were not covered by tests
}

ret = PyBytes_FromStringAndSize((char *)raw.data, raw.size);
Expand Down Expand Up @@ -712,8 +752,8 @@

self = PyObject_New(AvifDecoderObject, &AvifDecoder_Type);
if (!self) {
PyErr_SetString(PyExc_RuntimeError, "could not create decoder object");
return NULL;

Check warning on line 756 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L755-L756

Added lines #L755 - L756 were not covered by tests
}
self->decoder = NULL;

Expand All @@ -740,15 +780,15 @@
PyBytes_GET_SIZE(self->data)
);
if (result != AVIF_RESULT_OK) {
PyErr_Format(
exc_type_for_avif_result(result),

Check warning on line 784 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L783-L784

Added lines #L783 - L784 were not covered by tests
"Setting IO memory failed: %s",
avifResultToString(result)

Check warning on line 786 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L786

Added line #L786 was not covered by tests
);
avifDecoderDestroy(self->decoder);
self->decoder = NULL;
Py_DECREF(self);
return NULL;

Check warning on line 791 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L788-L791

Added lines #L788 - L791 were not covered by tests
}

result = avifDecoderParse(self->decoder);
Expand Down Expand Up @@ -806,14 +846,15 @@
}

ret = Py_BuildValue(
"IIIsSSS",
"IIIsSSSI",
image->width,
image->height,
decoder->imageCount,
self->mode,
NULL == icc ? Py_None : icc,
NULL == exif ? Py_None : exif,
NULL == xmp ? Py_None : xmp
NULL == xmp ? Py_None : xmp,
irot_imir_to_exif_orientation(image)
);

Py_XDECREF(xmp);
Expand All @@ -838,18 +879,18 @@
decoder = self->decoder;

if (!PyArg_ParseTuple(args, "I", &frame_index)) {
return NULL;

Check warning on line 882 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L882

Added line #L882 was not covered by tests
}

result = avifDecoderNthImage(decoder, frame_index);
if (result != AVIF_RESULT_OK) {
PyErr_Format(
exc_type_for_avif_result(result),

Check warning on line 888 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L887-L888

Added lines #L887 - L888 were not covered by tests
"Failed to decode frame %u: %s",
decoder->imageIndex + 1,
avifResultToString(result)

Check warning on line 891 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L890-L891

Added lines #L890 - L891 were not covered by tests
);
return NULL;

Check warning on line 893 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L893

Added line #L893 was not covered by tests
}

image = decoder->image;
Expand All @@ -869,31 +910,31 @@
row_bytes = rgb.width * avifRGBImagePixelSize(&rgb);

if (rgb.height > PY_SSIZE_T_MAX / row_bytes) {
PyErr_SetString(PyExc_MemoryError, "Integer overflow in pixel size");
return NULL;

Check warning on line 914 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L913-L914

Added lines #L913 - L914 were not covered by tests
}

result = avifRGBImageAllocatePixels(&rgb);
if (result != AVIF_RESULT_OK) {
PyErr_Format(
exc_type_for_avif_result(result),

Check warning on line 920 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L919-L920

Added lines #L919 - L920 were not covered by tests
"Pixel allocation failed: %s",
avifResultToString(result)

Check warning on line 922 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L922

Added line #L922 was not covered by tests
);
return NULL;

Check warning on line 924 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L924

Added line #L924 was not covered by tests
}

Py_BEGIN_ALLOW_THREADS result = avifImageYUVToRGB(image, &rgb);
Py_END_ALLOW_THREADS

if (result != AVIF_RESULT_OK) {
PyErr_Format(
exc_type_for_avif_result(result),

Check warning on line 932 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L931-L932

Added lines #L931 - L932 were not covered by tests
"Conversion from YUV failed: %s",
avifResultToString(result)

Check warning on line 934 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L934

Added line #L934 was not covered by tests
);
avifRGBImageFreePixels(&rgb);
return NULL;

Check warning on line 937 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L936-L937

Added lines #L936 - L937 were not covered by tests
}

size = rgb.rowBytes * rgb.height;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this guaranteed to not overflow, even in the face of invalid input?

Copy link
Contributor Author

@fdintino fdintino Jan 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

libavif currently restricts images to a maximum of 2^28 pixels. If the dimensions are larger than 16384x16384 then the function that sets decoder->image->width and decoder->image->height fails. So I suppose that a 4-channel 16384x16384 8-bit image could overflow on a 32-bit platform. I'm not certain because the codecs used by libavif have their own overflow limit checks. For instance, dav1d enforces a maximum of 2^26 pixels on 32-bit systems. Should I add a check against PY_SSIZE_T_MAX to be sure? (edit: answering my own question and adding this check)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added here

Pillow/src/_avif.c

Lines 619 to 622 in b84a8e0

if (rgb.height > PY_SSIZE_T_MAX / row_bytes) {
PyErr_SetString(PyExc_MemoryError, "Integer overflow in pixel size");
return NULL;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically, I'm the one who will get a CVE on this if there's a problem, and I'd like really clear guidelines about what the assumptions are for sizes of things and where they come from for dangerous operations like memset, malloc, and pointer reads/writes. This isn't so much for now, but a couple years down the line, things need to be clear. This will be fuzzed, this will be run under valgrind, so hopefully there won't be problems.

I've basically had to reverse engineer how SgiRleDecode works over the last month or so, and I'd like to be preventing that sort of experience in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does raising a MemoryError if rgb.height > PY_SSIZE_T_MAX / row_bytes (as I have in the latest PR push) suffice to address that concern?

Expand Down Expand Up @@ -969,16 +1010,16 @@

PyObject *v = PyUnicode_FromString(avifVersion());
if (PyDict_SetItemString(d, "libavif_version", v) < 0) {
Py_DECREF(v);
return -1;

Check warning on line 1014 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L1013-L1014

Added lines #L1013 - L1014 were not covered by tests
}
Py_DECREF(v);

v = Py_True;
Py_INCREF(v);
if (PyDict_SetItemString(d, "HAVE_AVIF", v) < 0) {
Py_DECREF(v);
return -1;

Check warning on line 1022 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L1021-L1022

Added lines #L1021 - L1022 were not covered by tests
}
Py_DECREF(v);

Expand All @@ -987,13 +1028,13 @@
);

if (PyDict_SetItemString(d, "VERSION", v) < 0) {
Py_DECREF(v);
return -1;

Check warning on line 1032 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L1031-L1032

Added lines #L1031 - L1032 were not covered by tests
}
Py_DECREF(v);

if (PyType_Ready(&AvifDecoder_Type) < 0 || PyType_Ready(&AvifEncoder_Type) < 0) {
return -1;

Check warning on line 1037 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L1037

Added line #L1037 was not covered by tests
}
return 0;
}
Expand All @@ -1011,7 +1052,7 @@

m = PyModule_Create(&module_def);
if (setup_module(m) < 0) {
return NULL;

Check warning on line 1055 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L1055

Added line #L1055 was not covered by tests
}

return m;
Expand Down
Loading