Skip to content

Commit a2d6971

Browse files
authored
PR: Make *OpenImageIO* and *ImageIO* optional. (#1340)
1 parent 7961e36 commit a2d6971

File tree

12 files changed

+158
-121
lines changed

12 files changed

+158
-121
lines changed

.github/workflows/continuous-integration-quality-unit-tests.yml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,6 @@ jobs:
5151
uv sync --all-extras --no-dev
5252
uv run python -c "import imageio;imageio.plugins.freeimage.download()"
5353
shell: bash
54-
- name: Install OpenImageIO (macOs)
55-
if: matrix.os == 'macOS-latest' && matrix.python-version == '3.13'
56-
run: |
57-
brew install openimageio
58-
ln -s /opt/homebrew/Cellar/openimageio/*/lib/python*/site-packages/OpenImageIO/OpenImageIO*.so ./.venv/lib/python${{ matrix.python-version }}/site-packages/OpenImageIO.so
59-
uv run python -c "import OpenImageIO;print(OpenImageIO.__version__)"
60-
shell: bash
6154
- name: Pre-Commit (All Files)
6255
run: |
6356
uv run pre-commit run --all-files

colour/examples/io/examples_fichet2021.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
import tempfile
88

99
import colour
10-
from colour.utilities import is_openimageio_installed, message_box
10+
from colour.utilities import is_imageio_installed, message_box
1111

12-
if is_openimageio_installed():
12+
if is_imageio_installed():
1313
ROOT_RESOURCES = os.path.join(
1414
os.path.dirname(__file__), "..", "..", "io", "tests", "resources"
1515
)

colour/io/fichet2021.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,8 @@ def from_spectral_image(path: str | PathLike) -> Specification_Fichet2021:
365365
rf"^T\.*{PATTERN_FICHET2021}\.*{PATTERN_FICHET2021}$"
366366
)
367367

368-
image_specification = ImageInput.open(path).spec()
368+
image_input = ImageInput.open(path)
369+
image_specification = image_input.spec()
369370
channels = image_specification.channelnames
370371

371372
for i, channel in enumerate(channels):
@@ -405,6 +406,8 @@ def from_spectral_image(path: str | PathLike) -> Specification_Fichet2021:
405406
for attribute in image_specification.extra_attribs
406407
]
407408

