From e0815adbd4e54b23333b080be1a8a725bb190cd9 Mon Sep 17 00:00:00 2001 From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Date: Sat, 6 Jan 2024 23:49:21 +0300 Subject: [PATCH] added preferred-decoder option (#193) Signed-off-by: Alexander Piskun --- CHANGELOG.md | 1 + docs/options.rst | 1 + pillow_heif/_pillow_heif.c | 25 ++++++++++++++++--------- pillow_heif/as_plugin.py | 4 ++++ pillow_heif/heif.py | 7 +++++++ pillow_heif/options.py | 21 ++++++++++++++++++++- tests/options_test.py | 6 ++++++ tests/read_test.py | 17 +++++++++++++++++ tests/write_test.py | 6 ++---- 9 files changed, 74 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 383a0ed4..4247dee0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. - `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 +- `options.PREFERRED_DECODER` - to use `decoder` different from the default one. #193 ### Changed diff --git a/docs/options.rst b/docs/options.rst index 06434d86..4f7936b1 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -11,6 +11,7 @@ Options .. autodata:: pillow_heif.options.ALLOW_INCORRECT_HEADERS .. autodata:: pillow_heif.options.SAVE_NCLX_PROFILE .. autodata:: pillow_heif.options.PREFERRED_ENCODER +.. autodata:: pillow_heif.options.PREFERRED_DECODER Example of use """""""""""""" diff --git a/pillow_heif/_pillow_heif.c b/pillow_heif/_pillow_heif.c index ef094526..372af7a9 100644 --- a/pillow_heif/_pillow_heif.c +++ b/pillow_heif/_pillow_heif.c @@ -89,6 +89,7 @@ typedef struct { int remove_stride; // private. decode option. int hdr_to_16bit; // private. decode option. int reload_size; // private. decode option. + char decoder_id[64]; // private. decode option. optional struct heif_image_handle *handle; // private struct heif_image *heif_image; // private const struct heif_depth_representation_info* depth_metadata; // only for image_type == 2 @@ -793,7 +794,8 @@ static void _CtxImage_destructor(CtxImageObject* self) { PyObject* _CtxImage(struct heif_image_handle* handle, int hdr_to_8bit, int bgr_mode, int remove_stride, int hdr_to_16bit, - int reload_size, int primary, PyObject* file_bytes) { + int reload_size, int primary, PyObject* file_bytes, + const char *decoder_id) { CtxImageObject *ctx_image = PyObject_New(CtxImageObject, &CtxImage_Type); if (!ctx_image) { heif_image_handle_release(handle); @@ -833,6 +835,7 @@ PyObject* _CtxImage(struct heif_image_handle* handle, int hdr_to_8bit, ctx_image->primary = primary; ctx_image->file_bytes = file_bytes; ctx_image->stride = get_stride(ctx_image); + strcpy(ctx_image->decoder_id, decoder_id); Py_INCREF(file_bytes); return (PyObject*)ctx_image; } @@ -1050,6 +1053,9 @@ int decode_image(CtxImageObject* self) { bytes_in_cc = 2; } + if (strlen(self->decoder_id) > 0) { + decode_options->decoder_id = self->decoder_id; + } error = heif_decode_image(self->handle, &self->heif_image, colorspace, chroma, decode_options); heif_decoding_options_free(decode_options); Py_END_ALLOW_THREADS @@ -1172,11 +1178,9 @@ static PyObject* _CtxWrite(PyObject* self, PyObject* args) { return NULL; struct heif_context* ctx = heif_context_alloc(); - 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; - } + if ((strlen(encoder_id) > 0) && + (heif_get_encoder_descriptors(heif_compression_undefined, encoder_id, encoders, 1) == 1) + ) { error = heif_context_get_encoder(ctx, encoders[0], &encoder); } else { @@ -1215,16 +1219,18 @@ static PyObject* _CtxWrite(PyObject* self, PyObject* args) { static PyObject* _load_file(PyObject* self, PyObject* args) { int hdr_to_8bit, threads_count, bgr_mode, remove_stride, hdr_to_16bit, reload_size; PyObject *heif_bytes; + const char *decoder_id; if (!PyArg_ParseTuple(args, - "Oiiiiii", + "Oiiiiiis", &heif_bytes, &threads_count, &hdr_to_8bit, &bgr_mode, &remove_stride, &hdr_to_16bit, - &reload_size)) + &reload_size, + &decoder_id)) return NULL; struct heif_context* heif_ctx = heif_context_alloc(); @@ -1272,7 +1278,8 @@ static PyObject* _load_file(PyObject* self, PyObject* args) { PyList_SET_ITEM(images_list, i, _CtxImage(handle, hdr_to_8bit, - bgr_mode, remove_stride, hdr_to_16bit, reload_size, primary, heif_bytes)); + bgr_mode, remove_stride, hdr_to_16bit, reload_size, primary, heif_bytes, + decoder_id)); else { Py_INCREF(Py_None); PyList_SET_ITEM(images_list, i, Py_None); diff --git a/pillow_heif/as_plugin.py b/pillow_heif/as_plugin.py index 21970db8..f513fbb8 100644 --- a/pillow_heif/as_plugin.py +++ b/pillow_heif/as_plugin.py @@ -246,6 +246,10 @@ def __options_update(**kwargs): options.ALLOW_INCORRECT_HEADERS = v elif k == "save_nclx_profile": options.SAVE_NCLX_PROFILE = v + elif k == "preferred_encoder": + options.PREFERRED_ENCODER = v + elif k == "preferred_decoder": + options.PREFERRED_DECODER = v else: warn(f"Unknown option: {k}", stacklevel=1) diff --git a/pillow_heif/heif.py b/pillow_heif/heif.py index 59680119..8c0da8b3 100644 --- a/pillow_heif/heif.py +++ b/pillow_heif/heif.py @@ -223,6 +223,12 @@ def __init__(self, fp=None, convert_hdr_to_8bit=True, bgr_mode=False, **kwargs): else: fp_bytes = _get_bytes(fp) mimetype = get_file_mimetype(fp_bytes) + if mimetype.find("avif") != -1: + preferred_decoder = options.PREFERRED_DECODER.get("AVIF", "") + elif mimetype.find("heic") != -1 or mimetype.find("heif") != -1: + preferred_decoder = options.PREFERRED_DECODER.get("HEIF", "") + else: + preferred_decoder = "" images = _pillow_heif.load_file( fp_bytes, options.DECODE_THREADS, @@ -231,6 +237,7 @@ def __init__(self, fp=None, convert_hdr_to_8bit=True, bgr_mode=False, **kwargs): kwargs.get("remove_stride", True), kwargs.get("hdr_to_16bit", True), kwargs.get("reload_size", options.ALLOW_INCORRECT_HEADERS), + preferred_decoder, ) self.mimetype = mimetype self._images: List[HeifImage] = [HeifImage(i) for i in images if i is not None] diff --git a/pillow_heif/options.py b/pillow_heif/options.py index 6b70d82f..8c023822 100644 --- a/pillow_heif/options.py +++ b/pillow_heif/options.py @@ -63,4 +63,23 @@ "AVIF": "", "HEIF": "", } -"""Use the specified encoder for format. You can get the available encoders IDs using ``libheif_info()`` function.""" +"""Use the specified encoder for format. + +You can get the available encoders IDs using ``libheif_info()`` function. + +When use pillow_heif as a plugin you can set this option with ``preferred_encoder`` key. + +.. note:: If the specified encoder is missing, the option will be ignored.""" + + +PREFERRED_DECODER = { + "AVIF": "", + "HEIF": "", +} +"""Use the specified decoder for format. + +You can get the available decoders IDs using ``libheif_info()`` function. + +When use pillow_heif as a plugin you can set this option with ``preferred_decoder`` key. + +.. note:: If the specified decoder is missing, the option will be ignored.""" diff --git a/tests/options_test.py b/tests/options_test.py index fd13f05c..85872ee4 100644 --- a/tests/options_test.py +++ b/tests/options_test.py @@ -34,6 +34,8 @@ def test_options_change_from_plugin_registering(register_opener): decode_threads=3, depth_images=False, save_nclx_profile=False, + preferred_encoder={"HEIF": "id1", "AVIF": "id2"}, + preferred_decoder={"HEIF": "id3", "AVIF": "id4"}, ) assert not options.THUMBNAILS assert options.QUALITY == 69 @@ -41,6 +43,8 @@ def test_options_change_from_plugin_registering(register_opener): assert options.DECODE_THREADS == 3 assert options.DEPTH_IMAGES is False assert options.SAVE_NCLX_PROFILE is False + assert options.PREFERRED_ENCODER == {"HEIF": "id1", "AVIF": "id2"} + assert options.PREFERRED_DECODER == {"HEIF": "id3", "AVIF": "id4"} finally: options.THUMBNAILS = True options.QUALITY = None @@ -48,6 +52,8 @@ def test_options_change_from_plugin_registering(register_opener): options.DECODE_THREADS = 4 options.DEPTH_IMAGES = True options.SAVE_NCLX_PROFILE = True + options.PREFERRED_ENCODER = {"HEIF": "", "AVIF": ""} + options.PREFERRED_DECODER = {"HEIF": "", "AVIF": ""} @pytest.mark.skipif(not hevc_enc(), reason="No HEVC encoder.") diff --git a/tests/read_test.py b/tests/read_test.py index 1162483f..709fe6fe 100644 --- a/tests/read_test.py +++ b/tests/read_test.py @@ -470,3 +470,20 @@ def test_depth_image(): assert depth_image.info["metadata"]["disparity_reference_view"] == 0 assert depth_image.info["metadata"]["nonlinear_representation_model_size"] == 0 assert im_pil.info == depth_image.info + + +def test_invalid_decoder(): + try: + pillow_heif.options.PREFERRED_DECODER["HEIF"] = "invalid_id" + Image.open("images/heif/RGB_8__128x128.heif").load() + finally: + pillow_heif.options.PREFERRED_DECODER["HEIF"] = "" + + +@pytest.mark.skipif("dav1d" not in pillow_heif.libheif_info()["decoders"], reason="Requires DAV1D AVIF decoder.") +def test_dav1d_decoder(): + try: + pillow_heif.options.PREFERRED_DECODER["AVIF"] = "dav1d" + Image.open("images/heif/RGB_8__128x128.avif").load() + finally: + pillow_heif.options.PREFERRED_DECODER["AVIF"] = "" diff --git a/tests/write_test.py b/tests/write_test.py index 4ee45d43..369614c2 100644 --- a/tests/write_test.py +++ b/tests/write_test.py @@ -589,10 +589,8 @@ def test_invalid_encoder(): 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") + im_rgb.save(buf, format="AVIF") + im_rgb.save(buf, format="HEIF") finally: pillow_heif.options.PREFERRED_ENCODER["AVIF"] = "" pillow_heif.options.PREFERRED_ENCODER["HEIF"] = ""