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
Removed qmin and qmax (#17)
  • Loading branch information
radarhere authored Jan 25, 2025
commit 1410d23453fca292060ca546bbd0aba8073d51e3
2 changes: 1 addition & 1 deletion .github/workflows/macos-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ brew install \
dav1d \
aom \
rav1e \
ninja
svt-av1
if [[ "$ImageOS" == "macos13" ]]; then
brew install --ignore-dependencies libraqm
else
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-mingw.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ jobs:
mingw-w64-x86_64-gcc \
mingw-w64-x86_64-ghostscript \
mingw-w64-x86_64-lcms2 \
mingw-w64-x86_64-libavif \
mingw-w64-x86_64-libimagequant \
mingw-w64-x86_64-libjpeg-turbo \
mingw-w64-x86_64-libraqm \
mingw-w64-x86_64-libavif \
mingw-w64-x86_64-libtiff \
mingw-w64-x86_64-libwebp \
mingw-w64-x86_64-openjpeg2 \
Expand Down
25 changes: 16 additions & 9 deletions docs/handbook/image-file-formats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1359,14 +1359,8 @@ as 8-bit RGB(A).
The :py:meth:`~PIL.Image.Image.save` method supports the following options:

**quality**
Integer, 1-100, defaults to 75. 0 gives the smallest size and poorest
quality, 100 the largest and best quality. The value of this setting
controls the ``qmin`` and ``qmax`` encoder options.

**qmin** / **qmax**
Integer, 0-63. The quality of images created by an AVIF encoder are
controlled by minimum and maximum quantizer values. The higher these
values are, the worse the quality.
Integer, 0-100, defaults to 75. 0 gives the smallest size and poorest
quality, 100 the largest and best quality.

**subsampling**
If present, sets the subsampling for the encoder. Defaults to ``4:2:0``.
Expand All @@ -1380,6 +1374,10 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**speed**
Quality/speed trade-off (0=slower-better, 10=fastest). Defaults to 6.

**max_threads**
Limit the number of active threads used. By default, there is no limit. If the aom
codec is used, there is a maximum of 64.

**range**
YUV range, either "full" or "limited". Defaults to "full"

Expand All @@ -1392,8 +1390,17 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
For tile encoding, the (log 2) number of tile rows and columns to use.
Valid values are 0-6, default 0.

**autotiling**
Split the image up to allow parallelization. Enabled automatically if "tile_rows"
and "tile_cols" both have their default values of zero. Requires libavif version
**0.11.0** or greater.

**alpha_premultiplied**
Encode the image with premultiplied alpha. Defaults to ``False``
Encode the image with premultiplied alpha. Defaults to ``False``. Requires libavif
version **0.9.0** or greater.

**advanced**
Codec specific options. Requires libavif version **0.8.2** or greater.

**icc_profile**
The ICC Profile to include in the saved file.
Expand Down
2 changes: 1 addition & 1 deletion docs/installation/building-from-source.rst
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ Many of Pillow's features require external libraries:
instead of installing libavif through Homebrew directly, you can use
Homebrew to install libavif's build dependencies::

brew install aom dav1d rav1e
brew install aom dav1d rav1e svt-av1

Then see ``depends/install_libavif.sh`` to install libavif.

Expand Down
4 changes: 0 additions & 4 deletions src/PIL/AvifImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@

def _get_default_max_threads():
if DEFAULT_MAX_THREADS:
return DEFAULT_MAX_THREADS

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

View check run for this annotation

Codecov / codecov/patch

src/PIL/AvifImagePlugin.py#L50

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

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

View check run for this annotation

Codecov / codecov/patch

src/PIL/AvifImagePlugin.py#L52

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

Expand Down Expand Up @@ -156,8 +156,6 @@

is_single_frame = total == 1

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:
msg = "Invalid quality setting"
Expand Down Expand Up @@ -218,8 +216,6 @@
im.size[0],
im.size[1],
subsampling,
qmin,
qmax,
quality,
speed,
max_threads,
Expand Down
2 changes: 1 addition & 1 deletion src/PIL/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,8 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None:
("freetype2", "FREETYPE2"),
("littlecms2", "LITTLECMS2"),
("webp", "WEBP"),
("jpg", "JPEG"),
("avif", "AVIF"),
("jpg", "JPEG"),
("jpg_2000", "OPENJPEG (JPEG2000)"),
("zlib", "ZLIB (PNG/ZIP)"),
("libtiff", "LIBTIFF"),
Expand Down
17 changes: 4 additions & 13 deletions src/_avif.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,22 @@
static PyTypeObject AvifDecoder_Type;

static int
normalize_quantize_value(int qvalue) {

Check warning on line 28 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L28

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

Check warning on line 30 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L30

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

Check warning on line 32 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L32

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

Check warning on line 34 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L34

Added line #L34 was not covered by tests
}
}

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