409+
image_input.close()
410+
408411
return Specification_Fichet2021(
409412
path,
410413
components,
@@ -516,7 +519,9 @@ def read_spectral_image_Fichet2021(
516519
bit_depth_specification = MAPPING_BIT_DEPTH[bit_depth]
517520

518521
specification = Specification_Fichet2021.from_spectral_image(path)
519-
image = ImageInput.open(path).read_image(bit_depth_specification.openimageio)
522+
image_input = ImageInput.open(path)
523+
image = image_input.read_image(bit_depth_specification.openimageio)
524+
image_input.close()
520525

521526
components = {}
522527
for component, wavelengths_indexes in specification.components.items():
@@ -870,6 +875,4 @@ def write_spectral_image_Fichet2021(
870875
image_buffer.specmod(), [*specification.attributes, *attributes]
871876
)
872877

873-
image_buffer.write(path)
874-
875-
return True
878+
return image_buffer.write(path)

colour/io/image.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
as_int_array,
3333
attest,
3434
filter_kwargs,
35+
is_imageio_installed,
3536
is_openimageio_installed,
3637
optional,
3738
required,
@@ -194,7 +195,6 @@ def add_attributes_to_image_specification_OpenImageIO(
194195
return image_specification
195196

196197

197-
@required("OpenImageIO")
198198
def image_specification_OpenImageIO(
199199
width: int,
200200
height: int,
@@ -454,6 +454,7 @@ def read_image_OpenImageIO(
454454
return image
455455

456456

457+
@required("Imageio")
457458
def read_image_Imageio(
458459
path: str | PathLike,
459460
bit_depth: Literal[
@@ -586,14 +587,14 @@ def read_image(
586587
dtype('float32')
587588
"""
588589

589-
method = validate_method(method, tuple(READ_IMAGE_METHODS))
590-
591-
if method == "openimageio" and not is_openimageio_installed(): # pragma: no cover
590+
if method.lower() == "imageio" and not is_imageio_installed(): # pragma: no cover
592591
usage_warning(
593-
'"OpenImageIO" related API features are not available, '
594-
'switching to "Imageio"!'
592+
'"Imageio" related API features are not available, '
593+
'switching to "OpenImageIO"!'
595594
)
596-
method = "Imageio"
595+
method = "openimageio"
596+
597+
method = validate_method(method, tuple(READ_IMAGE_METHODS))
597598

598599
function = READ_IMAGE_METHODS[method]
599600

@@ -666,7 +667,7 @@ def write_image_OpenImageIO(
666667
667668
Writing an "ACES" compliant "EXR" file:
668669
669-
>>> if is_openimageio_installed(): # doctest: +SKIP
670+
>>> if is_imageio_installed(): # doctest: +SKIP
670671
... from OpenImageIO import TypeDesc
671672
...
672673
... chromaticities = (
@@ -722,13 +723,14 @@ def write_image_OpenImageIO(
722723
image_output = ImageOutput.create(path)
723724

724725
image_output.open(path, image_specification)
725-
image_output.write_image(image)
726+
success = image_output.write_image(image)
726727

727728
image_output.close()
728729

729-
return True
730+
return success
730731

731732

733+
@required("Imageio")
732734
def write_image_Imageio(
733735
image: ArrayLike,
734736
path: str | PathLike,
@@ -903,14 +905,14 @@ def write_image(
903905
True
904906
""" # noqa: D405, D407, D410, D411, D414
905907

906-
method = validate_method(method, tuple(WRITE_IMAGE_METHODS))
907-
908-
if method == "openimageio" and not is_openimageio_installed(): # pragma: no cover
908+
if method.lower() == "imageio" and not is_imageio_installed(): # pragma: no cover
909909
usage_warning(
910-
'"OpenImageIO" related API features are not available, '
910+
'"Imageio" related API features are not available, '
911911
'switching to "Imageio"!'
912912
)
913-
method = "Imageio"
913+
method = "openimageio"
914+
915+
method = validate_method(method, tuple(WRITE_IMAGE_METHODS))
914916

915917
function = WRITE_IMAGE_METHODS[method]
916918

colour/io/tests/test_fichet2021.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
match_groups_to_nm,
3030
sds_and_msds_to_components_Fichet2021,
3131
)
32-
from colour.utilities import is_openimageio_installed
3332

3433
__author__ = "Colour Developers"
3534
__copyright__ = "Copyright 2013 Colour Developers"
@@ -180,9 +179,6 @@ def test_components_to_sRGB_Fichet2021(self) -> None:
180179
definition.
181180
"""
182181

183-
if not is_openimageio_installed():
184-
return
185-
186182
specification = Specification_Fichet2021(is_emissive=True)
187183
components = sds_and_msds_to_components_Fichet2021(
188184
SDS_ILLUMINANTS["D65"], specification
@@ -465,9 +461,6 @@ def test_read_spectral_image_Fichet2021(self) -> None:
465461
definition.
466462
"""
467463

468-
if not is_openimageio_installed():
469-
return
470-
471464
_test_spectral_image_D65(os.path.join(ROOT_RESOURCES, "D65.exr"))
472465

473466
_test_spectral_image_Ohta1997(os.path.join(ROOT_RESOURCES, "Ohta1997.exr"))
@@ -499,9 +492,6 @@ def test_write_spectral_image_Fichet2021(self) -> None:
499492
definition.
500493
"""
501494

502-
if not is_openimageio_installed():
503-
return
504-
505495
path = os.path.join(self._temporary_directory, "D65.exr")
506496
specification = Specification_Fichet2021(is_emissive=True)
507497
write_spectral_image_Fichet2021(

colour/io/tests/test_image.py

Lines changed: 48 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
write_image_Imageio,
2424
write_image_OpenImageIO,
2525
)
26-
from colour.utilities import attest, full, is_openimageio_installed
26+
from colour.utilities import attest, full
2727

2828
__author__ = "Colour Developers"
2929
__copyright__ = "Copyright 2013 Colour Developers"
@@ -60,9 +60,6 @@ def test_image_specification_OpenImageIO(self) -> None: # pragma: no cover
6060
definition.
6161
"""
6262

63-
if not is_openimageio_installed():
64-
return
65-
6663
from OpenImageIO import HALF # pyright: ignore
6764

6865
compression = Image_Specification_Attribute("Compression", "none")
@@ -275,9 +272,6 @@ class TestReadImageOpenImageIO:
275272
def test_read_image_OpenImageIO(self) -> None: # pragma: no cover
276273
"""Test :func:`colour.io.image.read_image_OpenImageIO` definition."""
277274

278-
if not is_openimageio_installed():
279-
return
280-
281275
image = read_image_OpenImageIO(
282276
os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr"),
283277
additional_data=False,
@@ -362,9 +356,6 @@ def teardown_method(self) -> None:
362356
def test_write_image_OpenImageIO(self) -> None: # pragma: no cover
363357
"""Test :func:`colour.io.image.write_image_OpenImageIO` definition."""
364358

365-
if not is_openimageio_installed():
366-
return
367-
368359
from OpenImageIO import TypeDesc # pyright: ignore
369360

370361
path = os.path.join(self._temporary_directory, "8-bit.png")
@@ -380,27 +371,30 @@ def test_write_image_OpenImageIO(self) -> None: # pragma: no cover
380371
np.testing.assert_equal(np.squeeze(RGB), image)
381372

382373
source_path = os.path.join(ROOT_RESOURCES, "Overflowing_Gradient.png")
374+
source_image = read_image_OpenImageIO(source_path, bit_depth="uint8")
383375
target_path = os.path.join(
384376
self._temporary_directory, "Overflowing_Gradient.png"
385377
)
386378
RGB = np.arange(0, 256, 1, dtype=np.uint8)[None] * 2
387379
write_image_OpenImageIO(RGB, target_path, bit_depth="uint8")
388-
image = read_image_OpenImageIO(source_path, bit_depth="uint8")
389-
np.testing.assert_equal(np.squeeze(RGB), image)
380+
target_image = read_image_OpenImageIO(source_path, bit_depth="uint8")
381+
np.testing.assert_equal(source_image, target_image)
382+
np.testing.assert_equal(np.squeeze(RGB), target_image)
390383

391384
source_path = os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr")
392-
target_path = os.path.join(self._temporary_directory, "CMS_Test_Pattern.exr")
393-
image = read_image_OpenImageIO(
385+
source_image = read_image_OpenImageIO(
394386
source_path,
395387
additional_data=False,
396388
)
397-
write_image_OpenImageIO(image, target_path)
398-
image = read_image_OpenImageIO(
389+
target_path = os.path.join(self._temporary_directory, "CMS_Test_Pattern.exr")
390+
write_image_OpenImageIO(source_image, target_path)
391+
target_image = read_image_OpenImageIO(
399392
target_path,
400393
additional_data=False,
401394
)
402-
assert image.shape == (1267, 1274, 3)
403-
assert image.dtype is np.dtype("float32")
395+
np.testing.assert_equal(source_image, target_image)
396+
assert target_image.shape == (1267, 1274, 3)
397+
assert target_image.dtype is np.dtype("float32")
404398

405399
chromaticities = (
406400
0.73470,
@@ -419,8 +413,8 @@ def test_write_image_OpenImageIO(self) -> None: # pragma: no cover
419413
),
420414
Image_Specification_Attribute("compression", "none"),
421415
]
422-
write_image_OpenImageIO(image, target_path, attributes=write_attributes)
423-
image, read_attributes = read_image_OpenImageIO(
416+
write_image_OpenImageIO(target_image, target_path, attributes=write_attributes)
417+
target_image, read_attributes = read_image_OpenImageIO(
424418
target_path, additional_data=True
425419
)
426420
for write_attribute in write_attributes:
@@ -517,35 +511,41 @@ def test_write_image_Imageio(self) -> None:
517511
"""Test :func:`colour.io.image.write_image_Imageio` definition."""
518512

519513
source_path = os.path.join(ROOT_RESOURCES, "Overflowing_Gradient.png")
514+
source_image = read_image_Imageio(source_path, bit_depth="uint8")
520515
target_path = os.path.join(
521516
self._temporary_directory, "Overflowing_Gradient.png"
522517
)
523518
RGB = np.arange(0, 256, 1, dtype=np.uint8)[None] * 2
524519
write_image_Imageio(RGB, target_path, bit_depth="uint8")
525-
image = read_image_Imageio(source_path, bit_depth="uint8")
526-
np.testing.assert_equal(np.squeeze(RGB), image)
520+
target_image = read_image_Imageio(target_path, bit_depth="uint8")
521+
np.testing.assert_equal(np.squeeze(RGB), target_image)
522+
np.testing.assert_equal(source_image, target_image)
527523

528-
source_path = os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr")
529-
target_path = os.path.join(self._temporary_directory, "CMS_Test_Pattern.exr")
530-
image = read_image_Imageio(source_path)
531-
write_image_Imageio(image, target_path)
532-
image = read_image_Imageio(target_path)
533-
assert image.shape == (1267, 1274, 3)
534-
assert image.dtype is np.dtype("float32")
535-
536-
# NOTE: Those unit tests are breaking unpredictably on Linux, skipping
537-
# for now.
524+
# NOTE: Those unit tests are breaking on Linux, skipping for now.
538525
if platform.system() != "Linux": # pragma: no cover
526+
source_path = os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr")
527+
source_image = read_image_Imageio(source_path)
528+
target_path = os.path.join(
529+
self._temporary_directory, "CMS_Test_Pattern.exr"
530+
)
531+
write_image_Imageio(source_image, target_path)
532+
target_image = read_image_Imageio(target_path)
533+
np.testing.assert_allclose(
534+
source_image, target_image, atol=TOLERANCE_ABSOLUTE_TESTS
535+
)
536+
assert target_image.shape == (1267, 1274, 3)
537+
assert target_image.dtype is np.dtype("float32")
538+
539539
target_path = os.path.join(self._temporary_directory, "Full_White.exr")
540-
image = full((32, 16, 3), 1e6, dtype=np.float16)
541-
write_image_Imageio(image, target_path)
542-
image = read_image_Imageio(target_path)
543-
assert np.max(image) == np.inf
540+
target_image = full((32, 16, 3), 1e6, dtype=np.float16)
541+
write_image_Imageio(target_image, target_path)
542+
target_image = read_image_Imageio(target_path)
543+
assert np.max(target_image) == np.inf
544544

545-
image = full((32, 16, 3), 1e6)
546-
write_image_Imageio(image, target_path)
547-
image = read_image_Imageio(target_path)
548-
assert np.max(image) == 1e6
545+
target_image = full((32, 16, 3), 1e6)
546+
write_image_Imageio(target_image, target_path)
547+
target_image = read_image_Imageio(target_path)
548+
assert np.max(target_image) == 1e6
549549

550550

551551
class TestReadImage:
@@ -582,12 +582,15 @@ def test_write_image(self) -> None:
582582
"""Test :func:`colour.io.image.write_image` definition."""
583583

584584
source_path = os.path.join(ROOT_RESOURCES, "CMS_Test_Pattern.exr")
585+
source_image = read_image(source_path)
585586
target_path = os.path.join(self._temporary_directory, "CMS_Test_Pattern.exr")
586-
image = read_image(source_path)
587-
write_image(image, target_path)
588-
image = read_image(target_path)
589-
assert image.shape == (1267, 1274, 3)
590-
assert image.dtype is np.dtype("float32")
587+
write_image(source_image, target_path)
588+
target_image = read_image(target_path)
589+
np.testing.assert_allclose(
590+
source_image, target_image, atol=TOLERANCE_ABSOLUTE_TESTS
591+
)
592+
assert target_image.shape == (1267, 1274, 3)
593+
assert target_image.dtype is np.dtype("float32")
591594

592595

593596
class TestAs3ChannelsImage:

colour/utilities/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,11 @@
4040
)
4141
from .requirements import (
4242
is_ctlrender_installed,
43+
is_imageio_installed,
44+
is_openimageio_installed,
4345
is_matplotlib_installed,
4446
is_networkx_installed,
4547
is_opencolorio_installed,
46-
is_openimageio_installed,
4748
is_pandas_installed,
4849
is_pydot_installed,
4950
is_tqdm_installed,
@@ -181,10 +182,11 @@
181182
]
182183
__all__ += [
183184
"is_ctlrender_installed",
185+
"is_imageio_installed",
186+
"is_openimageio_installed",
184187
"is_matplotlib_installed",
185188
"is_networkx_installed",
186189
"is_opencolorio_installed",
187-
"is_openimageio_installed",
188190
"is_pandas_installed",
189191
"is_pydot_installed",
190192
"is_tqdm_installed",

0 commit comments

Comments
 (0)