2023-12-21 14:13:31 +03:00
|
|
|
from __future__ import annotations
|
2024-01-20 14:23:03 +03:00
|
|
|
|
2016-03-02 22:50:14 +03:00
|
|
|
import datetime
|
2019-07-06 23:40:53 +03:00
|
|
|
import os
|
2020-02-12 19:29:19 +03:00
|
|
|
import re
|
2020-09-13 06:53:58 +03:00
|
|
|
import shutil
|
2024-01-01 02:47:39 +03:00
|
|
|
import sys
|
2019-07-06 23:40:53 +03:00
|
|
|
from io import BytesIO
|
2024-01-31 12:12:58 +03:00
|
|
|
from pathlib import Path
|
2024-02-20 13:27:30 +03:00
|
|
|
from typing import Any
|
2012-10-16 00:26:38 +04:00
|
|
|
|
2020-02-03 12:11:32 +03:00
|
|
|
import pytest
|
2020-08-07 13:28:33 +03:00
|
|
|
|
2024-01-01 02:47:39 +03:00
|
|
|
from PIL import Image, ImageMode, ImageWin, features
|
2014-06-10 13:10:47 +04:00
|
|
|
|
2021-02-21 14:22:29 +03:00
|
|
|
from .helper import (
|
|
|
|
assert_image,
|
|
|
|
assert_image_equal,
|
|
|
|
assert_image_similar,
|
|
|
|
assert_image_similar_tofile,
|
|
|
|
hopper,
|
2024-01-02 02:25:39 +03:00
|
|
|
is_pypy,
|
2021-02-21 14:22:29 +03:00
|
|
|
)
|
2015-04-24 02:26:52 +03:00
|
|
|
|
2012-10-16 00:26:38 +04:00
|
|
|
try:
|
|
|
|
from PIL import ImageCms
|
2014-07-30 08:20:11 +04:00
|
|
|
from PIL.ImageCms import ImageCmsProfile
|
2019-06-13 18:54:46 +03:00
|
|
|
|
2014-04-04 03:04:29 +04:00
|
|
|
ImageCms.core.profile_open
|
2018-10-18 21:49:14 +03:00
|
|
|
except ImportError:
|
2020-02-12 19:29:19 +03:00
|
|
|
# Skipped via setup_module()
|
2014-06-10 13:10:47 +04:00
|
|
|
pass
|
2014-06-02 14:19:01 +04:00
|
|
|
|
2013-10-12 10:39:16 +04:00
|
|
|
|
2014-09-25 08:25:42 +04:00
|
|
|
SRGB = "Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc"
|
|
|
|
HAVE_PROFILE = os.path.exists(SRGB)
|
2013-10-12 10:39:16 +04:00
|
|
|
|
2015-04-24 02:26:52 +03:00
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def setup_module() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
try:
|
|
|
|
from PIL import ImageCms
|
2019-06-13 18:54:46 +03:00
|
|
|
|
2020-02-12 19:29:19 +03:00
|
|
|
# need to hit getattr to trigger the delayed import error
|
|
|
|
ImageCms.core.profile_open
|
|
|
|
except ImportError as v:
|
|
|
|
pytest.skip(str(v))
|
2014-06-10 13:10:47 +04:00
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def skip_missing() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
if not HAVE_PROFILE:
|
|
|
|
pytest.skip("SRGB profile not available")
|
2015-04-24 02:26:52 +03:00
|
|
|
|
2014-06-10 13:10:47 +04:00
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_sanity() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
# basic smoke test.
|
|
|
|
# this mostly follows the cms_test outline.
|
2024-01-01 22:52:47 +03:00
|
|
|
with pytest.warns(DeprecationWarning):
|
|
|
|
v = ImageCms.versions() # should return four strings
|
2020-02-12 19:29:19 +03:00
|
|
|
assert v[0] == "1.0.0 pil"
|
|
|
|
assert list(map(type, v)) == [str, str, str, str]
|
2014-06-10 13:10:47 +04:00
|
|
|
|
2020-02-12 19:29:19 +03:00
|
|
|
# internal version number
|
2020-10-12 04:58:08 +03:00
|
|
|
assert re.search(r"\d+\.\d+(\.\d+)?$", features.version_module("littlecms2"))
|
2014-06-10 13:10:47 +04:00
|
|
|
|
2020-02-12 19:29:19 +03:00
|
|
|
skip_missing()
|
|
|
|
i = ImageCms.profileToProfile(hopper(), SRGB, SRGB)
|
|
|
|
assert_image(i, "RGB", (128, 128))
|
|
|
|
|
|
|
|
i = hopper()
|
|
|
|
ImageCms.profileToProfile(i, SRGB, SRGB, inPlace=True)
|
|
|
|
assert_image(i, "RGB", (128, 128))
|
|
|
|
|
|
|
|
t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB")
|
|
|
|
i = ImageCms.applyTransform(hopper(), t)
|
|
|
|
assert_image(i, "RGB", (128, 128))
|
2014-06-10 13:10:47 +04:00
|
|
|
|
2020-02-12 19:29:19 +03:00
|
|
|
with hopper() as i:
|
2014-06-10 13:10:47 +04:00
|
|
|
t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB")
|
2020-02-12 19:29:19 +03:00
|
|
|
ImageCms.applyTransform(hopper(), t, inPlace=True)
|
2020-01-30 17:56:07 +03:00
|
|
|
assert_image(i, "RGB", (128, 128))
|
2014-06-10 13:10:47 +04:00
|
|
|
|
2020-02-12 19:29:19 +03:00
|
|
|
p = ImageCms.createProfile("sRGB")
|
|
|
|
o = ImageCms.getOpenProfile(SRGB)
|
|
|
|
t = ImageCms.buildTransformFromOpenProfiles(p, o, "RGB", "RGB")
|
|
|
|
i = ImageCms.applyTransform(hopper(), t)
|
|
|
|
assert_image(i, "RGB", (128, 128))
|
|
|
|
|
|
|
|
t = ImageCms.buildProofTransform(SRGB, SRGB, SRGB, "RGB", "RGB")
|
|
|
|
assert t.inputMode == "RGB"
|
|
|
|
assert t.outputMode == "RGB"
|
|
|
|
i = ImageCms.applyTransform(hopper(), t)
|
|
|
|
assert_image(i, "RGB", (128, 128))
|
|
|
|
|
|
|
|
# test PointTransform convenience API
|
|
|
|
hopper().point(t)
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_flags() -> None:
|
2024-01-01 22:05:16 +03:00
|
|
|
assert ImageCms.Flags.NONE == 0
|
|
|
|
assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE
|
|
|
|
assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE
|
|
|
|
|
|
|
|
assert ImageCms.Flags.GRIDPOINTS(255) == (255 << 16)
|
|
|
|
assert ImageCms.Flags.GRIDPOINTS(-1) == ImageCms.Flags.GRIDPOINTS(255)
|
|
|
|
assert ImageCms.Flags.GRIDPOINTS(511) == ImageCms.Flags.GRIDPOINTS(255)
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_name() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
skip_missing()
|
|
|
|
# get profile information for file
|
|
|
|
assert (
|
|
|
|
ImageCms.getProfileName(SRGB).strip()
|
|
|
|
== "IEC 61966-2-1 Default RGB Colour Space - sRGB"
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_info() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
skip_missing()
|
|
|
|
assert ImageCms.getProfileInfo(SRGB).splitlines() == [
|
|
|
|
"sRGB IEC61966-2-1 black scaled",
|
|
|
|
"",
|
|
|
|
"Copyright International Color Consortium, 2009",
|
|
|
|
"",
|
|
|
|
]
|
2014-06-10 13:10:47 +04:00
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_copyright() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
skip_missing()
|
|
|
|
assert (
|
|
|
|
ImageCms.getProfileCopyright(SRGB).strip()
|
|
|
|
== "Copyright International Color Consortium, 2009"
|
|
|
|
)
|
|
|
|
|
2014-06-10 13:10:47 +04:00
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_manufacturer() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
skip_missing()
|
|
|
|
assert ImageCms.getProfileManufacturer(SRGB).strip() == ""
|
2014-06-10 13:10:47 +04:00
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_model() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
skip_missing()
|
|
|
|
assert (
|
|
|
|
ImageCms.getProfileModel(SRGB).strip()
|
|
|
|
== "IEC 61966-2-1 Default RGB Colour Space - sRGB"
|
|
|
|
)
|
2014-06-10 13:10:47 +04:00
|
|
|
|
2017-09-01 13:36:51 +03:00
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_description() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
skip_missing()
|
|
|
|
assert (
|
|
|
|
ImageCms.getProfileDescription(SRGB).strip() == "sRGB IEC61966-2-1 black scaled"
|
|
|
|
)
|
2017-09-01 13:36:51 +03:00
|
|
|
|
2014-06-10 13:10:47 +04:00
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_intent() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
skip_missing()
|
|
|
|
assert ImageCms.getDefaultIntent(SRGB) == 0
|
|
|
|
support = ImageCms.isIntentSupported(
|
2022-01-15 01:02:31 +03:00
|
|
|
SRGB, ImageCms.Intent.ABSOLUTE_COLORIMETRIC, ImageCms.Direction.INPUT
|
2020-02-12 19:29:19 +03:00
|
|
|
)
|
|
|
|
assert support == 1
|
2014-06-10 13:10:47 +04:00
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_profile_object() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
# same, using profile object
|
|
|
|
p = ImageCms.createProfile("sRGB")
|
|
|
|
# assert ImageCms.getProfileName(p).strip() == "sRGB built-in - (lcms internal)"
|
|
|
|
# assert ImageCms.getProfileInfo(p).splitlines() ==
|
|
|
|
# ["sRGB built-in", "", "WhitePoint : D65 (daylight)", "", ""]
|
|
|
|
assert ImageCms.getDefaultIntent(p) == 0
|
|
|
|
support = ImageCms.isIntentSupported(
|
2022-01-15 01:02:31 +03:00
|
|
|
p, ImageCms.Intent.ABSOLUTE_COLORIMETRIC, ImageCms.Direction.INPUT
|
2020-02-12 19:29:19 +03:00
|
|
|
)
|
|
|
|
assert support == 1
|
2014-06-10 13:10:47 +04:00
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_extensions() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
# extensions
|
2014-06-10 13:10:47 +04:00
|
|
|
|
2020-02-12 19:29:19 +03:00
|
|
|
with Image.open("Tests/images/rgb.jpg") as i:
|
|
|
|
p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"]))
|
|
|
|
assert (
|
|
|
|
ImageCms.getProfileName(p).strip()
|
|
|
|
== "IEC 61966-2.1 Default RGB colour space - sRGB"
|
|
|
|
)
|
2014-06-10 13:10:47 +04:00
|
|
|
|
2014-09-27 03:07:44 +04:00
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_exceptions() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
# Test mode mismatch
|
|
|
|
psRGB = ImageCms.createProfile("sRGB")
|
|
|
|
pLab = ImageCms.createProfile("LAB")
|
|
|
|
t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB")
|
2022-06-15 21:42:04 +03:00
|
|
|
with pytest.raises(ValueError, match="mode mismatch"):
|
2020-02-12 19:29:19 +03:00
|
|
|
t.apply_in_place(hopper("RGBA"))
|
2014-06-10 13:10:47 +04:00
|
|
|
|
2020-02-12 19:29:19 +03:00
|
|
|
# the procedural pyCMS API uses PyCMSError for all sorts of errors
|
|
|
|
with hopper() as im:
|
2022-06-15 21:42:04 +03:00
|
|
|
with pytest.raises(ImageCms.PyCMSError, match="cannot open profile file"):
|
2020-02-12 19:29:19 +03:00
|
|
|
ImageCms.profileToProfile(im, "foo", "bar")
|
2022-06-15 21:42:04 +03:00
|
|
|
|
|
|
|
with pytest.raises(ImageCms.PyCMSError, match="cannot open profile file"):
|
2020-02-12 19:29:19 +03:00
|
|
|
ImageCms.buildTransform("foo", "bar", "RGB", "RGB")
|
2022-06-15 21:42:04 +03:00
|
|
|
|
|
|
|
with pytest.raises(ImageCms.PyCMSError, match="Invalid type for Profile"):
|
2020-02-12 19:29:19 +03:00
|
|
|
ImageCms.getProfileName(None)
|
|
|
|
skip_missing()
|
2022-06-15 21:42:04 +03:00
|
|
|
|
2022-06-19 15:22:02 +03:00
|
|
|
# Python <= 3.9: "an integer is required (got type NoneType)"
|
|
|
|
# Python > 3.9: "'NoneType' object cannot be interpreted as an integer"
|
2022-06-15 21:57:20 +03:00
|
|
|
with pytest.raises(ImageCms.PyCMSError, match="integer"):
|
2020-02-12 19:29:19 +03:00
|
|
|
ImageCms.isIntentSupported(SRGB, None, None)
|
2014-06-10 13:10:47 +04:00
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_display_profile() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
# try fetching the profile for the current display device
|
|
|
|
ImageCms.get_display_profile()
|
2014-06-10 13:10:47 +04:00
|
|
|
|
2024-01-01 02:47:39 +03:00
|
|
|
if sys.platform == "win32":
|
|
|
|
ImageCms.get_display_profile(ImageWin.HDC(0))
|
|
|
|
ImageCms.get_display_profile(ImageWin.HWND(0))
|
|
|
|
|
2013-10-12 10:39:16 +04:00
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_lab_color_profile() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
ImageCms.createProfile("LAB", 5000)
|
|
|
|
ImageCms.createProfile("LAB", 6500)
|
2014-06-10 13:10:47 +04:00
|
|
|
|
2014-07-30 08:20:11 +04:00
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_unsupported_color_space() -> None:
|
2022-06-15 21:42:04 +03:00
|
|
|
with pytest.raises(
|
|
|
|
ImageCms.PyCMSError,
|
|
|
|
match=re.escape(
|
|
|
|
"Color space not supported for on-the-fly profile creation (unsupported)"
|
|
|
|
),
|
|
|
|
):
|
2020-02-12 19:29:19 +03:00
|
|
|
ImageCms.createProfile("unsupported")
|
2013-10-12 10:39:16 +04:00
|
|
|
|
2014-06-02 13:41:48 +04:00
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_invalid_color_temperature() -> None:
|
2022-06-15 21:42:04 +03:00
|
|
|
with pytest.raises(
|
|
|
|
ImageCms.PyCMSError,
|
|
|
|
match='Color temperature must be numeric, "invalid" not valid',
|
|
|
|
):
|
2020-02-12 19:29:19 +03:00
|
|
|
ImageCms.createProfile("LAB", "invalid")
|
2013-10-12 10:39:16 +04:00
|
|
|
|
2014-07-30 08:20:11 +04:00
|
|
|
|
2022-06-15 21:34:16 +03:00
|
|
|
@pytest.mark.parametrize("flag", ("my string", -1))
|
2024-02-20 07:41:20 +03:00
|
|
|
def test_invalid_flag(flag: str | int) -> None:
|
2022-06-15 21:34:16 +03:00
|
|
|
with hopper() as im:
|
|
|
|
with pytest.raises(
|
|
|
|
ImageCms.PyCMSError, match="flags must be an integer between 0 and "
|
|
|
|
):
|
|
|
|
ImageCms.profileToProfile(im, "foo", "bar", flags=flag)
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_simple_lab() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
i = Image.new("RGB", (10, 10), (128, 128, 128))
|
2014-06-02 13:41:48 +04:00
|
|
|
|
2020-02-12 19:29:19 +03:00
|
|
|
psRGB = ImageCms.createProfile("sRGB")
|
|
|
|
pLab = ImageCms.createProfile("LAB")
|
|
|
|
t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB")
|
2013-10-12 10:39:16 +04:00
|
|
|
|
2020-02-12 19:29:19 +03:00
|
|
|
i_lab = ImageCms.applyTransform(i, t)
|
2013-10-12 10:39:16 +04:00
|
|
|
|
2020-02-12 19:29:19 +03:00
|
|
|
assert i_lab.mode == "LAB"
|
2014-07-30 07:44:17 +04:00
|
|
|
|
2020-02-12 19:29:19 +03:00
|
|
|
k = i_lab.getpixel((0, 0))
|
|
|
|
# not a linear luminance map. so L != 128:
|
|
|
|
assert k == (137, 128, 128)
|
2014-07-30 07:44:17 +04:00
|
|
|
|
2020-02-12 19:29:19 +03:00
|
|
|
l_data = i_lab.getdata(0)
|
|
|
|
a_data = i_lab.getdata(1)
|
|
|
|
b_data = i_lab.getdata(2)
|
2014-07-30 07:44:17 +04:00
|
|
|
|
2020-02-12 19:29:19 +03:00
|
|
|
assert list(l_data) == [137] * 100
|
|
|
|
assert list(a_data) == [128] * 100
|
|
|
|
assert list(b_data) == [128] * 100
|
2016-03-02 22:50:14 +03:00
|
|
|
|
2016-09-03 05:17:22 +03:00
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_lab_color() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
psRGB = ImageCms.createProfile("sRGB")
|
|
|
|
pLab = ImageCms.createProfile("LAB")
|
|
|
|
t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB")
|
2019-06-13 18:54:46 +03:00
|
|
|
|
2020-02-12 19:29:19 +03:00
|
|
|
# Need to add a type mapping for some PIL type to TYPE_Lab_8 in findLCMSType, and
|
|
|
|
# have that mapping work back to a PIL mode (likely RGB).
|
|
|
|
i = ImageCms.applyTransform(hopper(), t)
|
|
|
|
assert_image(i, "LAB", (128, 128))
|
2016-06-28 17:31:18 +03:00
|
|
|
|
2020-02-12 19:29:19 +03:00
|
|
|
# i.save('temp.lab.tif') # visually verified vs PS.
|
|
|
|
|
2021-02-21 14:22:29 +03:00
|
|
|
assert_image_similar_tofile(i, "Tests/images/hopper.Lab.tif", 3.5)
|
2020-02-12 19:29:19 +03:00
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_lab_srgb() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
psRGB = ImageCms.createProfile("sRGB")
|
|
|
|
pLab = ImageCms.createProfile("LAB")
|
|
|
|
t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB")
|
|
|
|
|
|
|
|
with Image.open("Tests/images/hopper.Lab.tif") as img:
|
|
|
|
img_srgb = ImageCms.applyTransform(img, t)
|
|
|
|
|
|
|
|
# img_srgb.save('temp.srgb.tif') # visually verified vs ps.
|
|
|
|
|
|
|
|
assert_image_similar(hopper(), img_srgb, 30)
|
|
|
|
assert img_srgb.info["icc_profile"]
|
|
|
|
|
|
|
|
profile = ImageCmsProfile(BytesIO(img_srgb.info["icc_profile"]))
|
|
|
|
assert "sRGB" in ImageCms.getProfileDescription(profile)
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_lab_roundtrip() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
# check to see if we're at least internally consistent.
|
|
|
|
psRGB = ImageCms.createProfile("sRGB")
|
|
|
|
pLab = ImageCms.createProfile("LAB")
|
|
|
|
t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB")
|
|
|
|
|
|
|
|
t2 = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB")
|
|
|
|
|
|
|
|
i = ImageCms.applyTransform(hopper(), t)
|
|
|
|
|
|
|
|
assert i.info["icc_profile"] == ImageCmsProfile(pLab).tobytes()
|
|
|
|
|
|
|
|
out = ImageCms.applyTransform(i, t2)
|
|
|
|
|
|
|
|
assert_image_similar(hopper(), out, 2)
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_profile_tobytes() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
with Image.open("Tests/images/rgb.jpg") as i:
|
|
|
|
p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"]))
|
|
|
|
|
|
|
|
p2 = ImageCms.getOpenProfile(BytesIO(p.tobytes()))
|
|
|
|
|
|
|
|
# not the same bytes as the original icc_profile, but it does roundtrip
|
|
|
|
assert p.tobytes() == p2.tobytes()
|
|
|
|
assert ImageCms.getProfileName(p) == ImageCms.getProfileName(p2)
|
|
|
|
assert ImageCms.getProfileDescription(p) == ImageCms.getProfileDescription(p2)
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_extended_information() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
skip_missing()
|
|
|
|
o = ImageCms.getOpenProfile(SRGB)
|
|
|
|
p = o.profile
|
|
|
|
|
2024-02-20 07:41:20 +03:00
|
|
|
def assert_truncated_tuple_equal(
|
2024-02-20 13:27:30 +03:00
|
|
|
tup1: tuple[Any, ...], tup2: tuple[Any, ...], digits: int = 10
|
2024-02-20 07:41:20 +03:00
|
|
|
) -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
# Helper function to reduce precision of tuples of floats
|
|
|
|
# recursively and then check equality.
|
2022-03-04 08:42:24 +03:00
|
|
|
power = 10**digits
|
2020-02-12 19:29:19 +03:00
|
|
|
|
2024-02-21 00:11:01 +03:00
|
|
|
def truncate_tuple(tuple_value: tuple[Any, ...]) -> tuple[Any, ...]:
|
2020-02-12 19:29:19 +03:00
|
|
|
return tuple(
|
2024-02-05 20:18:49 +03:00
|
|
|
(
|
|
|
|
truncate_tuple(val)
|
|
|
|
if isinstance(val, tuple)
|
|
|
|
else int(val * power) / power
|
|
|
|
)
|
2024-02-21 00:11:01 +03:00
|
|
|
for val in tuple_value
|
2020-02-12 19:29:19 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
assert truncate_tuple(tup1) == truncate_tuple(tup2)
|
|
|
|
|
|
|
|
assert p.attributes == 4294967296
|
|
|
|
assert_truncated_tuple_equal(
|
|
|
|
p.blue_colorant,
|
|
|
|
(
|
|
|
|
(0.14306640625, 0.06060791015625, 0.7140960693359375),
|
|
|
|
(0.1558847490315394, 0.06603820639433387, 0.06060791015625),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
assert_truncated_tuple_equal(
|
|
|
|
p.blue_primary,
|
|
|
|
(
|
|
|
|
(0.14306641366715667, 0.06060790921083026, 0.7140960805782015),
|
|
|
|
(0.15588475410450106, 0.06603820408959558, 0.06060790921083026),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
assert_truncated_tuple_equal(
|
|
|
|
p.chromatic_adaptation,
|
|
|
|
(
|
2019-06-13 18:54:46 +03:00
|
|
|
(
|
2020-02-12 19:29:19 +03:00
|
|
|
(1.04791259765625, 0.0229339599609375, -0.050201416015625),
|
|
|
|
(0.02960205078125, 0.9904632568359375, -0.0170745849609375),
|
|
|
|
(-0.009246826171875, 0.0150604248046875, 0.7517852783203125),
|
2019-06-13 18:54:46 +03:00
|
|
|
),
|
|
|
|
(
|
2020-02-12 19:29:19 +03:00
|
|
|
(1.0267159024652783, 0.022470062342089134, 0.0229339599609375),
|
|
|
|
(0.02951378324103937, 0.9875098886387147, 0.9904632568359375),
|
|
|
|
(-0.012205438066465256, 0.01987915407854985, 0.0150604248046875),
|
2019-06-13 18:54:46 +03:00
|
|
|
),
|
2020-02-12 19:29:19 +03:00
|
|
|
),
|
|
|
|
)
|
|
|
|
assert p.chromaticity is None
|
|
|
|
assert p.clut == {
|
|
|
|
0: (False, False, True),
|
|
|
|
1: (False, False, True),
|
|
|
|
2: (False, False, True),
|
|
|
|
3: (False, False, True),
|
|
|
|
}
|
|
|
|
|
|
|
|
assert p.colorant_table is None
|
|
|
|
assert p.colorant_table_out is None
|
|
|
|
assert p.colorimetric_intent is None
|
|
|
|
assert p.connection_space == "XYZ "
|
|
|
|
assert p.copyright == "Copyright International Color Consortium, 2009"
|
|
|
|
assert p.creation_date == datetime.datetime(2009, 2, 27, 21, 36, 31)
|
|
|
|
assert p.device_class == "mntr"
|
|
|
|
assert_truncated_tuple_equal(
|
|
|
|
p.green_colorant,
|
|
|
|
(
|
|
|
|
(0.3851470947265625, 0.7168731689453125, 0.097076416015625),
|
|
|
|
(0.32119769927720654, 0.5978443449048152, 0.7168731689453125),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
assert_truncated_tuple_equal(
|
|
|
|
p.green_primary,
|
|
|
|
(
|
|
|
|
(0.3851470888162112, 0.7168731974161346, 0.09707641738998518),
|
|
|
|
(0.32119768793686687, 0.5978443567149709, 0.7168731974161346),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
assert p.header_flags == 0
|
|
|
|
assert p.header_manufacturer == "\x00\x00\x00\x00"
|
|
|
|
assert p.header_model == "\x00\x00\x00\x00"
|
|
|
|
assert p.icc_measurement_condition == {
|
|
|
|
"backing": (0.0, 0.0, 0.0),
|
|
|
|
"flare": 0.0,
|
|
|
|
"geo": "unknown",
|
|
|
|
"observer": 1,
|
|
|
|
"illuminant_type": "D65",
|
|
|
|
}
|
|
|
|
assert p.icc_version == 33554432
|
|
|
|
assert p.icc_viewing_condition is None
|
|
|
|
assert p.intent_supported == {
|
|
|
|
0: (True, True, True),
|
|
|
|
1: (True, True, True),
|
|
|
|
2: (True, True, True),
|
|
|
|
3: (True, True, True),
|
|
|
|
}
|
|
|
|
assert p.is_matrix_shaper
|
|
|
|
assert p.luminance == ((0.0, 80.0, 0.0), (0.0, 1.0, 80.0))
|
|
|
|
assert p.manufacturer is None
|
|
|
|
assert_truncated_tuple_equal(
|
|
|
|
p.media_black_point,
|
|
|
|
(
|
|
|
|
(0.012054443359375, 0.0124969482421875, 0.01031494140625),
|
|
|
|
(0.34573304157549234, 0.35842450765864337, 0.0124969482421875),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
assert_truncated_tuple_equal(
|
|
|
|
p.media_white_point,
|
|
|
|
(
|
|
|
|
(0.964202880859375, 1.0, 0.8249053955078125),
|
|
|
|
(0.3457029219802284, 0.3585375327567059, 1.0),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
assert_truncated_tuple_equal(
|
|
|
|
(p.media_white_point_temperature,), (5000.722328847392,)
|
|
|
|
)
|
|
|
|
assert p.model == "IEC 61966-2-1 Default RGB Colour Space - sRGB"
|
|
|
|
|
|
|
|
assert p.perceptual_rendering_intent_gamut is None
|
|
|
|
|
|
|
|
assert p.profile_description == "sRGB IEC61966-2-1 black scaled"
|
|
|
|
assert p.profile_id == b")\xf8=\xde\xaf\xf2U\xaexB\xfa\xe4\xca\x839\r"
|
|
|
|
assert_truncated_tuple_equal(
|
|
|
|
p.red_colorant,
|
|
|
|
(
|
|
|
|
(0.436065673828125, 0.2224884033203125, 0.013916015625),
|
|
|
|
(0.6484536316398539, 0.3308524880306778, 0.2224884033203125),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
assert_truncated_tuple_equal(
|
|
|
|
p.red_primary,
|
|
|
|
(
|
|
|
|
(0.43606566581047446, 0.22248840582960838, 0.013916015621759925),
|
|
|
|
(0.6484536250319214, 0.3308524944738204, 0.22248840582960838),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
assert p.rendering_intent == 0
|
|
|
|
assert p.saturation_rendering_intent_gamut is None
|
|
|
|
assert p.screening_description is None
|
|
|
|
assert p.target is None
|
|
|
|
assert p.technology == "CRT "
|
|
|
|
assert p.version == 2.0
|
|
|
|
assert p.viewing_condition == "Reference Viewing Condition in IEC 61966-2-1"
|
|
|
|
assert p.xcolor_space == "RGB "
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_non_ascii_path(tmp_path: Path) -> None:
|
2020-09-13 06:53:58 +03:00
|
|
|
skip_missing()
|
|
|
|
tempfile = str(tmp_path / ("temp_" + chr(128) + ".icc"))
|
|
|
|
try:
|
|
|
|
shutil.copy(SRGB, tempfile)
|
|
|
|
except UnicodeEncodeError:
|
|
|
|
pytest.skip("Non-ASCII path could not be created")
|
|
|
|
|
|
|
|
o = ImageCms.getOpenProfile(tempfile)
|
|
|
|
p = o.profile
|
|
|
|
assert p.model == "IEC 61966-2-1 Default RGB Colour Space - sRGB"
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_profile_typesafety() -> None:
|
2024-01-02 02:25:39 +03:00
|
|
|
# does not segfault
|
2022-06-15 21:42:04 +03:00
|
|
|
with pytest.raises(TypeError, match="Invalid type for Profile"):
|
2020-02-12 19:29:19 +03:00
|
|
|
ImageCms.ImageCmsProfile(0).tobytes()
|
2022-06-15 21:42:04 +03:00
|
|
|
with pytest.raises(TypeError, match="Invalid type for Profile"):
|
2020-02-12 19:29:19 +03:00
|
|
|
ImageCms.ImageCmsProfile(1).tobytes()
|
|
|
|
|
2024-01-01 02:48:13 +03:00
|
|
|
# also check core function
|
|
|
|
with pytest.raises(TypeError):
|
|
|
|
ImageCms.core.profile_tobytes(0)
|
|
|
|
with pytest.raises(TypeError):
|
|
|
|
ImageCms.core.profile_tobytes(1)
|
|
|
|
|
2024-01-02 02:25:39 +03:00
|
|
|
if not is_pypy():
|
|
|
|
# core profile should not be directly instantiable
|
|
|
|
with pytest.raises(TypeError):
|
|
|
|
ImageCms.core.CmsProfile()
|
|
|
|
with pytest.raises(TypeError):
|
|
|
|
ImageCms.core.CmsProfile(0)
|
|
|
|
|
2020-02-12 19:29:19 +03:00
|
|
|
|
2024-01-02 02:25:57 +03:00
|
|
|
@pytest.mark.skipif(is_pypy(), reason="fails on PyPy")
|
|
|
|
def test_transform_typesafety() -> None:
|
|
|
|
# core transform should not be directly instantiable
|
|
|
|
with pytest.raises(TypeError):
|
2024-04-01 11:00:19 +03:00
|
|
|
ImageCms.core.CmsTransform()
|
2024-01-02 02:25:57 +03:00
|
|
|
with pytest.raises(TypeError):
|
2024-04-01 11:00:19 +03:00
|
|
|
ImageCms.core.CmsTransform(0)
|
2024-01-02 02:25:57 +03:00
|
|
|
|
|
|
|
|
2024-02-20 07:41:20 +03:00
|
|
|
def assert_aux_channel_preserved(
|
|
|
|
mode: str, transform_in_place: bool, preserved_channel: str
|
|
|
|
) -> None:
|
|
|
|
def create_test_image() -> Image.Image:
|
2020-02-12 19:29:19 +03:00
|
|
|
# set up test image with something interesting in the tested aux channel.
|
|
|
|
# fmt: off
|
2020-06-06 13:15:17 +03:00
|
|
|
nine_grid_deltas = [
|
2020-02-12 19:29:19 +03:00
|
|
|
(-1, -1), (-1, 0), (-1, 1),
|
2020-06-06 05:07:57 +03:00
|
|
|
(0, -1), (0, 0), (0, 1),
|
|
|
|
(1, -1), (1, 0), (1, 1),
|
2017-01-28 23:04:49 +03:00
|
|
|
]
|
2020-02-12 19:29:19 +03:00
|
|
|
# fmt: on
|
|
|
|
chans = []
|
|
|
|
bands = ImageMode.getmode(mode).bands
|
|
|
|
for band_ndx in range(len(bands)):
|
|
|
|
channel_type = "L" # 8-bit unorm
|
|
|
|
channel_pattern = hopper(channel_type)
|
|
|
|
|
|
|
|
# paste pattern with varying offsets to avoid correlation
|
|
|
|
# potentially hiding some bugs (like channels getting mixed).
|
|
|
|
paste_offset = (
|
|
|
|
int(band_ndx / len(bands) * channel_pattern.size[0]),
|
|
|
|
int(band_ndx / (len(bands) * 2) * channel_pattern.size[1]),
|
|
|
|
)
|
|
|
|
channel_data = Image.new(channel_type, channel_pattern.size)
|
|
|
|
for delta in nine_grid_deltas:
|
|
|
|
channel_data.paste(
|
|
|
|
channel_pattern,
|
|
|
|
tuple(
|
|
|
|
paste_offset[c] + delta[c] * channel_pattern.size[c]
|
|
|
|
for c in range(2)
|
|
|
|
),
|
|
|
|
)
|
|
|
|
chans.append(channel_data)
|
|
|
|
return Image.merge(mode, chans)
|
|
|
|
|
|
|
|
source_image = create_test_image()
|
|
|
|
source_image_aux = source_image.getchannel(preserved_channel)
|
|
|
|
|
|
|
|
# create some transform, it doesn't matter which one
|
|
|
|
source_profile = ImageCms.createProfile("sRGB")
|
|
|
|
destination_profile = ImageCms.createProfile("sRGB")
|
|
|
|
t = ImageCms.buildTransform(
|
|
|
|
source_profile, destination_profile, inMode=mode, outMode=mode
|
|
|
|
)
|
|
|
|
|
|
|
|
# apply transform
|
|
|
|
if transform_in_place:
|
|
|
|
ImageCms.applyTransform(source_image, t, inPlace=True)
|
|
|
|
result_image = source_image
|
|
|
|
else:
|
|
|
|
result_image = ImageCms.applyTransform(source_image, t, inPlace=False)
|
|
|
|
result_image_aux = result_image.getchannel(preserved_channel)
|
|
|
|
|
|
|
|
assert_image_equal(source_image_aux, result_image_aux)
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_preserve_auxiliary_channels_rgba() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
assert_aux_channel_preserved(
|
|
|
|
mode="RGBA", transform_in_place=False, preserved_channel="A"
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_preserve_auxiliary_channels_rgba_in_place() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
assert_aux_channel_preserved(
|
|
|
|
mode="RGBA", transform_in_place=True, preserved_channel="A"
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_preserve_auxiliary_channels_rgbx() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
assert_aux_channel_preserved(
|
|
|
|
mode="RGBX", transform_in_place=False, preserved_channel="X"
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_preserve_auxiliary_channels_rgbx_in_place() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
assert_aux_channel_preserved(
|
|
|
|
mode="RGBX", transform_in_place=True, preserved_channel="X"
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-01-31 12:12:58 +03:00
|
|
|
def test_auxiliary_channels_isolated() -> None:
|
2020-02-12 19:29:19 +03:00
|
|
|
# test data in aux channels does not affect non-aux channels
|
|
|
|
aux_channel_formats = [
|
|
|
|
# format, profile, color-only format, source test image
|
|
|
|
("RGBA", "sRGB", "RGB", hopper("RGBA")),
|
|
|
|
("RGBX", "sRGB", "RGB", hopper("RGBX")),
|
|
|
|
("LAB", "LAB", "LAB", Image.open("Tests/images/hopper.Lab.tif")),
|
|
|
|
]
|
|
|
|
for src_format in aux_channel_formats:
|
|
|
|
for dst_format in aux_channel_formats:
|
|
|
|
for transform_in_place in [True, False]:
|
|
|
|
# inplace only if format doesn't change
|
|
|
|
if transform_in_place and src_format[0] != dst_format[0]:
|
|
|
|
continue
|
|
|
|
|
|
|
|
# convert with and without AUX data, test colors are equal
|
|
|
|
source_profile = ImageCms.createProfile(src_format[1])
|
|
|
|
destination_profile = ImageCms.createProfile(dst_format[1])
|
|
|
|
source_image = src_format[3]
|
|
|
|
test_transform = ImageCms.buildTransform(
|
|
|
|
source_profile,
|
|
|
|
destination_profile,
|
|
|
|
inMode=src_format[0],
|
|
|
|
outMode=dst_format[0],
|
|
|
|
)
|
2017-01-28 23:04:49 +03:00
|
|
|
|
2020-02-12 19:29:19 +03:00
|
|
|
# test conversion from aux-ful source
|
|
|
|
if transform_in_place:
|
|
|
|
test_image = source_image.copy()
|
|
|
|
ImageCms.applyTransform(test_image, test_transform, inPlace=True)
|
|
|
|
else:
|
|
|
|
test_image = ImageCms.applyTransform(
|
|
|
|
source_image, test_transform, inPlace=False
|
2019-06-13 18:54:46 +03:00
|
|
|
)
|
2018-06-24 15:32:25 +03:00
|
|
|
|
2020-02-12 19:29:19 +03:00
|
|
|
# reference conversion from aux-less source
|
|
|
|
reference_transform = ImageCms.buildTransform(
|
|
|
|
source_profile,
|
|
|
|
destination_profile,
|
|
|
|
inMode=src_format[2],
|
|
|
|
outMode=dst_format[2],
|
|
|
|
)
|
|
|
|
reference_image = ImageCms.applyTransform(
|
|
|
|
source_image.convert(src_format[2]), reference_transform
|
|
|
|
)
|
|
|
|
|
|
|
|
assert_image_equal(test_image.convert(dst_format[2]), reference_image)
|
2022-01-15 02:07:07 +03:00
|
|
|
|
|
|
|
|
2024-02-22 10:56:26 +03:00
|
|
|
def test_long_modes() -> None:
|
|
|
|
p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc")
|
|
|
|
ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI")
|
|
|
|
|
|
|
|
|
2023-01-06 12:09:47 +03:00
|
|
|
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
|
2024-02-20 07:41:20 +03:00
|
|
|
def test_rgb_lab(mode: str) -> None:
|
2023-01-06 12:09:47 +03:00
|
|
|
im = Image.new(mode, (1, 1))
|
|
|
|
converted_im = im.convert("LAB")
|
|
|
|
assert converted_im.getpixel((0, 0)) == (0, 128, 128)
|
|
|
|
|
|
|
|
im = Image.new("LAB", (1, 1), (255, 0, 0))
|
|
|
|
converted_im = im.convert(mode)
|
|
|
|
assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255)
|
2024-01-01 22:52:47 +03:00
|
|
|
|
|
|
|
|
2024-01-11 04:08:46 +03:00
|
|
|
def test_deprecation() -> None:
|
2024-01-01 22:52:47 +03:00
|
|
|
with pytest.warns(DeprecationWarning):
|
|
|
|
assert ImageCms.DESCRIPTION.strip().startswith("pyCMS")
|
|
|
|
with pytest.warns(DeprecationWarning):
|
|
|
|
assert ImageCms.VERSION == "1.0.0 pil"
|
|
|
|
with pytest.warns(DeprecationWarning):
|
|
|
|
assert isinstance(ImageCms.FLAGS, dict)
|