Commits

Robert Kern committed f881357

Separate profiles into profiles and transforms.

Comments (0)

Files changed (4)

examples/example.py

 from numpy import *
+import numpy as np
 from scipy.sandbox import image
 
 import lcms
 
+def adapt_whitepoint(src='D50', dst='D65'):
+    """ Compute the adaptation matrix for converting XYZ tristimulus values from
+    a one standard illuminant to another using the Bradford transform.
+
+    This implementation follows the presentation on
+        http://www.color.org/chadtag.html
+
+    The results are cached.
+
+    Parameters
+    ----------
+    src : str, optional
+    dst : str, optional
+        The names of the standard illuminants to convert from and to,
+        respectively. Valid values are the keys of image.whitepoints. The
+        default is to convert from D50 to D65.
+
+    Returns
+    -------
+    adapt : float array (3, 3)
+        Matrix-multiply this matrix against XYZ values with the `src` whitepoint
+        to get XYZ values with the `dst` whitepoint.
+    """
+
+    # Check the cache first.
+    key = (src, dst)
+    if key not in adapt_whitepoint.cache:
+        bradford = np.array([[0.8951, 0.2664, -0.1614],
+                             [-0.7502, 1.7135, 0.0367],
+                             [0.0389, -0.0685, 1.0296]])
+
+        src_whitepoint = image.whitepoints[src][-1]
+        src_rgb = np.dot(bradford, src_whitepoint)
+        dst_whitepoint = image.whitepoints[dst][-1]
+        dst_rgb = np.dot(bradford, dst_whitepoint)
+
+        scales = np.diag(dst_rgb/src_rgb)
+        adapt_whitepoint.cache[key] = np.linalg.solve(bradford, 
+            np.dot(scales, bradford))
+
+    adapt = adapt_whitepoint.cache[key]
+
+    return adapt
+
+# Create the cache.
+adapt_whitepoint.cache = {}
+
+
 r, g, b = mgrid[0.:1:17j, 0.:1:17j, 0.:1:17j]
 srgb_image = column_stack([r.flat, g.flat, b.flat])
 scipy_xyz_image = image.rgb2xyz(image.rgbp2rgb(srgb_image))
 
-xyz = lcms.ICCProfile()
-xyz.xyz_profile()
-srgb = lcms.ICCProfile()
-srgb.srgb_profile()
-xform = lcms.Transform()
-xform.between_profiles(srgb, xyz, lcms.formats.rgb, lcms.formats.xyz)
+xyz = lcms.XYZProfile()
+srgb = lcms.sRGBProfile()
+xform = lcms.TransformBetweenProfiles(srgb, xyz, lcms.formats.rgb,
+    lcms.formats.xyz, flags=lcms.flags.not_precalc)
 
-lcms_xyz_image = xform.do_transform(srgb_image)
+lcms_xyz_image = image.convert(adapt_whitepoint('D50', 'D65'),
+    xform(srgb_image))
 
 
 #### EOF #######################################################################
 
     int cmsFLAGS_GRIDPOINTS(int n)
 
+    # ICC standard enums.
+    ctypedef enum icColorSpaceSignature:
+        icSigXYZData
+        icSigLabData
+        icSigLuvData
+        icSigYCbCrData
+        icSigYxyData
+        icSigRgbData
+        icSigGrayData
+        icSigHsvData
+        icSigHlsData
+        icSigCmykData
+        icSigCmyData
+        icSig2colorData
+        icSig3colorData
+        icSig4colorData
+        icSig5colorData
+        icSig6colorData
+        icSig7colorData
+        icSig8colorData
+        icSig9colorData
+        icSig10colorData
+        icSig11colorData
+        icSig12colorData
+        icSig13colorData
+        icSig14colorData
+        icSig15colorData
+        icMaxEnumData
+
+    ctypedef enum icProfileClassSignature:
+        icSigInputClass
+        icSigDisplayClass
+        icSigOutputClass
+        icSigLinkClass
+        icSigAbstractClass
+        icSigColorSpaceClass
+        icSigNamedColorClass
+        icMaxEnumClass
+
+    ctypedef enum icPlatformSignature:
+        icSigMacintosh
+        icSigMicrosoft
+        icSigSolaris
+        icSigSGI
+        icSigTaligent
+        icMaxEnumPlatform
 
     # Colorspace structures.
     ctypedef struct cmsCIEXYZ:
     cmsHPROFILE cmsCreateXYZProfile()
     cmsHPROFILE cmsCreate_sRGBProfile()
     cmsHPROFILE cmsCreateNULLProfile()
