Skip to content
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

libheif_info: Extended the returning results #189

Merged
merged 1 commit into from
Dec 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
23 changes: 17 additions & 6 deletions pillow_heif/_lib_info.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Functions to get versions of underlying libraries."""

import typing

try:
import _pillow_heif
except ImportError as ex:
Expand All @@ -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()
60 changes: 42 additions & 18 deletions pillow_heif/_pillow_heif.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "" };
Expand Down Expand Up @@ -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}
};

Expand Down Expand Up @@ -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;

Expand All @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion pillow_heif/_version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Version of pillow_heif/pi_heif."""

__version__ = "0.14.0"
__version__ = "0.15.0.dev0"
4 changes: 2 additions & 2 deletions pillow_heif/as_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pillow_heif/heif.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
7 changes: 4 additions & 3 deletions tests/basic_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand All @@ -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
5 changes: 4 additions & 1 deletion tests/write_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down