Repository: python-pillow/Pillow

Problem statement:
Drop support for LCMS version 1
Due to security issue: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718682



Hints:

Patch:
diff --git a/PIL/Image.py b/PIL/Image.py
--- a/PIL/Image.py
+++ b/PIL/Image.py
@@ -200,6 +200,7 @@ def isImageType(t):
     "RGBA": ("RGB", "L", ("R", "G", "B", "A")),
     "CMYK": ("RGB", "L", ("C", "M", "Y", "K")),
     "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")),
+    "LAB": ("RGB", "L", ("L", "A", "B")),
 
     # Experimental modes include I;16, I;16L, I;16B, RGBa, BGR;15, and
     # BGR;24.  Use these modes only if you know exactly what you're
@@ -224,6 +225,7 @@ def isImageType(t):
     "RGBA": ('|u1', 4),
     "CMYK": ('|u1', 4),
     "YCbCr": ('|u1', 3),
+    "LAB": ('|u1', 3), # UNDONE - unsigned |u1i1i1
     "I;16": ('=u2', None),
     "I;16B": ('>u2', None),
     "I;16L": ('<u2', None),
diff --git a/PIL/ImageCms.py b/PIL/ImageCms.py
--- a/PIL/ImageCms.py
+++ b/PIL/ImageCms.py
@@ -42,6 +42,8 @@
 
     Version History:
 
+        1.0.0 pil       Oct 2013 Port to LCMS 2.
+
         0.1.0 pil mod   March 10, 2009
 
                         Renamed display profile to proof profile. The proof