Check warning on line 41 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L41

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

Check warning on line 43 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L43

Added line #L43 was not covered by tests
} else {
return value;
}
Expand All @@ -57,8 +57,8 @@
case AVIF_RESULT_TRUNCATED_DATA:
case AVIF_RESULT_NO_CONTENT:
return PyExc_SyntaxError;
default:
return PyExc_RuntimeError;

Check warning on line 61 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L60-L61

Added lines #L60 - L61 were not covered by tests
}
}

Expand All @@ -79,15 +79,15 @@
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 82 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L82

Added line #L82 was not covered by tests
}
if (angle == 2) {
if (imir) {
return axis

Check warning on line 86 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L86

Added line #L86 was not covered by tests
? 4 // 180 degrees anti-clockwise then swap left and right.
: 2; // 180 degrees anti-clockwise then swap top and bottom.

Check warning on line 88 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L88

Added line #L88 was not covered by tests
}
return 3; // 180 degrees anti-clockwise.

Check warning on line 90 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L90

Added line #L90 was not covered by tests
}
if (angle == 3) {
if (imir) {
Expand All @@ -95,7 +95,7 @@
? 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 98 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L98

Added line #L98 was not covered by tests
}
}
if (imir) {
Expand All @@ -120,11 +120,11 @@
image->imir.mode = 1;
#endif
break;
case 3: // The 0th row is at the visual bottom of the image, and the 0th column

Check warning on line 123 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L123

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

Check warning on line 127 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L125-L127

Added lines #L125 - L127 were 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 |= AVIF_TRANSFORM_IMIR;
Expand All @@ -135,22 +135,22 @@
image->irot.angle = 1; // applied before imir according to MIAF spec
// ISO/IEC 28002-12:2021 - section 7.3.6.7
break;
case 6: // The 0th row is the visual right-hand side of the image, and the 0th

Check warning on line 138 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L138

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

Check warning on line 142 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L140-L142

Added lines #L140 - L142 were 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 |= AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR;
image->irot.angle = 3; // applied before imir according to MIAF spec
// ISO/IEC 28002-12:2021 - section 7.3.6.7
break;
case 8: // The 0th row is the visual left-hand side of the image, and the 0th

Check warning on line 149 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L149

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

Check warning on line 153 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L151-L153

Added lines #L151 - L153 were not covered by tests
}
}

Expand Down Expand Up @@ -189,38 +189,38 @@
Py_ssize_t i, size;
PyObject *keyval, *py_key, *py_val;
if (!PyTuple_Check(opts)) {
PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options");
return 1;

Check warning on line 193 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L192-L193

Added lines #L192 - L193 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 201 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L200-L201

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

Check warning on line 207 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L206-L207

Added lines #L206 - L207 were not covered by tests
}
const char *key = PyUnicode_AsUTF8(py_key);
const char *val = PyUnicode_AsUTF8(py_val);
if (key == NULL || val == NULL) {
PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options");
return 1;

Check warning on line 213 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L212-L213

Added lines #L212 - L213 were not covered by tests
}

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

Check warning on line 219 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L218-L219

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

Check warning on line 221 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L221

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

Check warning on line 223 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L223

Added line #L223 was not covered by tests
}
}
return 0;
Expand All @@ -234,8 +234,6 @@
avifEncoder *encoder;

char *subsampling;
int qmin;
int qmax;
int quality;
int speed;
int exif_orientation;
Expand All @@ -255,12 +253,10 @@

