1040 lines
37 KiB
C
1040 lines
37 KiB
C
//---------------------------------------------------------------------------------
|
|
//
|
|
// Little Color Management System
|
|
// Copyright (c) 1998-2010 Marti Maria Saguer
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining
|
|
// a copy of this software and associated documentation files (the "Software"),
|
|
// to deal in the Software without restriction, including without limitation
|
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
// and/or sell copies of the Software, and to permit persons to whom the Software
|
|
// is furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
|
|
// THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
//
|
|
//---------------------------------------------------------------------------------
|
|
//
|
|
|
|
#include "lcms2_internal.h"
|
|
|
|
|
|
// Link several profiles to obtain a single LUT modelling the whole color transform. Intents, Black point
|
|
// compensation and Adaptation parameters may vary across profiles. BPC and Adaptation refers to the PCS
|
|
// after the profile. I.e, BPC[0] refers to connexion between profile(0) and profile(1)
|
|
cmsPipeline* _cmsLinkProfiles(cmsContext ContextID,
|
|
cmsUInt32Number nProfiles,
|
|
cmsUInt32Number Intents[],
|
|
cmsHPROFILE hProfiles[],
|
|
cmsBool BPC[],
|
|
cmsFloat64Number AdaptationStates[],
|
|
cmsUInt32Number dwFlags);
|
|
|
|
//---------------------------------------------------------------------------------
|
|
|
|
// This is the default routine for ICC-style intents. A user may decide to override it by using a plugin.
|
|
// Supported intents are perceptual, relative colorimetric, saturation and ICC-absolute colorimetric
|
|
static
|
|
cmsPipeline* DefaultICCintents(cmsContext ContextID,
|
|
cmsUInt32Number nProfiles,
|
|
cmsUInt32Number Intents[],
|
|
cmsHPROFILE hProfiles[],
|
|
cmsBool BPC[],
|
|
cmsFloat64Number AdaptationStates[],
|
|
cmsUInt32Number dwFlags);
|
|
|
|
//---------------------------------------------------------------------------------
|
|
|
|
// This is the entry for black-preserving K-only intents, which are non-ICC. Last profile have to be a output profile
|
|
// to do the trick (no devicelinks allowed at that position)
|
|
static
|
|
cmsPipeline* BlackPreservingKOnlyIntents(cmsContext ContextID,
|
|
cmsUInt32Number nProfiles,
|
|
cmsUInt32Number Intents[],
|
|
cmsHPROFILE hProfiles[],
|
|
cmsBool BPC[],
|
|
cmsFloat64Number AdaptationStates[],
|
|
cmsUInt32Number dwFlags);
|
|
|
|
//---------------------------------------------------------------------------------
|
|
|
|
// This is the entry for black-plane preserving, which are non-ICC. Again, Last profile have to be a output profile
|
|
// to do the trick (no devicelinks allowed at that position)
|
|
static
|
|
cmsPipeline* BlackPreservingKPlaneIntents(cmsContext ContextID,
|
|
cmsUInt32Number nProfiles,
|
|
cmsUInt32Number Intents[],
|
|
cmsHPROFILE hProfiles[],
|
|
cmsBool BPC[],
|
|
cmsFloat64Number AdaptationStates[],
|
|
cmsUInt32Number dwFlags);
|
|
|
|
//---------------------------------------------------------------------------------
|
|
|
|
|
|
// This is a structure holding implementations for all supported intents.
|
|
typedef struct _cms_intents_list {
|
|
|
|
cmsUInt32Number Intent;
|
|
char Description[256];
|
|
cmsIntentFn Link;
|
|
struct _cms_intents_list* Next;
|
|
|
|
} cmsIntentsList;
|
|
|
|
|
|
// Built-in intents
|
|
static cmsIntentsList DefaultIntents[] = {
|
|
|
|
{ INTENT_PERCEPTUAL, "Perceptual", DefaultICCintents, &DefaultIntents[1] },
|
|
{ INTENT_RELATIVE_COLORIMETRIC, "Relative colorimetric", DefaultICCintents, &DefaultIntents[2] },
|
|
{ INTENT_SATURATION, "Saturation", DefaultICCintents, &DefaultIntents[3] },
|
|
{ INTENT_ABSOLUTE_COLORIMETRIC, "Absolute colorimetric", DefaultICCintents, &DefaultIntents[4] },
|
|
{ INTENT_PRESERVE_K_ONLY_PERCEPTUAL, "Perceptual preserving black ink", BlackPreservingKOnlyIntents, &DefaultIntents[5] },
|
|
{ INTENT_PRESERVE_K_ONLY_RELATIVE_COLORIMETRIC, "Relative colorimetric preserving black ink", BlackPreservingKOnlyIntents, &DefaultIntents[6] },
|
|
{ INTENT_PRESERVE_K_ONLY_SATURATION, "Saturation preserving black ink", BlackPreservingKOnlyIntents, &DefaultIntents[7] },
|
|
{ INTENT_PRESERVE_K_PLANE_PERCEPTUAL, "Perceptual preserving black plane", BlackPreservingKPlaneIntents, &DefaultIntents[8] },
|
|
{ INTENT_PRESERVE_K_PLANE_RELATIVE_COLORIMETRIC,"Relative colorimetric preserving black plane", BlackPreservingKPlaneIntents, &DefaultIntents[9] },
|
|
{ INTENT_PRESERVE_K_PLANE_SATURATION, "Saturation preserving black plane", BlackPreservingKPlaneIntents, NULL }
|
|
};
|
|
|
|
|
|
// A pointer to the begining of the list
|
|
static cmsIntentsList *Intents = DefaultIntents;
|
|
|
|
// Search the list for a suitable intent. Returns NULL if not found
|
|
static
|
|
cmsIntentsList* SearchIntent(cmsUInt32Number Intent)
|
|
{
|
|
cmsIntentsList* pt;
|
|
|
|
for (pt = Intents; pt != NULL; pt = pt -> Next)
|
|
if (pt ->Intent == Intent) return pt;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
// Black point compensation. Implemented as a linear scaling in XYZ. Black points
|
|
// should come relative to the white point. Fills an matrix/offset element m
|
|
// which is organized as a 4x4 matrix.
|
|
static
|
|
void ComputeBlackPointCompensation(const cmsCIEXYZ* BlackPointIn,
|
|
const cmsCIEXYZ* BlackPointOut,
|
|
cmsMAT3* m, cmsVEC3* off)
|
|
{
|
|
cmsFloat64Number ax, ay, az, bx, by, bz, tx, ty, tz;
|
|
|
|
// Now we need to compute a matrix plus an offset m and of such of
|
|
// [m]*bpin + off = bpout
|
|
// [m]*D50 + off = D50
|
|
//
|
|
// This is a linear scaling in the form ax+b, where
|
|
// a = (bpout - D50) / (bpin - D50)
|
|
// b = - D50* (bpout - bpin) / (bpin - D50)
|
|
|
|
tx = BlackPointIn->X - cmsD50_XYZ()->X;
|
|
ty = BlackPointIn->Y - cmsD50_XYZ()->Y;
|
|
tz = BlackPointIn->Z - cmsD50_XYZ()->Z;
|
|
|
|
ax = (BlackPointOut->X - cmsD50_XYZ()->X) / tx;
|
|
ay = (BlackPointOut->Y - cmsD50_XYZ()->Y) / ty;
|
|
az = (BlackPointOut->Z - cmsD50_XYZ()->Z) / tz;
|
|
|
|
bx = - cmsD50_XYZ()-> X * (BlackPointOut->X - BlackPointIn->X) / tx;
|
|
by = - cmsD50_XYZ()-> Y * (BlackPointOut->Y - BlackPointIn->Y) / ty;
|
|
bz = - cmsD50_XYZ()-> Z * (BlackPointOut->Z - BlackPointIn->Z) / tz;
|
|
|
|
_cmsVEC3init(&m ->v[0], ax, 0, 0);
|
|
_cmsVEC3init(&m ->v[1], 0, ay, 0);
|
|
_cmsVEC3init(&m ->v[2], 0, 0, az);
|
|
_cmsVEC3init(off, bx, by, bz);
|
|
|
|
}
|
|
|
|
|
|
// Approximate a blackbody illuminant based on CHAD information
|
|
static
|
|
cmsFloat64Number CHAD2Temp(const cmsMAT3* Chad)
|
|
{
|
|
// Convert D50 across CHAD to get the absolute white point
|
|
cmsVEC3 d, s;
|
|
cmsCIEXYZ Dest;
|
|
cmsCIExyY DestChromaticity;
|
|
cmsFloat64Number TempK;
|
|
|
|
s.n[VX] = cmsD50_XYZ() -> X;
|
|
s.n[VY] = cmsD50_XYZ() -> Y;
|
|
s.n[VZ] = cmsD50_XYZ() -> Z;
|
|
|
|
_cmsMAT3eval(&d, Chad, &s);
|
|
|
|
Dest.X = d.n[VX];
|
|
Dest.Y = d.n[VY];
|
|
Dest.Z = d.n[VZ];
|
|
|
|
cmsXYZ2xyY(&DestChromaticity, &Dest);
|
|
|
|
if (!cmsTempFromWhitePoint(&TempK, &DestChromaticity))
|
|
return -1.0;
|
|
|
|
return TempK;
|
|
}
|
|
|
|
// Compute a CHAD based on a given temperature
|
|
static
|
|
void Temp2CHAD(cmsMAT3* Chad, cmsFloat64Number Temp)
|
|
{
|
|
cmsCIEXYZ White;
|
|
cmsCIExyY ChromaticityOfWhite;
|
|
|
|
cmsWhitePointFromTemp(&ChromaticityOfWhite, Temp);
|
|
cmsxyY2XYZ(&White, &ChromaticityOfWhite);
|
|
_cmsAdaptationMatrix(Chad, NULL, cmsD50_XYZ(), &White);
|
|
|
|
}
|
|
|
|
// Join scalings to obtain relative input to absolute and then to relative output.
|
|
// Result is stored in a 3x3 matrix
|
|
static
|
|
cmsBool ComputeAbsoluteIntent(cmsFloat64Number AdaptationState,
|
|
const cmsCIEXYZ* WhitePointIn,
|
|
const cmsMAT3* ChromaticAdaptationMatrixIn,
|
|
const cmsCIEXYZ* WhitePointOut,
|
|
const cmsMAT3* ChromaticAdaptationMatrixOut,
|
|
cmsMAT3* m)
|
|
{
|
|
cmsMAT3 Scale, m1, m2, m3;
|
|
|
|
// Adaptation state
|
|
if (AdaptationState == 1.0) {
|
|
|
|
// Observer is fully adapted. Keep chromatic adaptation.
|
|
// That is the standard V4 behaviour
|
|
_cmsVEC3init(&m->v[0], WhitePointIn->X / WhitePointOut->X, 0, 0);
|
|
_cmsVEC3init(&m->v[1], 0, WhitePointIn->Y / WhitePointOut->Y, 0);
|
|
_cmsVEC3init(&m->v[2], 0, 0, WhitePointIn->Z / WhitePointOut->Z);
|
|
|
|
}
|
|
else {
|
|
|
|
// Incomplete adaptation. This is an advanced feature.
|
|
_cmsVEC3init(&Scale.v[0], WhitePointIn->X / WhitePointOut->X, 0, 0);
|
|
_cmsVEC3init(&Scale.v[1], 0, WhitePointIn->Y / WhitePointOut->Y, 0);
|
|
_cmsVEC3init(&Scale.v[2], 0, 0, WhitePointIn->Z / WhitePointOut->Z);
|
|
|
|
m1 = *ChromaticAdaptationMatrixIn;
|
|
if (!_cmsMAT3inverse(&m1, &m2)) return FALSE;
|
|
_cmsMAT3per(&m3, &m2, &Scale);
|
|
|
|
// m3 holds CHAD from input white to D50 times abs. col. scaling
|
|
if (AdaptationState == 0.0) {
|
|
|
|
// Observer is not adapted, undo the chromatic adaptation
|
|
_cmsMAT3per(m, &m3, ChromaticAdaptationMatrixOut);
|
|
|
|
} else {
|
|
|
|
cmsMAT3 MixedCHAD;
|
|
cmsFloat64Number TempSrc, TempDest, Temp;
|
|
|
|
TempSrc = CHAD2Temp(ChromaticAdaptationMatrixIn); // K for source white
|
|
TempDest = CHAD2Temp(ChromaticAdaptationMatrixOut); // K for dest white
|
|
|
|
if (TempSrc < 0.0 || TempDest < 0.0) return FALSE; // Something went wrong
|
|
|
|
if (_cmsMAT3isIdentity(&Scale) && fabs(TempSrc - TempDest) < 0.01) {
|
|
|
|
_cmsMAT3identity(m);
|
|
return TRUE;
|
|
}
|
|
|
|
Temp = AdaptationState * TempSrc + (1.0 - AdaptationState) * TempDest;
|
|
|
|
// Get a CHAD from D50 to whatever output temperature. This replaces output CHAD
|
|
Temp2CHAD(&MixedCHAD, Temp);
|
|
|
|
_cmsMAT3per(m, &m3, &MixedCHAD);
|
|
}
|
|
|
|
}
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
// Just to see if m matrix should be applied
|
|
static
|
|
cmsBool IsEmptyLayer(cmsMAT3* m, cmsVEC3* off)
|
|
{
|
|
cmsFloat64Number diff = 0;
|
|
cmsMAT3 Ident;
|
|
int i;
|
|
|
|
if (m == NULL && off == NULL) return TRUE; // NULL is allowed as an empty layer
|
|
if (m == NULL && off != NULL) return FALSE; // This is an internal error
|
|
|
|
_cmsMAT3identity(&Ident);
|
|
|
|
for (i=0; i < 3*3; i++)
|
|
diff += fabs(((cmsFloat64Number*)m)[i] - ((cmsFloat64Number*)&Ident)[i]);
|
|
|
|
for (i=0; i < 3; i++)
|
|
diff += fabs(((cmsFloat64Number*)off)[i]);
|
|
|
|
|
|
return (diff < 0.002);
|
|
}
|
|
|
|
|
|
// Compute the conversion layer
|
|
static
|
|
cmsBool ComputeConversion(int i, cmsHPROFILE hProfiles[],
|
|
cmsUInt32Number Intent,
|
|
cmsBool BPC,
|
|
cmsFloat64Number AdaptationState,
|
|
cmsMAT3* m, cmsVEC3* off)
|
|
{
|
|
|
|
int k;
|
|
|
|
// m and off are set to identity and this is detected latter on
|
|
_cmsMAT3identity(m);
|
|
_cmsVEC3init(off, 0, 0, 0);
|
|
|
|
// If intent is abs. colorimetric,
|
|
if (Intent == INTENT_ABSOLUTE_COLORIMETRIC) {
|
|
|
|
cmsCIEXYZ WhitePointIn, WhitePointOut;
|
|
cmsMAT3 ChromaticAdaptationMatrixIn, ChromaticAdaptationMatrixOut;
|
|
|
|
_cmsReadMediaWhitePoint(&WhitePointIn, hProfiles[i-1]);
|
|
_cmsReadCHAD(&ChromaticAdaptationMatrixIn, hProfiles[i-1]);
|
|
|
|
_cmsReadMediaWhitePoint(&WhitePointOut, hProfiles[i]);
|
|
_cmsReadCHAD(&ChromaticAdaptationMatrixOut, hProfiles[i]);
|
|
|
|
if (!ComputeAbsoluteIntent(AdaptationState,
|
|
&WhitePointIn, &ChromaticAdaptationMatrixIn,
|
|
&WhitePointOut, &ChromaticAdaptationMatrixOut, m)) return FALSE;
|
|
|
|
}
|
|
else {
|
|
// Rest of intents may apply BPC.
|
|
|
|
if (BPC) {
|
|
|
|
cmsCIEXYZ BlackPointIn, BlackPointOut;
|
|
|
|
cmsDetectBlackPoint(&BlackPointIn, hProfiles[i-1], Intent, 0);
|
|
cmsDetectBlackPoint(&BlackPointOut, hProfiles[i], Intent, 0);
|
|
|
|
// If black points are equal, then do nothing
|
|
if (BlackPointIn.X != BlackPointOut.X ||
|
|
BlackPointIn.Y != BlackPointOut.Y ||
|
|
BlackPointIn.Z != BlackPointOut.Z)
|
|
ComputeBlackPointCompensation(&BlackPointIn, &BlackPointOut, m, off);
|
|
}
|
|
}
|
|
|
|
// Offset should be adjusted because the encoding. We encode XYZ normalized to 0..1.0,
|
|
// to do that, we divide by MAX_ENCODEABLE_XZY. The conversion stage goes XYZ -> XYZ so
|
|
// we have first to convert from encoded to XYZ and then convert back to encoded.
|
|
// y = Mx + Off
|
|
// x = x'c
|
|
// y = M x'c + Off
|
|
// y = y'c; y' = y / c
|
|
// y' = (Mx'c + Off) /c = Mx' + (Off / c)
|
|
|
|
for (k=0; k < 3; k++) {
|
|
off ->n[k] /= MAX_ENCODEABLE_XYZ;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
// Add a conversion stage if needed. If a matrix/offset m is given, it applies to XYZ space
|
|
static
|
|
cmsBool AddConversion(cmsPipeline* Result, cmsColorSpaceSignature InPCS, cmsColorSpaceSignature OutPCS, cmsMAT3* m, cmsVEC3* off)
|
|
{
|
|
cmsFloat64Number* m_as_dbl = (cmsFloat64Number*) m;
|
|
cmsFloat64Number* off_as_dbl = (cmsFloat64Number*) off;
|
|
|
|
// Handle PCS mismatches. A specialized stage is added to the LUT in such case
|
|
switch (InPCS) {
|
|
|
|
case cmsSigXYZData: // Input profile operates in XYZ
|
|
|
|
switch (OutPCS) {
|
|
|
|
case cmsSigXYZData: // XYZ -> XYZ
|
|
if (!IsEmptyLayer(m, off))
|
|
cmsPipelineInsertStage(Result, cmsAT_END, cmsStageAllocMatrix(Result ->ContextID, 3, 3, m_as_dbl, off_as_dbl));
|
|
break;
|
|
|
|
case cmsSigLabData: // XYZ -> Lab
|
|
if (!IsEmptyLayer(m, off))
|
|
cmsPipelineInsertStage(Result, cmsAT_END, cmsStageAllocMatrix(Result ->ContextID, 3, 3, m_as_dbl, off_as_dbl));
|
|
cmsPipelineInsertStage(Result, cmsAT_END, _cmsStageAllocXYZ2Lab(Result ->ContextID));
|
|
break;
|
|
|
|
default:
|
|
return FALSE; // Colorspace mismatch
|
|
}
|
|
break;
|
|
|
|
|
|
case cmsSigLabData: // Input profile operates in Lab
|
|
|
|
switch (OutPCS) {
|
|
|
|
case cmsSigXYZData: // Lab -> XYZ
|
|
|
|
cmsPipelineInsertStage(Result, cmsAT_END, _cmsStageAllocLab2XYZ(Result ->ContextID));
|
|
if (!IsEmptyLayer(m, off))
|
|
cmsPipelineInsertStage(Result, cmsAT_END, cmsStageAllocMatrix(Result ->ContextID, 3, 3, m_as_dbl, off_as_dbl));
|
|
break;
|
|
|
|
case cmsSigLabData: // Lab -> Lab
|
|
|
|
if (!IsEmptyLayer(m, off)) {
|
|
cmsPipelineInsertStage(Result, cmsAT_END, _cmsStageAllocLab2XYZ(Result ->ContextID));
|
|
cmsPipelineInsertStage(Result, cmsAT_END, cmsStageAllocMatrix(Result ->ContextID, 3, 3, m_as_dbl, off_as_dbl));
|
|
cmsPipelineInsertStage(Result, cmsAT_END, _cmsStageAllocXYZ2Lab(Result ->ContextID));
|
|
}
|
|
break;
|
|
|
|
default:
|
|
return FALSE; // Mismatch
|
|
}
|
|
break;
|
|
|
|
|
|
// On colorspaces other than PCS, check for same space
|
|
default:
|
|
if (InPCS != OutPCS) return FALSE;
|
|
break;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
// Is a given space compatible with another?
|
|
static
|
|
cmsBool ColorSpaceIsCompatible(cmsColorSpaceSignature a, cmsColorSpaceSignature b)
|
|
{
|
|
// If they are same, they are compatible.
|
|
if (a == b) return TRUE;
|
|
|
|
// Check for XYZ/Lab. Those spaces are interchangeable as they can be computed one from other.
|
|
if ((a == cmsSigXYZData) && (b == cmsSigLabData)) return TRUE;
|
|
if ((a == cmsSigLabData) && (b == cmsSigXYZData)) return TRUE;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
// Default handler for ICC-style intents
|
|
static
|
|
cmsPipeline* DefaultICCintents(cmsContext ContextID,
|
|
cmsUInt32Number nProfiles,
|
|
cmsUInt32Number TheIntents[],
|
|
cmsHPROFILE hProfiles[],
|
|
cmsBool BPC[],
|
|
cmsFloat64Number AdaptationStates[],
|
|
cmsUInt32Number dwFlags)
|
|
{
|
|
cmsPipeline* Lut, *Result;
|
|
cmsHPROFILE hProfile;
|
|
cmsMAT3 m;
|
|
cmsVEC3 off;
|
|
cmsColorSpaceSignature ColorSpaceIn, ColorSpaceOut, CurrentColorSpace;
|
|
cmsProfileClassSignature ClassSig;
|
|
cmsUInt32Number i, Intent;
|
|
|
|
// For safety
|
|
if (nProfiles == 0) return NULL;
|
|
|
|
// Allocate an empty LUT for holding the result. 0 as channel count means 'undefined'
|
|
Result = cmsPipelineAlloc(ContextID, 0, 0);
|
|
if (Result == NULL) return NULL;
|
|
|
|
CurrentColorSpace = cmsGetColorSpace(hProfiles[0]);
|
|
|
|
for (i=0; i < nProfiles; i++) {
|
|
|
|
cmsBool lIsDeviceLink, lIsInput;
|
|
|
|
hProfile = hProfiles[i];
|
|
ClassSig = cmsGetDeviceClass(hProfile);
|
|
lIsDeviceLink = (ClassSig == cmsSigLinkClass || ClassSig == cmsSigAbstractClass );
|
|
|
|
// First profile is used as input unless devicelink or abstract
|
|
if ((i == 0) && !lIsDeviceLink) {
|
|
lIsInput = TRUE;
|
|
}
|
|
else {
|
|
// Else use profile in the input direction if current space is not PCS
|
|
lIsInput = (CurrentColorSpace != cmsSigXYZData) &&
|
|
(CurrentColorSpace != cmsSigLabData);
|
|
}
|
|
|
|
Intent = TheIntents[i];
|
|
|
|
if (lIsInput || lIsDeviceLink) {
|
|
|
|
ColorSpaceIn = cmsGetColorSpace(hProfile);
|
|
ColorSpaceOut = cmsGetPCS(hProfile);
|
|
}
|
|
else {
|
|
|
|
ColorSpaceIn = cmsGetPCS(hProfile);
|
|
ColorSpaceOut = cmsGetColorSpace(hProfile);
|
|
}
|
|
|
|
if (!ColorSpaceIsCompatible(ColorSpaceIn, CurrentColorSpace)) {
|
|
|
|
cmsSignalError(ContextID, cmsERROR_COLORSPACE_CHECK, "ColorSpace mismatch");
|
|
goto Error;
|
|
}
|
|
|
|
// If devicelink is found, then no custom intent is allowed and we can
|
|
// read the LUT to be applied. Settings don't apply here.
|
|
if (lIsDeviceLink) {
|
|
|
|
// Get the involved LUT from the profile
|
|
Lut = _cmsReadDevicelinkLUT(hProfile, Intent);
|
|
if (Lut == NULL) goto Error;
|
|
|
|
// What about abstract profiles?
|
|
if (ClassSig == cmsSigAbstractClass && i > 0) {
|
|
if (!ComputeConversion(i, hProfiles, Intent, BPC[i], AdaptationStates[i], &m, &off)) goto Error;
|
|
}
|
|
else {
|
|
_cmsMAT3identity(&m);
|
|
_cmsVEC3init(&off, 0, 0, 0);
|
|
}
|
|
|
|
|
|
if (!AddConversion(Result, CurrentColorSpace, ColorSpaceIn, &m, &off)) goto Error;
|
|
|
|
}
|
|
else {
|
|
|
|
if (lIsInput) {
|
|
// Input direction means non-pcs connection, so proceed like devicelinks
|
|
Lut = _cmsReadInputLUT(hProfile, Intent);
|
|
if (Lut == NULL) goto Error;
|
|
}
|
|
else {
|
|
|
|
// Output direction means PCS connection. Intent may apply here
|
|
Lut = _cmsReadOutputLUT(hProfile, Intent);
|
|
if (Lut == NULL) goto Error;
|
|
|
|
|
|
if (!ComputeConversion(i, hProfiles, Intent, BPC[i], AdaptationStates[i], &m, &off)) goto Error;
|
|
if (!AddConversion(Result, CurrentColorSpace, ColorSpaceIn, &m, &off)) goto Error;
|
|
|
|
}
|
|
}
|
|
|
|
// Concatenate to the output LUT
|
|
cmsPipelineCat(Result, Lut);
|
|
cmsPipelineFree(Lut);
|
|
|
|
// Update current space
|
|
CurrentColorSpace = ColorSpaceOut;
|
|
}
|
|
|
|
return Result;
|
|
|
|
Error:
|
|
|
|
if (Result != NULL) cmsPipelineFree(Result);
|
|
return NULL;
|
|
|
|
cmsUNUSED_PARAMETER(dwFlags);
|
|
}
|
|
|
|
|
|
// Wrapper for DLL calling convention
|
|
cmsPipeline* CMSEXPORT _cmsDefaultICCintents(cmsContext ContextID,
|
|
cmsUInt32Number nProfiles,
|
|
cmsUInt32Number TheIntents[],
|
|
cmsHPROFILE hProfiles[],
|
|
cmsBool BPC[],
|
|
cmsFloat64Number AdaptationStates[],
|
|
cmsUInt32Number dwFlags)
|
|
{
|
|
return DefaultICCintents(ContextID, nProfiles, TheIntents, hProfiles, BPC, AdaptationStates, dwFlags);
|
|
}
|
|
|
|
// Black preserving intents ---------------------------------------------------------------------------------------------
|
|
|
|
// Translate black-preserving intents to ICC ones
|
|
static
|
|
int TranslateNonICCIntents(int Intent)
|
|
{
|
|
switch (Intent) {
|
|
case INTENT_PRESERVE_K_ONLY_PERCEPTUAL:
|
|
case INTENT_PRESERVE_K_PLANE_PERCEPTUAL:
|
|
return INTENT_PERCEPTUAL;
|
|
|
|
case INTENT_PRESERVE_K_ONLY_RELATIVE_COLORIMETRIC:
|
|
case INTENT_PRESERVE_K_PLANE_RELATIVE_COLORIMETRIC:
|
|
return INTENT_RELATIVE_COLORIMETRIC;
|
|
|
|
case INTENT_PRESERVE_K_ONLY_SATURATION:
|
|
case INTENT_PRESERVE_K_PLANE_SATURATION:
|
|
return INTENT_SATURATION;
|
|
|
|
default: return Intent;
|
|
}
|
|
}
|
|
|
|
// Sampler for Black-only preserving CMYK->CMYK transforms
|
|
|
|
typedef struct {
|
|
cmsPipeline* cmyk2cmyk; // The original transform
|
|
cmsToneCurve* KTone; // Black-to-black tone curve
|
|
|
|
} GrayOnlyParams;
|
|
|
|
|
|
// Preserve black only if that is the only ink used
|
|
static
|
|
int BlackPreservingGrayOnlySampler(register const cmsUInt16Number In[], register cmsUInt16Number Out[], register void* Cargo)
|
|
{
|
|
GrayOnlyParams* bp = (GrayOnlyParams*) Cargo;
|
|
|
|
// If going across black only, keep black only
|
|
if (In[0] == 0 && In[1] == 0 && In[2] == 0) {
|
|
|
|
// TAC does not apply because it is black ink!
|
|
Out[0] = Out[1] = Out[2] = 0;
|
|
Out[3] = cmsEvalToneCurve16(bp->KTone, In[3]);
|
|
return TRUE;
|
|
}
|
|
|
|
// Keep normal transform for other colors
|
|
bp ->cmyk2cmyk ->Eval16Fn(In, Out, bp ->cmyk2cmyk->Data);
|
|
return TRUE;
|
|
}
|
|
|
|
// This is the entry for black-preserving K-only intents, which are non-ICC
|
|
static
|
|
cmsPipeline* BlackPreservingKOnlyIntents(cmsContext ContextID,
|
|
cmsUInt32Number nProfiles,
|
|
cmsUInt32Number TheIntents[],
|
|
cmsHPROFILE hProfiles[],
|
|
cmsBool BPC[],
|
|
cmsFloat64Number AdaptationStates[],
|
|
cmsUInt32Number dwFlags)
|
|
{
|
|
GrayOnlyParams bp;
|
|
cmsPipeline* Result;
|
|
cmsUInt32Number ICCIntents[256];
|
|
cmsStage* CLUT;
|
|
cmsUInt32Number i, nGridPoints;
|
|
|
|
|
|
// Sanity check
|
|
if (nProfiles < 1 || nProfiles > 255) return NULL;
|
|
|
|
// Translate black-preserving intents to ICC ones
|
|
for (i=0; i < nProfiles; i++)
|
|
ICCIntents[i] = TranslateNonICCIntents(TheIntents[i]);
|
|
|
|
// Check for non-cmyk profiles
|
|
if (cmsGetColorSpace(hProfiles[0]) != cmsSigCmykData ||
|
|
cmsGetColorSpace(hProfiles[nProfiles-1]) != cmsSigCmykData)
|
|
return DefaultICCintents(ContextID, nProfiles, ICCIntents, hProfiles, BPC, AdaptationStates, dwFlags);
|
|
|
|
memset(&bp, 0, sizeof(bp));
|
|
|
|
// Allocate an empty LUT for holding the result
|
|
Result = cmsPipelineAlloc(ContextID, 4, 4);
|
|
if (Result == NULL) return NULL;
|
|
|
|
// Create a LUT holding normal ICC transform
|
|
bp.cmyk2cmyk = DefaultICCintents(ContextID,
|
|
nProfiles,
|
|
ICCIntents,
|
|
hProfiles,
|
|
BPC,
|
|
AdaptationStates,
|
|
dwFlags);
|
|
|
|
if (bp.cmyk2cmyk == NULL) goto Error;
|
|
|
|
// Now, compute the tone curve
|
|
bp.KTone = _cmsBuildKToneCurve(ContextID,
|
|
4096,
|
|
nProfiles,
|
|
ICCIntents,
|
|
hProfiles,
|
|
BPC,
|
|
AdaptationStates,
|
|
dwFlags);
|
|
|
|
if (bp.KTone == NULL) goto Error;
|
|
|
|
|
|
// How many gridpoints are we going to use?
|
|
nGridPoints = _cmsReasonableGridpointsByColorspace(cmsSigCmykData, dwFlags);
|
|
|
|
// Create the CLUT. 16 bits
|
|
CLUT = cmsStageAllocCLut16bit(ContextID, nGridPoints, 4, 4, NULL);
|
|
if (CLUT == NULL) goto Error;
|
|
|
|
// This is the one and only MPE in this LUT
|
|
cmsPipelineInsertStage(Result, cmsAT_BEGIN, CLUT);
|
|
|
|
// Sample it. We cannot afford pre/post linearization this time.
|
|
if (!cmsStageSampleCLut16bit(CLUT, BlackPreservingGrayOnlySampler, (void*) &bp, 0))
|
|
goto Error;
|
|
|
|
// Get rid of xform and tone curve
|
|
cmsPipelineFree(bp.cmyk2cmyk);
|
|
cmsFreeToneCurve(bp.KTone);
|
|
|
|
return Result;
|
|
|
|
Error:
|
|
|
|
if (bp.cmyk2cmyk != NULL) cmsPipelineFree(bp.cmyk2cmyk);
|
|
if (bp.KTone != NULL) cmsFreeToneCurve(bp.KTone);
|
|
if (Result != NULL) cmsPipelineFree(Result);
|
|
return NULL;
|
|
|
|
}
|
|
|
|
// K Plane-preserving CMYK to CMYK ------------------------------------------------------------------------------------
|
|
|
|
typedef struct {
|
|
|
|
cmsPipeline* cmyk2cmyk; // The original transform
|
|
cmsHTRANSFORM hProofOutput; // Output CMYK to Lab (last profile)
|
|
cmsHTRANSFORM cmyk2Lab; // The input chain
|
|
cmsToneCurve* KTone; // Black-to-black tone curve
|
|
cmsPipeline* LabK2cmyk; // The output profile
|
|
cmsFloat64Number MaxError;
|
|
|
|
cmsHTRANSFORM hRoundTrip;
|
|
cmsFloat64Number MaxTAC;
|
|
|
|
|
|
} PreserveKPlaneParams;
|
|
|
|
|
|
// The CLUT will be stored at 16 bits, but calculations are performed at cmsFloat32Number precision
|
|
static
|
|
int BlackPreservingSampler(register const cmsUInt16Number In[], register cmsUInt16Number Out[], register void* Cargo)
|
|
{
|
|
int i;
|
|
cmsFloat32Number Inf[4], Outf[4];
|
|
cmsFloat32Number LabK[4];
|
|
cmsFloat64Number SumCMY, SumCMYK, Error, Ratio;
|
|
cmsCIELab ColorimetricLab, BlackPreservingLab;
|
|
PreserveKPlaneParams* bp = (PreserveKPlaneParams*) Cargo;
|
|
|
|
// Convert from 16 bits to floating point
|
|
for (i=0; i < 4; i++)
|
|
Inf[i] = (cmsFloat32Number) (In[i] / 65535.0);
|
|
|
|
// Get the K across Tone curve
|
|
LabK[3] = cmsEvalToneCurveFloat(bp ->KTone, Inf[3]);
|
|
|
|
// If going across black only, keep black only
|
|
if (In[0] == 0 && In[1] == 0 && In[2] == 0) {
|
|
|
|
Out[0] = Out[1] = Out[2] = 0;
|
|
Out[3] = _cmsQuickSaturateWord(LabK[3] * 65535.0);
|
|
return TRUE;
|
|
}
|
|
|
|
// Try the original transform,
|
|
cmsPipelineEvalFloat( Inf, Outf, bp ->cmyk2cmyk);
|
|
|
|
// Store a copy of the floating point result into 16-bit
|
|
for (i=0; i < 4; i++)
|
|
Out[i] = _cmsQuickSaturateWord(Outf[i] * 65535.0);
|
|
|
|
// Maybe K is already ok (mostly on K=0)
|
|
if ( fabs(Outf[3] - LabK[3]) < (3.0 / 65535.0) ) {
|
|
return TRUE;
|
|
}
|
|
|
|
// K differ, mesure and keep Lab measurement for further usage
|
|
// this is done in relative colorimetric intent
|
|
cmsDoTransform(bp->hProofOutput, Out, &ColorimetricLab, 1);
|
|
|
|
// Is not black only and the transform doesn't keep black.
|
|
// Obtain the Lab of output CMYK. After that we have Lab + K
|
|
cmsDoTransform(bp ->cmyk2Lab, Outf, LabK, 1);
|
|
|
|
// Obtain the corresponding CMY using reverse interpolation
|
|
// (K is fixed in LabK[3])
|
|
if (!cmsPipelineEvalReverseFloat(LabK, Outf, Outf, bp ->LabK2cmyk)) {
|
|
|
|
// Cannot find a suitable value, so use colorimetric xform
|
|
// which is already stored in Out[]
|
|
return TRUE;
|
|
}
|
|
|
|
// Make sure to pass thru K (which now is fixed)
|
|
Outf[3] = LabK[3];
|
|
|
|
// Apply TAC if needed
|
|
SumCMY = Outf[0] + Outf[1] + Outf[2];
|
|
SumCMYK = SumCMY + Outf[3];
|
|
|
|
if (SumCMYK > bp ->MaxTAC) {
|
|
|
|
Ratio = 1 - ((SumCMYK - bp->MaxTAC) / SumCMY);
|
|
if (Ratio < 0)
|
|
Ratio = 0;
|
|
}
|
|
else
|
|
Ratio = 1.0;
|
|
|
|
Out[0] = _cmsQuickSaturateWord(Outf[0] * Ratio * 65535.0); // C
|
|
Out[1] = _cmsQuickSaturateWord(Outf[1] * Ratio * 65535.0); // M
|
|
Out[2] = _cmsQuickSaturateWord(Outf[2] * Ratio * 65535.0); // Y
|
|
Out[3] = _cmsQuickSaturateWord(Outf[3] * 65535.0);
|
|
|
|
// Estimate the error (this goes 16 bits to Lab DBL)
|
|
cmsDoTransform(bp->hProofOutput, Out, &BlackPreservingLab, 1);
|
|
Error = cmsDeltaE(&ColorimetricLab, &BlackPreservingLab);
|
|
if (Error > bp -> MaxError)
|
|
bp->MaxError = Error;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
// This is the entry for black-plane preserving, which are non-ICC
|
|
static
|
|
cmsPipeline* BlackPreservingKPlaneIntents(cmsContext ContextID,
|
|
cmsUInt32Number nProfiles,
|
|
cmsUInt32Number TheIntents[],
|
|
cmsHPROFILE hProfiles[],
|
|
cmsBool BPC[],
|
|
cmsFloat64Number AdaptationStates[],
|
|
cmsUInt32Number dwFlags)
|
|
{
|
|
PreserveKPlaneParams bp;
|
|
cmsPipeline* Result = NULL;
|
|
cmsUInt32Number ICCIntents[256];
|
|
cmsStage* CLUT;
|
|
cmsUInt32Number i, nGridPoints;
|
|
cmsHPROFILE hLab;
|
|
|
|
// Sanity check
|
|
if (nProfiles < 1 || nProfiles > 255) return NULL;
|
|
|
|
// Translate black-preserving intents to ICC ones
|
|
for (i=0; i < nProfiles; i++)
|
|
ICCIntents[i] = TranslateNonICCIntents(TheIntents[i]);
|
|
|
|
// Check for non-cmyk profiles
|
|
if (cmsGetColorSpace(hProfiles[0]) != cmsSigCmykData ||
|
|
cmsGetColorSpace(hProfiles[nProfiles-1]) != cmsSigCmykData)
|
|
return DefaultICCintents(ContextID, nProfiles, ICCIntents, hProfiles, BPC, AdaptationStates, dwFlags);
|
|
|
|
// Allocate an empty LUT for holding the result
|
|
Result = cmsPipelineAlloc(ContextID, 4, 4);
|
|
if (Result == NULL) return NULL;
|
|
|
|
|
|
memset(&bp, 0, sizeof(bp));
|
|
|
|
// We need the input LUT of the last profile, assuming this one is responsible of
|
|
// black generation. This LUT will be seached in inverse order.
|
|
bp.LabK2cmyk = _cmsReadInputLUT(hProfiles[nProfiles-1], INTENT_RELATIVE_COLORIMETRIC);
|
|
if (bp.LabK2cmyk == NULL) goto Cleanup;
|
|
|
|
// Get total area coverage (in 0..1 domain)
|
|
bp.MaxTAC = cmsDetectTAC(hProfiles[nProfiles-1]) / 100.0;
|
|
|
|
// Create a LUT holding normal ICC transform
|
|
bp.cmyk2cmyk = DefaultICCintents(ContextID,
|
|
nProfiles,
|
|
ICCIntents,
|
|
hProfiles,
|
|
BPC,
|
|
AdaptationStates,
|
|
dwFlags);
|
|
|
|
// Now the tone curve
|
|
bp.KTone = _cmsBuildKToneCurve(ContextID, 4096, nProfiles,
|
|
ICCIntents,
|
|
hProfiles,
|
|
BPC,
|
|
AdaptationStates,
|
|
dwFlags);
|
|
|
|
|
|
// To measure the output, Last profile to Lab
|
|
hLab = cmsCreateLab4ProfileTHR(ContextID, NULL);
|
|
bp.hProofOutput = cmsCreateTransformTHR(ContextID, hProfiles[nProfiles-1],
|
|
CHANNELS_SH(4)|BYTES_SH(2), hLab, TYPE_Lab_DBL,
|
|
INTENT_RELATIVE_COLORIMETRIC,
|
|
cmsFLAGS_NOCACHE|cmsFLAGS_NOOPTIMIZE);
|
|
|
|
// Same as anterior, but lab in the 0..1 range
|
|
bp.cmyk2Lab = cmsCreateTransformTHR(ContextID, hProfiles[nProfiles-1],
|
|
FLOAT_SH(1)|CHANNELS_SH(4)|BYTES_SH(4), hLab,
|
|
FLOAT_SH(1)|CHANNELS_SH(3)|BYTES_SH(4),
|
|
INTENT_RELATIVE_COLORIMETRIC,
|
|
cmsFLAGS_NOCACHE|cmsFLAGS_NOOPTIMIZE);
|
|
cmsCloseProfile(hLab);
|
|
|
|
// Error estimation (for debug only)
|
|
bp.MaxError = 0;
|
|
|
|
// How many gridpoints are we going to use?
|
|
nGridPoints = _cmsReasonableGridpointsByColorspace(cmsSigCmykData, dwFlags);
|
|
|
|
|
|
CLUT = cmsStageAllocCLut16bit(ContextID, nGridPoints, 4, 4, NULL);
|
|
if (CLUT == NULL) goto Cleanup;
|
|
|
|
cmsPipelineInsertStage(Result, cmsAT_BEGIN, CLUT);
|
|
|
|
cmsStageSampleCLut16bit(CLUT, BlackPreservingSampler, (void*) &bp, 0);
|
|
|
|
Cleanup:
|
|
|
|
if (bp.cmyk2cmyk) cmsPipelineFree(bp.cmyk2cmyk);
|
|
if (bp.cmyk2Lab) cmsDeleteTransform(bp.cmyk2Lab);
|
|
if (bp.hProofOutput) cmsDeleteTransform(bp.hProofOutput);
|
|
|
|
if (bp.KTone) cmsFreeToneCurve(bp.KTone);
|
|
if (bp.LabK2cmyk) cmsPipelineFree(bp.LabK2cmyk);
|
|
|
|
return Result;
|
|
}
|
|
|
|
// Link routines ------------------------------------------------------------------------------------------------------
|
|
|
|
// Chain several profiles into a single LUT. It just checks the parameters and then calls the handler
|
|
// for the first intent in chain. The handler may be user-defined. Is up to the handler to deal with the
|
|
// rest of intents in chain. A maximum of 255 profiles at time are supported, which is pretty reasonable.
|
|
cmsPipeline* _cmsLinkProfiles(cmsContext ContextID,
|
|
cmsUInt32Number nProfiles,
|
|
cmsUInt32Number TheIntents[],
|
|
cmsHPROFILE hProfiles[],
|
|
cmsBool BPC[],
|
|
cmsFloat64Number AdaptationStates[],
|
|
cmsUInt32Number dwFlags)
|
|
{
|
|
cmsUInt32Number i;
|
|
cmsIntentsList* Intent;
|
|
|
|
// Make sure a reasonable number of profiles is provided
|
|
if (nProfiles <= 0 || nProfiles > 255) {
|
|
cmsSignalError(ContextID, cmsERROR_RANGE, "Couldn't link '%d' profiles", nProfiles);
|
|
return NULL;
|
|
}
|
|
|
|
for (i=0; i < nProfiles; i++) {
|
|
|
|
// Check if black point is really needed or allowed. Note that
|
|
// following Adobe's document:
|
|
// BPC does not apply to devicelink profiles, nor to abs colorimetric,
|
|
// and applies always on V4 perceptual and saturation.
|
|
|
|
if (TheIntents[i] == INTENT_ABSOLUTE_COLORIMETRIC)
|
|
BPC[i] = FALSE;
|
|
|
|
if (TheIntents[i] == INTENT_PERCEPTUAL || TheIntents[i] == INTENT_SATURATION) {
|
|
|
|
// Force BPC for V4 profiles in perceptual and saturation
|
|
if (cmsGetProfileVersion(hProfiles[i]) >= 4.0)
|
|
BPC[i] = TRUE;
|
|
}
|
|
}
|
|
|
|
// Search for a handler. The first intent in the chain defines the handler. That would
|
|
// prevent using multiple custom intents in a multiintent chain, but the behaviour of
|
|
// this case would present some issues if the custom intent tries to do things like
|
|
// preserve primaries. This solution is not perfect, but works well on most cases.
|
|
|
|
Intent = SearchIntent(TheIntents[0]);
|
|
if (Intent == NULL) {
|
|
cmsSignalError(ContextID, cmsERROR_UNKNOWN_EXTENSION, "Unsupported intent '%d'", TheIntents[0]);
|
|
return NULL;
|
|
}
|
|
|
|
// Call the handler
|
|
return Intent ->Link(ContextID, nProfiles, TheIntents, hProfiles, BPC, AdaptationStates, dwFlags);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------------------
|
|
|
|
// Get information about available intents. nMax is the maximum space for the supplied "Codes"
|
|
// and "Descriptions" the function returns the total number of intents, which may be greater
|
|
// than nMax, although the matrices are not populated beyond this level.
|
|
cmsUInt32Number CMSEXPORT cmsGetSupportedIntents(cmsUInt32Number nMax, cmsUInt32Number* Codes, char** Descriptions)
|
|
{
|
|
cmsIntentsList* pt;
|
|
cmsUInt32Number nIntents;
|
|
|
|
for (nIntents=0, pt = Intents; pt != NULL; pt = pt -> Next)
|
|
{
|
|
if (nIntents < nMax) {
|
|
if (Codes != NULL)
|
|
Codes[nIntents] = pt ->Intent;
|
|
|
|
if (Descriptions != NULL)
|
|
Descriptions[nIntents] = pt ->Description;
|
|
}
|
|
|
|
nIntents++;
|
|
}
|
|
|
|
return nIntents;
|
|
}
|
|
|
|
// The plug-in registration. User can add new intents or override default routines
|
|
cmsBool _cmsRegisterRenderingIntentPlugin(cmsPluginBase* Data)
|
|
{
|
|
cmsPluginRenderingIntent* Plugin = (cmsPluginRenderingIntent*) Data;
|
|
cmsIntentsList* fl;
|
|
|
|
// Do we have to reset the intents?
|
|
if (Data == NULL) {
|
|
|
|
Intents = DefaultIntents;
|
|
return TRUE;
|
|
}
|
|
|
|
fl = SearchIntent(Plugin ->Intent);
|
|
|
|
if (fl == NULL) {
|
|
fl = (cmsIntentsList*) _cmsPluginMalloc(sizeof(cmsIntentsList));
|
|
if (fl == NULL) return FALSE;
|
|
}
|
|
|
|
fl ->Intent = Plugin ->Intent;
|
|
strncpy(fl ->Description, Plugin ->Description, 255);
|
|
fl ->Description[255] = 0;
|
|
|
|
fl ->Link = Plugin ->Link;
|
|
|
|
fl ->Next = Intents;
|
|
Intents = fl;
|
|
|
|
return TRUE;
|
|
}
|
|
|