blob: 082f1f8119182cb14848fea81493578408606dfd [file] [log] [blame]
//---------------------------------------------------------------------------------
//
// Little Color Management System
// Copyright (c) 1998-2017 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 beginning of the list
_cmsIntentsPluginChunkType _cmsIntentsPluginChunk = { NULL };
// Duplicates the zone of memory used by the plug-in in the new context
static
void DupPluginIntentsList(struct _cmsContext_struct* ctx,
const struct _cmsContext_struct* src)
{
_cmsIntentsPluginChunkType newHead = { NULL };
cmsIntentsList* entry;
cmsIntentsList* Anterior = NULL;
_cmsIntentsPluginChunkType* head = (_cmsIntentsPluginChunkType*) src->chunks[IntentPlugin];
// Walk the list copying all nodes
for (entry = head->Intents;
entry != NULL;
entry = entry ->Next) {
cmsIntentsList *newEntry = ( cmsIntentsList *) _cmsSubAllocDup(ctx ->MemPool, entry, sizeof(cmsIntentsList));
if (newEntry == NULL)
return;
// We want to keep the linked list order, so this is a little bit tricky
newEntry -> Next = NULL;
if (Anterior)
Anterior -> Next = newEntry;
Anterior = newEntry;
if (newHead.Intents == NULL)
newHead.Intents = newEntry;
}
ctx ->chunks[IntentPlugin] = _cmsSubAllocDup(ctx->MemPool, &newHead, sizeof(_cmsIntentsPluginChunkType));
}
void _cmsAllocIntentsPluginChunk(struct _cmsContext_struct* ctx,
const struct _cmsContext_struct* src)
{
if (src != NULL) {
// Copy all linked list
DupPluginIntentsList(ctx, src);
}
else {
static _cmsIntentsPluginChunkType IntentsPluginChunkType = { NULL };
ctx ->chunks[IntentPlugin] = _cmsSubAllocDup(ctx ->MemPool, &IntentsPluginChunkType, sizeof(_cmsIntentsPluginChunkType));
}
}
// Search the list for a suitable intent. Returns NULL if not found
static
cmsIntentsList* SearchIntent(cmsContext ContextID, cmsUInt32Number Intent)
{
_cmsIntentsPluginChunkType* ctx = ( _cmsIntentsPluginChunkType*) _cmsContextGetClientChunk(ContextID, IntentPlugin);
cmsIntentsList* pt;
for (pt = ctx -> Intents; pt != NULL; pt = pt -> Next)
if (pt ->Intent == Intent) return pt;
for (pt = DefaultIntents; 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 inverse CHAD to get the absolute white point
cmsVEC3 d, s;
cmsCIEXYZ Dest;
cmsCIExyY DestChromaticity;
cmsFloat64Number TempK;
cmsMAT3 m1, m2;
m1 = *Chad;
if (!_cmsMAT3inverse(&m1, &m2)) return FALSE;
s.n[VX] = cmsD50_XYZ() -> X;
s.n[VY] = cmsD50_XYZ() -> Y;
s.n[VZ] = cmsD50_XYZ() -> Z;
_cmsMAT3eval(&d, &m2, &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, &White, cmsD50_XYZ());
}
// 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, m4;
// TODO: Follow Marc Mahy's recommendation to check if CHAD is same by using M1*M2 == M2*M1. If so, do nothing.
// TODO: Add support for ArgyllArts tag
// 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);
if (AdaptationState == 0.0) {
m1 = *ChromaticAdaptationMatrixOut;
_cmsMAT3per(&m2, &m1, &Scale);
// m2 holds CHAD from output white to D50 times abs. col. scaling
// Observer is not adapted, undo the chromatic adaptation
_cmsMAT3per(m, &m2, ChromaticAdaptationMatrixOut);
m3 = *ChromaticAdaptationMatrixIn;
if (!_cmsMAT3inverse(&m3, &m4)) return FALSE;
_cmsMAT3per(m, &m2, &m4);
} else {
cmsMAT3 MixedCHAD;
cmsFloat64Number TempSrc, TempDest, Temp;
m1 = *ChromaticAdaptationMatrixIn;
if (!_cmsMAT3inverse(&m1, &m2)) return FALSE;
_cmsMAT3per(&m3, &m2, &Scale);
// m3 holds CHAD from input white to D50 times abs. col. scaling
TempSrc = CHAD2Temp(ChromaticAdaptationMatrixIn);
TempDest = CHAD2Temp(ChromaticAdaptationMatrixOut);
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 = (1.0 - AdaptationState) * TempDest + AdaptationState * TempSrc;
// Get a CHAD from whatever output temperature to D50. 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(cmsUInt32Number 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);
cmsDetectDestinationBlackPoint(&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)))
return FALSE;
break;
case cmsSigLabData: // XYZ -> Lab
if (!IsEmptyLayer(m, off) &&
!cmsPipelineInsertStage(Result, cmsAT_END, cmsStageAllocMatrix(Result ->ContextID, 3, 3, m_as_dbl, off_as_dbl)))
return FALSE;
if (!cmsPipelineInsertStage(Result, cmsAT_END, _cmsStageAllocXYZ2Lab(Result ->ContextID)))
return FALSE;
break;
default:
return FALSE; // Colorspace mismatch
}
break;
case cmsSigLabData: // Input profile operates in Lab
switch (OutPCS) {
case cmsSigXYZData: // Lab -> XYZ
if (!cmsPipelineInsertStage(Result, cmsAT_END, _cmsStageAllocLab2XYZ(Result ->ContextID)))
return FALSE;
if (!IsEmptyLayer(m, off) &&
!cmsPipelineInsertStage(Result, cmsAT_END, cmsStageAllocMatrix(Result ->ContextID, 3, 3, m_as_dbl, off_as_dbl)))
return FALSE;
break;
case cmsSigLabData: // Lab -> Lab
if (!IsEmptyLayer(m, off)) {
if (!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)))
return FALSE;
}
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 MCH4 substitution of CMYK
if ((a == cmsSig4colorData) && (b == cmsSigCmykData)) return TRUE;
if ((a == cmsSigCmykData) && (b == cmsSig4colorData)) 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 = NULL;
cmsPipeline* Result;
cmsHPROFILE hProfile;
cmsMAT3 m;
cmsVEC3 off;
cmsColorSpaceSignature ColorSpaceIn, ColorSpaceOut = cmsSigLabData, 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 || ((ClassSig == cmsSigNamedColorClass) && (nProfiles == 1))) {
// 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
if (!cmsPipelineCat(Result, Lut))
goto Error;
cmsPipelineFree(Lut);
Lut = NULL;
// Update current space
CurrentColorSpace = ColorSpaceOut;
}
// Check for non-negatives clip
if (dwFlags & cmsFLAGS_NONEGATIVES) {
if (ColorSpaceOut == cmsSigGrayData ||
ColorSpaceOut == cmsSigRgbData ||
ColorSpaceOut == cmsSigCmykData) {
cmsStage* clip = _cmsStageClipNegatives(Result->ContextID, cmsChannelsOf(ColorSpaceOut));
if (clip == NULL) goto Error;
if (!cmsPipelineInsertStage(Result, cmsAT_END, clip))
goto Error;
}
}
return Result;
Error:
if (Lut != NULL) cmsPipelineFree(Lut);
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
cmsUInt32Number TranslateNonICCIntents(cmsUInt32Number 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
if (!cmsPipelineInsertStage(Result, cmsAT_BEGIN, CLUT))
goto Error;
// 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 through 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 ||
cmsGetDeviceClass(hProfiles[nProfiles-1]) == cmsSigOutputClass))
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 searched 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;
if (bp.MaxTAC <= 0) goto Cleanup;
// Create a LUT holding normal ICC transform
bp.cmyk2cmyk = DefaultICCintents(ContextID,
nProfiles,
ICCIntents,
hProfiles,
BPC,
AdaptationStates,
dwFlags);
if (bp.cmyk2cmyk == NULL) goto Cleanup;
// Now the tone curve
bp.KTone = _cmsBuildKToneCurve(ContextID, 4096, nProfiles,
ICCIntents,
hProfiles,
BPC,
AdaptationStates,
dwFlags);
if (bp.KTone == NULL) goto Cleanup;
// 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);
if ( bp.hProofOutput == NULL) goto Cleanup;
// 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);
if (bp.cmyk2Lab == NULL) goto Cleanup;
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;
if (!cmsPipelineInsertStage(Result, cmsAT_BEGIN, CLUT))
goto Cleanup;
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 (cmsGetEncodedICCversion(hProfiles[i]) >= 0x4000000)
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(ContextID, 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 cmsGetSupportedIntentsTHR(cmsContext ContextID, cmsUInt32Number nMax, cmsUInt32Number* Codes, char** Descriptions)
{
_cmsIntentsPluginChunkType* ctx = ( _cmsIntentsPluginChunkType*) _cmsContextGetClientChunk(ContextID, IntentPlugin);
cmsIntentsList* pt;
cmsUInt32Number nIntents;
for (nIntents=0, pt = ctx->Intents; pt != NULL; pt = pt -> Next)
{
if (nIntents < nMax) {
if (Codes != NULL)
Codes[nIntents] = pt ->Intent;
if (Descriptions != NULL)
Descriptions[nIntents] = pt ->Description;
}
nIntents++;
}
for (nIntents=0, pt = DefaultIntents; 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;
}
cmsUInt32Number CMSEXPORT cmsGetSupportedIntents(cmsUInt32Number nMax, cmsUInt32Number* Codes, char** Descriptions)
{
return cmsGetSupportedIntentsTHR(NULL, nMax, Codes, Descriptions);
}
// The plug-in registration. User can add new intents or override default routines
cmsBool _cmsRegisterRenderingIntentPlugin(cmsContext id, cmsPluginBase* Data)
{
_cmsIntentsPluginChunkType* ctx = ( _cmsIntentsPluginChunkType*) _cmsContextGetClientChunk(id, IntentPlugin);
cmsPluginRenderingIntent* Plugin = (cmsPluginRenderingIntent*) Data;
cmsIntentsList* fl;
// Do we have to reset the custom intents?
if (Data == NULL) {
ctx->Intents = NULL;
return TRUE;
}
fl = (cmsIntentsList*) _cmsPluginMalloc(id, sizeof(cmsIntentsList));
if (fl == NULL) return FALSE;
fl ->Intent = Plugin ->Intent;
strncpy(fl ->Description, Plugin ->Description, sizeof(fl ->Description)-1);
fl ->Description[sizeof(fl ->Description)-1] = 0;
fl ->Link = Plugin ->Link;
fl ->Next = ctx ->Intents;
ctx ->Intents = fl;
return TRUE;
}