if (!PyArg_ParseTuple(
args,
"IIsiiiiissiiOOSSiSO",
"IIsiiissiiOOSSiSO",
&width,
&height,
&subsampling,
&qmin,
&qmax,
&quality,
&speed,
&max_threads,
Expand Down Expand Up @@ -308,9 +304,9 @@

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

Check warning on line 309 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L307-L309

Added lines #L307 - L309 were not covered by tests
}
image->width = width;
image->height = height;
Expand All @@ -327,17 +323,12 @@
_codec_available("aom", AVIF_CODEC_FLAG_CAN_ENCODE));
encoder->maxThreads = is_aom_encode && max_threads > 64 ? 64 : max_threads;

if (qmin == -1 || qmax == -1) {
#if AVIF_VERSION >= 1000000
encoder->quality = quality;
encoder->quality = quality;
#else
encoder->minQuantizer = normalize_quantize_value(64 - quality);
encoder->maxQuantizer = normalize_quantize_value(100 - quality);
encoder->minQuantizer = normalize_quantize_value(64 - quality);
encoder->maxQuantizer = normalize_quantize_value(100 - quality);
#endif
} else {
encoder->minQuantizer = normalize_quantize_value(qmin);
encoder->maxQuantizer = normalize_quantize_value(qmax);
}

if (strcmp(codec, "auto") == 0) {
encoder->codecChoice = AVIF_CODEC_CHOICE_AUTO;
Expand All @@ -345,9 +336,9 @@
encoder->codecChoice = avifCodecChoiceFromName(codec);
}
if (speed < AVIF_SPEED_SLOWEST) {
speed = AVIF_SPEED_SLOWEST;

Check warning on line 339 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L339

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

Check warning on line 341 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L341

Added line #L341 was not covered by tests
}
encoder->speed = speed;
encoder->timescale = (uint64_t)1000;
Expand All @@ -361,9 +352,9 @@
if (advanced != Py_None) {
#if AVIF_VERSION >= 80200
if (_add_codec_specific_options(encoder, advanced)) {
avifImageDestroy(image);
avifEncoderDestroy(encoder);
return NULL;

Check warning on line 357 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L355-L357

Added lines #L355 - L357 were not covered by tests
}
#else
PyErr_SetString(
Expand All @@ -377,10 +368,10 @@

self = PyObject_New(AvifEncoderObject, &AvifEncoder_Type);
if (!self) {
PyErr_SetString(PyExc_RuntimeError, "could not create encoder object");
avifImageDestroy(image);
avifEncoderDestroy(encoder);
return NULL;

Check warning on line 374 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L371-L374

Added lines #L371 - L374 were not covered by tests
}
self->frame_index = -1;
self->icc_bytes = NULL;
Expand All @@ -397,16 +388,16 @@
image, (uint8_t *)PyBytes_AS_STRING(icc_bytes), size
);
if (result != AVIF_RESULT_OK) {
PyErr_Format(
exc_type_for_avif_result(result),

Check warning on line 392 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L391-L392

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

Check warning on line 394 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L394

Added line #L394 was not covered by tests
);
avifImageDestroy(image);
avifEncoderDestroy(encoder);
Py_XDECREF(self->icc_bytes);
PyObject_Del(self);
return NULL;

Check warning on line 400 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L396-L400

Added lines #L396 - L400 were not covered by tests
}
// colorPrimaries and transferCharacteristics are ignored when an ICC
// profile is present, so set them to UNSPECIFIED.
Expand All @@ -427,17 +418,17 @@
image, (uint8_t *)PyBytes_AS_STRING(exif_bytes), size
);
if (result != AVIF_RESULT_OK) {
PyErr_Format(
exc_type_for_avif_result(result),

Check warning on line 422 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L421-L422

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

Check warning on line 424 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L424

Added line #L424 was not covered by tests
);
avifImageDestroy(image);
avifEncoderDestroy(encoder);
Py_XDECREF(self->icc_bytes);
Py_XDECREF(self->exif_bytes);
PyObject_Del(self);
return NULL;

Check warning on line 431 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L426-L431

Added lines #L426 - L431 were not covered by tests
}
}

