diff --git a/CHANGELOG.md b/CHANGELOG.md index 70ac1295..383a0ed4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,11 @@ All notable changes to this project will be documented in this file. -## [0.15.0 - 2024-01-xx] +## [0.15.0 - 2024-0x-xx] ### Added - `libheif_info` function: added `encoders` and `decoders` keys to the result, for future libheif plugins support. #189 +- `options.PREFERRED_ENCODER` - to use `encoder` different from the default one. #192 ### Changed diff --git a/docs/options.rst b/docs/options.rst index d912fb21..06434d86 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -10,6 +10,7 @@ Options .. autodata:: pillow_heif.options.SAVE_HDR_TO_12_BIT .. autodata:: pillow_heif.options.ALLOW_INCORRECT_HEADERS .. autodata:: pillow_heif.options.SAVE_NCLX_PROFILE +.. autodata:: pillow_heif.options.PREFERRED_ENCODER Example of use """""""""""""" diff --git a/pillow_heif/_pillow_heif.c b/pillow_heif/_pillow_heif.c index 9df7cbe5..ef094526 100644 --- a/pillow_heif/_pillow_heif.c +++ b/pillow_heif/_pillow_heif.c @@ -1161,16 +1161,28 @@ static struct PyGetSetDef _CtxImage_getseters[] = { /* =========== Functions ======== */ static PyObject* _CtxWrite(PyObject* self, PyObject* args) { - /* compression_format: int, quality: int */ + /* compression_format: int, quality: int, encoder_id: str */ struct heif_encoder* encoder; struct heif_error error; int compression_format, quality; + const char *encoder_id; + const struct heif_encoder_descriptor* encoders[1]; - if (!PyArg_ParseTuple(args, "ii", &compression_format, &quality)) + if (!PyArg_ParseTuple(args, "iis", &compression_format, &quality, &encoder_id)) return NULL; struct heif_context* ctx = heif_context_alloc(); - error = heif_context_get_encoder_for_format(ctx, compression_format, &encoder); + if (strlen(encoder_id) > 0) { + if (heif_get_encoder_descriptors(heif_compression_undefined, encoder_id, encoders, 1) != 1) { + PyErr_SetString(PyExc_RuntimeError, "could not find encoder with provided ID"); + return NULL; + } + error = heif_context_get_encoder(ctx, encoders[0], &encoder); + } + else { + error = heif_context_get_encoder_for_format(ctx, compression_format, &encoder); + } + if (check_error(error)) { heif_context_free(ctx); return NULL; diff --git a/pillow_heif/constants.py b/pillow_heif/constants.py index ddd04109..98e64b2e 100644 --- a/pillow_heif/constants.py +++ b/pillow_heif/constants.py @@ -49,19 +49,23 @@ class HeifCompressionFormat(IntEnum): UNDEFINED = 0 """The compression format is not defined.""" HEVC = 1 - """The compression format is HEVC.""" + """Equivalent to H.265.""" AVC = 2 - """The compression format is AVC.""" + """Equivalent to H.264. Defined in ISO/IEC 14496-10.""" JPEG = 3 - """The compression format is JPEG.""" + """JPEG compression. Defined in ISO/IEC 10918-1.""" AV1 = 4 - """The compression format is AV1.""" + """AV1 compression, used for AVIF images.""" VVC = 5 - """The compression format is VVC.""" + """Equivalent to H.266. Defined in ISO/IEC 23090-3.""" EVC = 6 - """The compression format is EVC.""" + """Equivalent to H.266. Defined in ISO/IEC 23094-1.""" JPEG2000 = 7 """The compression format is JPEG200 ISO/IEC 15444-16:2021""" + UNCOMPRESSED = 8 + """Defined in ISO/IEC 23001-17:2023 (Final Draft International Standard).""" + MASK = 9 + """Mask image encoding. See ISO/IEC 23008-12:2022 Section 6.10.2""" class HeifColorPrimaries(IntEnum): diff --git a/pillow_heif/misc.py b/pillow_heif/misc.py index a948c208..d62a9729 100644 --- a/pillow_heif/misc.py +++ b/pillow_heif/misc.py @@ -327,7 +327,11 @@ class CtxEncode: def __init__(self, compression_format: HeifCompressionFormat, **kwargs): quality = kwargs.get("quality", options.QUALITY) - self.ctx_write = _pillow_heif.CtxWrite(compression_format, -2 if quality is None else quality) + self.ctx_write = _pillow_heif.CtxWrite( + compression_format, + -2 if quality is None else quality, + options.PREFERRED_ENCODER.get("HEIF" if compression_format == HeifCompressionFormat.HEVC else "AVIF", ""), + ) enc_params = kwargs.get("enc_params", {}) chroma = kwargs.get("chroma", None) if chroma is None and "subsampling" in kwargs: diff --git a/pillow_heif/options.py b/pillow_heif/options.py index ceedcfaa..6b70d82f 100644 --- a/pillow_heif/options.py +++ b/pillow_heif/options.py @@ -57,3 +57,10 @@ .. note:: `save_nclx_profile` specified during calling ``save`` has higher priority than this. When use pillow_heif as a plugin you can unset it with: `register_*_opener(save_nclx_profile=False)`""" + + +PREFERRED_ENCODER = { + "AVIF": "", + "HEIF": "", +} +"""Use the specified encoder for format. You can get the available encoders IDs using ``libheif_info()`` function.""" diff --git a/tests/write_test.py b/tests/write_test.py index ed978e02..4ee45d43 100644 --- a/tests/write_test.py +++ b/tests/write_test.py @@ -581,3 +581,54 @@ def test_lossless_encoding_rgba(save_format): buf = BytesIO() im_rgb.save(buf, format=save_format, quality=-1, chroma=444, matrix_coefficients=0) helpers.assert_image_equal(im_rgb, Image.open(buf)) + + +def test_invalid_encoder(): + im_rgb = helpers.gradient_rgba() + buf = BytesIO() + try: + pillow_heif.options.PREFERRED_ENCODER["AVIF"] = "invalid_id" + pillow_heif.options.PREFERRED_ENCODER["HEIF"] = "invalid_id" + with pytest.raises(RuntimeError): + im_rgb.save(buf, format="AVIF") + with pytest.raises(RuntimeError): + im_rgb.save(buf, format="HEIF") + finally: + pillow_heif.options.PREFERRED_ENCODER["AVIF"] = "" + pillow_heif.options.PREFERRED_ENCODER["HEIF"] = "" + + +@pytest.mark.skipif("svt" not in pillow_heif.libheif_info()["encoders"], reason="Requires SVT AVIF encoder.") +@pytest.mark.skipif("aom" not in pillow_heif.libheif_info()["encoders"], reason="Requires AOM AVIF encoder.") +def test_svt_encoder(): + im_rgb = helpers.gradient_rgb() + buf_aom = BytesIO() + im_rgb.save(buf_aom, format="AVIF") + buf_svt = BytesIO() + try: + pillow_heif.options.PREFERRED_ENCODER["AVIF"] = "svt" + im_rgb.save(buf_svt, format="AVIF") + finally: + pillow_heif.options.PREFERRED_ENCODER["AVIF"] = "" + aom_img_data = Image.open(buf_aom).tobytes() + svt_image_data = Image.open(buf_svt).tobytes() + # print(f"AOM size: {len(aom_img_data)} , SVT size: {len(svt_image_data)}", ) + assert aom_img_data != svt_image_data # Suppose that: different decoders by default will have different results + + +@pytest.mark.skipif("rav1e" not in pillow_heif.libheif_info()["encoders"], reason="Requires RAV1E AVIF encoder.") +@pytest.mark.skipif("aom" not in pillow_heif.libheif_info()["encoders"], reason="Requires AOM AVIF encoder.") +def test_rav1e_encoder(): + im_rgb = helpers.gradient_rgb() + buf_aom = BytesIO() + im_rgb.save(buf_aom, format="AVIF") + buf_rav1e = BytesIO() + try: + pillow_heif.options.PREFERRED_ENCODER["AVIF"] = "rav1e" + im_rgb.save(buf_rav1e, format="AVIF") + finally: + pillow_heif.options.PREFERRED_ENCODER["AVIF"] = "" + aom_img_data = Image.open(buf_aom).tobytes() + rav1e_image_data = Image.open(buf_rav1e).tobytes() + # print(f"AOM size: {len(aom_img_data)} , RAV1E size: {len(rav1e_image_data)}", ) + assert aom_img_data != rav1e_image_data # Suppose that: different decoders by default will have different results