Skip to content

Commit 00c0d8e

Browse files
authored
NCLX color profile rework (#66)
* Adds support for read nclx values and fixes #64 * CI & CD: trigger push analyze only on main branch
1 parent e865a21 commit 00c0d8e

File tree

8 files changed

+60
-22
lines changed

8 files changed

+60
-22
lines changed

.github/workflows/analysis-coverage.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ on:
88
- 'setup.*'
99
- 'pyproject.toml'
1010
push:
11+
branches: [master]
1112
paths:
1213
- 'pillow_heif/*.*'
1314
- 'tests/**'

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@ _# Changelog
22

33
All notable changes to this project will be documented in this file.
44

5-
## [0.9.1 - 202x-xx-xx]
5+
## [0.9.1 - 2023-01-xx]
66

77
### Added
88

99
### Changed
1010

1111
- Drop support for Python 3.6
12+
- info["nclx_profile"] changed type from bytes(format of which was not described) to dict.
1213

1314
### Fixed
1415

16+
- Small memory leak when opening image with `nclx` color profile. #64
17+
1518
## [0.9.0 - 2022-12-15]
1619

1720
### Added

docs/reference/HeifImage.rst

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,9 @@ HeifImage object
3636
Possible values: ``prof`` or ``rICC``. Can be absent.
3737

3838
.. py:attribute:: info["nclx_profile"]
39-
:type: bytes
40-
41-
Can be absent. Maybe later, will be added some stuff to work with nclx profiles.
39+
:type: dict
4240

43-
If you need it now, look at `public_api.h` for struct describing it in ``C`` language.
41+
Can be absent. Look at `public_api.h` for struct describing it in ``C`` language.
4442

4543
.. py:attribute:: info["primary"]
4644
:type: bool

examples/heif_dump_info.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
else:
3333
print("\tICC: Empty")
3434
if image.info.get("nclx_profile", None):
35-
print("\tNCLX:", "soon :)")
35+
print("\tNCLX:", image.info["nclx_profile"])
3636
if image.info.get("exif", None):
3737
print("\tExif:")
3838
exif_dict = piexif.load(image.info["exif"], key_is_name=True)

examples/pillow_dump_info.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
else:
3030
print("\tICC: Empty")
3131
if image.info.get("nclx_profile", None):
32-
print("\tNCLX:", "will do soon :)")
32+
print("\tNCLX:", image.info["nclx_profile"])
3333
exif = image.getexif()
3434
if exif:
3535
exif = exif._get_merged_dict() # noqa

pillow_heif/private.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -114,24 +114,38 @@ def get_pure_stride(mode: str, width: int) -> int:
114114
}
115115

116116

