diff options
Diffstat (limited to 'third_party/lcms2-2.6/src/cmscnvrt.c')
-rw-r--r-- | third_party/lcms2-2.6/src/cmscnvrt.c | 1142 |
1 files changed, 1142 insertions, 0 deletions
diff --git a/third_party/lcms2-2.6/src/cmscnvrt.c b/third_party/lcms2-2.6/src/cmscnvrt.c new file mode 100644 index 0000000000..1a93e83f90 --- /dev/null +++ b/third_party/lcms2-2.6/src/cmscnvrt.c @@ -0,0 +1,1142 @@ +//--------------------------------------------------------------------------------- +// +// Little Color Management System +// Copyright (c) 1998-2012 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 +_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; + + // 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(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); + 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, 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; + } + + 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 +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 + 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 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 || + 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 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; + 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 (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(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; +} + |