Expand All @@ -450,18 +441,18 @@
image, (uint8_t *)PyBytes_AS_STRING(xmp_bytes), size
);
if (result != AVIF_RESULT_OK) {
PyErr_Format(
exc_type_for_avif_result(result),

Check warning on line 445 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L444-L445

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

Check warning on line 447 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L447

Added line #L447 was not covered by tests
);
avifImageDestroy(image);
avifEncoderDestroy(encoder);
Py_XDECREF(self->icc_bytes);
Py_XDECREF(self->exif_bytes);
Py_XDECREF(self->xmp_bytes);
PyObject_Del(self);
return NULL;

Check warning on line 455 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L449-L455

Added lines #L449 - L455 were not covered by tests
}
}
if (exif_orientation > 1) {
Expand Down Expand Up @@ -518,7 +509,7 @@
&mode,
&is_single_frame
)) {
return NULL;

Check warning on line 512 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L512

Added line #L512 was not covered by tests
}

is_first_frame = self->frame_index == -1;
Expand Down Expand Up @@ -568,25 +559,25 @@

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

Check warning on line 563 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L562-L563

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

Check warning on line 565 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L565

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

Check warning on line 567 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L567

Added line #L567 was not covered by tests
}

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

Check warning on line 572 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L571-L572

Added lines #L571 - L572 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 577 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L574-L577

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

Check warning on line 580 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L579-L580

Added lines #L579 - L580 were not covered by tests
}

// rgb.pixels is safe for writes
Expand All @@ -597,13 +588,13 @@
Py_END_ALLOW_THREADS;

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

Check warning on line 592 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L591-L592

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

Check warning on line 594 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L594

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

Check warning on line 597 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L596-L597

Added lines #L596 - L597 were not covered by tests
}

uint32_t addImageFlags = AVIF_ADD_IMAGE_FLAG_NONE;
Expand Down Expand Up @@ -652,13 +643,13 @@
Py_END_ALLOW_THREADS;

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

Check warning on line 647 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L646-L647

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

Check warning on line 649 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L649

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

Check warning on line 652 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L651-L652

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

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

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

Check warning on line 688 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L687-L688

Added lines #L687 - L688 were not covered by tests
}

Py_INCREF(avif_bytes);
Expand All @@ -718,14 +709,14 @@
decoder, (uint8_t *)PyBytes_AS_STRING(self->data), PyBytes_GET_SIZE(self->data)
);
if (result != AVIF_RESULT_OK) {
PyErr_Format(
exc_type_for_avif_result(result),

Check warning on line 713 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L712-L713

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

Check warning on line 715 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L715

Added line #L715 was not covered by tests
);
avifDecoderDestroy(decoder);
PyObject_Del(self);
return NULL;

Check warning on line 719 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L717-L719

Added lines #L717 - L719 were not covered by tests
}

result = avifDecoderParse(decoder);
Expand Down Expand Up @@ -817,18 +808,18 @@
decoder = self->decoder;

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

Check warning on line 811 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L811

Added line #L811 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 817 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L816-L817

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

Check warning on line 820 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L819-L820

Added lines #L819 - L820 were not covered by tests
);
return NULL;

Check warning on line 822 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L822

Added line #L822 was not covered by tests
}

image = decoder->image;
Expand All @@ -848,18 +839,18 @@
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 843 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L842-L843

Added lines #L842 - L843 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 849 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L848-L849

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

Check warning on line 851 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L851

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

Check warning on line 853 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L853

Added line #L853 was not covered by tests
}

Py_BEGIN_ALLOW_THREADS;
Expand All @@ -867,13 +858,13 @@
Py_END_ALLOW_THREADS;

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

Check warning on line 862 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L861-L862

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

Check warning on line 864 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L864

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

Check warning on line 867 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L866-L867

Added lines #L866 - L867 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 @@ -946,7 +937,7 @@
static int
setup_module(PyObject *m) {
if (PyType_Ready(&AvifDecoder_Type) < 0 || PyType_Ready(&AvifEncoder_Type) < 0) {
return -1;

Check warning on line 940 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L940

Added line #L940 was not covered by tests
}

PyObject *d = PyModule_GetDict(m);
Expand All @@ -970,8 +961,8 @@

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

Check warning on line 965 in src/_avif.c

View check run for this annotation

Codecov / codecov/patch

src/_avif.c#L964-L965

Added lines #L964 - L965 were not covered by tests
}

return m;
Expand Down
Loading