@@ -77,7 +79,7 @@
 
 """
 
-VERSION = "0.1.0 pil"
+VERSION = "1.0.0 pil"
 
 # --------------------------------------------------------------------.
 
@@ -151,8 +153,8 @@ def _set(self, profile, filename=None):
         self.profile = profile
         self.filename = filename
         if profile:
-            self.product_name = profile.product_name
-            self.product_info = profile.product_info
+            self.product_name = None #profile.product_name
+            self.product_info = None #profile.product_info
         else:
             self.product_name = None
             self.product_info = None
@@ -564,10 +566,10 @@ def createProfile(colorSpace, colorTemp=-1):
         raise PyCMSError("Color space not supported for on-the-fly profile creation (%s)" % colorSpace)
 
     if colorSpace == "LAB":
-        if isinstance(colorTemp, float):
-            colorTemp = int(colorTemp + 0.5)
-        if not isinstance(colorTemp, int):
-            raise PyCMSError("Color temperature must be a positive integer, \"%s\" not valid" % colorTemp)
+        try:
+            colorTemp = float(colorTemp)
+        except:
+            raise PyCMSError("Color temperature must be numeric, \"%s\" not valid" % colorTemp)
 
     try:
         return core.createProfile(colorSpace, colorTemp)
@@ -597,7 +599,19 @@ def getProfileName(profile):
         # add an extra newline to preserve pyCMS compatibility
         if not isinstance(profile, ImageCmsProfile):
             profile = ImageCmsProfile(profile)
-        return profile.profile.product_name + "\n"
+        # do it in python, not c.
+        #    // name was "%s - %s" (model, manufacturer) || Description , 
+        #    // but if the Model and Manufacturer were the same or the model 
+        #    // was long, Just the model,  in 1.x
+        model = profile.profile.product_model
+        manufacturer = profile.profile.product_manufacturer
+
+        if not (model or manufacturer):
+            return profile.profile.product_description+"\n"
+        if not manufacturer or len(model) > 30:
+            return model + "\n"
+        return "%s - %s\n" % (model, manufacturer)
+
     except (AttributeError, IOError, TypeError, ValueError) as v:
         raise PyCMSError(v)
 
@@ -625,10 +639,130 @@ def getProfileInfo(profile):
         if not isinstance(profile, ImageCmsProfile):
             profile = ImageCmsProfile(profile)
         # add an extra newline to preserve pyCMS compatibility
-        return profile.product_info + "\n"
+        # Python, not C. the white point bits weren't working well, so skipping.
+        #    // info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint
+        description = profile.profile.product_description
+        cpright = profile.profile.product_copyright
+        arr = []
+        for elt in (description, cpright):
+            if elt:
+                arr.append(elt)
+        return "\r\n\r\n".join(arr)+"\r\n\r\n"
+
+    except (AttributeError, IOError, TypeError, ValueError) as v:
+        raise PyCMSError(v)
+
+
+##
+# (pyCMS) Gets the copyright for the given profile.
+#
+# If profile isn't a valid CmsProfile object or filename to a profile,
+# a PyCMSError is raised.
+#
+# If an error occurs while trying to obtain the copyright tag, a PyCMSError
+# is raised
+#
+# Use this function to obtain the information stored in the profile's
+# copyright tag.  
+#
+# @param profile EITHER a valid CmsProfile object, OR a string of the filename
+#     of an ICC profile.
+# @return A string containing the internal profile information stored in an ICC
+#     tag.
+# @exception PyCMSError
+
+def getProfileCopyright(profile):
+    try:
+        # add an extra newline to preserve pyCMS compatibility
+        if not isinstance(profile, ImageCmsProfile):
+            profile = ImageCmsProfile(profile)
+        return profile.profile.product_copyright + "\n"
     except (AttributeError, IOError, TypeError, ValueError) as v:
         raise PyCMSError(v)
 
+##
+# (pyCMS) Gets the manufacturer for the given profile.
+#
+# If profile isn't a valid CmsProfile object or filename to a profile,
+# a PyCMSError is raised.
+#
+# If an error occurs while trying to obtain the manufacturer tag, a PyCMSError
+# is raised
+#
+# Use this function to obtain the information stored in the profile's
+# manufacturer tag.  
+#
+# @param profile EITHER a valid CmsProfile object, OR a string of the filename
+#     of an ICC profile.
+# @return A string containing the internal profile information stored in an ICC
+#     tag.
+# @exception PyCMSError
+
+def getProfileManufacturer(profile):
+    try:
+        # add an extra newline to preserve pyCMS compatibility
+        if not isinstance(profile, ImageCmsProfile):
+            profile = ImageCmsProfile(profile)
+        return profile.profile.product_manufacturer + "\n"
+    except (AttributeError, IOError, TypeError, ValueError) as v:
+        raise PyCMSError(v)
+
+##
+# (pyCMS) Gets the model for the given profile.
+#
+# If profile isn't a valid CmsProfile object or filename to a profile,
+# a PyCMSError is raised.
+#
+# If an error occurs while trying to obtain the model tag, a PyCMSError
+# is raised
+#
+# Use this function to obtain the information stored in the profile's
+# model tag.  
+#
+# @param profile EITHER a valid CmsProfile object, OR a string of the filename
+#     of an ICC profile.
+# @return A string containing the internal profile information stored in an ICC
+#     tag.
+# @exception PyCMSError
+
+def getProfileModel(profile):
+    try:
+        # add an extra newline to preserve pyCMS compatibility
+        if not isinstance(profile, ImageCmsProfile):
+            profile = ImageCmsProfile(profile)
+        return profile.profile.product_model + "\n"
+    except (AttributeError, IOError, TypeError, ValueError) as v:
+        raise PyCMSError(v)
+
+##
+# (pyCMS) Gets the description for the given profile.
+#
+# If profile isn't a valid CmsProfile object or filename to a profile,
+# a PyCMSError is raised.
+#
+# If an error occurs while trying to obtain the description tag, a PyCMSError
+# is raised
+#
+# Use this function to obtain the information stored in the profile's
+# description tag.  
+#
+# @param profile EITHER a valid CmsProfile object, OR a string of the filename
+#     of an ICC profile.
+# @return A string containing the internal profile information stored in an ICC
+#     tag.
+# @exception PyCMSError
+
+def getProfileDescription(profile):
+    try:
+        # add an extra newline to preserve pyCMS compatibility
+        if not isinstance(profile, ImageCmsProfile):
+            profile = ImageCmsProfile(profile)
+        return profile.profile.product_description + "\n"
+    except (AttributeError, IOError, TypeError, ValueError) as v:
+        raise PyCMSError(v)
+
+
+
 ##
 # (pyCMS) Gets the default intent name for the given profile.
 #
diff --git a/selftest.py b/selftest.py
--- a/selftest.py
+++ b/selftest.py
@@ -195,7 +195,7 @@ def check_codec(feature, codec):
     check_codec("ZLIB (PNG/ZIP)", "zip")
     check_codec("G4 TIFF", "group4")
     check_module("FREETYPE2", "PIL._imagingft")
-    check_module("LITTLECMS", "PIL._imagingcms")
+    check_module("LITTLECMS2", "PIL._imagingcms")
     check_module("WEBP", "PIL._webp")
     try:
         from PIL import _webp
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -331,8 +331,8 @@ def build_extensions(self):
                         _add_directory(self.compiler.include_dirs, dir, 0)
 
         if feature.want('lcms'):
-            if _find_include_file(self, "lcms.h"):
-                if _find_library_file(self, "lcms"):
+            if _find_include_file(self, "lcms2.h"):
+                if _find_library_file(self, "lcms2"):
                     feature.lcms = "lcms"
 
         if _tkinter and _find_include_file(self, "tk.h"):
@@ -415,7 +415,7 @@ def build_extensions(self):
             if sys.platform == "win32":
                 extra.extend(["user32", "gdi32"])
             exts.append(Extension(
-                "PIL._imagingcms", ["_imagingcms.c"], libraries=["lcms"] + extra))
+                "PIL._imagingcms", ["_imagingcms.c"], libraries=["lcms2"] + extra))
 
         if os.path.isfile("_webp.c") and feature.webp:
             libs = ["webp"]
@@ -491,7 +491,7 @@ def summary_report(self, feature, unsafe_zlib):
             (feature.zlib, "ZLIB (PNG/ZIP)"),
             (feature.tiff, "TIFF G3/G4 (experimental)"),
             (feature.freetype, "FREETYPE2"),
-            (feature.lcms, "LITTLECMS"),
+            (feature.lcms, "LITTLECMS2"),
             (feature.webp, "WEBP"),
             (feature.webpmux, "WEBPMUX"), ]
 


Test patch:
diff --git a/Tests/images/lab-green.tif b/Tests/images/lab-green.tif
new file mode 100644
Binary files /dev/null and b/Tests/images/lab-green.tif differ
diff --git a/Tests/images/lab-red.tif b/Tests/images/lab-red.tif
new file mode 100644
Binary files /dev/null and b/Tests/images/lab-red.tif differ
diff --git a/Tests/images/lab.tif b/Tests/images/lab.tif
new file mode 100644
Binary files /dev/null and b/Tests/images/lab.tif differ
diff --git a/Tests/images/lena.Lab.tif b/Tests/images/lena.Lab.tif
new file mode 100644
Binary files /dev/null and b/Tests/images/lena.Lab.tif differ
diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py
new file mode 100644
--- /dev/null
+++ b/Tests/test_format_lab.py
@@ -0,0 +1,41 @@
+from tester import *
+
+from PIL import Image
+
+def test_white():
+    i = Image.open('Tests/images/lab.tif')
+
+    bits = i.load()
+    
+    assert_equal(i.mode, 'LAB')
+
+    assert_equal(i.getbands(), ('L','A', 'B'))
+
+    k = i.getpixel((0,0))
+    assert_equal(k, (255,128,128))
+
+    L  = i.getdata(0)
+    a = i.getdata(1)
+    b = i.getdata(2)
+
+    assert_equal(list(L), [255]*100)
+    assert_equal(list(a), [128]*100)
+    assert_equal(list(b), [128]*100)
+    
+
+def test_green():
+    # l= 50 (/100), a = -100 (-128 .. 128) b=0 in PS
+    # == RGB: 0, 152, 117
+    i = Image.open('Tests/images/lab-green.tif')
+
+    k = i.getpixel((0,0))
+    assert_equal(k, (128,28,128))
+
+
+def test_red():
+    # l= 50 (/100), a = 100 (-128 .. 128) b=0 in PS
+    # == RGB: 255, 0, 124
+    i = Image.open('Tests/images/lab-red.tif')
+
+    k = i.getpixel((0,0))
+    assert_equal(k, (128,228,128))
diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py
--- a/Tests/test_imagecms.py
+++ b/Tests/test_imagecms.py
@@ -14,7 +14,7 @@ def test_sanity():
     # this mostly follows the cms_test outline.
 
     v = ImageCms.versions() # should return four strings
-    assert_equal(v[0], '0.1.0 pil')
+    assert_equal(v[0], '1.0.0 pil')
     assert_equal(list(map(type, v)), [str, str, str, str])
 
     # internal version number
@@ -39,44 +39,121 @@ def test_sanity():
     i = ImageCms.applyTransform(lena(), t)
     assert_image(i, "RGB", (128, 128))
 
+    # test PointTransform convenience API
+    im = lena().point(t)
+
+def test_name():
     # get profile information for file
     assert_equal(ImageCms.getProfileName(SRGB).strip(),
                  'IEC 61966-2.1 Default RGB colour space - sRGB')
+def x_test_info():
     assert_equal(ImageCms.getProfileInfo(SRGB).splitlines(),
                  ['sRGB IEC61966-2.1', '',
                   'Copyright (c) 1998 Hewlett-Packard Company', '',
                   'WhitePoint : D65 (daylight)', '',
                   'Tests/icc/sRGB.icm'])
+
+def test_intent():
     assert_equal(ImageCms.getDefaultIntent(SRGB), 0)
     assert_equal(ImageCms.isIntentSupported(
             SRGB, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC,
             ImageCms.DIRECTION_INPUT), 1)
 
+def test_profile_object():
     # same, using profile object
     p = ImageCms.createProfile("sRGB")
-    assert_equal(ImageCms.getProfileName(p).strip(),
-                 'sRGB built-in - (lcms internal)')
-    assert_equal(ImageCms.getProfileInfo(p).splitlines(),
-                 ['sRGB built-in', '', 'WhitePoint : D65 (daylight)', '', ''])
+#    assert_equal(ImageCms.getProfileName(p).strip(),
+#                 'sRGB built-in - (lcms internal)')
+#    assert_equal(ImageCms.getProfileInfo(p).splitlines(),
+#                 ['sRGB built-in', '', 'WhitePoint : D65 (daylight)', '', ''])
     assert_equal(ImageCms.getDefaultIntent(p), 0)
     assert_equal(ImageCms.isIntentSupported(
             p, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC,
             ImageCms.DIRECTION_INPUT), 1)
 
+def test_extensions():
     # extensions
     i = Image.open("Tests/images/rgb.jpg")
     p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"]))
     assert_equal(ImageCms.getProfileName(p).strip(),
                  'IEC 61966-2.1 Default RGB colour space - sRGB')
 
+def test_exceptions():
     # the procedural pyCMS API uses PyCMSError for all sorts of errors
     assert_exception(ImageCms.PyCMSError, lambda: ImageCms.profileToProfile(lena(), "foo", "bar"))
     assert_exception(ImageCms.PyCMSError, lambda: ImageCms.buildTransform("foo", "bar", "RGB", "RGB"))
     assert_exception(ImageCms.PyCMSError, lambda: ImageCms.getProfileName(None))
     assert_exception(ImageCms.PyCMSError, lambda: ImageCms.isIntentSupported(SRGB, None, None))
 
-    # test PointTransform convenience API
-    im = lena().point(t)
 
+def test_display_profile():
     # try fetching the profile for the current display device
     assert_no_exception(lambda: ImageCms.get_display_profile())
+
+
+def test_lab_color_profile():
+    pLab = ImageCms.createProfile("LAB", 5000)
+    pLab = ImageCms.createProfile("LAB", 6500)
+
+def test_simple_lab():
+    i = Image.new('RGB', (10,10), (128,128,128))
+
+    pLab = ImageCms.createProfile("LAB")    
+    t = ImageCms.buildTransform(SRGB, pLab, "RGB", "LAB")
+
+    i_lab = ImageCms.applyTransform(i, t)
+
+
+    assert_equal(i_lab.mode, 'LAB')
+
+    k = i_lab.getpixel((0,0))
+    assert_equal(k, (137,128,128)) # not a linear luminance map. so L != 128
+
+    L  = i_lab.getdata(0)
+    a = i_lab.getdata(1)
+    b = i_lab.getdata(2)
+
+    assert_equal(list(L), [137]*100)
+    assert_equal(list(a), [128]*100)
+    assert_equal(list(b), [128]*100)
+
+    
+def test_lab_color():
+    pLab = ImageCms.createProfile("LAB")    
+    t = ImageCms.buildTransform(SRGB, pLab, "RGB", "LAB")
+    # 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(lena(), t)
+    assert_image(i, "LAB", (128, 128))
+    
+    # i.save('temp.lab.tif')  # visually verified vs PS. 
+
+    target = Image.open('Tests/images/lena.Lab.tif')
+
+    assert_image_similar(i, target, 30)
+
+def test_lab_srgb():
+    pLab = ImageCms.createProfile("LAB")    
+    t = ImageCms.buildTransform(pLab, SRGB, "LAB", "RGB")
+
+    img = Image.open('Tests/images/lena.Lab.tif')
+
+    img_srgb = ImageCms.applyTransform(img, t)
+
+    # img_srgb.save('temp.srgb.tif') # visually verified vs ps. 
+    
+    assert_image_similar(lena(), img_srgb, 30)
+
+def test_lab_roundtrip():
+    # check to see if we're at least internally consistent. 
+    pLab = ImageCms.createProfile("LAB")    
+    t = ImageCms.buildTransform(SRGB, pLab, "RGB", "LAB")
+
+    t2 = ImageCms.buildTransform(pLab, SRGB, "LAB", "RGB")
+
+    i = ImageCms.applyTransform(lena(), t)
+    out = ImageCms.applyTransform(i, t2)
+
+    assert_image_similar(lena(), out, 2)
+
+