+    char* cmsTakeProductName(cmsHPROFILE hProfile)
+    char* cmsTakeProductDesc(cmsHPROFILE hProfile)
+    char* cmsTakeProductInfo(cmsHPROFILE hProfile)
+    char* cmsTakeManufacturer(cmsHPROFILE hProfile)
+    char* cmsTakeModel(cmsHPROFILE hProfile)
+    char* cmsTakeCopyright(cmsHPROFILE hProfile)
+    icColorSpaceSignature cmsGetPCS(cmsHPROFILE hProfile)
+    icColorSpaceSignature cmsGetColorSpace(cmsHPROFILE hProfile)
+    icProfileClassSignature cmsGetDeviceClass(cmsHPROFILE hProfile)
+    int _cmsChannelsOf(icColorSpaceSignature ColorSpace)
 
     # Colorimetric space conversions.
     void cmsXYZ2xyY(LPcmsCIExyY Dest, cmsCIEXYZ* Source)
 _ignore_errors()
 
 
+class LcmsError(Exception):
+    """ An error from the lcms library.
+    """
+
+
 cdef class Intent:
     """ Constants for rendering intents.
     """
         def __get__(self):
             return cmsFLAGS_NOTCACHE
 
+    property not_precalc:
+        def __get__(self):
+            return cmsFLAGS_NOTPRECALC
+
     property null_transform:
         def __get__(self):
             return cmsFLAGS_NULLTRANSFORM
 
 
 cdef class ICCProfile:
-    """ An ICC profile.
-
-    Instantiate ICCProfile without arguments. To actually load data into the
-    profile, use one of the methods like .file_profile() to load a profile from
-    file, or .lab_profile(), .xyz_profile(), or .srgb_profile() to instantiate
-    a standard profile.
+    """ Base class for ICC profiles.
     """
 
     cdef cmsHPROFILE profile_ptr
 
+    property name:
+        def __get__(self):
+            return cmsTakeProductName(self.profile_ptr)
+
+    property desc:
+        def __get__(self):
+            return cmsTakeProductDesc(self.profile_ptr)
+
+    property info:
+        def __get__(self):
+            return cmsTakeProductInfo(self.profile_ptr)
+
+    property manufacturer:
+        def __get__(self):
+            return cmsTakeManufacturer(self.profile_ptr)
+
+    property model:
+        def __get__(self):
+            return cmsTakeModel(self.profile_ptr)
+
+    property copyright:
+        def __get__(self):
+            return cmsTakeCopyright(self.profile_ptr)
+
+    property colorspace:
+        def __get__(self):
+            cdef icColorSpaceSignature space
+
+            space = cmsGetColorSpace(self.profile_ptr)
+            if space == icSigXYZData:
+                return ('X', 'Y', 'Z')
+            elif space == icSigLabData:
+                return ('L*', 'a*', 'b*')
+            elif space == icSigLuvData:
+                return ('L*', 'u*', 'v*')
+            elif space == icSigYCbCrData:
+                return ('Y', 'Cb', 'Cr')
+            elif space == icSigYxyData:
+                return ('Y', 'x', 'y')
+            elif space == icSigRgbData:
+                return ('R', 'G', 'B')
+            elif space == icSigGrayData:
+                return ('Gray',)
+            elif space == icSigHsvData:
+                return ('H', 'S', 'V')
+            elif space == icSigHlsData:
+                return ('H', 'L', 'S')
+            elif space == icSigCmykData:
+                return ('C', 'M', 'Y', 'K')
+            elif space == icSigCmyData:
+                return ('C', 'M', 'Y')
+            else:
+                nchannels = _cmsChannelsOf(space)
+                return ('?',) * nchannels
 
     def __new__(self, *args, **kwds):
         self.profile_ptr = NULL
 
 
-    def __init__(self):
-        pass
+    def __init__(self, *args, **kwds):
+        raise NotImplementedError("instantiate a subclass of ICCProfile instead")
 
 
     def __dealloc__(self):
             cmsCloseProfile(self.profile_ptr)
 
 
-    def file_profile(self, char* filename):
-        """ Open an ICC profile from a file.
+cdef class FileProfile(ICCProfile):
+    """ Load an ICC profile from a file.
+    """
 
