diff --git a/CHANGELOG.md b/CHANGELOG.md index d64c517f..70ac1295 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ All notable changes to this project will be documented in this file. -## [0.1x.x - 2024-0x-xx] +## [0.15.0 - 2024-01-xx] + +### Added + +- `libheif_info` function: added `encoders` and `decoders` keys to the result, for future libheif plugins support. #189 ### Changed diff --git a/pillow_heif/_lib_info.py b/pillow_heif/_lib_info.py index bf392d1d..70c08aca 100644 --- a/pillow_heif/_lib_info.py +++ b/pillow_heif/_lib_info.py @@ -1,5 +1,7 @@ """Functions to get versions of underlying libraries.""" +import typing + try: import _pillow_heif except ImportError as ex: @@ -10,18 +12,27 @@ def libheif_version() -> str: """Returns ``libheif`` version.""" - return _pillow_heif.lib_info["libheif"] + return _pillow_heif.get_lib_info()["libheif"] -def libheif_info() -> dict: +def libheif_info() -> dict[str, typing.Union[str, dict[str, str]]]: """Returns a dictionary with version information. - The keys `libheif`, `HEIF`, `AVIF` are always present, but values for `HEIF`/`AVIF` can be empty. + The keys `libheif`, `HEIF`, `AVIF`, `encoders`, `decoders` are always present, but values for all except + `libheif` can be empty. { - 'libheif': '1.14.2', + 'libheif': '1.15.2', 'HEIF': 'x265 HEVC encoder (3.4+31-6722fce1f)', - 'AVIF': 'AOMedia Project AV1 Encoder 3.5.0' + 'AVIF': 'AOMedia Project AV1 Encoder 3.5.0', + 'encoders': { + 'encoder1_id': 'encoder1_full_name', + 'encoder2_id': 'encoder2_full_name', + }, + 'decoders': { + 'decoder1_id': 'decoder1_full_name', + 'decoder2_id': 'decoder2_full_name', + }, } """ - return _pillow_heif.lib_info + return _pillow_heif.get_lib_info() diff --git a/pillow_heif/_pillow_heif.c b/pillow_heif/_pillow_heif.c index a175df5e..9df7cbe5 100644 --- a/pillow_heif/_pillow_heif.c +++ b/pillow_heif/_pillow_heif.c @@ -6,6 +6,9 @@ /* =========== Common stuff ======== */ +#define MAX_ENCODERS 20 +#define MAX_DECODERS 20 + #define RETURN_NONE Py_INCREF(Py_None); return Py_None; static struct heif_error heif_error_no = { .code = 0, .subcode = 0, .message = "" }; @@ -1268,11 +1271,50 @@ static PyObject* _load_file(PyObject* self, PyObject* args) { return images_list; } +static PyObject* _get_lib_info(PyObject* self) { + PyObject* lib_info_dict = PyDict_New(); + PyObject* encoders_dict = PyDict_New(); + PyObject* decoders_dict = PyDict_New(); + if ((!lib_info_dict) || (!encoders_dict) || (!decoders_dict)) { + PyErr_SetString(PyExc_OSError, "Out of Memory"); + return NULL; + } + __PyDict_SetItemString(lib_info_dict, "libheif", PyUnicode_FromString(heif_get_version())); + + const struct heif_encoder_descriptor* encoder_descriptor; + const char* x265_version = ""; + if (heif_get_encoder_descriptors(heif_compression_HEVC, NULL, &encoder_descriptor, 1)) + x265_version = heif_encoder_descriptor_get_name(encoder_descriptor); + __PyDict_SetItemString(lib_info_dict, "HEIF", PyUnicode_FromString(x265_version)); + const char* aom_version = ""; + if (heif_get_encoder_descriptors(heif_compression_AV1, NULL, &encoder_descriptor, 1)) + aom_version = heif_encoder_descriptor_get_name(encoder_descriptor); + __PyDict_SetItemString(lib_info_dict, "AVIF", PyUnicode_FromString(aom_version)); + + __PyDict_SetItemString(lib_info_dict, "encoders", encoders_dict); + __PyDict_SetItemString(lib_info_dict, "decoders", decoders_dict); + + const struct heif_encoder_descriptor* encoders[MAX_ENCODERS]; + int encoders_count = heif_get_encoder_descriptors(heif_compression_undefined, NULL, encoders, MAX_ENCODERS); + for (int i = 0; i < encoders_count; i++) { + __PyDict_SetItemString(encoders_dict, heif_encoder_descriptor_get_id_name(encoders[i]), PyUnicode_FromString(heif_encoder_descriptor_get_name(encoders[i]))); + } + + const struct heif_decoder_descriptor* decoders[MAX_DECODERS]; + int decoders_count = heif_get_decoder_descriptors(heif_compression_undefined, decoders, MAX_DECODERS); + for (int i = 0; i < decoders_count; i++) { + __PyDict_SetItemString(decoders_dict, heif_decoder_descriptor_get_id_name(decoders[i]), PyUnicode_FromString(heif_decoder_descriptor_get_name(decoders[i]))); + } + + return lib_info_dict; +} + /* =========== Module =========== */ static PyMethodDef heifMethods[] = { {"CtxWrite", (PyCFunction)_CtxWrite, METH_VARARGS}, {"load_file", (PyCFunction)_load_file, METH_VARARGS}, + {"get_lib_info", (PyCFunction)_get_lib_info, METH_NOARGS}, {NULL, NULL} }; @@ -1307,8 +1349,6 @@ static PyTypeObject CtxImage_Type = { }; static int setup_module(PyObject* m) { - PyObject* d = PyModule_GetDict(m); - if (PyType_Ready(&CtxWriteImage_Type) < 0) return -1; @@ -1319,22 +1359,6 @@ static int setup_module(PyObject* m) { return -1; heif_init(NULL); - - const struct heif_encoder_descriptor* encoder_descriptor; - const char* x265_version = ""; - if (heif_context_get_encoder_descriptors(NULL, heif_compression_HEVC, NULL, &encoder_descriptor, 1)) - x265_version = heif_encoder_descriptor_get_name(encoder_descriptor); - const char* aom_version = ""; - if (heif_context_get_encoder_descriptors(NULL, heif_compression_AV1, NULL, &encoder_descriptor, 1)) - aom_version = heif_encoder_descriptor_get_name(encoder_descriptor); - - PyObject* version_dict = PyDict_New(); - __PyDict_SetItemString(version_dict, "libheif", PyUnicode_FromString(heif_get_version())); - __PyDict_SetItemString(version_dict, "HEIF", PyUnicode_FromString(x265_version)); - __PyDict_SetItemString(version_dict, "AVIF", PyUnicode_FromString(aom_version)); - - if (__PyDict_SetItemString(d, "lib_info", version_dict) < 0) - return -1; return 0; } diff --git a/pillow_heif/_version.py b/pillow_heif/_version.py index 6e117bab..bb478ad2 100644 --- a/pillow_heif/_version.py +++ b/pillow_heif/_version.py @@ -1,3 +1,3 @@ """Version of pillow_heif/pi_heif.""" -__version__ = "0.14.0" +__version__ = "0.15.0.dev0" diff --git a/pillow_heif/as_plugin.py b/pillow_heif/as_plugin.py index d0361015..21970db8 100644 --- a/pillow_heif/as_plugin.py +++ b/pillow_heif/as_plugin.py @@ -175,7 +175,7 @@ def register_heif_opener(**kwargs) -> None: """ __options_update(**kwargs) Image.register_open(HeifImageFile.format, HeifImageFile, _is_supported_heif) - if _pillow_heif.lib_info["HEIF"]: + if _pillow_heif.get_lib_info()["HEIF"]: Image.register_save(HeifImageFile.format, _save_heif) Image.register_save_all(HeifImageFile.format, _save_all_heif) extensions = [".heic", ".heics", ".heif", ".heifs", ".hif"] @@ -216,7 +216,7 @@ def register_avif_opener(**kwargs) -> None: :param kwargs: dictionary with values to set in options. See: :ref:`options`. """ - if not _pillow_heif.lib_info["AVIF"]: + if not _pillow_heif.get_lib_info()["AVIF"]: warn("This version of `pillow-heif` was built without AVIF support.", stacklevel=1) return __options_update(**kwargs) diff --git a/pillow_heif/heif.py b/pillow_heif/heif.py index 136fa7c8..59680119 100644 --- a/pillow_heif/heif.py +++ b/pillow_heif/heif.py @@ -544,7 +544,7 @@ def encode(mode: str, size: tuple, data, fp, **kwargs) -> None: def _encode_images(images: List[HeifImage], fp, **kwargs) -> None: compression = kwargs.get("format", "HEIF") compression_format = HeifCompressionFormat.AV1 if compression == "AVIF" else HeifCompressionFormat.HEVC - if not _pillow_heif.lib_info[compression]: + if not _pillow_heif.get_lib_info()[compression]: raise RuntimeError(f"No {compression} encoder found.") images_to_save: List[HeifImage] = images + kwargs.get("append_images", []) if not kwargs.get("save_all", True): diff --git a/tests/basic_test.py b/tests/basic_test.py index e9f46e67..5523424c 100644 --- a/tests/basic_test.py +++ b/tests/basic_test.py @@ -13,11 +13,9 @@ def test_libheif_info(): info = pillow_heif.libheif_info() - for key in ("HEIF", "AVIF"): + for key in ("HEIF", "AVIF", "encoders", "decoders"): assert key in info assert pillow_heif.libheif_version() in ( - "1.14.1", - "1.14.2", "1.15.1", "1.15.2", "1.16.1", @@ -112,6 +110,8 @@ def test_full_build(): info = pillow_heif.libheif_info() assert info["AVIF"] assert info["HEIF"] + assert info["encoders"] + assert info["decoders"] expected_version = os.getenv("EXP_PH_LIBHEIF_VERSION", "1.17.6") if expected_version: assert info["libheif"] == expected_version @@ -122,6 +122,7 @@ def test_light_build(): info = pillow_heif.libheif_info() assert not info["AVIF"] assert not info["HEIF"] + assert info["decoders"] expected_version = os.getenv("EXP_PH_LIBHEIF_VERSION", "1.17.6") if expected_version: assert info["libheif"] == expected_version diff --git a/tests/write_test.py b/tests/write_test.py index 38724afd..ed978e02 100644 --- a/tests/write_test.py +++ b/tests/write_test.py @@ -98,7 +98,10 @@ def test_pillow_save_one_all(): def test_heif_no_encoder(): - with mock.patch.dict("pillow_heif.heif._pillow_heif.lib_info", {"libheif": "1.14.2", "HEIF": "", "AVIF": ""}): + def get_lib_info(): + return {"libheif": "1.17.5", "HEIF": "", "AVIF": "", "encoders": {}, "decoders": {}} + + with mock.patch("pillow_heif.heif._pillow_heif.get_lib_info", side_effect=get_lib_info): im_heif = pillow_heif.from_pillow(Image.new("L", (64, 64))) out_buffer = BytesIO() with pytest.raises(RuntimeError):