|  | //--------------------------------------------------------------------------------- | 
|  | // | 
|  | //  Little Color Management System | 
|  | //  Copyright (c) 1998-2023 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" | 
|  |  | 
|  |  | 
|  | // 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; | 
|  |  | 
|  | if (!_cmsReadMediaWhitePoint(&WhitePointIn, hProfiles[i - 1])) return FALSE; | 
|  | if (!_cmsReadCHAD(&ChromaticAdaptationMatrixIn, hProfiles[i - 1])) return FALSE; | 
|  |  | 
|  | if (!_cmsReadMediaWhitePoint(&WhitePointOut, hProfiles[i])) return FALSE; | 
|  | if (!_cmsReadCHAD(&ChromaticAdaptationMatrixOut, hProfiles[i])) return FALSE; | 
|  |  | 
|  | if (!ComputeAbsoluteIntent(AdaptationState, | 
|  | &WhitePointIn,  &ChromaticAdaptationMatrixIn, | 
|  | &WhitePointOut, &ChromaticAdaptationMatrixOut, m)) return FALSE; | 
|  |  | 
|  | } | 
|  | else { | 
|  | // Rest of intents may apply BPC. | 
|  |  | 
|  | if (BPC) { | 
|  |  | 
|  | cmsCIEXYZ BlackPointIn = { 0, 0, 0}, BlackPointOut = { 0, 0, 0 }; | 
|  |  | 
|  | 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, cmsChannelsOfColorSpace(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(CMSREGISTER const cmsUInt16Number In[], CMSREGISTER cmsUInt16Number Out[], CMSREGISTER 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; | 
|  | cmsUInt32Number lastProfilePos; | 
|  | cmsUInt32Number preservationProfilesCount; | 
|  | cmsHPROFILE hLastProfile; | 
|  |  | 
|  |  | 
|  | // 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]); | 
|  |  | 
|  |  | 
|  | // Trim all CMYK devicelinks at the end | 
|  | lastProfilePos = nProfiles - 1; | 
|  | hLastProfile = hProfiles[lastProfilePos]; | 
|  |  | 
|  | while (lastProfilePos > 1) | 
|  | { | 
|  | hLastProfile = hProfiles[--lastProfilePos]; | 
|  | if (cmsGetColorSpace(hLastProfile) != cmsSigCmykData || | 
|  | cmsGetDeviceClass(hLastProfile) != cmsSigLinkClass) | 
|  | break; | 
|  | } | 
|  |  | 
|  | preservationProfilesCount = lastProfilePos + 1; | 
|  |  | 
|  | // Check for non-cmyk profiles | 
|  | if (cmsGetColorSpace(hProfiles[0]) != cmsSigCmykData || | 
|  | !(cmsGetColorSpace(hLastProfile) == cmsSigCmykData || | 
|  | cmsGetDeviceClass(hLastProfile) == 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)); | 
|  |  | 
|  | // Create a LUT holding normal ICC transform | 
|  | bp.cmyk2cmyk = DefaultICCintents(ContextID, | 
|  | preservationProfilesCount, | 
|  | ICCIntents, | 
|  | hProfiles, | 
|  | BPC, | 
|  | AdaptationStates, | 
|  | dwFlags); | 
|  |  | 
|  | if (bp.cmyk2cmyk == NULL) goto Error; | 
|  |  | 
|  | // Now, compute the tone curve | 
|  | bp.KTone = _cmsBuildKToneCurve(ContextID, | 
|  | 4096, | 
|  | preservationProfilesCount, | 
|  | 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; | 
|  |  | 
|  |  | 
|  | // Insert possible devicelinks at the end | 
|  | for (i = lastProfilePos + 1; i < nProfiles; i++) | 
|  | { | 
|  | cmsPipeline* devlink = _cmsReadDevicelinkLUT(hProfiles[i], ICCIntents[i]); | 
|  | if (devlink == NULL) | 
|  | goto Error; | 
|  |  | 
|  | if (!cmsPipelineCat(Result, devlink)) | 
|  | 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(CMSREGISTER const cmsUInt16Number In[], CMSREGISTER cmsUInt16Number Out[], CMSREGISTER 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 (fabsf(Outf[3] - LabK[3]) < (3.0 / 65535.0)) { | 
|  | return TRUE; | 
|  | } | 
|  |  | 
|  | // K differ, measure 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   = (cmsFloat64Number) 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; | 
|  | cmsUInt32Number lastProfilePos; | 
|  | cmsUInt32Number preservationProfilesCount; | 
|  | cmsHPROFILE hLastProfile; | 
|  | 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]); | 
|  |  | 
|  | // Trim all CMYK devicelinks at the end | 
|  | lastProfilePos = nProfiles - 1; | 
|  | hLastProfile = hProfiles[lastProfilePos]; | 
|  |  | 
|  | while (lastProfilePos > 1) | 
|  | { | 
|  | hLastProfile = hProfiles[--lastProfilePos]; | 
|  | if (cmsGetColorSpace(hLastProfile) != cmsSigCmykData || | 
|  | cmsGetDeviceClass(hLastProfile) != cmsSigLinkClass) | 
|  | break; | 
|  | } | 
|  |  | 
|  | preservationProfilesCount = lastProfilePos + 1; | 
|  |  | 
|  | // Check for non-cmyk profiles | 
|  | if (cmsGetColorSpace(hProfiles[0]) != cmsSigCmykData || | 
|  | !(cmsGetColorSpace(hLastProfile) == cmsSigCmykData || | 
|  | cmsGetDeviceClass(hLastProfile) == 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(hLastProfile, INTENT_RELATIVE_COLORIMETRIC); | 
|  | if (bp.LabK2cmyk == NULL) goto Cleanup; | 
|  |  | 
|  | // Get total area coverage (in 0..1 domain) | 
|  | bp.MaxTAC = cmsDetectTAC(hLastProfile) / 100.0; | 
|  | if (bp.MaxTAC <= 0) goto Cleanup; | 
|  |  | 
|  |  | 
|  | // Create a LUT holding normal ICC transform | 
|  | bp.cmyk2cmyk = DefaultICCintents(ContextID, | 
|  | preservationProfilesCount, | 
|  | ICCIntents, | 
|  | hProfiles, | 
|  | BPC, | 
|  | AdaptationStates, | 
|  | dwFlags); | 
|  | if (bp.cmyk2cmyk == NULL) goto Cleanup; | 
|  |  | 
|  | // Now the tone curve | 
|  | bp.KTone = _cmsBuildKToneCurve(ContextID, 4096, preservationProfilesCount, | 
|  | 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, hLastProfile, | 
|  | 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, hLastProfile, | 
|  | 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); | 
|  |  | 
|  | // Insert possible devicelinks at the end | 
|  | for (i = lastProfilePos + 1; i < nProfiles; i++) | 
|  | { | 
|  | cmsPipeline* devlink = _cmsReadDevicelinkLUT(hProfiles[i], ICCIntents[i]); | 
|  | if (devlink == NULL) | 
|  | goto Cleanup; | 
|  |  | 
|  | if (!cmsPipelineCat(Result, devlink)) | 
|  | goto Cleanup; | 
|  | } | 
|  |  | 
|  |  | 
|  | 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; | 
|  | } | 
|  |  |