-        Parameters
-        ----------
-        filename : str
-        """
+    def __init__(self, char* filename):
+        self.profile_ptr = cmsOpenProfileFromFile(filename, 'r')
+        if self.profile_ptr == NULL:
+            raise LcmsError("could not open file %r for reading" % filename)
 
-        if self.profile_ptr != NULL:
-            cmsCloseProfile(self.profile_ptr)
-            self.profile_ptr = NULL
-        self.profile_ptr = cmsOpenProfileFromFile(filename, 'r')
 
+cdef class LabProfile(ICCProfile):
+    """ The standard CIE L*a*b* profile.
 
-    def lab_profile(self, whitepoint=(0.312713, 0.329016, 1.0)):
-        """ Create a CIE L*a*b* profile.
+    Parameters
+    ----------
+    whitepoint : 3-tuple (x, y, Y), optional
+        The whitepoint of the L*a*b* profile in CIE xyY format. The default is
+        the CIE D50 illuminant.
+    
+    """
 
-        Parameters
-        ----------
-        whitepoint : 3-tuple (x, y, Y), optional
-            The whitepoint of the L*a*b* profile in CIE xyY format. The default is
-            the CIE D65 illuminant.
-        """
+    def __init__(self, whitepoint=None):
+        cdef cmsCIExyY cwhitepoint
+        cdef LPcmsCIExyY ptr
 
-        cdef cmsCIExyY cwhitepoint
+        if whitpoint is not None:
+            cwhitepoint.x = whitepoint[0]
+            cwhitepoint.y = whitepoint[1]
+            cwhitepoint.Y = whitepoint[2]
+            ptr = &cwhitepoint
+        else:
+            ptr = NULL
+        self.profile_ptr = cmsCreateLabProfile(ptr)
+        if self.profile_ptr == NULL:
+            raise LcmsError("could not create L*a*b* profile")
 
-        if self.profile_ptr != NULL:
-            cmsCloseProfile(self.profile_ptr)
-            self.profile_ptr = NULL
 
-        cwhitepoint.x = whitepoint[0]
-        cwhitepoint.y = whitepoint[1]
-        cwhitepoint.Y = whitepoint[2]
-        self.profile_ptr = cmsCreateLabProfile(&cwhitepoint)
+cdef class XYZProfile(ICCProfile):
+    """ The standard XYZ profile.
 
