From cf43c5f87722192e417a3dc15c8f1b1b5ff184a5 Mon Sep 17 00:00:00 2001 From: johncf Date: Wed, 9 Oct 2024 00:37:32 +0530 Subject: [PATCH 01/16] expose all aux images --- pillow_heif/_pillow_heif.c | 88 ++++++++++++++++++++++++++++++++++++++ pillow_heif/heif.py | 31 +++++++++++++- 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/pillow_heif/_pillow_heif.c b/pillow_heif/_pillow_heif.c index be80ef46..6eb0fa9d 100644 --- a/pillow_heif/_pillow_heif.c +++ b/pillow_heif/_pillow_heif.c @@ -732,6 +732,54 @@ static struct PyMethodDef _CtxWrite_methods[] = { {NULL, NULL} }; +/* =========== CtxAuxImage ======== */ + +PyObject* _CtxAuxImage(struct heif_image_handle* main_handle, heif_item_id aux_image_id, + int remove_stride, int hdr_to_16bit, PyObject* file_bytes) { + struct heif_image_handle* aux_handle; + if (check_error(heif_image_handle_get_auxiliary_image_handle(main_handle, aux_image_id, &aux_handle))) { + Py_RETURN_NONE; + } + CtxImageObject *ctx_image = PyObject_New(CtxImageObject, &CtxImage_Type); + if (!ctx_image) { + heif_image_handle_release(aux_handle); + Py_RETURN_NONE; + } + ctx_image->depth_metadata = NULL; + ctx_image->image_type = PhHeifImage; + ctx_image->width = heif_image_handle_get_width(aux_handle); + ctx_image->height = heif_image_handle_get_height(aux_handle); + ctx_image->alpha = 0; + ctx_image->n_channels = 1; + ctx_image->bits = heif_image_handle_get_luma_bits_per_pixel(aux_handle); + strcpy(ctx_image->mode, "L"); + if (ctx_image->bits > 8) { + if (hdr_to_16bit) { + strcpy(ctx_image->mode, "I;16"); + } + else if (ctx_image->bits == 10) { + strcpy(ctx_image->mode, "I;10"); + } + else { + strcpy(ctx_image->mode, "I;12"); + } + } + ctx_image->hdr_to_8bit = 0; + ctx_image->bgr_mode = 0; + ctx_image->colorspace = heif_colorspace_monochrome; + ctx_image->chroma = heif_chroma_monochrome; + ctx_image->handle = aux_handle; + ctx_image->heif_image = NULL; + ctx_image->data = NULL; + ctx_image->remove_stride = remove_stride; + ctx_image->hdr_to_16bit = hdr_to_16bit; + ctx_image->reload_size = 1; + ctx_image->file_bytes = file_bytes; + ctx_image->stride = get_stride(ctx_image); + Py_INCREF(file_bytes); + return (PyObject*)ctx_image; +} + /* =========== CtxDepthImage ======== */ PyObject* _CtxDepthImage(struct heif_image_handle* main_handle, heif_item_id depth_image_id, @@ -1183,6 +1231,44 @@ static PyObject* _CtxImage_depth_image_list(CtxImageObject* self, void* closure) return images_list; } +static PyObject* _CtxImage_aux_image_list(CtxImageObject* self, void* closure) { + int aux_filter = LIBHEIF_AUX_IMAGE_FILTER_OMIT_ALPHA | LIBHEIF_AUX_IMAGE_FILTER_OMIT_DEPTH; + int n_images = heif_image_handle_get_number_of_auxiliary_images(self->handle, aux_filter); + if (n_images == 0) + return PyList_New(0); + heif_item_id* images_ids = (heif_item_id*)malloc(n_images * sizeof(heif_item_id)); + if (!images_ids) + return PyList_New(0); + + n_images = heif_image_handle_get_list_of_auxiliary_image_IDs(self->handle, aux_filter, images_ids, n_images); + PyObject* images_list = PyList_New(n_images); + if (!images_list) { + free(images_ids); + return PyList_New(0); + } + + for (int i = 0; i < n_images; i++) { + PyList_SET_ITEM(images_list, + i, + _CtxAuxImage( + self->handle, images_ids[i], self->remove_stride, self->hdr_to_16bit, self->file_bytes + )); + } + free(images_ids); + return images_list; +} + +static PyObject* _CtxImage_aux_image_type(CtxImageObject* self, void* closure) { + const char* aux_type_c = NULL; + struct heif_error error = heif_image_handle_get_auxiliary_type(self->handle, &aux_type_c); + if (check_error(error)) { + Py_RETURN_NONE; + } + PyObject *aux_image_type = PyUnicode_FromString(aux_type_c); + heif_image_handle_release_auxiliary_type(self->handle, &aux_type_c); + return aux_image_type; +} + /* =========== CtxImage Experimental Part ======== */ static PyObject* _CtxImage_camera_intrinsic_matrix(CtxImageObject* self, void* closure) { @@ -1245,6 +1331,8 @@ static struct PyGetSetDef _CtxImage_getseters[] = { {"stride", (getter)_CtxImage_stride, NULL, NULL, NULL}, {"data", (getter)_CtxImage_data, NULL, NULL, NULL}, {"depth_image_list", (getter)_CtxImage_depth_image_list, NULL, NULL, NULL}, + {"aux_image_list", (getter)_CtxImage_aux_image_list, NULL, NULL, NULL}, + {"aux_image_type", (getter)_CtxImage_aux_image_type, NULL, NULL, NULL}, {"camera_intrinsic_matrix", (getter)_CtxImage_camera_intrinsic_matrix, NULL, NULL, NULL}, {"camera_extrinsic_matrix_rot", (getter)_CtxImage_camera_extrinsic_matrix_rot, NULL, NULL, NULL}, {NULL, NULL, NULL, NULL, NULL} diff --git a/pillow_heif/heif.py b/pillow_heif/heif.py index b9df9adc..4b9e5e23 100644 --- a/pillow_heif/heif.py +++ b/pillow_heif/heif.py @@ -38,7 +38,7 @@ class BaseImage: - """Base class for :py:class:`HeifImage` and :py:class:`HeifDepthImage`.""" + """Base class for :py:class:`HeifImage`, :py:class:`HeifDepthImage` and :py:class:`HeifAuxImage`.""" size: tuple[int, int] """Width and height of the image.""" @@ -140,6 +140,31 @@ def to_pillow(self) -> Image.Image: return image +class HeifAuxImage(BaseImage): + """Class representing the auxiliary image associated with the :py:class:`~pillow_heif.HeifImage` class.""" + + def __init__(self, c_image): + super().__init__(c_image) + _image_type = c_image.aux_image_type + self.info = { + "aux_image_type": _image_type, + } + save_colorspace_chroma(c_image, self.info) + + def __repr__(self): + _bytes = f"{len(self.data)} bytes" if self._data or isinstance(self._c_image, MimCImage) else "no" + return f"<{self.__class__.__name__} {self.size[0]}x{self.size[1]} {self.mode}>" + + def to_pillow(self) -> Image.Image: + """Helper method to create :external:py:class:`~PIL.Image.Image` class. + + :returns: :external:py:class:`~PIL.Image.Image` class created from an image. + """ + image = super().to_pillow() + image.info = self.info.copy() + return image + + class HeifImage(BaseImage): """One image in a :py:class:`~pillow_heif.HeifFile` container.""" @@ -152,6 +177,9 @@ def __init__(self, c_image): _depth_images: list[HeifDepthImage | None] = ( [HeifDepthImage(i) for i in c_image.depth_image_list if i is not None] if options.DEPTH_IMAGES else [] ) + _aux_images: list[HeifAuxImage | None] = ( + [HeifAuxImage(i) for i in c_image.aux_image_list if i is not None] # if options.AUX_IMAGES else [] + ) _heif_meta = _get_heif_meta(c_image) self.info = { "primary": bool(c_image.primary), @@ -160,6 +188,7 @@ def __init__(self, c_image): "metadata": _metadata, "thumbnails": _thumbnails, "depth_images": _depth_images, + "aux_images": _aux_images, } if _xmp: self.info["xmp"] = _xmp From 0e2ea000f7a26d638db27eb39a024b30292096a7 Mon Sep 17 00:00:00 2001 From: johncf Date: Wed, 9 Oct 2024 14:09:56 +0530 Subject: [PATCH 02/16] remove unnecessary copy and _bytes variables these are only meaningful in HeifImage class --- pillow_heif/heif.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pillow_heif/heif.py b/pillow_heif/heif.py index 4b9e5e23..09c30aea 100644 --- a/pillow_heif/heif.py +++ b/pillow_heif/heif.py @@ -127,7 +127,6 @@ def __init__(self, c_image): save_colorspace_chroma(c_image, self.info) def __repr__(self): - _bytes = f"{len(self.data)} bytes" if self._data or isinstance(self._c_image, MimCImage) else "no" return f"<{self.__class__.__name__} {self.size[0]}x{self.size[1]} {self.mode}>" def to_pillow(self) -> Image.Image: @@ -136,7 +135,6 @@ def to_pillow(self) -> Image.Image: :returns: :external:py:class:`~PIL.Image.Image` class created from an image. """ image = super().to_pillow() - image.info = self.info.copy() return image @@ -152,7 +150,6 @@ def __init__(self, c_image): save_colorspace_chroma(c_image, self.info) def __repr__(self): - _bytes = f"{len(self.data)} bytes" if self._data or isinstance(self._c_image, MimCImage) else "no" return f"<{self.__class__.__name__} {self.size[0]}x{self.size[1]} {self.mode}>" def to_pillow(self) -> Image.Image: @@ -161,7 +158,6 @@ def to_pillow(self) -> Image.Image: :returns: :external:py:class:`~PIL.Image.Image` class created from an image. """ image = super().to_pillow() - image.info = self.info.copy() return image From d61b4f77c010df56d0196a9e6da92fd624a3cb01 Mon Sep 17 00:00:00 2001 From: johncf Date: Wed, 9 Oct 2024 21:57:40 +0530 Subject: [PATCH 03/16] split aux_image_list method; and cleanup --- pillow_heif/_pillow_heif.c | 50 +++++++++++++++++++++++++++++++------- pillow_heif/heif.py | 8 +++--- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/pillow_heif/_pillow_heif.c b/pillow_heif/_pillow_heif.c index 6eb0fa9d..8c7ea178 100644 --- a/pillow_heif/_pillow_heif.c +++ b/pillow_heif/_pillow_heif.c @@ -1231,7 +1231,7 @@ static PyObject* _CtxImage_depth_image_list(CtxImageObject* self, void* closure) return images_list; } -static PyObject* _CtxImage_aux_image_list(CtxImageObject* self, void* closure) { +static PyObject* _CtxImage_aux_image_ids(CtxImageObject* self, void* closure) { int aux_filter = LIBHEIF_AUX_IMAGE_FILTER_OMIT_ALPHA | LIBHEIF_AUX_IMAGE_FILTER_OMIT_DEPTH; int n_images = heif_image_handle_get_number_of_auxiliary_images(self->handle, aux_filter); if (n_images == 0) @@ -1250,23 +1250,48 @@ static PyObject* _CtxImage_aux_image_list(CtxImageObject* self, void* closure) { for (int i = 0; i < n_images; i++) { PyList_SET_ITEM(images_list, i, - _CtxAuxImage( - self->handle, images_ids[i], self->remove_stride, self->hdr_to_16bit, self->file_bytes - )); + PyLong_FromUnsignedLong(images_ids[i])); } free(images_ids); return images_list; } -static PyObject* _CtxImage_aux_image_type(CtxImageObject* self, void* closure) { +static PyObject* _CtxImage_aux_type(CtxImageObject* self, void* closure) { const char* aux_type_c = NULL; struct heif_error error = heif_image_handle_get_auxiliary_type(self->handle, &aux_type_c); if (check_error(error)) { Py_RETURN_NONE; } - PyObject *aux_image_type = PyUnicode_FromString(aux_type_c); + PyObject *aux_type = PyUnicode_FromString(aux_type_c); heif_image_handle_release_auxiliary_type(self->handle, &aux_type_c); - return aux_image_type; + return aux_type; +} + +static PyObject* _CtxImage_get_aux_image(CtxImageObject* self, PyObject* arg_image_id) { + heif_item_id aux_image_id = (heif_item_id)PyLong_AsUnsignedLong(arg_image_id); + return _CtxAuxImage( + self->handle, aux_image_id, self->remove_stride, self->hdr_to_16bit, self->file_bytes + ); +} + +// TODO change to get_aux_metadata which returns a dictionary with: +// aux_type, luma_bits, chroma_bits, preferred_colorspace (and more?) +static PyObject* _CtxImage_get_aux_type(CtxImageObject* self, PyObject* arg_image_id) { + heif_item_id aux_image_id = (heif_item_id)PyLong_AsUnsignedLong(arg_image_id); + struct heif_image_handle* aux_handle; + if (check_error(heif_image_handle_get_auxiliary_image_handle(self->handle, aux_image_id, &aux_handle))) { + Py_RETURN_NONE; + } + const char* aux_type_c = NULL; + struct heif_error error = heif_image_handle_get_auxiliary_type(aux_handle, &aux_type_c); + if (check_error(error)) { + heif_image_handle_release(aux_handle); + Py_RETURN_NONE; + } + PyObject *aux_type = PyUnicode_FromString(aux_type_c); + heif_image_handle_release_auxiliary_type(aux_handle, &aux_type_c); + heif_image_handle_release(aux_handle); + return aux_type; } /* =========== CtxImage Experimental Part ======== */ @@ -1331,13 +1356,19 @@ static struct PyGetSetDef _CtxImage_getseters[] = { {"stride", (getter)_CtxImage_stride, NULL, NULL, NULL}, {"data", (getter)_CtxImage_data, NULL, NULL, NULL}, {"depth_image_list", (getter)_CtxImage_depth_image_list, NULL, NULL, NULL}, - {"aux_image_list", (getter)_CtxImage_aux_image_list, NULL, NULL, NULL}, - {"aux_image_type", (getter)_CtxImage_aux_image_type, NULL, NULL, NULL}, + {"aux_image_ids", (getter)_CtxImage_aux_image_ids, NULL, NULL, NULL}, + {"aux_type", (getter)_CtxImage_aux_type, NULL, NULL, NULL}, {"camera_intrinsic_matrix", (getter)_CtxImage_camera_intrinsic_matrix, NULL, NULL, NULL}, {"camera_extrinsic_matrix_rot", (getter)_CtxImage_camera_extrinsic_matrix_rot, NULL, NULL, NULL}, {NULL, NULL, NULL, NULL, NULL} }; +static struct PyMethodDef _CtxImage_methods[] = { + {"get_aux_image", (PyCFunction)_CtxImage_get_aux_image, METH_O}, + {"get_aux_type", (PyCFunction)_CtxImage_get_aux_type, METH_O}, + {NULL, NULL} +}; + /* =========== Functions ======== */ static PyObject* _CtxWrite(PyObject* self, PyObject* args) { @@ -1574,6 +1605,7 @@ static PyTypeObject CtxImage_Type = { .tp_dealloc = (destructor)_CtxImage_destructor, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_getset = _CtxImage_getseters, + .tp_methods = _CtxImage_methods, }; static int setup_module(PyObject* m) { diff --git a/pillow_heif/heif.py b/pillow_heif/heif.py index 09c30aea..beb97319 100644 --- a/pillow_heif/heif.py +++ b/pillow_heif/heif.py @@ -143,9 +143,8 @@ class HeifAuxImage(BaseImage): def __init__(self, c_image): super().__init__(c_image) - _image_type = c_image.aux_image_type self.info = { - "aux_image_type": _image_type, + "aux_type": c_image.aux_type, } save_colorspace_chroma(c_image, self.info) @@ -173,8 +172,9 @@ def __init__(self, c_image): _depth_images: list[HeifDepthImage | None] = ( [HeifDepthImage(i) for i in c_image.depth_image_list if i is not None] if options.DEPTH_IMAGES else [] ) + _ctx_aux_images = [c_image.get_aux_image(aux_id) for aux_id in c_image.aux_image_ids] _aux_images: list[HeifAuxImage | None] = ( - [HeifAuxImage(i) for i in c_image.aux_image_list if i is not None] # if options.AUX_IMAGES else [] + [HeifAuxImage(img) for img in _ctx_aux_images if img is not None] # if options.AUX_IMAGES else [] ) _heif_meta = _get_heif_meta(c_image) self.info = { @@ -184,7 +184,7 @@ def __init__(self, c_image): "metadata": _metadata, "thumbnails": _thumbnails, "depth_images": _depth_images, - "aux_images": _aux_images, + "aux": _aux_images, } if _xmp: self.info["xmp"] = _xmp From c2057f48b90337e1ee04f7521629eba34056512b Mon Sep 17 00:00:00 2001 From: johncf Date: Thu, 10 Oct 2024 11:42:46 +0530 Subject: [PATCH 04/16] get aux images in a separate step --- pillow_heif/_pillow_heif.c | 69 ++++++++++++++++++++++++++------------ pillow_heif/heif.py | 31 +++++++++++------ 2 files changed, 68 insertions(+), 32 deletions(-) diff --git a/pillow_heif/_pillow_heif.c b/pillow_heif/_pillow_heif.c index 8c7ea178..c9792639 100644 --- a/pillow_heif/_pillow_heif.c +++ b/pillow_heif/_pillow_heif.c @@ -1256,42 +1256,68 @@ static PyObject* _CtxImage_aux_image_ids(CtxImageObject* self, void* closure) { return images_list; } -static PyObject* _CtxImage_aux_type(CtxImageObject* self, void* closure) { +static PyObject* _CtxImage_get_aux_image(CtxImageObject* self, PyObject* arg_image_id) { + heif_item_id aux_image_id = (heif_item_id)PyLong_AsUnsignedLong(arg_image_id); + return _CtxAuxImage( + self->handle, aux_image_id, self->remove_stride, self->hdr_to_16bit, self->file_bytes + ); +} + +static PyObject* _get_aux_type(const struct heif_image_handle* aux_handle) { const char* aux_type_c = NULL; - struct heif_error error = heif_image_handle_get_auxiliary_type(self->handle, &aux_type_c); + struct heif_error error = heif_image_handle_get_auxiliary_type(aux_handle, &aux_type_c); if (check_error(error)) { Py_RETURN_NONE; } PyObject *aux_type = PyUnicode_FromString(aux_type_c); - heif_image_handle_release_auxiliary_type(self->handle, &aux_type_c); + heif_image_handle_release_auxiliary_type(aux_handle, &aux_type_c); return aux_type; } -static PyObject* _CtxImage_get_aux_image(CtxImageObject* self, PyObject* arg_image_id) { - heif_item_id aux_image_id = (heif_item_id)PyLong_AsUnsignedLong(arg_image_id); - return _CtxAuxImage( - self->handle, aux_image_id, self->remove_stride, self->hdr_to_16bit, self->file_bytes - ); +static PyObject* _get_aux_colorspace(const struct heif_image_handle* aux_handle) { + enum heif_colorspace colorspace; + enum heif_chroma chroma; + struct heif_error error; + error = heif_image_handle_get_preferred_decoding_colorspace(aux_handle, &colorspace, &chroma); + if (error.code != heif_error_Ok) { + Py_RETURN_NONE; + } + const char* colorspace_str; + switch (colorspace) { + case heif_colorspace_undefined: + colorspace_str = "undefined"; + break; + case heif_colorspace_monochrome: + colorspace_str = "monochrome"; + break; + case heif_colorspace_RGB: + colorspace_str = "RGB"; + break; + case heif_colorspace_YCbCr: + colorspace_str = "YCbCr"; + break; + default: + colorspace_str = "unknown"; + } + return PyUnicode_FromString(colorspace_str); } -// TODO change to get_aux_metadata which returns a dictionary with: -// aux_type, luma_bits, chroma_bits, preferred_colorspace (and more?) -static PyObject* _CtxImage_get_aux_type(CtxImageObject* self, PyObject* arg_image_id) { +static PyObject* _CtxImage_get_aux_metadata(CtxImageObject* self, PyObject* arg_image_id) { heif_item_id aux_image_id = (heif_item_id)PyLong_AsUnsignedLong(arg_image_id); struct heif_image_handle* aux_handle; if (check_error(heif_image_handle_get_auxiliary_image_handle(self->handle, aux_image_id, &aux_handle))) { Py_RETURN_NONE; } - const char* aux_type_c = NULL; - struct heif_error error = heif_image_handle_get_auxiliary_type(aux_handle, &aux_type_c); - if (check_error(error)) { - heif_image_handle_release(aux_handle); - Py_RETURN_NONE; - } - PyObject *aux_type = PyUnicode_FromString(aux_type_c); - heif_image_handle_release_auxiliary_type(aux_handle, &aux_type_c); + PyObject* metadata = PyDict_New(); + PyObject* aux_type = _get_aux_type(aux_handle); + __PyDict_SetItemString(metadata, "type", aux_type); + PyObject* luma_bits = PyLong_FromLong(heif_image_handle_get_luma_bits_per_pixel(aux_handle)); + __PyDict_SetItemString(metadata, "bit_depth", luma_bits); + PyObject* colorspace = _get_aux_colorspace(aux_handle); + __PyDict_SetItemString(metadata, "colorspace", colorspace); + // anything more to add? heif_image_handle_get_chroma_bits_per_pixel? heif_image_handle_release(aux_handle); - return aux_type; + return metadata; } /* =========== CtxImage Experimental Part ======== */ @@ -1357,7 +1383,6 @@ static struct PyGetSetDef _CtxImage_getseters[] = { {"data", (getter)_CtxImage_data, NULL, NULL, NULL}, {"depth_image_list", (getter)_CtxImage_depth_image_list, NULL, NULL, NULL}, {"aux_image_ids", (getter)_CtxImage_aux_image_ids, NULL, NULL, NULL}, - {"aux_type", (getter)_CtxImage_aux_type, NULL, NULL, NULL}, {"camera_intrinsic_matrix", (getter)_CtxImage_camera_intrinsic_matrix, NULL, NULL, NULL}, {"camera_extrinsic_matrix_rot", (getter)_CtxImage_camera_extrinsic_matrix_rot, NULL, NULL, NULL}, {NULL, NULL, NULL, NULL, NULL} @@ -1365,7 +1390,7 @@ static struct PyGetSetDef _CtxImage_getseters[] = { static struct PyMethodDef _CtxImage_methods[] = { {"get_aux_image", (PyCFunction)_CtxImage_get_aux_image, METH_O}, - {"get_aux_type", (PyCFunction)_CtxImage_get_aux_type, METH_O}, + {"get_aux_metadata", (PyCFunction)_CtxImage_get_aux_metadata, METH_O}, {NULL, NULL} }; diff --git a/pillow_heif/heif.py b/pillow_heif/heif.py index beb97319..54bdf9a2 100644 --- a/pillow_heif/heif.py +++ b/pillow_heif/heif.py @@ -141,11 +141,9 @@ def to_pillow(self) -> Image.Image: class HeifAuxImage(BaseImage): """Class representing the auxiliary image associated with the :py:class:`~pillow_heif.HeifImage` class.""" - def __init__(self, c_image): + def __init__(self, c_image, info): super().__init__(c_image) - self.info = { - "aux_type": c_image.aux_type, - } + self.info = info save_colorspace_chroma(c_image, self.info) def __repr__(self): @@ -172,11 +170,7 @@ def __init__(self, c_image): _depth_images: list[HeifDepthImage | None] = ( [HeifDepthImage(i) for i in c_image.depth_image_list if i is not None] if options.DEPTH_IMAGES else [] ) - _ctx_aux_images = [c_image.get_aux_image(aux_id) for aux_id in c_image.aux_image_ids] - _aux_images: list[HeifAuxImage | None] = ( - [HeifAuxImage(img) for img in _ctx_aux_images if img is not None] # if options.AUX_IMAGES else [] - ) - _heif_meta = _get_heif_meta(c_image) + _ctx_aux_meta = {aux_id: c_image.get_aux_metadata(aux_id) for aux_id in c_image.aux_image_ids} self.info = { "primary": bool(c_image.primary), "bit_depth": int(c_image.bit_depth), @@ -184,8 +178,9 @@ def __init__(self, c_image): "metadata": _metadata, "thumbnails": _thumbnails, "depth_images": _depth_images, - "aux": _aux_images, + "aux": _ctx_aux_meta, } + _heif_meta = _get_heif_meta(c_image) if _xmp: self.info["xmp"] = _xmp if _heif_meta: @@ -231,6 +226,15 @@ def to_pillow(self) -> Image.Image: image.info["original_orientation"] = set_orientation(image.info) return image + def get_aux_image(self, aux_id: int) -> HeifAuxImage | None: + aux_info = self._c_image.get_aux_metadata(aux_id) + if aux_info is None: + return None + aux_image = self._c_image.get_aux_image(aux_id) + if aux_image is None: + return None + return HeifAuxImage(aux_image, aux_info) + class HeifFile: """Representation of the :py:class:`~pillow_heif.HeifImage` classes container. @@ -506,6 +510,13 @@ def __copy(self): _im_copy.primary_index = self.primary_index return _im_copy + def get_aux_image(self, aux_id): + """`get_aux_image`` method of the primary :class:`~pillow_heif.HeifImage` in the container. + + :exception IndexError: If there are no images. + """ + return self._images[self.primary_index].get_aux_image(aux_id) + __copy__ = __copy From 94bb74964335f1bb39d7acf330dd6180421e2452 Mon Sep 17 00:00:00 2001 From: johncf Date: Thu, 10 Oct 2024 20:10:15 +0530 Subject: [PATCH 05/16] restrict support to only 8-bit monocrome auxiliary images --- pillow_heif/_pillow_heif.c | 21 +++++++-------------- pillow_heif/heif.py | 15 +++++++++++---- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pillow_heif/_pillow_heif.c b/pillow_heif/_pillow_heif.c index c9792639..199c3295 100644 --- a/pillow_heif/_pillow_heif.c +++ b/pillow_heif/_pillow_heif.c @@ -735,7 +735,7 @@ static struct PyMethodDef _CtxWrite_methods[] = { /* =========== CtxAuxImage ======== */ PyObject* _CtxAuxImage(struct heif_image_handle* main_handle, heif_item_id aux_image_id, - int remove_stride, int hdr_to_16bit, PyObject* file_bytes) { + int remove_stride, int hdr_to_16bit, PyObject* file_bytes) { struct heif_image_handle* aux_handle; if (check_error(heif_image_handle_get_auxiliary_image_handle(main_handle, aux_image_id, &aux_handle))) { Py_RETURN_NONE; @@ -750,20 +750,10 @@ PyObject* _CtxAuxImage(struct heif_image_handle* main_handle, heif_item_id aux_i ctx_image->width = heif_image_handle_get_width(aux_handle); ctx_image->height = heif_image_handle_get_height(aux_handle); ctx_image->alpha = 0; + // note: in HeifImage.get_aux_image(..), we only allow 8-bit monochrome images ctx_image->n_channels = 1; - ctx_image->bits = heif_image_handle_get_luma_bits_per_pixel(aux_handle); + ctx_image->bits = 8; strcpy(ctx_image->mode, "L"); - if (ctx_image->bits > 8) { - if (hdr_to_16bit) { - strcpy(ctx_image->mode, "I;16"); - } - else if (ctx_image->bits == 10) { - strcpy(ctx_image->mode, "I;10"); - } - else { - strcpy(ctx_image->mode, "I;12"); - } - } ctx_image->hdr_to_8bit = 0; ctx_image->bgr_mode = 0; ctx_image->colorspace = heif_colorspace_monochrome; @@ -1266,7 +1256,8 @@ static PyObject* _CtxImage_get_aux_image(CtxImageObject* self, PyObject* arg_ima static PyObject* _get_aux_type(const struct heif_image_handle* aux_handle) { const char* aux_type_c = NULL; struct heif_error error = heif_image_handle_get_auxiliary_type(aux_handle, &aux_type_c); - if (check_error(error)) { + if (error.code != heif_error_Ok) { + // note: we are silently ignoring the error Py_RETURN_NONE; } PyObject *aux_type = PyUnicode_FromString(aux_type_c); @@ -1280,6 +1271,7 @@ static PyObject* _get_aux_colorspace(const struct heif_image_handle* aux_handle) struct heif_error error; error = heif_image_handle_get_preferred_decoding_colorspace(aux_handle, &colorspace, &chroma); if (error.code != heif_error_Ok) { + // note: we are silently ignoring the error Py_RETURN_NONE; } const char* colorspace_str; @@ -1297,6 +1289,7 @@ static PyObject* _get_aux_colorspace(const struct heif_image_handle* aux_handle) colorspace_str = "YCbCr"; break; default: + // note: this means the upstream API has changed colorspace_str = "unknown"; } return PyUnicode_FromString(colorspace_str); diff --git a/pillow_heif/heif.py b/pillow_heif/heif.py index 54bdf9a2..b5382a08 100644 --- a/pillow_heif/heif.py +++ b/pillow_heif/heif.py @@ -226,13 +226,20 @@ def to_pillow(self) -> Image.Image: image.info["original_orientation"] = set_orientation(image.info) return image - def get_aux_image(self, aux_id: int) -> HeifAuxImage | None: + def get_aux_image(self, aux_id: int) -> HeifAuxImage: aux_info = self._c_image.get_aux_metadata(aux_id) - if aux_info is None: - return None + if aux_info is None or aux_info["colorspace"] is None: + raise RuntimeError("Error while getting auxiliary information.") + colorspace, bit_depth = aux_info["colorspace"], aux_info["bit_depth"] + if colorspace != "monochrome": + raise NotImplementedError(f"{colorspace} color space is not supported for auxiliary images at the moment. " + "Please file an issue with an example HEIF file.") + if bit_depth != 8: + raise NotImplementedError(f"{bit_depth}-bit auxiliary images are not supported at the moment. " + "Please file an issue with an example HEIF file.") aux_image = self._c_image.get_aux_image(aux_id) if aux_image is None: - return None + raise RuntimeError("Error while decoding the auxiliary image.") return HeifAuxImage(aux_image, aux_info) From 6259064af83ce710ce78a76cdd413795eb142a19 Mon Sep 17 00:00:00 2001 From: johncf Date: Thu, 10 Oct 2024 21:14:57 +0530 Subject: [PATCH 06/16] fix pre-commit issues --- pillow_heif/heif.py | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/pillow_heif/heif.py b/pillow_heif/heif.py index b5382a08..39e980b8 100644 --- a/pillow_heif/heif.py +++ b/pillow_heif/heif.py @@ -129,14 +129,6 @@ def __init__(self, c_image): def __repr__(self): return f"<{self.__class__.__name__} {self.size[0]}x{self.size[1]} {self.mode}>" - def to_pillow(self) -> Image.Image: - """Helper method to create :external:py:class:`~PIL.Image.Image` class. - - :returns: :external:py:class:`~PIL.Image.Image` class created from an image. - """ - image = super().to_pillow() - return image - class HeifAuxImage(BaseImage): """Class representing the auxiliary image associated with the :py:class:`~pillow_heif.HeifImage` class.""" @@ -149,14 +141,6 @@ def __init__(self, c_image, info): def __repr__(self): return f"<{self.__class__.__name__} {self.size[0]}x{self.size[1]} {self.mode}>" - def to_pillow(self) -> Image.Image: - """Helper method to create :external:py:class:`~PIL.Image.Image` class. - - :returns: :external:py:class:`~PIL.Image.Image` class created from an image. - """ - image = super().to_pillow() - return image - class HeifImage(BaseImage): """One image in a :py:class:`~pillow_heif.HeifFile` container.""" @@ -227,16 +211,24 @@ def to_pillow(self) -> Image.Image: return image def get_aux_image(self, aux_id: int) -> HeifAuxImage: + """Method to retrieve the auxiliary image at the given ID. + + :returns: a :py:class:`~pillow_heif.HeifAuxImage` class instance. + """ aux_info = self._c_image.get_aux_metadata(aux_id) if aux_info is None or aux_info["colorspace"] is None: raise RuntimeError("Error while getting auxiliary information.") colorspace, bit_depth = aux_info["colorspace"], aux_info["bit_depth"] if colorspace != "monochrome": - raise NotImplementedError(f"{colorspace} color space is not supported for auxiliary images at the moment. " - "Please file an issue with an example HEIF file.") + raise NotImplementedError( + f"{colorspace} color space is not supported for auxiliary images at the moment. " + "Please consider filing an issue with an example HEIF file." + ) if bit_depth != 8: - raise NotImplementedError(f"{bit_depth}-bit auxiliary images are not supported at the moment. " - "Please file an issue with an example HEIF file.") + raise NotImplementedError( + f"{bit_depth}-bit auxiliary images are not supported at the moment. " + "Please consider filing an issue with an example HEIF file." + ) aux_image = self._c_image.get_aux_image(aux_id) if aux_image is None: raise RuntimeError("Error while decoding the auxiliary image.") From a5a6ee753072421a1c0c1f636de0d3999139e5b3 Mon Sep 17 00:00:00 2001 From: johncf Date: Thu, 10 Oct 2024 23:00:33 +0530 Subject: [PATCH 07/16] correct error handling of aux methods --- pillow_heif/_pillow_heif.c | 11 ++++++----- pillow_heif/heif.py | 4 +--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pillow_heif/_pillow_heif.c b/pillow_heif/_pillow_heif.c index 199c3295..cfce7ae9 100644 --- a/pillow_heif/_pillow_heif.c +++ b/pillow_heif/_pillow_heif.c @@ -738,12 +738,13 @@ PyObject* _CtxAuxImage(struct heif_image_handle* main_handle, heif_item_id aux_i int remove_stride, int hdr_to_16bit, PyObject* file_bytes) { struct heif_image_handle* aux_handle; if (check_error(heif_image_handle_get_auxiliary_image_handle(main_handle, aux_image_id, &aux_handle))) { - Py_RETURN_NONE; + return NULL; } CtxImageObject *ctx_image = PyObject_New(CtxImageObject, &CtxImage_Type); if (!ctx_image) { heif_image_handle_release(aux_handle); - Py_RETURN_NONE; + PyErr_SetString(PyExc_RuntimeError, "Could not create CtxImage object"); + return NULL; } ctx_image->depth_metadata = NULL; ctx_image->image_type = PhHeifImage; @@ -1228,13 +1229,13 @@ static PyObject* _CtxImage_aux_image_ids(CtxImageObject* self, void* closure) { return PyList_New(0); heif_item_id* images_ids = (heif_item_id*)malloc(n_images * sizeof(heif_item_id)); if (!images_ids) - return PyList_New(0); + return PyErr_NoMemory(); n_images = heif_image_handle_get_list_of_auxiliary_image_IDs(self->handle, aux_filter, images_ids, n_images); PyObject* images_list = PyList_New(n_images); if (!images_list) { free(images_ids); - return PyList_New(0); + return PyErr_NoMemory(); } for (int i = 0; i < n_images; i++) { @@ -1299,7 +1300,7 @@ static PyObject* _CtxImage_get_aux_metadata(CtxImageObject* self, PyObject* arg_ heif_item_id aux_image_id = (heif_item_id)PyLong_AsUnsignedLong(arg_image_id); struct heif_image_handle* aux_handle; if (check_error(heif_image_handle_get_auxiliary_image_handle(self->handle, aux_image_id, &aux_handle))) { - Py_RETURN_NONE; + return NULL; } PyObject* metadata = PyDict_New(); PyObject* aux_type = _get_aux_type(aux_handle); diff --git a/pillow_heif/heif.py b/pillow_heif/heif.py index 39e980b8..63940617 100644 --- a/pillow_heif/heif.py +++ b/pillow_heif/heif.py @@ -216,7 +216,7 @@ def get_aux_image(self, aux_id: int) -> HeifAuxImage: :returns: a :py:class:`~pillow_heif.HeifAuxImage` class instance. """ aux_info = self._c_image.get_aux_metadata(aux_id) - if aux_info is None or aux_info["colorspace"] is None: + if aux_info["colorspace"] is None: raise RuntimeError("Error while getting auxiliary information.") colorspace, bit_depth = aux_info["colorspace"], aux_info["bit_depth"] if colorspace != "monochrome": @@ -230,8 +230,6 @@ def get_aux_image(self, aux_id: int) -> HeifAuxImage: "Please consider filing an issue with an example HEIF file." ) aux_image = self._c_image.get_aux_image(aux_id) - if aux_image is None: - raise RuntimeError("Error while decoding the auxiliary image.") return HeifAuxImage(aux_image, aux_info) From f5fe402486854ba467ccc962a4434a5bbadf796a Mon Sep 17 00:00:00 2001 From: johncf Date: Sat, 12 Oct 2024 20:09:00 +0530 Subject: [PATCH 08/16] cleanup aux error handling and naming --- pillow_heif/_pillow_heif.c | 21 ++++++++------------- pillow_heif/heif.py | 6 +++--- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/pillow_heif/_pillow_heif.c b/pillow_heif/_pillow_heif.c index cfce7ae9..c8361e7e 100644 --- a/pillow_heif/_pillow_heif.c +++ b/pillow_heif/_pillow_heif.c @@ -743,7 +743,6 @@ PyObject* _CtxAuxImage(struct heif_image_handle* main_handle, heif_item_id aux_i CtxImageObject *ctx_image = PyObject_New(CtxImageObject, &CtxImage_Type); if (!ctx_image) { heif_image_handle_release(aux_handle); - PyErr_SetString(PyExc_RuntimeError, "Could not create CtxImage object"); return NULL; } ctx_image->depth_metadata = NULL; @@ -1237,11 +1236,8 @@ static PyObject* _CtxImage_aux_image_ids(CtxImageObject* self, void* closure) { free(images_ids); return PyErr_NoMemory(); } - for (int i = 0; i < n_images; i++) { - PyList_SET_ITEM(images_list, - i, - PyLong_FromUnsignedLong(images_ids[i])); + PyList_SET_ITEM(images_list, i, PyLong_FromUnsignedLong(images_ids[i])); } free(images_ids); return images_list; @@ -1257,10 +1253,8 @@ static PyObject* _CtxImage_get_aux_image(CtxImageObject* self, PyObject* arg_ima static PyObject* _get_aux_type(const struct heif_image_handle* aux_handle) { const char* aux_type_c = NULL; struct heif_error error = heif_image_handle_get_auxiliary_type(aux_handle, &aux_type_c); - if (error.code != heif_error_Ok) { - // note: we are silently ignoring the error - Py_RETURN_NONE; - } + if (check_error(error)) + return NULL; PyObject *aux_type = PyUnicode_FromString(aux_type_c); heif_image_handle_release_auxiliary_type(aux_handle, &aux_type_c); return aux_type; @@ -1296,14 +1290,15 @@ static PyObject* _get_aux_colorspace(const struct heif_image_handle* aux_handle) return PyUnicode_FromString(colorspace_str); } -static PyObject* _CtxImage_get_aux_metadata(CtxImageObject* self, PyObject* arg_image_id) { +static PyObject* _CtxImage_get_aux_info(CtxImageObject* self, PyObject* arg_image_id) { heif_item_id aux_image_id = (heif_item_id)PyLong_AsUnsignedLong(arg_image_id); struct heif_image_handle* aux_handle; - if (check_error(heif_image_handle_get_auxiliary_image_handle(self->handle, aux_image_id, &aux_handle))) { + if (check_error(heif_image_handle_get_auxiliary_image_handle(self->handle, aux_image_id, &aux_handle))) return NULL; - } PyObject* metadata = PyDict_New(); PyObject* aux_type = _get_aux_type(aux_handle); + if (!aux_type) + return NULL; __PyDict_SetItemString(metadata, "type", aux_type); PyObject* luma_bits = PyLong_FromLong(heif_image_handle_get_luma_bits_per_pixel(aux_handle)); __PyDict_SetItemString(metadata, "bit_depth", luma_bits); @@ -1384,7 +1379,7 @@ static struct PyGetSetDef _CtxImage_getseters[] = { static struct PyMethodDef _CtxImage_methods[] = { {"get_aux_image", (PyCFunction)_CtxImage_get_aux_image, METH_O}, - {"get_aux_metadata", (PyCFunction)_CtxImage_get_aux_metadata, METH_O}, + {"get_aux_info", (PyCFunction)_CtxImage_get_aux_info, METH_O}, {NULL, NULL} }; diff --git a/pillow_heif/heif.py b/pillow_heif/heif.py index 63940617..0c7763fb 100644 --- a/pillow_heif/heif.py +++ b/pillow_heif/heif.py @@ -154,7 +154,7 @@ def __init__(self, c_image): _depth_images: list[HeifDepthImage | None] = ( [HeifDepthImage(i) for i in c_image.depth_image_list if i is not None] if options.DEPTH_IMAGES else [] ) - _ctx_aux_meta = {aux_id: c_image.get_aux_metadata(aux_id) for aux_id in c_image.aux_image_ids} + _ctx_aux_info = {aux_id: c_image.get_aux_info(aux_id) for aux_id in c_image.aux_image_ids} self.info = { "primary": bool(c_image.primary), "bit_depth": int(c_image.bit_depth), @@ -162,7 +162,7 @@ def __init__(self, c_image): "metadata": _metadata, "thumbnails": _thumbnails, "depth_images": _depth_images, - "aux": _ctx_aux_meta, + "aux": _ctx_aux_info, } _heif_meta = _get_heif_meta(c_image) if _xmp: @@ -215,7 +215,7 @@ def get_aux_image(self, aux_id: int) -> HeifAuxImage: :returns: a :py:class:`~pillow_heif.HeifAuxImage` class instance. """ - aux_info = self._c_image.get_aux_metadata(aux_id) + aux_info = self._c_image.get_aux_info(aux_id) if aux_info["colorspace"] is None: raise RuntimeError("Error while getting auxiliary information.") colorspace, bit_depth = aux_info["colorspace"], aux_info["bit_depth"] From 4861592306c699f0acf4b1bafe276a4415f91f57 Mon Sep 17 00:00:00 2001 From: johncf Date: Sat, 12 Oct 2024 21:07:33 +0530 Subject: [PATCH 09/16] better way to list aux ids --- pillow_heif/_pillow_heif.c | 13 +++++++++++++ pillow_heif/as_plugin.py | 2 ++ pillow_heif/heif.py | 10 ++++++++-- pillow_heif/options.py | 6 ++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/pillow_heif/_pillow_heif.c b/pillow_heif/_pillow_heif.c index c8361e7e..1bcc1a67 100644 --- a/pillow_heif/_pillow_heif.c +++ b/pillow_heif/_pillow_heif.c @@ -1309,6 +1309,18 @@ static PyObject* _CtxImage_get_aux_info(CtxImageObject* self, PyObject* arg_imag return metadata; } +static PyObject* _CtxImage_get_aux_type(CtxImageObject* self, PyObject* arg_image_id) { + heif_item_id aux_image_id = (heif_item_id)PyLong_AsUnsignedLong(arg_image_id); + struct heif_image_handle* aux_handle; + if (check_error(heif_image_handle_get_auxiliary_image_handle(self->handle, aux_image_id, &aux_handle))) + return NULL; + PyObject* aux_type = _get_aux_type(aux_handle); + if (!aux_type) + return NULL; + heif_image_handle_release(aux_handle); + return aux_type; +} + /* =========== CtxImage Experimental Part ======== */ static PyObject* _CtxImage_camera_intrinsic_matrix(CtxImageObject* self, void* closure) { @@ -1380,6 +1392,7 @@ static struct PyGetSetDef _CtxImage_getseters[] = { static struct PyMethodDef _CtxImage_methods[] = { {"get_aux_image", (PyCFunction)_CtxImage_get_aux_image, METH_O}, {"get_aux_info", (PyCFunction)_CtxImage_get_aux_info, METH_O}, + {"get_aux_type", (PyCFunction)_CtxImage_get_aux_type, METH_O}, {NULL, NULL} }; diff --git a/pillow_heif/as_plugin.py b/pillow_heif/as_plugin.py index fbe3786a..a1c85409 100644 --- a/pillow_heif/as_plugin.py +++ b/pillow_heif/as_plugin.py @@ -226,6 +226,8 @@ def __options_update(**kwargs): options.THUMBNAILS = v elif k == "depth_images": options.DEPTH_IMAGES = v + elif k == "aux_images": + options.AUX_IMAGES = v elif k == "quality": options.QUALITY = v elif k == "save_to_12bit": diff --git a/pillow_heif/heif.py b/pillow_heif/heif.py index 0c7763fb..e0c8a4c5 100644 --- a/pillow_heif/heif.py +++ b/pillow_heif/heif.py @@ -154,7 +154,6 @@ def __init__(self, c_image): _depth_images: list[HeifDepthImage | None] = ( [HeifDepthImage(i) for i in c_image.depth_image_list if i is not None] if options.DEPTH_IMAGES else [] ) - _ctx_aux_info = {aux_id: c_image.get_aux_info(aux_id) for aux_id in c_image.aux_image_ids} self.info = { "primary": bool(c_image.primary), "bit_depth": int(c_image.bit_depth), @@ -162,8 +161,15 @@ def __init__(self, c_image): "metadata": _metadata, "thumbnails": _thumbnails, "depth_images": _depth_images, - "aux": _ctx_aux_info, } + if options.AUX_IMAGES: + _ctx_aux_info = {} + for aux_id in c_image.aux_image_ids: + aux_type = c_image.get_aux_type(aux_id) + if aux_type not in _ctx_aux_info: + _ctx_aux_info[aux_type] = [] + _ctx_aux_info[aux_type].append(aux_id) + self.info["aux"] = _ctx_aux_info _heif_meta = _get_heif_meta(c_image) if _xmp: self.info["xmp"] = _xmp diff --git a/pillow_heif/options.py b/pillow_heif/options.py index 8c023822..0bbd90b5 100644 --- a/pillow_heif/options.py +++ b/pillow_heif/options.py @@ -18,6 +18,12 @@ When use pillow_heif as a plugin you can set it with: `register_*_opener(depth_images=False)`""" +AUX_IMAGES = True +"""Option to enable/disable auxiliary image support + +When use pillow_heif as a plugin you can set it with: `register_*_opener(aux_images=False)`""" + + QUALITY = None """Default encoding quality From 0fb9370e7a5c473af76dd687a1165464be12c742 Mon Sep 17 00:00:00 2001 From: johncf Date: Sat, 12 Oct 2024 21:24:19 +0530 Subject: [PATCH 10/16] fix pytest errors this partially reverts changes from: 0e2ea000 and 6259064a --- pillow_heif/heif.py | 18 ++++++++++++++++++ pillow_heif/misc.py | 1 + 2 files changed, 19 insertions(+) diff --git a/pillow_heif/heif.py b/pillow_heif/heif.py index e0c8a4c5..fc0f451d 100644 --- a/pillow_heif/heif.py +++ b/pillow_heif/heif.py @@ -129,6 +129,15 @@ def __init__(self, c_image): def __repr__(self): return f"<{self.__class__.__name__} {self.size[0]}x{self.size[1]} {self.mode}>" + def to_pillow(self) -> Image.Image: + """Helper method to create :external:py:class:`~PIL.Image.Image` class. + + :returns: :external:py:class:`~PIL.Image.Image` class created from an image. + """ + image = super().to_pillow() + image.info = self.info.copy() + return image + class HeifAuxImage(BaseImage): """Class representing the auxiliary image associated with the :py:class:`~pillow_heif.HeifImage` class.""" @@ -141,6 +150,15 @@ def __init__(self, c_image, info): def __repr__(self): return f"<{self.__class__.__name__} {self.size[0]}x{self.size[1]} {self.mode}>" + def to_pillow(self) -> Image.Image: + """Helper method to create :external:py:class:`~PIL.Image.Image` class. + + :returns: :external:py:class:`~PIL.Image.Image` class created from an image. + """ + image = super().to_pillow() + image.info = self.info.copy() + return image + class HeifImage(BaseImage): """One image in a :py:class:`~pillow_heif.HeifFile` container.""" diff --git a/pillow_heif/misc.py b/pillow_heif/misc.py index 375ced85..08c01e73 100644 --- a/pillow_heif/misc.py +++ b/pillow_heif/misc.py @@ -478,6 +478,7 @@ def __init__(self, mode: str, size: tuple[int, int], data: bytes, **kwargs): self.color_profile = None self.thumbnails: list[int] = [] self.depth_image_list: list = [] + self.aux_image_ids: list[int] = [] self.primary = False self.chroma = HeifChroma.UNDEFINED.value self.colorspace = HeifColorspace.UNDEFINED.value From 5fdb446d7eb8b8cdf68d121196bd0667363b96d3 Mon Sep 17 00:00:00 2001 From: johncf Date: Sat, 12 Oct 2024 22:36:36 +0530 Subject: [PATCH 11/16] minor: rename aux info key "type" to "aux_type" --- pillow_heif/_pillow_heif.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pillow_heif/_pillow_heif.c b/pillow_heif/_pillow_heif.c index 1bcc1a67..94e5f6f8 100644 --- a/pillow_heif/_pillow_heif.c +++ b/pillow_heif/_pillow_heif.c @@ -1299,7 +1299,7 @@ static PyObject* _CtxImage_get_aux_info(CtxImageObject* self, PyObject* arg_imag PyObject* aux_type = _get_aux_type(aux_handle); if (!aux_type) return NULL; - __PyDict_SetItemString(metadata, "type", aux_type); + __PyDict_SetItemString(metadata, "aux_type", aux_type); PyObject* luma_bits = PyLong_FromLong(heif_image_handle_get_luma_bits_per_pixel(aux_handle)); __PyDict_SetItemString(metadata, "bit_depth", luma_bits); PyObject* colorspace = _get_aux_colorspace(aux_handle); From a39d49e0cadc54e15b0e4234d7d6c8fec7d7310f Mon Sep 17 00:00:00 2001 From: johncf Date: Sat, 12 Oct 2024 23:49:06 +0530 Subject: [PATCH 12/16] simplify auxiliary image verification logic --- pillow_heif/_pillow_heif.c | 81 ++++++++++++++------------------------ pillow_heif/heif.py | 29 +------------- 2 files changed, 32 insertions(+), 78 deletions(-) diff --git a/pillow_heif/_pillow_heif.c b/pillow_heif/_pillow_heif.c index 94e5f6f8..dcccf2cc 100644 --- a/pillow_heif/_pillow_heif.c +++ b/pillow_heif/_pillow_heif.c @@ -734,12 +734,42 @@ static struct PyMethodDef _CtxWrite_methods[] = { /* =========== CtxAuxImage ======== */ +static const char* _colorspace_to_str(enum heif_colorspace colorspace) { + switch (colorspace) { + case heif_colorspace_undefined: + return "undefined"; + case heif_colorspace_monochrome: + return "monochrome"; + case heif_colorspace_RGB: + return "RGB"; + case heif_colorspace_YCbCr: + return "YCbCr"; + default: // note: this means the upstream API has changed + return "unknown"; + } +} + PyObject* _CtxAuxImage(struct heif_image_handle* main_handle, heif_item_id aux_image_id, int remove_stride, int hdr_to_16bit, PyObject* file_bytes) { struct heif_image_handle* aux_handle; if (check_error(heif_image_handle_get_auxiliary_image_handle(main_handle, aux_image_id, &aux_handle))) { return NULL; } + int luma_bits = heif_image_handle_get_luma_bits_per_pixel(aux_handle); + enum heif_colorspace colorspace; + enum heif_chroma chroma; + if (check_error(heif_image_handle_get_preferred_decoding_colorspace(aux_handle, &colorspace, &chroma))) { + return NULL; + } + if (luma_bits != 8 || colorspace != heif_colorspace_monochrome) { + const char* colorspace_str = _colorspace_to_str(colorspace); + PyErr_Format( + PyExc_NotImplementedError, + "Only 8-bit monochrome auxiliary images are currently supported. Got %d-bit %s image. " + "Please consider filing an issue with an example HEIF file.", + luma_bits, colorspace_str); + return NULL; + } CtxImageObject *ctx_image = PyObject_New(CtxImageObject, &CtxImage_Type); if (!ctx_image) { heif_image_handle_release(aux_handle); @@ -750,7 +780,6 @@ PyObject* _CtxAuxImage(struct heif_image_handle* main_handle, heif_item_id aux_i ctx_image->width = heif_image_handle_get_width(aux_handle); ctx_image->height = heif_image_handle_get_height(aux_handle); ctx_image->alpha = 0; - // note: in HeifImage.get_aux_image(..), we only allow 8-bit monochrome images ctx_image->n_channels = 1; ctx_image->bits = 8; strcpy(ctx_image->mode, "L"); @@ -1260,55 +1289,6 @@ static PyObject* _get_aux_type(const struct heif_image_handle* aux_handle) { return aux_type; } -static PyObject* _get_aux_colorspace(const struct heif_image_handle* aux_handle) { - enum heif_colorspace colorspace; - enum heif_chroma chroma; - struct heif_error error; - error = heif_image_handle_get_preferred_decoding_colorspace(aux_handle, &colorspace, &chroma); - if (error.code != heif_error_Ok) { - // note: we are silently ignoring the error - Py_RETURN_NONE; - } - const char* colorspace_str; - switch (colorspace) { - case heif_colorspace_undefined: - colorspace_str = "undefined"; - break; - case heif_colorspace_monochrome: - colorspace_str = "monochrome"; - break; - case heif_colorspace_RGB: - colorspace_str = "RGB"; - break; - case heif_colorspace_YCbCr: - colorspace_str = "YCbCr"; - break; - default: - // note: this means the upstream API has changed - colorspace_str = "unknown"; - } - return PyUnicode_FromString(colorspace_str); -} - -static PyObject* _CtxImage_get_aux_info(CtxImageObject* self, PyObject* arg_image_id) { - heif_item_id aux_image_id = (heif_item_id)PyLong_AsUnsignedLong(arg_image_id); - struct heif_image_handle* aux_handle; - if (check_error(heif_image_handle_get_auxiliary_image_handle(self->handle, aux_image_id, &aux_handle))) - return NULL; - PyObject* metadata = PyDict_New(); - PyObject* aux_type = _get_aux_type(aux_handle); - if (!aux_type) - return NULL; - __PyDict_SetItemString(metadata, "aux_type", aux_type); - PyObject* luma_bits = PyLong_FromLong(heif_image_handle_get_luma_bits_per_pixel(aux_handle)); - __PyDict_SetItemString(metadata, "bit_depth", luma_bits); - PyObject* colorspace = _get_aux_colorspace(aux_handle); - __PyDict_SetItemString(metadata, "colorspace", colorspace); - // anything more to add? heif_image_handle_get_chroma_bits_per_pixel? - heif_image_handle_release(aux_handle); - return metadata; -} - static PyObject* _CtxImage_get_aux_type(CtxImageObject* self, PyObject* arg_image_id) { heif_item_id aux_image_id = (heif_item_id)PyLong_AsUnsignedLong(arg_image_id); struct heif_image_handle* aux_handle; @@ -1391,7 +1371,6 @@ static struct PyGetSetDef _CtxImage_getseters[] = { static struct PyMethodDef _CtxImage_methods[] = { {"get_aux_image", (PyCFunction)_CtxImage_get_aux_image, METH_O}, - {"get_aux_info", (PyCFunction)_CtxImage_get_aux_info, METH_O}, {"get_aux_type", (PyCFunction)_CtxImage_get_aux_type, METH_O}, {NULL, NULL} }; diff --git a/pillow_heif/heif.py b/pillow_heif/heif.py index fc0f451d..4c697cce 100644 --- a/pillow_heif/heif.py +++ b/pillow_heif/heif.py @@ -142,23 +142,12 @@ def to_pillow(self) -> Image.Image: class HeifAuxImage(BaseImage): """Class representing the auxiliary image associated with the :py:class:`~pillow_heif.HeifImage` class.""" - def __init__(self, c_image, info): + def __init__(self, c_image): super().__init__(c_image) - self.info = info - save_colorspace_chroma(c_image, self.info) def __repr__(self): return f"<{self.__class__.__name__} {self.size[0]}x{self.size[1]} {self.mode}>" - def to_pillow(self) -> Image.Image: - """Helper method to create :external:py:class:`~PIL.Image.Image` class. - - :returns: :external:py:class:`~PIL.Image.Image` class created from an image. - """ - image = super().to_pillow() - image.info = self.info.copy() - return image - class HeifImage(BaseImage): """One image in a :py:class:`~pillow_heif.HeifFile` container.""" @@ -239,22 +228,8 @@ def get_aux_image(self, aux_id: int) -> HeifAuxImage: :returns: a :py:class:`~pillow_heif.HeifAuxImage` class instance. """ - aux_info = self._c_image.get_aux_info(aux_id) - if aux_info["colorspace"] is None: - raise RuntimeError("Error while getting auxiliary information.") - colorspace, bit_depth = aux_info["colorspace"], aux_info["bit_depth"] - if colorspace != "monochrome": - raise NotImplementedError( - f"{colorspace} color space is not supported for auxiliary images at the moment. " - "Please consider filing an issue with an example HEIF file." - ) - if bit_depth != 8: - raise NotImplementedError( - f"{bit_depth}-bit auxiliary images are not supported at the moment. " - "Please consider filing an issue with an example HEIF file." - ) aux_image = self._c_image.get_aux_image(aux_id) - return HeifAuxImage(aux_image, aux_info) + return HeifAuxImage(aux_image) class HeifFile: From 7b4d63b95dea1d0e1fc1759303ca43e9b1e59fb1 Mon Sep 17 00:00:00 2001 From: johncf Date: Sun, 13 Oct 2024 00:04:34 +0530 Subject: [PATCH 13/16] prevent aux_handle leak --- pillow_heif/_pillow_heif.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pillow_heif/_pillow_heif.c b/pillow_heif/_pillow_heif.c index dcccf2cc..90c35aa0 100644 --- a/pillow_heif/_pillow_heif.c +++ b/pillow_heif/_pillow_heif.c @@ -759,6 +759,7 @@ PyObject* _CtxAuxImage(struct heif_image_handle* main_handle, heif_item_id aux_i enum heif_colorspace colorspace; enum heif_chroma chroma; if (check_error(heif_image_handle_get_preferred_decoding_colorspace(aux_handle, &colorspace, &chroma))) { + heif_image_handle_release(aux_handle); return NULL; } if (luma_bits != 8 || colorspace != heif_colorspace_monochrome) { @@ -768,6 +769,7 @@ PyObject* _CtxAuxImage(struct heif_image_handle* main_handle, heif_item_id aux_i "Only 8-bit monochrome auxiliary images are currently supported. Got %d-bit %s image. " "Please consider filing an issue with an example HEIF file.", luma_bits, colorspace_str); + heif_image_handle_release(aux_handle); return NULL; } CtxImageObject *ctx_image = PyObject_New(CtxImageObject, &CtxImage_Type); From 900b6ac26acbcf1a1c9d2d0467ff2dc57155f6bc Mon Sep 17 00:00:00 2001 From: johncf Date: Sun, 13 Oct 2024 21:32:41 +0530 Subject: [PATCH 14/16] tests: add test_aux_image --- pillow_heif/__init__.py | 1 + tests/read_test.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/pillow_heif/__init__.py b/pillow_heif/__init__.py index 4fd5be4a..41f814af 100644 --- a/pillow_heif/__init__.py +++ b/pillow_heif/__init__.py @@ -16,6 +16,7 @@ HeifTransferCharacteristics, ) from .heif import ( + HeifAuxImage, HeifDepthImage, HeifFile, HeifImage, diff --git a/tests/read_test.py b/tests/read_test.py index bcbf4ca8..182b3a4e 100644 --- a/tests/read_test.py +++ b/tests/read_test.py @@ -496,6 +496,19 @@ def test_depth_image(): assert im_pil.info == depth_image.info +def test_aux_image(): + im = pillow_heif.open_heif("images/heif_other/pug.heic") + assert len(im.info["aux"]) == 1 + assert "urn:com:apple:photo:2020:aux:hdrgainmap" in im.info["aux"] + assert len(im.info["aux"]["urn:com:apple:photo:2020:aux:hdrgainmap"]) == 1 + aux_id = im.info["aux"]["urn:com:apple:photo:2020:aux:hdrgainmap"][0] + aux_image = im.get_aux_image(aux_id) + assert isinstance(aux_image, pillow_heif.HeifAuxImage) + aux_pil = aux_image.to_pillow() + assert aux_pil.size == (2016, 1512) + assert aux_pil.mode == "L" + + @pytest.mark.skipif( parse_version(pillow_heif.libheif_version()) < parse_version("1.18.0"), reason="requires LibHeif 1.18+" ) From 0b95e0e6f062102400bb3c522d31a21161f9e82b Mon Sep 17 00:00:00 2001 From: johncf Date: Sun, 13 Oct 2024 21:36:12 +0530 Subject: [PATCH 15/16] fix pylint error --- pillow_heif/heif.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pillow_heif/heif.py b/pillow_heif/heif.py index 4c697cce..ddecd1b4 100644 --- a/pillow_heif/heif.py +++ b/pillow_heif/heif.py @@ -142,9 +142,6 @@ def to_pillow(self) -> Image.Image: class HeifAuxImage(BaseImage): """Class representing the auxiliary image associated with the :py:class:`~pillow_heif.HeifImage` class.""" - def __init__(self, c_image): - super().__init__(c_image) - def __repr__(self): return f"<{self.__class__.__name__} {self.size[0]}x{self.size[1]} {self.mode}>" From 62f733f7fc72fe39a28e556765861e9aa5add2ad Mon Sep 17 00:00:00 2001 From: johncf Date: Mon, 14 Oct 2024 21:38:21 +0530 Subject: [PATCH 16/16] tests: add aux_images to options_test --- tests/options_test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/options_test.py b/tests/options_test.py index 670c6251..cbfbade3 100644 --- a/tests/options_test.py +++ b/tests/options_test.py @@ -32,6 +32,7 @@ def test_options_change_from_plugin_registering(register_opener): save_to_12bit=True, decode_threads=3, depth_images=False, + aux_images=False, save_nclx_profile=False, preferred_encoder={"HEIF": "id1", "AVIF": "id2"}, preferred_decoder={"HEIF": "id3", "AVIF": "id4"}, @@ -41,6 +42,7 @@ def test_options_change_from_plugin_registering(register_opener): assert options.SAVE_HDR_TO_12_BIT assert options.DECODE_THREADS == 3 assert options.DEPTH_IMAGES is False + assert options.AUX_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"} @@ -50,6 +52,7 @@ def test_options_change_from_plugin_registering(register_opener): options.SAVE_HDR_TO_12_BIT = False options.DECODE_THREADS = 4 options.DEPTH_IMAGES = True + options.AUX_IMAGES = True options.SAVE_NCLX_PROFILE = True options.PREFERRED_ENCODER = {"HEIF": "", "AVIF": ""} options.PREFERRED_DECODER = {"HEIF": "", "AVIF": ""}