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

Add support for parsing auxiliary images #297

Merged
merged 18 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
139 changes: 139 additions & 0 deletions pillow_heif/_pillow_heif.c
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,45 @@ 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))) {
return NULL;
}
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;
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;
// 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");
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,
Expand Down Expand Up @@ -1183,6 +1222,98 @@ static PyObject* _CtxImage_depth_image_list(CtxImageObject* self, void* closure)
return images_list;
}

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)
return PyList_New(0);
heif_item_id* images_ids = (heif_item_id*)malloc(n_images * sizeof(heif_item_id));
if (!images_ids)
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 PyErr_NoMemory();
}

for (int i = 0; i < n_images; i++) {
PyList_SET_ITEM(images_list,
i,
PyLong_FromUnsignedLong(images_ids[i]));
}
free(images_ids);
return images_list;
}

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(aux_handle, &aux_type_c);
if (error.code != heif_error_Ok) {
// note: we are silently ignoring the error
Py_RETURN_NONE;
}
PyObject *aux_type = PyUnicode_FromString(aux_type_c);
heif_image_handle_release_auxiliary_type(aux_handle, &aux_type_c);
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_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))) {
return NULL;
}
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 metadata;
}

/* =========== CtxImage Experimental Part ======== */

static PyObject* _CtxImage_camera_intrinsic_matrix(CtxImageObject* self, void* closure) {
Expand Down Expand Up @@ -1245,11 +1376,18 @@ 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_ids", (getter)_CtxImage_aux_image_ids, 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_metadata", (PyCFunction)_CtxImage_get_aux_metadata, METH_O},
{NULL, NULL}
};

/* =========== Functions ======== */

static PyObject* _CtxWrite(PyObject* self, PyObject* args) {
Expand Down Expand Up @@ -1486,6 +1624,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) {
Expand Down
53 changes: 43 additions & 10 deletions pillow_heif/heif.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -127,17 +127,19 @@ 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:
bigcat88 marked this conversation as resolved.
Show resolved Hide resolved
"""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."""

def __init__(self, c_image, info):
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}>"


class HeifImage(BaseImage):
Expand All @@ -152,15 +154,17 @@ 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 []
)
_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),
"exif": _exif,
"metadata": _metadata,
"thumbnails": _thumbnails,
"depth_images": _depth_images,
"aux": _ctx_aux_meta,
}
_heif_meta = _get_heif_meta(c_image)
if _xmp:
self.info["xmp"] = _xmp
if _heif_meta:
Expand Down Expand Up @@ -206,6 +210,28 @@ 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:
"""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["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)


class HeifFile:
"""Representation of the :py:class:`~pillow_heif.HeifImage` classes container.
Expand Down Expand Up @@ -481,6 +507,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


Expand Down
Loading