+    Note: This assumes a D50 illuminant according to the ICC spec.
+    """
 
-    def xyz_profile(self):
-        """ Create a CIE XYZ profile.
-        """
+    def __init__(self):
+        self.profile_ptr = cmsCreateXYZProfile()
+        if self.profile_ptr == NULL:
+            raise LcmsError("could not create XYZ profile")
 
-        if self.profile_ptr != NULL:
-            cmsCloseProfile(self.profile_ptr)
-            self.profile_ptr = NULL
-        self.profile_ptr = cmsCreateXYZProfile()
 
+cdef class sRGBProfile(ICCProfile):
+    """ The standard IEC sRGB profile.
+    """
 
-    def srgb_profile(self):
-        """ Create an sRGB profile.
-        """
-
-        if self.profile_ptr != NULL:
-            cmsCloseProfile(self.profile_ptr)
-            self.profile_ptr = NULL
+    def __init__(self):
         self.profile_ptr = cmsCreate_sRGBProfile()
+        if self.profile_ptr == NULL:
+            raise LcmsError("could not create sRGB profile")
 
 
 cdef class Transform:
         self.transform_ptr = NULL
 
 
-    def __init__(self):
-        pass
+    def __init__(self, *args, **kwds):
+        raise NotImplementedError("instantiate a subclass of Transform instead")
 
 
     def __dealloc__(self):
             cmsDeleteTransform(self.transform_ptr)
 
 
-    def between_profiles(self, ICCProfile src, ICCProfile dst, int src_format,
-        int dst_format, int intent=intent.perceptual, int flags=0):
-        """ Instantiate the transform between two profiles.
-
-        Parameters
-        ----------
-        src : ICCProfile
-        dst : ICCProfile
-            The source and destination profiles of the transform.
-        src_format : int
-        dst_format : int
-            The format codes for the source and destination images. Use only the
-            attributes of the `formats` object.
-        intent : int, optional
-            The intent of the transform. This determines how the transformation
-            handles colors outside the gamut of the destination profile. Only
-            use one of the attributes of the `intent` object.
-        flags : int, optional
-            Flags to apply to the transformation. Only use bitwise-ORs of the
-            attributes of the `flags` object.
-        """
-        
-        if self.transform_ptr != NULL:
-            cmsDeleteTransform(self.transform_ptr)
-            self.transform_ptr = NULL
-        self.transform_ptr = cmsCreateTransform(src.profile_ptr, src_format,
-            dst.profile_ptr, dst_format, intent, flags)
-        self.src_format = src_format
-        self.dst_format = dst_format
-
-
-    def soft_proof(self, ICCProfile src, ICCProfile dst, ICCProfile proof, 
-        int src_format, int dst_format, int intent=intent.perceptual,
-        int proofing_intent=intent.absolute_colorimetric, int flags=0):
-        """ Instantiate the transform from one profile to another in order to
-        emulate a third profile.
-
-        This kind of transform is usually used to emulate a hardcopy (the
-        `proof` profile) on a monitor (the `dst` profile).
-
-        Parameters
-        ----------
-        src : ICCProfile
-        dst : ICCProfile
-            The source and destination profiles of the transform.
-        proof : ICCProfile
-            The profile of the device to emulate on the `dst` profile.
-        src_format : int
-        dst_format : int
-            The format codes for the source and destination images. Use only the
-            attributes of the `formats` object.
-        intent : int, optional
-        proofing_intent : int, optional
-            The intents of the transform. These determine how the transformation
-            handles colors outside the gamut of the destination and proofing
-            profiles. Only use one of the attributes of the `intent` object.
-        flags : int, optional
-            Flags to apply to the transformation. Only use bitwise-ORs of the
-            attributes of the `flags` object.
-        """
-
-        if self.transform_ptr != NULL:
-            cmsDeleteTransform(self.transform_ptr)
-            self.transform_ptr = NULL
-        self.transform_ptr = cmsCreateProofingTransform(src.profile_ptr,
-            src_format, dst.profile_ptr, dst_format, proof.profile_ptr, intent,
-            proofing_intent, flags)
-        self.src_format = src_format
-        self.dst_format = dst_format
-
-
-    def multiple_profiles(self, profiles, int src_format, int dst_format,
-        int intent=intent.perceptual, int flags=0):
-        """ Instantiate the transform with a chain of >2 profiles.
-
-        Parameters
-        ----------
-        profiles : sequence of ICCProfiles
-            The sequence of profiles starts with the source profile and goes to
-            the destination profile.
-        src_format : int
-        dst_format : int
-            The format codes for the source and destination images. Use only the
-            attributes of the `formats` object.
-        intent : int, optional
-            The intent of the transform. This determines how the transformation
-            handles colors outside the gamut of the destination profile. Only
-            use one of the attributes of the `intent` object.
-        flags : int, optional
-            Flags to apply to the transformation. Only use bitwise-ORs of the
-            attributes of the `flags` object.
-        """
-
-        cdef cmsHPROFILE* cprofiles
-        
-        if self.transform_ptr != NULL:
-            cmsDeleteTransform(self.transform_ptr)
-            self.transform_ptr = NULL
-
-        n = len(profiles)
-        cprofiles = <cmsHPROFILE*>PyMem_Malloc(n*sizeof(cmsHPROFILE))
-        for i, profile in enumerate(profiles):
-            cprofiles[i] = (<ICCProfile>profile).profile_ptr
-        self.transform_ptr = cmsCreateMultiprofileTransform(cprofiles, n, src_format,
-            dst_format, intent, flags)
-        PyMem_Free(cprofiles)
-        self.src_format = src_format
-        self.dst_format = dst_format
-
-
-    def do_transform(self, image):
+    def __call__(self, image):
         """ Transform the colors of an image.
 
         Parameters
         return dst_image
 
 
