-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
Add AVIF plugin (decoder + encoder using libavif) #5201
base: main
Are you sure you want to change the base?
Conversation
Tests/helper.py
Outdated
@@ -206,6 +207,7 @@ def _test_leak(self, core): | |||
start_mem = self._get_mem_usage() | |||
for cycle in range(self.iterations): | |||
core() | |||
gc.collect() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you want to talk about why you added this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I accidentally left this in here while I was debugging. I'll remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I realized now why I added this: without it the leak tests are non-deterministic. I could pad the memory limit to counteract the fact that it may not have hit the gc generation threshold before it checks the memory, but forcing garbage collection after each iteration ensures that the test is deterministic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line has been moved into test_file_avif.py
src/_avif.c
Outdated
} | ||
|
||
avifRGBImageAllocatePixels(&rgb); | ||
memcpy(rgb.pixels, rgb_bytes, size); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please document in a comment that this is safe for r/w, and potentially add an explict check that the rgb_bytes/rgb.pixels is large enough.
src/_avif.c
Outdated
return NULL; | ||
} | ||
|
||
memcpy(self->data, avif_bytes, size); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Document here as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't entirely sure what you wanted documented for this line. I added this, let me know if it's what you had in mind:
Lines 484 to 485 in b84a8e0
// We need to allocate storage for the decoder for the lifetime of the object | |
// (avifDecoderSetIOMemory does not copy the data passed into it) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was able to avoid a memcpy here by having PyArg_ParseTuple
pass in a PyBytesObject
and incrementing the reference in the new / decrementing in the dealloc. That also avoids an unnecessary malloc during decoding.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Realized it would probably be better to have you resolve these conversations, to confirm that the feedback has indeed been addressed.
return NULL; | ||
} | ||
|
||
size = rgb.rowBytes * rgb.height; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added here
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; | |
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
|
||
|
||
@skip_unless_feature("avif") | ||
class TestAvifLeaks(PillowLeakTestCase): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer not iterating a leak test in the standard test suite, as that can be expensive from a time POV. It's ok for the initial cut, but I'd rather not have it long term.
Adding libavif to MSYS2 fails to compile due to a few missing defines ( |
@nulano it looks like those defines were only added in libavif 0.8.3. I'll figure some |
@nulano Is it okay if I cherry-pick your MSYS commit into this PR? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nulano Is it okay if I cherry-pick your MSYS commit into this PR?
Of course, cherry-pick away!
I have a few nitpicks for winbuild/build_prepare
, I haven't looked at the rest yet.
@radarhere @wiredfool @nulano I think I've addressed all feedback (except for the requests for docs on building), but I've left it up to you all to resolve conversations (or not). Is this PR generally on the right track? I've held off on writing docs until I've gotten a signal one way or the other. |
7efcefc
to
3ae762e
Compare
Since it's been a month since I asked my question without response, I'll try to reframe it as more specific questions that might be more answerable.
|
b433571
to
ff56a9c
Compare
This might be a libavif bug, but I find that if I run this PR, libavif has stopped working for macOS. https://github.com/radarhere/Pillow/runs/2201531959#step:8:1174
LIBYUV_UNLIMITED_DATA was a change introduced in libyuv in the last month - https://chromium.googlesource.com/libyuv/libyuv/+/ba033a11e3948e4b3%5E%21/#F2 |
We're going to need to add the required libraries to the docker images as well, and we're going to need to add these to the oss-fuzz builder to get fuzzer support. Might as well make a PR to the Pillow-wheels for whatever needs to happen on build. That will also be potentially helpful for getting the dependencies into oss-fuzz. |
a9b00e0
to
09567f6
Compare
b851ca6
to
649f5f3
Compare
* Removed skip_unless_feature on methods when class is already skipped * Test speed less than slowest and greater than fastest * Updated type hints * Only access angle when AVIF_TRANSFORM_IROT flag is present * Added AVIF_ROOT * Only define normalize_quantize_value if it will be used * Build libavif after libjpeg * Use rgb.rowBytes in overflow check * Group EXIF info * Removed __loaded * If brew is not installed, use /usr prefix * Sort AVIF codecs alphabetically * Updated rav1e license * Fixed catching warning, as per python-pillow#8505 * Simplified code * Fixed typos * Test further scenarios * Use y* to parse bytes --------- Co-authored-by: Andrew Murray <[email protected]>
Co-authored-by: Andrew Murray <[email protected]>
* Simplify Python code by receiving tuple from C, as per python-pillow#8740 * Use default PyTypeObject value * Removed AVIF_TRUE * Width and height are already set on first frame * Removed memset * Depth is set by avifRGBImageSetDefaults * Replace PyObject with int * After a failed pixel allocation, destroy non-first frame * Added error if avifImageCreateEmpty returns NULL * Python images cannot have negative dimensions * Test invalid canvas dimensions * Use boolean format argument * Handle avifDecoderCreate and avifEncoderCreate errors * tileRowsLog2 and tileColsLog2 are ignored if autotiling is enabled * Only define _add_codec_specific_options if it may be used * Test non-string advanced value * Simplified error handling in AvifEncoderNew * Corrected heading --------- Co-authored-by: Andrew Murray <[email protected]>
range_ = info.get("range", "full") | ||
tile_rows_log2 = info.get("tile_rows", 0) | ||
tile_cols_log2 = info.get("tile_cols", 0) | ||
alpha_premultiplied = bool(info.get("alpha_premultiplied", False)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need alpha_premultiplied
as an argument? To my simple way of thinking, it would cause the saved image to no longer be accurate to the Pillow image being saved. We do have a separate mode for premultiplied alpha, RGBa
, see https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've created fdintino#23
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I receive a comment on my PR that
I think if the underlying pixel data in the AVIF image was RGBa then it would make sense, because you would then presumably get RGBa back from the decoder. But because it is converting to YUV, and because alphaPremultiply is specified separately for the RGB and YUV image, with the former only existing so that libavif can do alpha multiplying and unmultiplying if necessary, I'm not so sure. To the extent that the RGBa mode is used in pillow, I imagine it is mostly for image compositing operations. But for an AVIF image it really only serves to get more efficient compression on images with detailed alpha planes, in a way that doesn't compromise perceptual quality. I think it wouldn't be obvious that the intended way to enable (what amounts to) a lossy compression flag would be to convert to a different mode, particularly when that mode is different from what is actually stored in the image.
It's not the default behaviour of the plugin, so I'm not going to fight too hard against this. I've closed my PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ubuntu Jammy has libavif 0.9.3, so I guess it makes sense to support versions before 1.0.0.
I would like someone else from the core team to review this for any license implications
- I've realised that this has essentially copied
avifImageGetExifOrientationFromIrotImir
andavifImageExtractExifOrientationToIrotImir
from libavif. Granted, they are relatively simple functions. - By introducing new dependencies - not just libavif, but also various codecs - there are different licenses to consider. Remember that we don't distribute libimagequant with our wheels
https://pillow.readthedocs.io/en/stable/installation/building-from-source.html#building-from-source
Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled.
@@ -50,6 +50,7 @@ LIBWEBP_VERSION=1.5.0 | |||
BZIP2_VERSION=1.0.8 | |||
LIBXCB_VERSION=1.17.0 | |||
BROTLI_VERSION=1.1.0 | |||
LIBAVIF_VERSION=1.1.1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Version 1.2.0 just got released, please let us know if that works for you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It failed one of our linux aarch64 builds. I've opened a bug report to libyuv here
Updates the pillow-avif-plugin code to more closely match the current state of the open Pillow PR, python-pillow/Pillow#5201. The differences that remain have to do with python 2.7 compatibility. Most of the code changes from the Pillow PR are stylistic, not functional, but there are two bug fixes included: - AvifImagePlugin.CHROMA_UPSAMPLING is now actually used by the decoder. Previously, although it was passed into the decoder, it did not have any effect. Note that this is different from the Pillow PR, where this functionality was removed instead. - AVIF images with irot and imir now have those values converted to an EXIF orientation when decoded. EXIF orientation has been preserved by the encoder since 1.4.2, which is when we started setting irot and imir. But if such an image was converted to another format, the orientation would have been lost.
Updates the pillow-avif-plugin code to more closely match the current state of the open Pillow PR, python-pillow/Pillow#5201. The differences that remain have to do with python 2.7 compatibility. Most of the code changes from the Pillow PR are stylistic, not functional, but there are two bug fixes included: - AvifImagePlugin.CHROMA_UPSAMPLING is now actually used by the decoder. Previously, although it was passed into the decoder, it did not have any effect. Note that this is different from the Pillow PR, where this functionality was removed instead. - AVIF images with irot and imir now have those values converted to an EXIF orientation when decoded. EXIF orientation has been preserved by the encoder since 1.4.2, which is when we started setting irot and imir. But if such an image was converted to another format the orientation would have been lost.
Resolves #7983
This adds support for AVIF encoding and decoding, including AVIF image sequences.
I've added tests, and integrated libavif into the windows, linux, and mac CI builds. I haven't done anything to integrate with the docker-images repo.
I chose libavif rather than libheif because the former has been embraced by AOMedia and it's what Chromium uses. Packaging support is spotty at the moment, but I expect that to change soon (currently it's in Debian testing, Fedora rawhide, Ubuntu hirsute, and Alpine edge).
A few notes on the implementation here:
The star.avifs test file is licensed as CC-BY
I linted the C code with the new clang-format settings, but made the following change so that it didn't make
PyObject_HEAD
and the threading macros look wonky: