Skip to content

Commit ed772d4

Browse files
authored
Added support for monochrome images decoding (#215)
Signed-off-by: Alexander Piskun <[email protected]>
1 parent b84e155 commit ed772d4

31 files changed

+138
-39
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
All notable changes to this project will be documented in this file.
22

3-
## [0.16.0 - 2024-02-2x]
3+
## [0.16.0 - 2024-04-02]
4+
5+
This release contains breaking change for monochrome images.
6+
7+
### Added
8+
9+
- Monochrome images **without alpha** channel, will be opened in `L` or `I;16` mode instead of `RGB`. #215
410

511
### Changed
612

13+
- `convert_hdr_to_8bit` value now ignores `monochrome` images. #215
714
- `subsampling` parameter for encoding has higher priority then `chroma`. #213
815
- the minimum required `libehif` version is `1.17.0`. #214
916

pillow_heif/_pillow_heif.c

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,11 @@ typedef struct {
8181
int height; // size[1];
8282
int bits; // one of: 8, 10, 12.
8383
int alpha; // one of: 0, 1.
84-
char mode[8]; // one of: RGB, RGBA, RGBa, BGR, BGRA, BGRa + Optional[;10/12/16]
84+
char mode[8]; // one of: L, RGB, RGBA, RGBa, BGR, BGRA, BGRa + Optional[;10/12/16]
8585
int n_channels; // 1, 2, 3, 4.
8686
int primary; // one of: 0, 1.
87+
enum heif_colorspace colorspace;
88+
enum heif_chroma chroma;
8789
int hdr_to_8bit; // private. decode option.
8890
int bgr_mode; // private. decode option.
8991
int remove_stride; // private. decode option.
@@ -767,6 +769,8 @@ PyObject* _CtxDepthImage(struct heif_image_handle* main_handle, heif_item_id dep
767769
}
768770
ctx_image->hdr_to_8bit = 0;
769771
ctx_image->bgr_mode = 0;
772+
ctx_image->colorspace = heif_colorspace_monochrome;
773+
ctx_image->chroma = heif_chroma_monochrome;
770774
ctx_image->handle = depth_handle;
771775
ctx_image->heif_image = NULL;
772776
ctx_image->data = NULL;
@@ -795,7 +799,9 @@ static void _CtxImage_destructor(CtxImageObject* self) {
795799
PyObject* _CtxImage(struct heif_image_handle* handle, int hdr_to_8bit,
796800
int bgr_mode, int remove_stride, int hdr_to_16bit,
797801
int reload_size, int primary, PyObject* file_bytes,
798-
const char *decoder_id) {
802+
const char *decoder_id,
803+
enum heif_colorspace colorspace, enum heif_chroma chroma
804+
) {
799805
CtxImageObject *ctx_image = PyObject_New(CtxImageObject, &CtxImage_Type);
800806
if (!ctx_image) {
801807
heif_image_handle_release(handle);
@@ -805,23 +811,41 @@ PyObject* _CtxImage(struct heif_image_handle* handle, int hdr_to_8bit,
805811
ctx_image->image_type = PhHeifImage;
806812
ctx_image->width = heif_image_handle_get_width(handle);
807813
ctx_image->height = heif_image_handle_get_height(handle);
808-
strcpy(ctx_image->mode, bgr_mode ? "BGR" : "RGB");
809814
ctx_image->alpha = heif_image_handle_has_alpha_channel(handle);
810-
ctx_image->n_channels = 3;
811-
if (ctx_image->alpha) {
812-
strcat(ctx_image->mode, heif_image_handle_is_premultiplied_alpha(handle) ? "a" : "A");
813-
ctx_image->n_channels = 4;
814-
}
815815
ctx_image->bits = heif_image_handle_get_luma_bits_per_pixel(handle);
816-
if ((ctx_image->bits > 8) && (!hdr_to_8bit)) {
817-
if (hdr_to_16bit) {
818-
strcat(ctx_image->mode, ";16");
816+
if ((chroma == heif_chroma_monochrome) && (colorspace == heif_colorspace_monochrome) && (!ctx_image->alpha)) {
817+
strcpy(ctx_image->mode, "L");
818+
if (ctx_image->bits > 8) {
819+
if (hdr_to_16bit) {
820+
strcpy(ctx_image->mode, "I;16");
821+
}
822+
else if (ctx_image->bits == 10) {
823+
strcpy(ctx_image->mode, "I;10");
824+
}
825+
else {
826+
strcpy(ctx_image->mode, "I;12");
827+
}
819828
}
820-
else if (ctx_image->bits == 10) {
821-
strcat(ctx_image->mode, ";10");
829+
ctx_image->n_channels = 1;
830+
bgr_mode = 0;
831+
hdr_to_8bit = 0;
832+
} else {
833+
strcpy(ctx_image->mode, bgr_mode ? "BGR" : "RGB");
834+
ctx_image->n_channels = 3;
835+
if (ctx_image->alpha) {
836+
strcat(ctx_image->mode, heif_image_handle_is_premultiplied_alpha(handle) ? "a" : "A");
837+
ctx_image->n_channels += 1;
822838
}
823-
else {
824-
strcat(ctx_image->mode, ";12");
839+
if ((ctx_image->bits > 8) && (!hdr_to_8bit)) {
840+
if (hdr_to_16bit) {
841+
strcat(ctx_image->mode, ";16");
842+
}
843+
else if (ctx_image->bits == 10) {
844+
strcat(ctx_image->mode, ";10");
845+
}
846+
else {
847+
strcat(ctx_image->mode, ";12");
848+
}
825849
}
826850
}
827851
ctx_image->hdr_to_8bit = hdr_to_8bit;
@@ -833,6 +857,8 @@ PyObject* _CtxImage(struct heif_image_handle* handle, int hdr_to_8bit,
833857
ctx_image->hdr_to_16bit = hdr_to_16bit;
834858
ctx_image->reload_size = reload_size;
835859
ctx_image->primary = primary;
860+
ctx_image->colorspace = colorspace;
861+
ctx_image->chroma = chroma;
836862
ctx_image->file_bytes = file_bytes;
837863
ctx_image->stride = get_stride(ctx_image);
838864
strcpy(ctx_image->decoder_id, decoder_id);
@@ -852,6 +878,14 @@ static PyObject* _CtxImage_bit_depth(CtxImageObject* self, void* closure) {
852878
return Py_BuildValue("i", self->bits);
853879
}
854880

881+
static PyObject* _CtxImage_colorspace(CtxImageObject* self, void* closure) {
882+
return Py_BuildValue("i", self->colorspace);
883+
}
884+
885+
static PyObject* _CtxImage_chroma(CtxImageObject* self, void* closure) {
886+
return Py_BuildValue("i", self->chroma);
887+
}
888+
855889
static PyObject* _CtxImage_color_profile(CtxImageObject* self, void* closure) {
856890
enum heif_color_profile_type profile_type = heif_image_handle_get_color_profile_type(self->handle);
857891
if (profile_type == heif_color_profile_type_not_present)
@@ -1155,6 +1189,8 @@ static struct PyGetSetDef _CtxImage_getseters[] = {
11551189
{"size_mode", (getter)_CtxImage_size_mode, NULL, NULL, NULL},
11561190
{"primary", (getter)_CtxImage_primary, NULL, NULL, NULL},
11571191
{"bit_depth", (getter)_CtxImage_bit_depth, NULL, NULL, NULL},
1192+
{"colorspace", (getter)_CtxImage_colorspace, NULL, NULL, NULL},
1193+
{"chroma", (getter)_CtxImage_chroma, NULL, NULL, NULL},
11581194
{"color_profile", (getter)_CtxImage_color_profile, NULL, NULL, NULL},
11591195
{"metadata", (getter)_CtxImage_metadata, NULL, NULL, NULL},
11601196
{"thumbnails", (getter)_CtxImage_thumbnails, NULL, NULL, NULL},
@@ -1264,6 +1300,8 @@ static PyObject* _load_file(PyObject* self, PyObject* args) {
12641300
return NULL;
12651301
}
12661302

1303+
enum heif_colorspace colorspace;
1304+
enum heif_chroma chroma;
12671305
struct heif_image_handle* handle;
12681306
struct heif_error error;
12691307
for (int i = 0; i < n_images; i++) {
@@ -1274,13 +1312,19 @@ static PyObject* _load_file(PyObject* self, PyObject* args) {
12741312
}
12751313
else
12761314
error = heif_context_get_image_handle(heif_ctx, images_ids[i], &handle);
1277-
if (error.code == heif_error_Ok)
1278-
PyList_SET_ITEM(images_list,
1279-
i,
1280-
_CtxImage(handle, hdr_to_8bit,
1315+
if (error.code == heif_error_Ok) {
1316+
error = heif_image_handle_get_preferred_decoding_colorspace(handle, &colorspace, &chroma);
1317+
if (error.code == heif_error_Ok) {
1318+
PyList_SET_ITEM(images_list,
1319+
i,
1320+
_CtxImage(handle, hdr_to_8bit,
12811321
bgr_mode, remove_stride, hdr_to_16bit, reload_size, primary, heif_bytes,
1282-
decoder_id));
1283-
else {
1322+
decoder_id, colorspace, chroma));
1323+
} else {
1324+
heif_image_handle_release(handle);
1325+
}
1326+
}
1327+
if (error.code != heif_error_Ok) {
12841328
Py_INCREF(Py_None);
12851329
PyList_SET_ITEM(images_list, i, Py_None);
12861330
}

pillow_heif/as_plugin.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,8 @@ def __init__(self, *args, **kwargs):
4242

4343
def _open(self):
4444
try:
45-
# when Pillow starts supporting 16-bit images:
46-
# set `convert_hdr_to_8bit` to False and `convert_hdr_to_8bit` to True
47-
_heif_file = HeifFile(self.fp, convert_hdr_to_8bit=True, remove_stride=False)
45+
# when Pillow starts supporting 16-bit multichannel images change `convert_hdr_to_8bit` to False
46+
_heif_file = HeifFile(self.fp, convert_hdr_to_8bit=True, hdr_to_16bit=True, remove_stride=False)
4847
except (OSError, ValueError, SyntaxError, RuntimeError, EOFError) as exception:
4948
raise SyntaxError(str(exception)) from None
5049
self.custom_mimetype = _heif_file.mimetype

pillow_heif/heif.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
_rotate_pil,
2323
_xmp_from_pillow,
2424
get_file_mimetype,
25+
save_colorspace_chroma,
2526
set_orientation,
2627
)
2728

@@ -120,6 +121,7 @@ def __init__(self, c_image):
120121
self.info = {
121122
"metadata": _metadata,
122123
}
124+
save_colorspace_chroma(c_image, self.info)
123125

124126
def __repr__(self):
125127
_bytes = f"{len(self.data)} bytes" if self._data or isinstance(self._c_image, MimCImage) else "no"
@@ -158,6 +160,7 @@ def __init__(self, c_image):
158160
"thumbnails": _thumbnails,
159161
"depth_images": _depth_images,
160162
}
163+
save_colorspace_chroma(c_image, self.info)
161164
_color_profile: Dict[str, Any] = c_image.color_profile
162165
if _color_profile:
163166
if _color_profile["type"] in ("rICC", "prof"):
@@ -493,7 +496,7 @@ def open_heif(fp, convert_hdr_to_8bit=True, bgr_mode=False, **kwargs) -> HeifFil
493496
:param fp: See parameter ``fp`` in :func:`is_supported`
494497
:param convert_hdr_to_8bit: Boolean indicating should 10 bit or 12 bit images
495498
be converted to 8-bit images during decoding. Otherwise, they will open in 16-bit mode.
496-
``Does not affect "depth images".``
499+
``Does not affect "monochrome" or "depth images".``
497500
:param bgr_mode: Boolean indicating should be `RGB(A)` images be opened in `BGR(A)` mode.
498501
:param kwargs: **hdr_to_16bit** a boolean value indicating that 10/12-bit image data
499502
should be converted to 16-bit mode during decoding. `Has lower priority than convert_hdr_to_8bit`!
@@ -518,7 +521,7 @@ def read_heif(fp, convert_hdr_to_8bit=True, bgr_mode=False, **kwargs) -> HeifFil
518521
:param fp: See parameter ``fp`` in :func:`is_supported`
519522
:param convert_hdr_to_8bit: Boolean indicating should 10 bit or 12 bit images
520523
be converted to 8-bit images during decoding. Otherwise, they will open in 16-bit mode.
521-
``Does not affect "depth images".``
524+
``Does not affect "monochrome" or "depth images".``
522525
:param bgr_mode: Boolean indicating should be `RGB(A)` images be opened in `BGR(A)` mode.
523526
:param kwargs: **hdr_to_16bit** a boolean value indicating that 10/12-bit image data
524527
should be converted to 16-bit mode during decoding. `Has lower priority than convert_hdr_to_8bit`!

pillow_heif/misc.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,20 @@
7878
"4:2:0": 420,
7979
}
8080

81+
LIBHEIF_CHROMA_MAP = {
82+
1: 420,
83+
2: 422,
84+
3: 444,
85+
}
86+
87+
88+
def save_colorspace_chroma(c_image, info: dict) -> None:
89+
"""Converts `chroma` value from `c_image` to useful values and stores them in ``info`` dict."""
90+
# Saving of `colorspace` was removed, as currently is not clear where to use that value.
91+
chroma = LIBHEIF_CHROMA_MAP.get(c_image.chroma, None)
92+
if chroma is not None:
93+
info["chroma"] = chroma
94+
8195

8296
def set_orientation(info: dict) -> Optional[int]:
8397
"""Reset orientation in ``EXIF`` to ``1`` if any orientation present.
@@ -440,6 +454,8 @@ def __init__(self, mode: str, size: tuple, data: bytes, **kwargs):
440454
self.thumbnails: List[int] = []
441455
self.depth_image_list: List = []
442456
self.primary = False
457+
self.chroma = HeifChroma.UNDEFINED.value
458+
self.colorspace = HeifColorspace.UNDEFINED.value
443459

444460
@property
445461
def size_mode(self):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ profile = "black"
7272
[tool.pylint]
7373
master.py-version = "3.8"
7474
master.extension-pkg-allow-list = ["_pillow_heif"]
75-
design.max-attributes = 9
75+
design.max-attributes = 12
7676
design.max-branches = 16
7777
design.max-locals = 18
7878
design.max-returns = 8

tests/basic_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,10 @@ def test_is_supported_fails(img):
8585

8686
def test_heif_str():
8787
str_img_nl_1 = "<HeifImage 64x64 RGB with no image data and 2 thumbnails>"
88-
str_img_nl_2 = "<HeifImage 64x64 RGB with no image data and 1 thumbnails>"
88+
str_img_nl_2 = "<HeifImage 64x64 L with no image data and 1 thumbnails>"
8989
str_img_nl_3 = "<HeifImage 96x64 RGB with no image data and 0 thumbnails>"
9090
str_img_l_1 = "<HeifImage 64x64 RGB with 12288 bytes image data and 2 thumbnails>"
91-
str_img_l_2 = "<HeifImage 64x64 RGB with 12288 bytes image data and 1 thumbnails>"
91+
str_img_l_2 = "<HeifImage 64x64 L with 4096 bytes image data and 1 thumbnails>"
9292
heif_file = pillow_heif.open_heif(Path("images/heif/zPug_3.heic"))
9393
assert str(heif_file) == f"<HeifFile with 3 images: ['{str_img_nl_1}', '{str_img_nl_2}', '{str_img_nl_3}']>"
9494
assert str(heif_file[0]) == str_img_nl_1
-5 Bytes
Binary file not shown.
2 Bytes
Binary file not shown.
4 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)