+cdef class TransformBetweenProfiles(Transform):
+    """ A direct transformation from one profile to another.
+
+    Parameters
+    ----------
+    src : ICCProfile
+    dst : ICCProfile
+        The source and destination profiles of the transform.
+    src_format : int
+    dst_format : int
+        The format codes for the source and destination images. Use only the
+        attributes of the `formats` object.
+    intent : int, optional
+        The intent of the transform. This determines how the transformation
+        handles colors outside the gamut of the destination profile. Only use
+        one of the attributes of the `intent` object.
+    flags : int, optional
+        Flags to apply to the transformation. Only use bitwise-ORs of the
+        attributes of the `flags` object.
+    """
+
+    def __init__(self, ICCProfile src, ICCProfile dst, int src_format,
+        int dst_format, int intent=intent.perceptual, int flags=0):
+        
+        self.transform_ptr = cmsCreateTransform(src.profile_ptr, src_format,
+            dst.profile_ptr, dst_format, intent, flags)
+        if self.transform_ptr == NULL:
+            raise LcmsError("could not create transform")
+        self.src_format = src_format
+        self.dst_format = dst_format
+
+
+cdef class SoftProofTransform(Transform):
+    """ A transform from one profile to another in order to emulate a third
+    profile.
+
+    This kind of transform is usually used to emulate the output of a printer
+    (the `proof` profile) on a monitor (the `dst` profile).
+
+    Parameters
+    ----------
+    src : ICCProfile
+    dst : ICCProfile
+        The source and destination profiles of the transform.
+    proof : ICCProfile
+        The profile of the device to emulate on the `dst` profile.
+    src_format : int
+    dst_format : int
+        The format codes for the source and destination images. Use only the
+        attributes of the `formats` object.
+    intent : int, optional
+    proofing_intent : int, optional
+        The intents of the transform. These determine how the transformation
+        handles colors outside the gamut of the destination and proofing
+        profiles. Only use one of the attributes of the `intent` object.
+    flags : int, optional
+        Flags to apply to the transformation. Only use bitwise-ORs of the
+        attributes of the `flags` object.
+    """
+
+    def __init__(self, ICCProfile src, ICCProfile dst, ICCProfile proof, 
+        int src_format, int dst_format, int intent=intent.perceptual,
+        int proofing_intent=intent.absolute_colorimetric, int flags=0):
+
+        self.transform_ptr = cmsCreateProofingTransform(src.profile_ptr,
+            src_format, dst.profile_ptr, dst_format, proof.profile_ptr, intent,
+            proofing_intent, flags)
+        if self.transform_ptr == NULL:
+            raise LcmsError("could not create transform")
+        self.src_format = src_format
+        self.dst_format = dst_format
+
+
+cdef class TransformBetweenMultipleProfiles(Transform):
+    """ A transform through a chain of >2 profiles.
+
+    Actual device profiles are on either end of the chain. In between should be
+    abstract profiles that implement effects. For example, these abstract
+    profiles could lighten or darken colors, or convert everything to a sepia
+    tone.
+
+    Parameters
+    ----------
+    profiles : sequence of ICCProfiles
+        The sequence of profiles starts with the source profile and goes to
+        the destination profile.
+    src_format : int
+    dst_format : int
+        The format codes for the source and destination images. Use only the
+        attributes of the `formats` object.
+    intent : int, optional
+        The intent of the transform. This determines how the transformation
+        handles colors outside the gamut of the destination profile. Only
+        use one of the attributes of the `intent` object.
+    flags : int, optional
+        Flags to apply to the transformation. Only use bitwise-ORs of the
+        attributes of the `flags` object.
+    """
+
+    def __init__(self, profiles, int src_format, int dst_format,
+        int intent=intent.perceptual, int flags=0):
+
+        cdef cmsHPROFILE* cprofiles
+        
+        n = len(profiles)
+        cprofiles = <cmsHPROFILE*>PyMem_Malloc(n*sizeof(cmsHPROFILE))
+        for i, profile in enumerate(profiles):
+            cprofiles[i] = (<ICCProfile>profile).profile_ptr
+        self.transform_ptr = cmsCreateMultiprofileTransform(cprofiles, n, src_format,
+            dst_format, intent, flags)
+        PyMem_Free(cprofiles)
+        if self.transform_ptr == NULL:
+            raise LcmsError("could not create transform")
+        self.src_format = src_format
+        self.dst_format = dst_format
+
+
+
+
 #### EOF #######################################################################
 )
 
 setup(
+    name='lcms',
     version='0.5',
     description="Wrapper for the lcms color management library.",
     author="Robert Kern",