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 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
1 change: 1 addition & 0 deletions pillow_heif/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
HeifTransferCharacteristics,
)
from .heif import (
HeifAuxImage,
HeifDepthImage,
HeifFile,
HeifImage,
Expand Down
128 changes: 128 additions & 0 deletions pillow_heif/_pillow_heif.c
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,75 @@ static struct PyMethodDef _CtxWrite_methods[] = {
{NULL, NULL}
};

/* =========== 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))) {
heif_image_handle_release(aux_handle);
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);
heif_image_handle_release(aux_handle);
return NULL;
}
CtxImageObject *ctx_image = PyObject_New(CtxImageObject, &CtxImage_Type);
if (!ctx_image) {
heif_image_handle_release(aux_handle);
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;
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 @@ -1203,6 +1272,57 @@ 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 (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;
}

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) {
Expand Down Expand Up @@ -1265,11 +1385,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_type", (PyCFunction)_CtxImage_get_aux_type, METH_O},
{NULL, NULL}
};

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

static PyObject* _CtxWrite(PyObject* self, PyObject* args) {
Expand Down Expand Up @@ -1517,6 +1644,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
2 changes: 2 additions & 0 deletions pillow_heif/as_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
35 changes: 32 additions & 3 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,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:
bigcat88 marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -140,6 +139,13 @@ 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 __repr__(self):
return f"<{self.__class__.__name__} {self.size[0]}x{self.size[1]} {self.mode}>"


class HeifImage(BaseImage):
"""One image in a :py:class:`~pillow_heif.HeifFile` container."""

Expand All @@ -152,7 +158,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 []
)
_heif_meta = _get_heif_meta(c_image)
self.info = {
"primary": bool(c_image.primary),
"bit_depth": int(c_image.bit_depth),
Expand All @@ -161,6 +166,15 @@ def __init__(self, c_image):
"thumbnails": _thumbnails,
"depth_images": _depth_images,
}
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
if _heif_meta:
Expand Down Expand Up @@ -206,6 +220,14 @@ 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_image = self._c_image.get_aux_image(aux_id)
return HeifAuxImage(aux_image)


class HeifFile:
"""Representation of the :py:class:`~pillow_heif.HeifImage` classes container.
Expand Down Expand Up @@ -481,6 +503,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
1 change: 1 addition & 0 deletions pillow_heif/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions pillow_heif/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions tests/options_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand All @@ -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"}
Expand All @@ -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": ""}
Expand Down
13 changes: 13 additions & 0 deletions tests/read_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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+"
)
Expand Down
Loading