Pillow/Tests/test_imagecms.py

443 lines
19 KiB
Python
Raw Normal View History

from helper import unittest, PillowTestCase, hopper
import datetime
from PIL import Image, ImageMode
2014-06-10 13:10:47 +04:00
from io import BytesIO
import os
2015-04-24 02:26:52 +03:00
try:
from PIL import ImageCms
from PIL.ImageCms import ImageCmsProfile
ImageCms.core.profile_open
2014-06-10 13:10:47 +04:00
except ImportError as v:
# Skipped via setUp()
pass
2014-06-02 14:19:01 +04:00
SRGB = "Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc"
HAVE_PROFILE = os.path.exists(SRGB)
2015-04-24 02:26:52 +03:00
2014-06-10 13:10:47 +04:00
class TestImageCms(PillowTestCase):
def setUp(self):
try:
from PIL import ImageCms
# need to hit getattr to trigger the delayed import error
ImageCms.core.profile_open
2014-06-10 13:10:47 +04:00
except ImportError as v:
self.skipTest(v)
def skip_missing(self):
if not HAVE_PROFILE:
self.skipTest("SRGB profile not available")
2014-06-10 13:10:47 +04:00
def test_sanity(self):
2015-04-24 02:26:52 +03:00
2014-06-10 13:10:47 +04:00
# basic smoke test.
# this mostly follows the cms_test outline.
v = ImageCms.versions() # should return four strings
self.assertEqual(v[0], '1.0.0 pil')
self.assertEqual(list(map(type, v)), [str, str, str, str])
# internal version number
self.assertRegexpMatches(ImageCms.core.littlecms_version, r"\d+\.\d+$")
2014-06-10 13:10:47 +04:00
self.skip_missing()
2014-09-14 23:58:23 +04:00
i = ImageCms.profileToProfile(hopper(), SRGB, SRGB)
2014-06-10 13:10:47 +04:00
self.assert_image(i, "RGB", (128, 128))
2014-09-14 23:58:23 +04:00
i = hopper()
2014-06-10 13:10:47 +04:00
ImageCms.profileToProfile(i, SRGB, SRGB, inPlace=True)
self.assert_image(i, "RGB", (128, 128))
t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB")
2014-09-14 23:58:23 +04:00
i = ImageCms.applyTransform(hopper(), t)
2014-06-10 13:10:47 +04:00
self.assert_image(i, "RGB", (128, 128))
2014-09-14 23:58:23 +04:00
i = hopper()
2014-06-10 13:10:47 +04:00
t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB")
2014-09-14 23:58:23 +04:00
ImageCms.applyTransform(hopper(), t, inPlace=True)
2014-06-10 13:10:47 +04:00
self.assert_image(i, "RGB", (128, 128))
p = ImageCms.createProfile("sRGB")
o = ImageCms.getOpenProfile(SRGB)
t = ImageCms.buildTransformFromOpenProfiles(p, o, "RGB", "RGB")
2014-09-14 23:58:23 +04:00
i = ImageCms.applyTransform(hopper(), t)
2014-06-10 13:10:47 +04:00
self.assert_image(i, "RGB", (128, 128))
t = ImageCms.buildProofTransform(SRGB, SRGB, SRGB, "RGB", "RGB")
self.assertEqual(t.inputMode, "RGB")
self.assertEqual(t.outputMode, "RGB")
2014-09-14 23:58:23 +04:00
i = ImageCms.applyTransform(hopper(), t)
2014-06-10 13:10:47 +04:00
self.assert_image(i, "RGB", (128, 128))
# test PointTransform convenience API
2014-09-14 23:58:23 +04:00
hopper().point(t)
2014-06-10 13:10:47 +04:00
def test_name(self):
self.skip_missing()
2014-06-10 13:10:47 +04:00
# get profile information for file
self.assertEqual(
ImageCms.getProfileName(SRGB).strip(),
'IEC 61966-2-1 Default RGB Colour Space - sRGB')
2014-06-10 13:10:47 +04:00
def test_info(self):
self.skip_missing()
2014-06-10 13:10:47 +04:00
self.assertEqual(
ImageCms.getProfileInfo(SRGB).splitlines(), [
'sRGB IEC61966-2-1 black scaled', '',
'Copyright International Color Consortium, 2009', ''])
2014-06-10 13:10:47 +04:00
def test_copyright(self):
self.skip_missing()
2014-06-10 13:10:47 +04:00
self.assertEqual(
ImageCms.getProfileCopyright(SRGB).strip(),
'Copyright International Color Consortium, 2009')
2014-06-10 13:10:47 +04:00
def test_manufacturer(self):
self.skip_missing()
2014-06-10 13:10:47 +04:00
self.assertEqual(
ImageCms.getProfileManufacturer(SRGB).strip(),
'')
2014-06-10 13:10:47 +04:00
def test_model(self):
self.skip_missing()
2014-06-10 13:10:47 +04:00
self.assertEqual(
ImageCms.getProfileModel(SRGB).strip(),
'IEC 61966-2-1 Default RGB Colour Space - sRGB')
2014-06-10 13:10:47 +04:00
def test_description(self):
self.skip_missing()
2014-06-10 13:10:47 +04:00
self.assertEqual(
ImageCms.getProfileDescription(SRGB).strip(),
'sRGB IEC61966-2-1 black scaled')
2014-06-10 13:10:47 +04:00
def test_intent(self):
self.skip_missing()
2014-06-10 13:10:47 +04:00
self.assertEqual(ImageCms.getDefaultIntent(SRGB), 0)
self.assertEqual(ImageCms.isIntentSupported(
SRGB, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC,
ImageCms.DIRECTION_INPUT), 1)
def test_profile_object(self):
# same, using profile object
p = ImageCms.createProfile("sRGB")
# self.assertEqual(ImageCms.getProfileName(p).strip(),
# 'sRGB built-in - (lcms internal)')
# self.assertEqual(ImageCms.getProfileInfo(p).splitlines(),
# ['sRGB built-in', '', 'WhitePoint : D65 (daylight)', '', ''])
self.assertEqual(ImageCms.getDefaultIntent(p), 0)
self.assertEqual(ImageCms.isIntentSupported(
p, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC,
ImageCms.DIRECTION_INPUT), 1)
def test_extensions(self):
# extensions
2014-06-10 13:10:47 +04:00
i = Image.open("Tests/images/rgb.jpg")
p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"]))
self.assertEqual(
ImageCms.getProfileName(p).strip(),
'IEC 61966-2.1 Default RGB colour space - sRGB')
def test_exceptions(self):
2017-09-01 13:36:51 +03:00
# Test mode mismatch
psRGB = ImageCms.createProfile("sRGB")
pLab = ImageCms.createProfile("LAB")
t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB")
2017-09-01 14:05:40 +03:00
self.assertRaises(ValueError, t.apply_in_place, hopper("RGBA"))
2017-09-01 13:36:51 +03:00
2014-06-10 13:10:47 +04:00
# the procedural pyCMS API uses PyCMSError for all sorts of errors
self.assertRaises(
ImageCms.PyCMSError,
2017-09-01 14:05:40 +03:00
ImageCms.profileToProfile, hopper(), "foo", "bar")
2014-06-10 13:10:47 +04:00
self.assertRaises(
ImageCms.PyCMSError,
2017-09-01 14:05:40 +03:00
ImageCms.buildTransform, "foo", "bar", "RGB", "RGB")
2014-06-10 13:10:47 +04:00
self.assertRaises(
ImageCms.PyCMSError,
2017-09-01 14:05:40 +03:00
ImageCms.getProfileName, None)
self.skip_missing()
2014-06-10 13:10:47 +04:00
self.assertRaises(
ImageCms.PyCMSError,
2017-09-01 14:05:40 +03:00
ImageCms.isIntentSupported, SRGB, None, None)
2014-06-10 13:10:47 +04:00
def test_display_profile(self):
# try fetching the profile for the current display device
ImageCms.get_display_profile()
def test_lab_color_profile(self):
ImageCms.createProfile("LAB", 5000)
ImageCms.createProfile("LAB", 6500)
2017-09-01 13:36:51 +03:00
def test_unsupported_color_space(self):
self.assertRaises(ImageCms.PyCMSError,
2017-09-01 14:05:40 +03:00
ImageCms.createProfile, "unsupported")
2017-09-01 13:36:51 +03:00
def test_invalid_color_temperature(self):
self.assertRaises(ImageCms.PyCMSError,
2017-09-01 14:05:40 +03:00
ImageCms.createProfile, "LAB", "invalid")
2017-09-01 13:36:51 +03:00
2014-06-10 13:10:47 +04:00
def test_simple_lab(self):
i = Image.new('RGB', (10, 10), (128, 128, 128))
psRGB = ImageCms.createProfile("sRGB")
2014-06-10 13:10:47 +04:00
pLab = ImageCms.createProfile("LAB")
t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB")
2014-06-10 13:10:47 +04:00
i_lab = ImageCms.applyTransform(i, t)
self.assertEqual(i_lab.mode, 'LAB')
k = i_lab.getpixel((0, 0))
# not a linear luminance map. so L != 128:
self.assertEqual(k, (137, 128, 128))
l = i_lab.getdata(0)
2014-06-10 13:10:47 +04:00
a = i_lab.getdata(1)
b = i_lab.getdata(2)
self.assertEqual(list(l), [137] * 100)
2014-06-10 13:10:47 +04:00
self.assertEqual(list(a), [128] * 100)
self.assertEqual(list(b), [128] * 100)
def test_lab_color(self):
psRGB = ImageCms.createProfile("sRGB")
2014-06-10 13:10:47 +04:00
pLab = ImageCms.createProfile("LAB")
t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB")
2014-09-27 03:07:44 +04:00
2014-06-10 13:10:47 +04: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)
2014-06-10 13:10:47 +04:00
self.assert_image(i, "LAB", (128, 128))
# i.save('temp.lab.tif') # visually verified vs PS.
target = Image.open('Tests/images/hopper.Lab.tif')
2014-06-10 13:10:47 +04:00
2016-12-07 04:39:36 +03:00
self.assert_image_similar(i, target, 3.5)
2014-06-10 13:10:47 +04:00
def test_lab_srgb(self):
psRGB = ImageCms.createProfile("sRGB")
2014-06-10 13:10:47 +04:00
pLab = ImageCms.createProfile("LAB")
t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB")
2014-06-10 13:10:47 +04:00
img = Image.open('Tests/images/hopper.Lab.tif')
2014-06-10 13:10:47 +04:00
img_srgb = ImageCms.applyTransform(img, t)
2014-06-10 13:10:47 +04:00
# img_srgb.save('temp.srgb.tif') # visually verified vs ps.
self.assert_image_similar(hopper(), img_srgb, 30)
self.assertTrue(img_srgb.info['icc_profile'])
profile = ImageCmsProfile(BytesIO(img_srgb.info['icc_profile']))
self.assertIn('sRGB', ImageCms.getProfileDescription(profile))
2014-06-10 13:10:47 +04:00
def test_lab_roundtrip(self):
# check to see if we're at least internally consistent.
psRGB = ImageCms.createProfile("sRGB")
2014-06-10 13:10:47 +04:00
pLab = ImageCms.createProfile("LAB")
t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB")
2014-06-02 13:41:48 +04:00
t2 = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB")
2014-09-14 23:58:23 +04:00
i = ImageCms.applyTransform(hopper(), t)
self.assertEqual(i.info['icc_profile'],
ImageCmsProfile(pLab).tobytes())
2014-06-02 13:41:48 +04:00
2014-09-14 23:58:23 +04:00
out = ImageCms.applyTransform(i, t2)
2014-09-14 23:58:23 +04:00
self.assert_image_similar(hopper(), out, 2)
def test_profile_tobytes(self):
i = Image.open("Tests/images/rgb.jpg")
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
2014-09-14 23:58:23 +04:00
self.assertEqual(p.tobytes(), p2.tobytes())
self.assertEqual(ImageCms.getProfileName(p),
ImageCms.getProfileName(p2))
self.assertEqual(ImageCms.getProfileDescription(p),
ImageCms.getProfileDescription(p2))
def test_extended_information(self):
self.skip_missing()
o = ImageCms.getOpenProfile(SRGB)
p = o.profile
def assert_truncated_tuple_equal(tup1, tup2, digits=10):
# Helper function to reduce precision of tuples of floats
# recursively and then check equality.
power = 10 ** digits
2016-09-03 05:17:22 +03:00
def truncate_tuple(tuple_or_float):
return tuple(
truncate_tuple(val) if isinstance(val, tuple)
else int(val * power) / power for val in tuple_or_float)
self.assertEqual(truncate_tuple(tup1), truncate_tuple(tup2))
self.assertEqual(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, (((1.04791259765625, 0.0229339599609375, -0.050201416015625), (0.02960205078125, 0.9904632568359375, -0.0170745849609375), (-0.009246826171875, 0.0150604248046875, 0.7517852783203125)), ((1.0267159024652783, 0.022470062342089134, 0.0229339599609375), (0.02951378324103937, 0.9875098886387147, 0.9904632568359375), (-0.012205438066465256, 0.01987915407854985, 0.0150604248046875))))
self.assertIsNone(p.chromaticity)
self.assertEqual(p.clut, {0: (False, False, True), 1: (False, False, True), 2: (False, False, True), 3: (False, False, True)})
self.assertEqual(p.color_space, 'RGB')
self.assertIsNone(p.colorant_table)
self.assertIsNone(p.colorant_table_out)
self.assertIsNone(p.colorimetric_intent)
self.assertEqual(p.connection_space, 'XYZ ')
self.assertEqual(p.copyright, 'Copyright International Color Consortium, 2009')
self.assertEqual(p.creation_date, datetime.datetime(2009, 2, 27, 21, 36, 31))
self.assertEqual(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)))
self.assertEqual(p.header_flags, 0)
self.assertEqual(p.header_manufacturer, '\x00\x00\x00\x00')
self.assertEqual(p.header_model, '\x00\x00\x00\x00')
self.assertEqual(p.icc_measurement_condition, {'backing': (0.0, 0.0, 0.0), 'flare': 0.0, 'geo': 'unknown', 'observer': 1, 'illuminant_type': 'D65'})
self.assertEqual(p.icc_version, 33554432)
self.assertIsNone(p.icc_viewing_condition)
self.assertEqual(p.intent_supported, {0: (True, True, True), 1: (True, True, True), 2: (True, True, True), 3: (True, True, True)})
self.assertTrue(p.is_matrix_shaper)
self.assertEqual(p.luminance, ((0.0, 80.0, 0.0), (0.0, 1.0, 80.0)))
self.assertIsNone(p.manufacturer)
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,))
self.assertEqual(p.model, 'IEC 61966-2-1 Default RGB Colour Space - sRGB')
self.assertEqual(p.pcs, 'XYZ')
self.assertIsNone(p.perceptual_rendering_intent_gamut)
self.assertEqual(p.product_copyright, 'Copyright International Color Consortium, 2009')
self.assertEqual(p.product_desc, 'sRGB IEC61966-2-1 black scaled')
self.assertEqual(p.product_description, 'sRGB IEC61966-2-1 black scaled')
self.assertEqual(p.product_manufacturer, '')
self.assertEqual(p.product_model, 'IEC 61966-2-1 Default RGB Colour Space - sRGB')
self.assertEqual(p.profile_description, 'sRGB IEC61966-2-1 black scaled')
self.assertEqual(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)))
self.assertEqual(p.rendering_intent, 0)
self.assertIsNone(p.saturation_rendering_intent_gamut)
self.assertIsNone(p.screening_description)
self.assertIsNone(p.target)
self.assertEqual(p.technology, 'CRT ')
self.assertEqual(p.version, 2.0)
self.assertEqual(p.viewing_condition, 'Reference Viewing Condition in IEC 61966-2-1')
self.assertEqual(p.xcolor_space, 'RGB ')
def test_profile_typesafety(self):
""" Profile init type safety
prepatch, these would segfault, postpatch they should emit a typeerror
"""
2016-12-07 04:39:36 +03:00
with self.assertRaises(TypeError):
ImageCms.ImageCmsProfile(0).tobytes()
with self.assertRaises(TypeError):
ImageCms.ImageCmsProfile(1).tobytes()
2016-12-07 04:39:36 +03:00
def assert_aux_channel_preserved(self, mode, transform_in_place, preserved_channel):
def create_test_image():
# set up test image with something interesting in the tested aux
# channel.
nine_grid_deltas = [
(-1, -1), (-1, 0), (-1, 1),
( 0, -1), ( 0, 0), ( 0, 1),
( 1, -1), ( 1, 0), ( 1, 1),
]
chans = []
bands = ImageMode.getmode(mode).bands
2017-03-03 13:05:41 +03:00
for band_ndx in range(len(bands)):
2017-04-20 14:14:23 +03:00
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 / float(len(bands)) * channel_pattern.size[0]),
int(band_ndx / float(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()
2017-08-09 02:36:07 +03:00
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)
2017-08-09 02:36:07 +03:00
result_image_aux = result_image.getchannel(preserved_channel)
self.assert_image_equal(source_image_aux, result_image_aux)
def test_preserve_auxiliary_channels_rgba(self):
self.assert_aux_channel_preserved(mode='RGBA', transform_in_place=False, preserved_channel='A')
def test_preserve_auxiliary_channels_rgba_in_place(self):
self.assert_aux_channel_preserved(mode='RGBA', transform_in_place=True, preserved_channel='A')
def test_preserve_auxiliary_channels_rgbx(self):
self.assert_aux_channel_preserved(mode='RGBX', transform_in_place=False, preserved_channel='X')
def test_preserve_auxiliary_channels_rgbx_in_place(self):
self.assert_aux_channel_preserved(mode='RGBX', transform_in_place=True, preserved_channel='X')
def test_auxiliary_channels_isolated(self):
# 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])
# 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)
# 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)
self.assert_image_equal(test_image.convert(dst_format[2]), reference_image)
2018-03-03 12:54:00 +03:00
2014-06-10 13:10:47 +04:00
if __name__ == '__main__':
unittest.main()