little cms Engine
http://www.littlecms.com
How to use the engine in your applications
by Martâ–¡Maria
Ver 1.08
---------------------------------------------------
Introduction
I. Basic Concepts
II. Step-by-step example
III. Embedded profiles
IV. Device-link profiles
V. On-the-fly and built-in profiles
VI. Proofing
VII. White/Black compensation
VIII. Error handling
IX. Getting information from profiles.
X. Helper functions
XI. Conclusion
Sample 1: How to convert RGB to CMYK and back
Sample 2: How to deal with Lab/XYZ spaces
Annex A. About intents
Annex B. Apparent bug in XYZ -> sRGB transforms
----------------------------
Introduction:
This file has been written to present the lcms core library to
would-be writers of applications. It first describes the
concepts on which the engine is based, and then how to use it
to obtain transformations, colorspace conversions and
separations. Then, a guided step-by-step example, shows how to
use the engine in a simple or more sophisticated way.
This document doesn't even try to explain the basic concepts of
color management. For a comprehensive explanation, I will
recommend the excellent color & gamma FAQs by Charles A.
Poynton,
http://www.inforamp.net/~poynton
For more details about profile architecture, you can reach the
latest ICC specs on:
http://www.color.org
**PLEASE NOTE THAN lcms IS NOT A ICC SUPPORTED LIBRARY**
I will assume the reader does have a working knowledge of the C
programming language. This don't mean lcms can only be used by
C applications, but it seems the easiest way to present the API
functionality. I currently have successfully used the lcms DLL
from Delphi, C++ Builder, Visual C++, Tcl/Tk, and even Visual
Basic.
DELPHI USERS:
If you plan to use lcms from Delphi, there is a separate
package in the site containing units for delphi interface. Rest
of document does refer to C API, but you can use same functions
on Delphi.
I. Basic Concepts:
lcms defines two kinds of structures, that are used to manage
the various abstractions required to access ICC profiles. These
are profiles and transforms.
In a care of good encapsulation, these objects are not directly
accessible from a client application. Rather, the user receives
a 'handle' for each object it queries and wants to use. This
handle is a stand-alone reference; it cannot be used like a
pointer to access directly the object's data.
There are typedef's for such handles:
cmsHPROFILE identifies a handle to an open profile.
cmsHTRANSFORM identifies a handle to a transform.
Conventions of use:
o All API functions and types have their label prefixed
by 'cms' (lower-case).
o #defined constants are always in upper case
o Some functions does accepts flags. In such cases,
you can build the flags specifier joining the values with the
bitwise-or operator '|'
o An important note is that the engine should not leak
memory when returning an error, e.g., querying the
creation of an object will allocate several internal
tables that will be freed if a disk error occurs during
a load.
Since these are a very generic conventions widely used, I will
no further discuss this stuff.
II. Step-by-step Example:
Here is an example to show, step by step, how a client
application can transform a bitmap between two ICC profiles using the
lcms API
This is an example of how to do the whole thing:
#include "lcms.h"
int main(void)
{
cmsHPROFILE hInProfile, hOutProfile;
cmsHTRANSFORM hTransform;
int i;
hInProfile = cmsOpenProfileFromFile("HPSJTW.ICM", "r");
hOutProfile = cmsOpenProfileFromFile("sRGBColorSpace.ICM", "r");
hTransform = cmsCreateTransform(hInProfile,
TYPE_BGR_8,
hOutProfile,
TYPE_BGR_8,
INTENT_PERCEPTUAL, 0);
for (i=0; i < AllScanlinesTilesOrWatseverBlocksYouUse; i++)
{
cmsDoTransform(hTransform, YourInputBuffer,
YourOutputBuffer,
YourBuffersSizeInPixels);
}
cmsDeleteTransform(hTransform);
cmsCloseProfile(hInProfile);
cmsCloseProfile(hOutProfile);
return 0;
}
Let's discuss how it works.
a) Open the profiles
You will need the profile handles for create the transform. In
this example, I will create a transform using a HP Scan Jet
profile present in Win95 as input, and sRGB profile as output.
This task can be done by following lines:
cmsHPROFILE hInProfile, hOutProfile;
hInProfile = cmsOpenProfileFromFile("HPSJTW.ICM", "r")
hOutProfile = cmsOpenProfileFromFile("sRGBColorSpace.ICM", "r")
You surely have noticed a second parameter with a small "r".
This parameter is currently unused, by required for futures
extensions, it describes the "opening mode" like the C
function fopen(). Currently lcms API only _documents_ read-only
mode, but is expected in future revisions to add some writting
capabilities.
NOTES:
This only will take a small fraction of memory. The BToA or AToB tables,
which usually are big, are only loaded at transform-time, and on demand.
You can safely open a lot of profiles if you wish.
If cmsOpenProfileFromFile() fails, it raises an error signal that can
or cannot be catched by the application depending of the state of the
error handler. In this example, I'm using the "if-error-abort-whole-
application" behaviour, corresponding with the LCMS_ERROR_ABORT setting of
cmsErrorAction(). See the error handling paragraph below for more
information.
lcms is a standalone color engine, it knows nothing about where
the profiles are placed. lcms does assume nothing about
a specific directory (as Windows does, currently expects profiles
to be located on SYSTEM/COLOR folder in main windows directory), so
for get this example working, you need to copy the profiles in the
local directory.
b) Identify the desired format of pixels.
lcms can handle a lot of formats. In fact, it can handle:
- 8 and 16 bits per sample
- up to 15 channels
- extra channels like alpha
- swaped-channels like BGR
- endian-swapped 16 bps formats like PNG
- chunky and planar organization
- Reversed (negative) channels
For describing such formats, lcms does use a 32-bit value, referred
below as "format specifiers".
There are several (most usual) encodings predefined as constants, but there
are a lot more. See lcms.h to review the current list.
TYPE_GRAY_8 Grayscale 8 bits
TYPE_GRAY_16 Grayscale 16 bits
TYPE_GRAY_16_SE Grayscale 16 bits, swap endian
TYPE_GRAYA_8 Grayscale + alpha, 8 bits
TYPE_GRAYA_16 Grayscale + alpha, 16 bits
TYPE_GRAYA_16_SE Grayscale + alpha, 16 bits
TYPE_GRAYA_8_PLANAR Grayscale + alpha, 8 bits, separate planes
TYPE_GRAYA_16_PLANAR Grayscale + alpha, 16 bits, separate planes
TYPE_RGB_8 RGB, 8 bits
TYPE_RGB_8_PLANAR RGB, 8 bits, separate planes
TYPE_BGR_8 BGR, 8 bits (windows uses this format for BMP)
TYPE_BGR_8_PLANAR BGR, 8 bits, separate planes
TYPE_RGB_16 RGB, 16 bits
TYPE_RGB_16_PLANAR ...
TYPE_RGB_16_SE
TYPE_BGR_16
TYPE_BGR_16_PLANAR
TYPE_BGR_16_SE
TYPE_RGBA_8 These ones with alpha channel
TYPE_RGBA_8_PLANAR
TYPE_RGBA_16
TYPE_RGBA_16_PLANAR
TYPE_RGBA_16_SE
TYPE_ABGR_8
TYPE_ABGR_16
TYPE_ABGR_16_PLANAR
TYPE_ABGR_16_SE
TYPE_CMY_8 These ones for CMY separations
TYPE_CMY_8_PLANAR
TYPE_CMY_16
TYPE_CMY_16_PLANAR
TYPE_CMY_16_SE
TYPE_CMYK_8 These ones for CMYK separations
TYPE_CMYK_8_PLANAR
TYPE_CMYK_16
TYPE_CMYK_16_PLANAR
TYPE_CMYK_16_SE
TYPE_KYMC_8 Reversed CMYK
TYPE_KYMC_16
TYPE_KYMC_16_SE
TYPE_XYZ_16 XYZ, xyY and CIELab
TYPE_Yxy_16
TYPE_Lab_8
TYPE_Lab_16
TYPE_CMYKcm_8 HiFi separations
TYPE_CMYKcm_8_PLANAR
TYPE_CMYKcm_16
TYPE_CMYKcm_16_PLANAR
TYPE_CMYKcm_16_SE
TYPE_CMYK7_8
TYPE_CMYK7_16
TYPE_CMYK7_16_SE
TYPE_KYMC7_8
TYPE_KYMC7_16
TYPE_KYMC7_16_SE
TYPE_CMYK8_8
TYPE_CMYK8_16
TYPE_CMYK8_16_SE
.. etc...
For example, if you are transforming a windows .bmp to a bitmap for
display, you will use TYPE_BGR_8 for both, input and output
buffers, windows does store images as B,G,R and not as R,G,B.
Other example, you need to convert from a CMYK separation to
RGB in order to display; then you would use TYPE_CMYK_8 on
input and TYPE_BGR_8 on output. If you need to do the
separation from a TIFF, TYPE_RGB_8 on input and TYPE_CMYK_8 on
output. Please note TYPE_RGB_8 and TYPE_BGR_8 are *not* same.
The format specifiers are usefull above color management. This will
provide a way to handle a lot of formats, converting them in a single,
well-known one. For example, if you need to deal with several pixel
layouts coming from a file (TIFF for example), you can use a fixed
output format, say TYPE_BGR_8 and then, vary the input format
on depending on the file parameters. lcms also provides a flag for
inhibit color management if you want speed and don't care about
profiles. see cmsFLAGS_NULLTRANSFORM for more info.
c) Create the transform
When creating transform, you are giving to lcms all information it
needs about how to translate your pixels. The syntax for simpler
transforms is:
cmsHTRANSFORM hTransform;
hTransform = cmsCreateTransform(hInputProfile,
TYPE_BGR_8,
hOutputProfile,
TYPE_BGR_8,
INTENT_PERCEPTUAL, 0);
You give the profile handles, the format of your buffers, the rendering
intent and a combination of flags controlling the transform behaviour.
It's out of scope of this document to define the exact meaning
of rendering intents. I will try to make a quick explanation
here, but often the meaning of intents depends on the profile
manufacturer. See appendix A for more information.
INTENT_PERCEPTUAL:
Hue hopefully maintained (but not required),
lightness and saturation sacrificed to maintain
the perceived color. White point changed to
result in neutral grays. Intended for images.
In lcms: Default intent of profiles is used
INTENT_RELATIVE_COLORIMETRIC:
Within and outside gamut; same as Absolute
Colorimetric. White point changed to result in
neutral grays.
In lcms: If adequate table is present in profile,
then, it is used. Else reverts to perceptual
intent.
INTENT_SATURATION:
Hue and saturation maintained with lightness
sacrificed to maintain saturation. White point
changed to result in neutral grays. Intended for
business graphics (make it colorful charts,
graphs, overheads, ...)
In lcms: If adequate table is present in profile,
then, it is used. Else reverts to perceptual
intent.
INTENT_ABSOLUTE_COLORIMETRIC:
Within the destination device gamut; hue,
lightness and saturation are maintained. Outside
the gamut; hue and lightness are maintained,
saturation is sacrificed. White point for source
and destination; unchanged. Intended for spot
colors (Pantone, TruMatch, logo colors, ...)
In lcms: relative colorimetric intent is used
with white/black point scaling.
Not all profiles does support all intents, there is a function
for inquiring which intents are really supported, but if you
specify a intent that the profile doesn't handle, lcms will
select default intent instead. Usually perceptual one. This
will force to "look nice", no matter the intent is not the one
really desired.
lcms tries to "smelt" input and output profiles in a single
matrix-shaper or in a big 3D CLUT of 33 points. This will
improve greatly the performance of the transform, but may
induce a small delay of 1-2 seconds on some 486-based machines.
If you are willing to transform just a palette or a few
colors, you don't need this precalculations. Then, the flag
cmsFLAGS_NOTPRECALC in cmsCreateTransform() can be used to
inhibit the 3D CLUT creation.
See the API reference for a more detailed discussion of the flags.
NOTES:
Some old display profiles, only archives absolute colorimetric
intents. For these profiles, default intents are absolute
colorimetric ones. This is really a rare case.
d) Next, you can translate your bitmap, calling repeatedly the processing
function:
cmsDoTransform(hTransform, YourInputBuffer,
YourOutputBuffer,
YourBuffersSize);
This function is intended to be quite fast. You can use this
function for translating a scan line, a tile, a strip, or whole
image at time.
NOTES:
Windows, stores the bitmaps in a particular way... for speed
purposes, does align the scan lines to double word boundaries,
a bitmap has in windows always a size multiple of 4. This is
ok, since no matter if you waste a couple of bytes, but if you
call cmsDoTransform() and passes it WHOLE image, lcms doesn't
know nothing about this extra padding bytes. It assumes that
you are passing a block of BGR triplets with no alignment at
all. This result in a strange looking "lines" in obtained
bitmap.
The solution most evident is to convert scan line by scan line
instead of whole image. This is as easy as to add a for()
loop, and the time penalty is so low that is impossible to
detect.
It is safe to use same block for input and output, but only if
the input and output are coded in same format. For example,
you can safely use only one buffer for RGB to RGB but you
cannot use same buffer for RGB as input and CMYK as output.
e) Last, free all stuff.
This can be done by calling
cmsDeleteTransform(hTransform);
cmsCloseProfile(hInputProfile);
cmsCloseProfile(hOutputProfile);
And you are done!
Note that cmsDeleteTransform() does NOT automatically free
associated profiles. This works in such way to let
programmers to use a open profile in more than one transform.
III. Embedded profiles
Some image file formats, like TIFF, JPEG or PNG, does include
the ability of embed profiles. This means that the input
profile for the bitmap is stored inside the image file. lcms
provides a specialised profile-opening function for deal with
such profiles.
cmsHPROFILE cmsOpenProfileFromMem(LPVOID MemPtr, DWORD dwSize);
This function works like cmsOpenProfileFromFile(), but assuming
that you are given full profile in a memory block rather than a
filename. Here is not any "r", since these profiles are always
read-only. A successful call will return a handle to an opened
profile that behaves just like any other file-based.
NOTES:
Once opened, you can safely FREE the memory block. lcms keeps a
temporary copy in disk for minimising memory overhead.
You can retrieve information of this profile, but generally
these are minimal shaper-matrix profiles with little if none
handy info present.
Be also warned that some software does embed WRONG profiles,
i.e., profiles marked as using different colorspace that
one the profile really manages. lcms is NOT likely to understand
these profiles since they will be wrong at all if not embedded.
IV. Device-link profiles
Device-link profiles are "smelted" profiles that represents
a whole transform rather than single-device profiles. Teorically,
device-link profiles may have greater precision that single ones
and are faster to load. If you plan to use device-link profiles,
be warned there are drawbacks about its interoperability and the
gain of speed is almost null.
Perhaps their only advantage is when restoration from CMYK
with great precission is required, since CMYK to pcs CLUTs can
become very, very big.
lcms does support device link profiles as a compatibility issue.
For creating a device-link transfor, you must open the device link
profile as usual, using cmsOpenProfileFromFile(). Then, create
the transform with the device link profile as input and the output
profile parameter equal to NULL:
hDeviceLink = cmsOpenProfileFromFile("MYDEVLINK.ICM", "r");
hTransform = cmsCreateTransform(hDeviceLink, TYPE_RGB_8,
NULL, TYPE_BGR_8,
INTENT_PERCEPTUAL,
0);
That's all. lcms will understand and transparently handle the
device-link profile.
V. On-the-fly and Built-in profiles.
There are several situations where it will be useful to build
a minimal profile using adjusts only available at run time.
There are several kinds of virtual profiles:
- RGB profiles
- L*a*b profiles
- XYZ profiles
Shurely you have seen the classical pattern-gray trick for adjusting
gamma: the end user moves a scroll bar and when pattern seems to
match background gray, then gamma is adjusted.
Another trick is to use a black background with some gray rectangles.
The user chooses the most neutral grey, giving the white point or the
temperature in oK.
All these visual gadgets are not part of lcms, you must implement
them by yourself if you like. But lcms will help you with a function for
generating a virtual profile based on the results of these tests.
Another usage would be to build colorimetric descriptors for
file images that does not include any embedded profile, but
does include fields for identifying original colorspace.
One example is TIFF files. The TIFF 6.0 spec talks about
"RGB Image Colorimetry" (See section 20) a "colorimetric" TIFF
image has all needed parameters (WhitePointTag=318,
PrimaryChromacitiesTag=318, TransferFunction=301,TransferRange=342)
Obtain a emulated profile from such files is easy since the contents
of these tags does match the cmsCreateRGBProfile() parameters.
Also PNG can come with information for build a virtual profile,
See the gAMA and cHRM chunks.
This is the main function for creating virtual RGB profiles:
cmsHPROFILE cmsCreateRGBProfile(LPcmsCIExyY WhitePoint,
LPcmsCIExyYTRIPLE Primaries,
LPGAMMATABLE TransferFunction[3]);
It takes as arguments the white point, the primaries and 3
gamma curves. The profile emulated is always operating in RGB
space. Once created, a handle to a profile is returned. This
opened profile behaves like any other file or memory based
profile.
Virtual RGB profiles are implemented as matrix-shaper, so they
cannot compete against CLUT based ones, but generally are good enough
to provide a reasonable alternative to generic profiles.
For simplify the parameters construction, there are additional
functions:
BOOL cmsWhitePointFromTemp(int TempK, LPcmsCIExyY WhitePoint);
This function computes the xyY chromacity of white point using
the temperature. Screen manufacturers often includes a white
point hard switch in monitors, but they refer as "Temperature"
instead of chromacity. Most extended temperatures are 5000K,
6500K and 9300K
It returns TRUE if a valid white point can be computed, or FALSE
if the temperature were non valid. You must give a pointer to a
cmsCIExyY struct for holding resulting white point.
For primaries, currently I don't know any trick or proof for
identifying primaries, so here are a few chromacities of most
extended. Y is always 1.0
RED GREEN BLUE
x y x y x y
---- ---- ---- ---- ---- ----
NTSC 0.67, 0.33, 0.21, 0.71, 0.14, 0.08
EBU(PAL/SECAM) 0.64, 0.33, 0.29, 0.60, 0.15, 0.06
SMPTE 0.630, 0.340, 0.310, 0.595, 0.155, 0.070
HDTV 0.670, 0.330, 0.210, 0.710, 0.150, 0.060
CIE 0.7355,0.2645,0.2658,0.7243,0.1669,0.0085
These are TRUE primaries, not colorants. lcms does include a
white-point balancing and a chromatic adaptation using a method
called Bradford Transform for D50 adaptation.
NOTE: Additional information about Bradford transform math can be found
on the sRGB site:
http://www.srgb.com/hpsrgbprof/index.htm
The gamma tables or transfer functions are stored in a simple way,
let's examine the GAMMATABLE typedef:
typedef struct {
int nEntries;
WORD GammaTable[1];
} GAMMATABLE, FAR* LPGAMMATABLE;
That is, first it comes a 32 integer for entry count, followed of
a variable number of words describing the table. The easiest way to
generate a gamma table is to use the function
LPGAMMATABLE cmsBuildGamma(int nEntries, double Gamma);
You must specify the number of entries your table will consist of,
and the float value for gamma. The generated table has linear and non-linear
steps, the linear ramp near 0 is for minimising noise.
If you want to fill yourself the values, you can allocate space for your
table by using
LPGAMMATABLE cmsAllocGamma(int nEntries);
This function only creates memory for the table. The entries does
not contain any useful value (garbage) since it is expected you will fill
this table after created.
You can find the inverse of a tabulated curve by using:
LPGAMMATABLE cmsReverseGamma(int nResultSamples, LPGAMMATABLE InGamma);
This function reverses the gamma table if it can be done. lcms does not
detect whatever a non-monotonic function is given, so wrong input can
result in ugly results: not to be a problem since "normal" gamma curves
are not collapsing inputs at same output value. The new curve will be
resampled to nResultSamples entries.
You can also smooth the curve by using:
BOOL cmsSmoothGamma(LPGAMMATABLE Tab, double lambda);
"Smooth" curves does work better and are more pleasant to eyes.
Finally, you can join two gamma curves with:
LPGAMMATABLE cmsJoinGamma(LPGAMMATABLE InGamma,
LPGAMMATABLE OutGamma);
This will let you to "refine" the generic gamma for monitors (2.1 or 2.2
are usual values) to match viewing conditions of more or less background
light. Note that this function uses TABULATED functions, so very exotic
curves can be obtained by combining transfer functions with reversed
gamma curves. Normally there is no need of worry about such gamma
manipulations, but the functionality is here if you wish to use.
You must free all gamma tables you allocate (or create via
cmsReverseGamma() or cmsJoinGamma()) by using:
void cmsFreeGamma(LPGAMMATABLE Gamma);
Ver 1.07 does include an experimental function for saving these profiles,
and this is how monitor profiler works, but I will discourage its use
since it only works with some matrix-shaper profiles, created by
cmsCreateRGBProfile(). Hopefully, next revisions will add full
saving capabilities. If despite this warning you want to use this function
anyway, it is called _cmsSaveProfile(). Note the underscore on begining
as an indication of this is a doomed function: it is shure it will vary on
next releases.
VI. Proofing.
A adittional ability of lcms is to create "proofing" transforms.
A proofing transform does emulate the colors that will appair if
a image is rendered on a specific device. That is, for example,
with a proofing transform I can see how will look a photo of my
little daughter if rendered on my EPSON Stylus. Since most printer
profiles does include some sort of gamut-remapping, it is likely
colors will not look *exactly* as the original. Using a proofing
transform, and if the printer profile does support previewing, it
can be done by using the apropiate function.
Note that this is an important feature for final users, it is worth
of all color-management stuff if the final media is not cheap.
The creation of a proofing transform involves three profiles, the input
and output ones as cmsCreateTransform() plus another, representing the
emulated profile.
cmsHTRANSFORM cmsCreateProofingTransform(cmsHPROFILE Input,
DWORD InputFormat,
cmsHPROFILE Output,
DWORD OutputFormat,
cmsHPROFILE Proofing,
int Intent,
int ProofingIntent,
DWORD dwFlags);
Also, there is another parameter for specifying the intent for
the proof. The nIntent here, represents the intent the user will select
when printing, and the proofing intent represent the intent system is
using for showing the proofed color. Since some printers can archive
colors that displays cannot render (darker ones) some gamut-remapping must
be done to accomodate such colors. Normally INTENT_ABSOLUTE_COLORIMETRIC
is to be used: is is likely the user wants to see the exact colors on screen,
cutting off these unrepresentable colors.
Proofing transforms can also be used to show the colors that are out of the
printer gamut. You can activate this feature by using the
cmsFLAGS_GAMUTCHECK flag in dwFlags field.
Then, the function:
void cmsSetAlarmCodes(int r, int g, int b);
Can be used to define the marker. rgb are expected to be integers
in range 0..255
But many profile manufacturers does use this tag for their own
purposes. Some uses gamut as a means to mark those colors that
gamut remapping is moving, others simply return all colors as
out of gamut. ICC comitee seems to be working in a new tag for
solving this, but for now most profiles are unuseable for gamut
checking.
VII. White/Black compensation
icc spec does state clearly that white must match white and black
must match black, but there are some profiles that does not fully
adheres this statement. For "patching" these profiles, lcms has
an special flag:
cmsFLAGS_WHITEBLACKCOMPENSATION
This is like the "goto statement" in color management, Better
don't use this flag, unless you really need it. It forces lcms
to map black on black, and white on white no matter what
profiles are saying. For doing this, lcms builds a 3D CLUT and
then patches this CLUT in order to get the matching. It can be
usefull when using CMYK profiles on both sides of transform
(converting separations from a printer to another printer) or
in a very special cases.
This is not a simple clip of black/white, but a gradient
interpolation to the desired values. Use it if your cmyk output
for pure black is other than pure black and printer driver
dithers text, or if white backgrounds results not in pure white
and the page print shows dispersed dots. If you are using
matrix shaper profiles, this will slow down a lot transforms,
probably to accomplish nothing, so, use with caution.
VIII. Error handling
lcms primary goal is to be quite simple, so error handling is managed
in a simple way. If you are using lcms as a DLL, you can tell lcms what is
supposed to happen when an error is detected. For doing that, you can use
this function.
void cmsErrorAction(int nAction);
'nAction' can be one of the following values:
LCMS_ERROR_ABORT 0
LCMS_ERROR_SHOW 1
LCMS_ERROR_IGNORE 2
Default is LCMS_ERROR_ABORT. That is, if an error is detected, lcms
will show a MessageBox with a small explanation about the error and
then WILL ABORT WHOLE APPLICATION. This behaviour is desirable when
debugging, but not in final releases. For inhibit such aborting,
you can use LCMS_ERROR_SHOW. This setting will show the error text, but
doesn't finish the application. Some functions like cmsOpenProfileFromFile()
or cmsCreateTransform() will return NULL instead of a valid handle as
error-marker. Others will return FALSE.
The last setting is LCMS_ERROR_IGNORE, that is, no message is displayed
and only a NULL or FALSE is returned if operation fails.
Note that if you use LCMS_ERROR_SHOW or LCMS_ERROR_IGNORE, your code
must check the return code. This is not necessary if you are using
LCMS_ERROR_ABORT, since the application will be terminated as soon as
the error is detected.
If you doesn't like this scheme, and you are using lcms as a linkable library,
you can provide your own error handling function: all remaining modules does
call this entry when a error is detected. This function must return to caller in order
to free any opened resource like memory of file handles.
void cmsSignalError(int ErrorCode, const char *ErrorText, ...)
It takes variable number of parameters like printf(). See the source module
cmserr.c for more info.
IX. Getting information from profiles.
Finally, there are some functions for retrieve information on opened
profiles. These are:
BOOL cmsIsTag(cmsHPROFILE hProfile, icTagSignature sig);
This one does check if a particular tag is present. Remaining does take
useful information about general parameters.
BOOL cmsTakeMediaWhitePoint(LPcmsCIEXYZ Dest, cmsHPROFILE hProfile);
BOOL cmsTakeMediaBlackPoint(LPcmsCIEXYZ Dest, cmsHPROFILE hProfile);
BOOL cmsTakeIluminant(LPcmsCIEXYZ Dest, cmsHPROFILE hProfile);
BOOL cmsTakeColorants(LPcmsCIEXYZTRIPLE Dest, cmsHPROFILE hProfile);
const char* cmsTakeProductName(cmsHPROFILE hProfile);
const char* cmsTakeProductDesc(cmsHPROFILE hProfile);
int cmsTakeRenderingIntent(cmsHPROFILE hProfile);
icColorSpaceSignature cmsGetPCS(cmsHPROFILE hProfile);
icColorSpaceSignature cmsGetColorSpace(cmsHPROFILE hProfile);
icProfileClassSignature cmsGetDeviceClass(cmsHPROFILE hProfile);
These functions are given mainly for building user interfaces,
you don't need to use them if you just want a plain translation.
Other usage would be to identify "families" of profiles.
The functions returning strings are using an static buffer that is
overwritten in each call, others does accept a pointer to an specific
struct that is filled if function is successful.
#define LCMS_USED_AS_INPUT 0
#define LCMS_USED_AS_OUTPUT 1
#define LCMS_USED_AS_PROOF 2
BOOL cmsIsIntentSupported(cmsHPROFILE hProfile, int Intent, int UsedDirection);
This one helps on inquiring if a determinate intent is
supported by an opened profile. You must give a handle to
profile, the intent and a third parameter specifying how the
profile would be used. The function does return TRUE if intent
is supported or FALSE if not. If the intent is not supported,
lcms will use default intent (usually perceptual).
X. Helper functions
Here are some functions that could be usefull. They are not needed
in "normal" usage.
They include:
xyY <-> XYZ conversion functions:
void cmsXYZ2xyY(LPcmsCIExyY Dest, CONST LPcmsCIEXYZ Source);
void cmsxyY2XYZ(LPcmsCIEXYZ Dest, CONST LPcmsCIExyY Source);
Chromatic Adaptation
BOOL cmsAdaptToIlluminant(LPcmsCIEXYZ Result, LPcmsCIExyY SourceWhitePt,
LPcmsCIExyY Illuminant, LPcmsCIEXYZ Value);
Build a balanced transfer matrix with chromatic adaptation, this
is equivalent to "cooking" required to conform a colorant matrix.
BOOL cmsBuildRGB2XYZtransferMatrix(LPMAT3 r,
LPcmsCIExyY WhitePoint,
LPcmsCIExyYTRIPLE Primaries);
XI. Conclusion.
That's almost all you must know to begin experimenting with profiles,
just a couple of words about the posibilities ICC profiles can
give to programmers:
o ColorSpace profiles are valueable tools for converting
from/to exotic file formats. I'm using lcms to read
Lab TIFF using the popular Sam Leffer's TIFFLib. Also,
the ability to restore separations are much better that
the infamuous 1-CMY method.
o Abstract profiles can be used to manipulate color of
images, contrast, brightness and true-gray reductions can
be done fast and accurately. Grayscale conversions can be
done exceptionally well, and even in tweaked colorspaces
that does emulate more gray levels that the output device
can effectively render.
o lcms does all calculation on 16 bit per component basis,
the display and output profiles can take advantage of these
precision and efficiently emulate more than 8 bits per sample.
You probably will not notice this effect on screen, but it can
be seen on printed or film media.
o There is a huge quantity of profiles moving around the net,
and there is very good software for generating them, so
future compatibility seems to be assured.
I thank you for your time and consideration.
Enjoy!
Sample 1: How to convert RGB to CMYK and back
=============================================
This is easy. Just use a transform between RGB profile to CMYK profile.
#include "lcms.h"
int main(void)
{
cmsHPROFILE hInProfile, hOutProfile;
cmsHTRANSFORM hTransform;
int i;
hInProfile = cmsOpenProfileFromFile("sRGBColorSpace.ICM", "r");
hOutProfile = cmsOpenProfileFromFile("MyCmyk.ICM", "r");
hTransform = cmsCreateTransform(hInProfile,
TYPE_RGB_8,
hOutProfile,
TYPE_CMYK_8,
INTENT_PERCEPTUAL, 0);
for (i=0; i < AllScanlinesTilesOrWatseverBlocksYouUse; i++)
{
cmsDoTransform(hTransform, YourInputBuffer,
YourOutputBuffer,
YourBuffersSizeInPixels);
}
cmsDeleteTransform(hTransform);
cmsCloseProfile(hInProfile);
cmsCloseProfile(hOutProfile);
return 0;
}
And Back....? Same. Just exchange profiles and format descriptors:
int main(void)
{
cmsHPROFILE hInProfile, hOutProfile;
cmsHTRANSFORM hTransform;
int i;
hInProfile = cmsOpenProfileFromFile("MyCmyk.ICM", "r");
hOutProfile = cmsOpenProfileFromFile("sRGBColorSpace.ICM", "r");
hTransform = cmsCreateTransform(hInProfile,
TYPE_CMYK_8,
hOutProfile,
TYPE_RGB_8,
INTENT_PERCEPTUAL, 0);
for (i=0; i < AllScanlinesTilesOrWatseverBlocksYouUse; i++)
{
cmsDoTransform(hTransform, YourInputBuffer,
YourOutputBuffer,
YourBuffersSizeInPixels);
}
cmsDeleteTransform(hTransform);
cmsCloseProfile(hInProfile);
cmsCloseProfile(hOutProfile);
return 0;
}
Sample 2: How to deal with Lab/XYZ spaces
==========================================
This is more elaborated. There is a Lab identity Built-In profile involved,
and a float-to-encoded conversion.
// Converts Lab(D50) to sRGB:
int main(void)
{
cmsHPROFILE hInProfile, hOutProfile;
cmsHTRANSFORM hTransform;
int i;
WORD LabEncoded[3];
BYTE RGB[3];
cmsCIELab LabFloat;
hInProfile = cmsCreateLabProfile(NULL);
hOutProfile = cmsOpenProfileFromFile("sRGBColorSpace.ICM", "r");
hTransform = cmsCreateTransform(hInProfile,
TYPE_Lab_16,
hOutProfile,
TYPE_RGB_8,
INTENT_PERCEPTUAL, 0);
for (i=0; i < AllLabValuesToConvert; i++)
{
// Fill in the Float Lab
Lab.L = Your L;
Lab.a = Your a;
Lab.b = Your b;
cmsFloat2LabEncoded(LabEncoded, &LabFloat);
cmsDoTransform(hTransform, LabEncoded, RGB, 1);
.. Do whatsever with the RGB values in RGB[3]
}
cmsDeleteTransform(hTransform);
cmsCloseProfile(hInProfile);
cmsCloseProfile(hOutProfile);
return 0;
}
Annex A. About intents
======================
Charles Cowens gives to me a clear explanation about
accomplished intents. Since it is very useful to understand how
intents are internally implemented, I will reproduce here.
AtoBX/BtoAX LUTs and Rendering Intents
The ICC spec is pretty detailed about the LUTs and their
varying meaning according to context in tables 20, 21, and 22
in section 6.3. My reading of this is that even though there
are 4 rendering intent selectors there are really 6 rendering
styles:
Relative Indefinite
(Relative) Perceptual
Relative Colorimetric
(Relative) Saturation
Absolute Indefinite
Absolute Colorimetric
If a device profile has a single-LUT or matrix:
* Perceptual, Relative Colorimetric, Saturation selectors
produce the same Relative Indefinite rendering style
* Absolute Colorimetric selector produces an Abolute
Indefinite rendering style derived from the single LUT or
matrix, the media white point tag, and the inverse of a
white point compensation method designated by the CMS
If a device profile has 3 LUTs:
* Perceptual, Relative Colorimetric, Saturation selectors
produce the appropiate rendering styles using the 0, 1, and
2 LUTs respectively
* Absolute Colorimetric selector produces an Abolute
Colorimetric rendering style derived from the Relative
Colorimetric LUT (numbered "1"), the media white point tag,
and the inverse of a white point compensation method
designated by the CMS
This would explain why perceptual is the default rendering style
because a single-LUT profile's LUT is numbered "0".
Annex B Apparent bug in XYZ -> sRGB transforms
==============================================
John van den Heuvel warns me about an apparent bug on
XYZ -> sRGB transforms. Ver 1.08 should minimize this effect.
The obtained results are visually ok, but numbers seems to be wrong.
It appairs only when following conditions:
a) You are using a transform from a colorspace with
a gamut a lot bigger that output space, i.e. XYZ.
Note than sRGB -> XYZ does work Ok.
b) You are using absolute colorimetric intent.
c) You transform a color near gamut hull boundary
d) The output profile is implemented as a matrix-shaper,
i.e. sRGB.
e) You are using precalculated device link tables.
The numbers lcms returns doesn't match closely that color, but other
perceptually close to the intended one.
It happens that since XYZ has a very big gamut, and sRGB a narrow
one on compared to XYZ, when lcms tries to compute the device link
between XYZ -> sRGB, got most values as negative RGB (out of gamut).
lcms assumes this is effectively out of gamut and clamps to 0.
Then, since (127, 0, 0) is just over gamut boundary (for example
(127, -1, -1) would be out of gamut), lcms does interpolate
wrongly, not between -n to n but between 0 to n.
I could put an If() in the code for dealing with such situation,
but I guess it is better not touch anything and document this
behaviour.
XYZ is almost never used as a storage space, and since most monitor
profiles are implemented as matrix shaper touching this would slow
down common operations. The solution is quite simple,
if you want to deal with numbers, then use cmsFLAGS_NOTPRECALC.
If you deal with images, let lcms optimize the transform.
Visual results should appair Ok, no matter numbers doesn't match.