117+
NCLX_FIELDS = ("color_primaries", "transfer_characteristics", "matrix_coefficients", "full_range_flag")
118+
NCLX_DECODE_ONLY_FIELDS = (
119+
"color_primary_red_x",
120+
"color_primary_red_y",
121+
"color_primary_green_x",
122+
"color_primary_green_y",
123+
"color_primary_blue_x",
124+
"color_primary_blue_y",
125+
"color_primary_white_x",
126+
"color_primary_white_y",
127+
)
128+
129+
117130
def read_color_profile(handle) -> dict:
118131
profile_type = lib.heif_image_handle_get_color_profile_type(handle)
119132
if profile_type == HeifColorProfileType.NOT_PRESENT:
120133
return {}
121134
if profile_type == HeifColorProfileType.NCLX:
122-
_type = "nclx"
123135
pp_data = ffi.new("struct heif_color_profile_nclx **")
124-
data_length = ffi.sizeof("struct heif_color_profile_nclx")
125136
error = lib.heif_image_handle_get_nclx_color_profile(handle, pp_data)
126-
p_data = pp_data[0]
137+
check_libheif_error(error)
138+
libheif_nclx_profile = pp_data[0]
127139
ffi.release(pp_data)
128-
else:
129-
_type = "prof" if profile_type == HeifColorProfileType.PROF else "rICC"
130-
data_length = lib.heif_image_handle_get_raw_color_profile_size(handle)
131-
if data_length == 0:
132-
return {"type": _type, "data": b""}
133-
p_data = ffi.new("char[]", data_length)
134-
error = lib.heif_image_handle_get_raw_color_profile(handle, p_data)
140+
nclx_profile = {i: getattr(libheif_nclx_profile, i) for i in NCLX_FIELDS + NCLX_DECODE_ONLY_FIELDS}
141+
lib.heif_nclx_color_profile_free(libheif_nclx_profile)
142+
return {"type": "nclx", "data": nclx_profile}
143+
_type = "prof" if profile_type == HeifColorProfileType.PROF else "rICC"
144+
data_length = lib.heif_image_handle_get_raw_color_profile_size(handle)
145+
if data_length == 0:
146+
return {"type": _type, "data": b""}
147+
p_data = ffi.new("char[]", data_length)
148+
error = lib.heif_image_handle_get_raw_color_profile(handle, p_data)
135149
check_libheif_error(error)
136150
data_buffer = ffi.buffer(p_data, data_length)
137151
return {"type": _type, "data": bytes(data_buffer)}
@@ -146,10 +160,14 @@ def set_color_profile(heif_img, info: dict) -> None:
146160
)
147161
check_libheif_error(error)
148162
elif info.get("nclx_profile", None):
149-
error = lib.heif_image_set_nclx_color_profile(
150-
heif_img,
151-
ffi.cast("const struct heif_color_profile_nclx*", ffi.from_buffer(info["nclx_profile"])),
152-
)
163+
nclx_profile = info["nclx_profile"]
164+
libheif_nclx_profile = lib.heif_nclx_color_profile_alloc()
165+
for i in NCLX_FIELDS:
166+
v = nclx_profile.get(i, None)
167+
if v is not None:
168+
setattr(libheif_nclx_profile, i, v)
169+
error = lib.heif_image_set_nclx_color_profile(heif_img, libheif_nclx_profile)
170+
lib.heif_nclx_color_profile_free(libheif_nclx_profile)
153171
check_libheif_error(error)
154172

155173

tests/helpers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ def compare_images_fields(heif_image: Union[HeifImage, HeifThumbnail], pillow_im
112112
assert heif_image.info[k] == pillow_image.info[k]
113113
else:
114114
assert len(heif_image.info[k]) == len(pillow_image.info[k])
115-
for k in ("icc_profile", "icc_profile_type", "nclx_profile"):
115+
assert heif_image.info.get("nclx_profile", None) == pillow_image.info.get("nclx_profile", None)
116+
for k in ("icc_profile", "icc_profile_type"):
116117
if heif_image.info.get(k, None):
117118
assert len(heif_image.info[k]) == len(pillow_image.info[k])
118119

tests/leaks_test.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,20 @@ def test_open_to_numpy_mem_leaks():
7979
mem_limit = mem + 1
8080
continue
8181
assert mem <= mem_limit, f"memory usage limit exceeded after {i + 1} iterations"
82+
83+
84+
@pytest.mark.skipif(sys.platform.lower() == "win32", reason="requires Unix or macOS")
85+
@pytest.mark.skipif(machine().find("x86_64") == -1, reason="run only on x86_64")
86+
def test_nclx_profile_leaks():
87+
mem_limit = None
88+
im_path = Path("images/heif_other/cat.hif")
89+
heif_file = pillow_heif.open_heif(im_path, convert_hdr_to_8bit=False)
90+
for i in range(800):
91+
_nclx = pillow_heif.private.read_color_profile(heif_file[0]._handle) # noqa
92+
_nclx = None # noqa
93+
gc.collect()
94+
mem = _get_mem_usage()
95+
if i < 300:
96+
mem_limit = mem + 1
97+
continue
98+
assert mem <= mem_limit, f"memory usage limit exceeded after {i + 1} iterations"

0 commit comments

Comments
 (0)