1 //---------------------------------------------------------------------------------
\r
3 // Little Color Management System
\r
4 // Copyright (c) 1998-2016 Marti Maria Saguer
\r
6 // Permission is hereby granted, free of charge, to any person obtaining
\r
7 // a copy of this software and associated documentation files (the "Software"),
\r
8 // to deal in the Software without restriction, including without limitation
\r
9 // the rights to use, copy, modify, merge, publish, distribute, sublicense,
\r
10 // and/or sell copies of the Software, and to permit persons to whom the Software
\r
11 // is furnished to do so, subject to the following conditions:
\r
13 // The above copyright notice and this permission notice shall be included in
\r
14 // all copies or substantial portions of the Software.
\r
16 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
\r
17 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
\r
18 // THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
\r
19 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
\r
20 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
\r
21 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
\r
22 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\r
24 //---------------------------------------------------------------------------------
\r
27 #include "lcms2_internal.h"
\r
30 // Auxiliar: append a Lab identity after the given sequence of profiles
\r
31 // and return the transform. Lab profile is closed, rest of profiles are kept open.
\r
32 cmsHTRANSFORM _cmsChain2Lab(cmsContext ContextID,
\r
33 cmsUInt32Number nProfiles,
\r
34 cmsUInt32Number InputFormat,
\r
35 cmsUInt32Number OutputFormat,
\r
36 const cmsUInt32Number Intents[],
\r
37 const cmsHPROFILE hProfiles[],
\r
38 const cmsBool BPC[],
\r
39 const cmsFloat64Number AdaptationStates[],
\r
40 cmsUInt32Number dwFlags)
\r
42 cmsHTRANSFORM xform;
\r
44 cmsHPROFILE ProfileList[256];
\r
45 cmsBool BPCList[256];
\r
46 cmsFloat64Number AdaptationList[256];
\r
47 cmsUInt32Number IntentList[256];
\r
50 // This is a rather big number and there is no need of dynamic memory
\r
51 // since we are adding a profile, 254 + 1 = 255 and this is the limit
\r
52 if (nProfiles > 254) return NULL;
\r
55 hLab = cmsCreateLab4ProfileTHR(ContextID, NULL);
\r
56 if (hLab == NULL) return NULL;
\r
58 // Create a copy of parameters
\r
59 for (i=0; i < nProfiles; i++) {
\r
61 ProfileList[i] = hProfiles[i];
\r
62 BPCList[i] = BPC[i];
\r
63 AdaptationList[i] = AdaptationStates[i];
\r
64 IntentList[i] = Intents[i];
\r
67 // Place Lab identity at chain's end.
\r
68 ProfileList[nProfiles] = hLab;
\r
69 BPCList[nProfiles] = 0;
\r
70 AdaptationList[nProfiles] = 1.0;
\r
71 IntentList[nProfiles] = INTENT_RELATIVE_COLORIMETRIC;
\r
73 // Create the transform
\r
74 xform = cmsCreateExtendedTransform(ContextID, nProfiles + 1, ProfileList,
\r
83 cmsCloseProfile(hLab);
\r
89 // Compute K -> L* relationship. Flags may include black point compensation. In this case,
\r
90 // the relationship is assumed from the profile with BPC to a black point zero.
\r
92 cmsToneCurve* ComputeKToLstar(cmsContext ContextID,
\r
93 cmsUInt32Number nPoints,
\r
94 cmsUInt32Number nProfiles,
\r
95 const cmsUInt32Number Intents[],
\r
96 const cmsHPROFILE hProfiles[],
\r
97 const cmsBool BPC[],
\r
98 const cmsFloat64Number AdaptationStates[],
\r
99 cmsUInt32Number dwFlags)
\r
101 cmsToneCurve* out = NULL;
\r
103 cmsHTRANSFORM xform;
\r
105 cmsFloat32Number cmyk[4];
\r
106 cmsFloat32Number* SampledPoints;
\r
108 xform = _cmsChain2Lab(ContextID, nProfiles, TYPE_CMYK_FLT, TYPE_Lab_DBL, Intents, hProfiles, BPC, AdaptationStates, dwFlags);
\r
109 if (xform == NULL) return NULL;
\r
111 SampledPoints = (cmsFloat32Number*) _cmsCalloc(ContextID, nPoints, sizeof(cmsFloat32Number));
\r
112 if (SampledPoints == NULL) goto Error;
\r
114 for (i=0; i < nPoints; i++) {
\r
119 cmyk[3] = (cmsFloat32Number) ((i * 100.0) / (nPoints-1));
\r
121 cmsDoTransform(xform, cmyk, &Lab, 1);
\r
122 SampledPoints[i]= (cmsFloat32Number) (1.0 - Lab.L / 100.0); // Negate K for easier operation
\r
125 out = cmsBuildTabulatedToneCurveFloat(ContextID, nPoints, SampledPoints);
\r
129 cmsDeleteTransform(xform);
\r
130 if (SampledPoints) _cmsFree(ContextID, SampledPoints);
\r
136 // Compute Black tone curve on a CMYK -> CMYK transform. This is done by
\r
137 // using the proof direction on both profiles to find K->L* relationship
\r
138 // then joining both curves. dwFlags may include black point compensation.
\r
139 cmsToneCurve* _cmsBuildKToneCurve(cmsContext ContextID,
\r
140 cmsUInt32Number nPoints,
\r
141 cmsUInt32Number nProfiles,
\r
142 const cmsUInt32Number Intents[],
\r
143 const cmsHPROFILE hProfiles[],
\r
144 const cmsBool BPC[],
\r
145 const cmsFloat64Number AdaptationStates[],
\r
146 cmsUInt32Number dwFlags)
\r
148 cmsToneCurve *in, *out, *KTone;
\r
150 // Make sure CMYK -> CMYK
\r
151 if (cmsGetColorSpace(hProfiles[0]) != cmsSigCmykData ||
\r
152 cmsGetColorSpace(hProfiles[nProfiles-1])!= cmsSigCmykData) return NULL;
\r
155 // Make sure last is an output profile
\r
156 if (cmsGetDeviceClass(hProfiles[nProfiles - 1]) != cmsSigOutputClass) return NULL;
\r
158 // Create individual curves. BPC works also as each K to L* is
\r
159 // computed as a BPC to zero black point in case of L*
\r
160 in = ComputeKToLstar(ContextID, nPoints, nProfiles - 1, Intents, hProfiles, BPC, AdaptationStates, dwFlags);
\r
161 if (in == NULL) return NULL;
\r
163 out = ComputeKToLstar(ContextID, nPoints, 1,
\r
164 Intents + (nProfiles - 1),
\r
165 &hProfiles [nProfiles - 1],
\r
166 BPC + (nProfiles - 1),
\r
167 AdaptationStates + (nProfiles - 1),
\r
170 cmsFreeToneCurve(in);
\r
174 // Build the relationship. This effectively limits the maximum accuracy to 16 bits, but
\r
175 // since this is used on black-preserving LUTs, we are not loosing accuracy in any case
\r
176 KTone = cmsJoinToneCurve(ContextID, in, out, nPoints);
\r
178 // Get rid of components
\r
179 cmsFreeToneCurve(in); cmsFreeToneCurve(out);
\r
181 // Something went wrong...
\r
182 if (KTone == NULL) return NULL;
\r
184 // Make sure it is monotonic
\r
185 if (!cmsIsToneCurveMonotonic(KTone)) {
\r
186 cmsFreeToneCurve(KTone);
\r
194 // Gamut LUT Creation -----------------------------------------------------------------------------------------
\r
196 // Used by gamut & softproofing
\r
200 cmsHTRANSFORM hInput; // From whatever input color space. 16 bits to DBL
\r
201 cmsHTRANSFORM hForward, hReverse; // Transforms going from Lab to colorant and back
\r
202 cmsFloat64Number Thereshold; // The thereshold after which is considered out of gamut
\r
206 // This sampler does compute gamut boundaries by comparing original
\r
207 // values with a transform going back and forth. Values above ERR_THERESHOLD
\r
208 // of maximum are considered out of gamut.
\r
210 #define ERR_THERESHOLD 5
\r
214 int GamutSampler(register const cmsUInt16Number In[], register cmsUInt16Number Out[], register void* Cargo)
\r
216 GAMUTCHAIN* t = (GAMUTCHAIN* ) Cargo;
\r
217 cmsCIELab LabIn1, LabOut1;
\r
218 cmsCIELab LabIn2, LabOut2;
\r
219 cmsUInt16Number Proof[cmsMAXCHANNELS], Proof2[cmsMAXCHANNELS];
\r
220 cmsFloat64Number dE1, dE2, ErrorRatio;
\r
222 // Assume in-gamut by default.
\r
225 // Convert input to Lab
\r
226 cmsDoTransform(t -> hInput, In, &LabIn1, 1);
\r
228 // converts from PCS to colorant. This always
\r
229 // does return in-gamut values,
\r
230 cmsDoTransform(t -> hForward, &LabIn1, Proof, 1);
\r
232 // Now, do the inverse, from colorant to PCS.
\r
233 cmsDoTransform(t -> hReverse, Proof, &LabOut1, 1);
\r
235 memmove(&LabIn2, &LabOut1, sizeof(cmsCIELab));
\r
237 // Try again, but this time taking Check as input
\r
238 cmsDoTransform(t -> hForward, &LabOut1, Proof2, 1);
\r
239 cmsDoTransform(t -> hReverse, Proof2, &LabOut2, 1);
\r
241 // Take difference of direct value
\r
242 dE1 = cmsDeltaE(&LabIn1, &LabOut1);
\r
244 // Take difference of converted value
\r
245 dE2 = cmsDeltaE(&LabIn2, &LabOut2);
\r
248 // if dE1 is small and dE2 is small, value is likely to be in gamut
\r
249 if (dE1 < t->Thereshold && dE2 < t->Thereshold)
\r
253 // if dE1 is small and dE2 is big, undefined. Assume in gamut
\r
254 if (dE1 < t->Thereshold && dE2 > t->Thereshold)
\r
257 // dE1 is big and dE2 is small, clearly out of gamut
\r
258 if (dE1 > t->Thereshold && dE2 < t->Thereshold)
\r
259 Out[0] = (cmsUInt16Number) _cmsQuickFloor((dE1 - t->Thereshold) + .5);
\r
262 // dE1 is big and dE2 is also big, could be due to perceptual mapping
\r
263 // so take error ratio
\r
267 ErrorRatio = dE1 / dE2;
\r
269 if (ErrorRatio > t->Thereshold)
\r
270 Out[0] = (cmsUInt16Number) _cmsQuickFloor((ErrorRatio - t->Thereshold) + .5);
\r
280 // Does compute a gamut LUT going back and forth across pcs -> relativ. colorimetric intent -> pcs
\r
281 // the dE obtained is then annotated on the LUT. Values truely out of gamut are clipped to dE = 0xFFFE
\r
282 // and values changed are supposed to be handled by any gamut remapping, so, are out of gamut as well.
\r
284 // **WARNING: This algorithm does assume that gamut remapping algorithms does NOT move in-gamut colors,
\r
285 // of course, many perceptual and saturation intents does not work in such way, but relativ. ones should.
\r
287 cmsPipeline* _cmsCreateGamutCheckPipeline(cmsContext ContextID,
\r
288 cmsHPROFILE hProfiles[],
\r
290 cmsUInt32Number Intents[],
\r
291 cmsFloat64Number AdaptationStates[],
\r
292 cmsUInt32Number nGamutPCSposition,
\r
293 cmsHPROFILE hGamut)
\r
296 cmsPipeline* Gamut;
\r
298 cmsUInt32Number dwFormat;
\r
300 int nChannels, nGridpoints;
\r
301 cmsColorSpaceSignature ColorSpace;
\r
303 cmsHPROFILE ProfileList[256];
\r
304 cmsBool BPCList[256];
\r
305 cmsFloat64Number AdaptationList[256];
\r
306 cmsUInt32Number IntentList[256];
\r
308 memset(&Chain, 0, sizeof(GAMUTCHAIN));
\r
311 if (nGamutPCSposition <= 0 || nGamutPCSposition > 255) {
\r
312 cmsSignalError(ContextID, cmsERROR_RANGE, "Wrong position of PCS. 1..255 expected, %d found.", nGamutPCSposition);
\r
316 hLab = cmsCreateLab4ProfileTHR(ContextID, NULL);
\r
317 if (hLab == NULL) return NULL;
\r
320 // The figure of merit. On matrix-shaper profiles, should be almost zero as
\r
321 // the conversion is pretty exact. On LUT based profiles, different resolutions
\r
322 // of input and output CLUT may result in differences.
\r
324 if (cmsIsMatrixShaper(hGamut)) {
\r
326 Chain.Thereshold = 1.0;
\r
329 Chain.Thereshold = ERR_THERESHOLD;
\r
333 // Create a copy of parameters
\r
334 for (i=0; i < nGamutPCSposition; i++) {
\r
335 ProfileList[i] = hProfiles[i];
\r
336 BPCList[i] = BPC[i];
\r
337 AdaptationList[i] = AdaptationStates[i];
\r
338 IntentList[i] = Intents[i];
\r
341 // Fill Lab identity
\r
342 ProfileList[nGamutPCSposition] = hLab;
\r
343 BPCList[nGamutPCSposition] = 0;
\r
344 AdaptationList[nGamutPCSposition] = 1.0;
\r
345 IntentList[nGamutPCSposition] = INTENT_RELATIVE_COLORIMETRIC;
\r
348 ColorSpace = cmsGetColorSpace(hGamut);
\r
350 nChannels = cmsChannelsOf(ColorSpace);
\r
351 nGridpoints = _cmsReasonableGridpointsByColorspace(ColorSpace, cmsFLAGS_HIGHRESPRECALC);
\r
352 dwFormat = (CHANNELS_SH(nChannels)|BYTES_SH(2));
\r
354 // 16 bits to Lab double
\r
355 Chain.hInput = cmsCreateExtendedTransform(ContextID,
\r
356 nGamutPCSposition + 1,
\r
362 dwFormat, TYPE_Lab_DBL,
\r
366 // Does create the forward step. Lab double to device
\r
367 dwFormat = (CHANNELS_SH(nChannels)|BYTES_SH(2));
\r
368 Chain.hForward = cmsCreateTransformTHR(ContextID,
\r
369 hLab, TYPE_Lab_DBL,
\r
371 INTENT_RELATIVE_COLORIMETRIC,
\r
374 // Does create the backwards step
\r
375 Chain.hReverse = cmsCreateTransformTHR(ContextID, hGamut, dwFormat,
\r
376 hLab, TYPE_Lab_DBL,
\r
377 INTENT_RELATIVE_COLORIMETRIC,
\r
382 if (Chain.hInput && Chain.hForward && Chain.hReverse) {
\r
384 // Go on, try to compute gamut LUT from PCS. This consist on a single channel containing
\r
385 // dE when doing a transform back and forth on the colorimetric intent.
\r
387 Gamut = cmsPipelineAlloc(ContextID, 3, 1);
\r
388 if (Gamut != NULL) {
\r
390 CLUT = cmsStageAllocCLut16bit(ContextID, nGridpoints, nChannels, 1, NULL);
\r
391 if (!cmsPipelineInsertStage(Gamut, cmsAT_BEGIN, CLUT)) {
\r
392 cmsPipelineFree(Gamut);
\r
396 cmsStageSampleCLut16bit(CLUT, GamutSampler, (void*) &Chain, 0);
\r
401 Gamut = NULL; // Didn't work...
\r
403 // Free all needed stuff.
\r
404 if (Chain.hInput) cmsDeleteTransform(Chain.hInput);
\r
405 if (Chain.hForward) cmsDeleteTransform(Chain.hForward);
\r
406 if (Chain.hReverse) cmsDeleteTransform(Chain.hReverse);
\r
407 if (hLab) cmsCloseProfile(hLab);
\r
409 // And return computed hull
\r
413 // Total Area Coverage estimation ----------------------------------------------------------------
\r
416 cmsUInt32Number nOutputChans;
\r
417 cmsHTRANSFORM hRoundTrip;
\r
418 cmsFloat32Number MaxTAC;
\r
419 cmsFloat32Number MaxInput[cmsMAXCHANNELS];
\r
424 // This callback just accounts the maximum ink dropped in the given node. It does not populate any
\r
425 // memory, as the destination table is NULL. Its only purpose it to know the global maximum.
\r
427 int EstimateTAC(register const cmsUInt16Number In[], register cmsUInt16Number Out[], register void * Cargo)
\r
429 cmsTACestimator* bp = (cmsTACestimator*) Cargo;
\r
430 cmsFloat32Number RoundTrip[cmsMAXCHANNELS];
\r
432 cmsFloat32Number Sum;
\r
435 // Evaluate the xform
\r
436 cmsDoTransform(bp->hRoundTrip, In, RoundTrip, 1);
\r
438 // All all amounts of ink
\r
439 for (Sum=0, i=0; i < bp ->nOutputChans; i++)
\r
440 Sum += RoundTrip[i];
\r
442 // If above maximum, keep track of input values
\r
443 if (Sum > bp ->MaxTAC) {
\r
447 for (i=0; i < bp ->nOutputChans; i++) {
\r
448 bp ->MaxInput[i] = In[i];
\r
454 cmsUNUSED_PARAMETER(Out);
\r
458 // Detect Total area coverage of the profile
\r
459 cmsFloat64Number CMSEXPORT cmsDetectTAC(cmsHPROFILE hProfile)
\r
461 cmsTACestimator bp;
\r
462 cmsUInt32Number dwFormatter;
\r
463 cmsUInt32Number GridPoints[MAX_INPUT_DIMENSIONS];
\r
465 cmsContext ContextID = cmsGetProfileContextID(hProfile);
\r
467 // TAC only works on output profiles
\r
468 if (cmsGetDeviceClass(hProfile) != cmsSigOutputClass) {
\r
472 // Create a fake formatter for result
\r
473 dwFormatter = cmsFormatterForColorspaceOfProfile(hProfile, 4, TRUE);
\r
475 bp.nOutputChans = T_CHANNELS(dwFormatter);
\r
476 bp.MaxTAC = 0; // Initial TAC is 0
\r
479 if (bp.nOutputChans >= cmsMAXCHANNELS) return 0;
\r
481 hLab = cmsCreateLab4ProfileTHR(ContextID, NULL);
\r
482 if (hLab == NULL) return 0;
\r
483 // Setup a roundtrip on perceptual intent in output profile for TAC estimation
\r
484 bp.hRoundTrip = cmsCreateTransformTHR(ContextID, hLab, TYPE_Lab_16,
\r
485 hProfile, dwFormatter, INTENT_PERCEPTUAL, cmsFLAGS_NOOPTIMIZE|cmsFLAGS_NOCACHE);
\r
487 cmsCloseProfile(hLab);
\r
488 if (bp.hRoundTrip == NULL) return 0;
\r
490 // For L* we only need black and white. For C* we need many points
\r
492 GridPoints[1] = 74;
\r
493 GridPoints[2] = 74;
\r
496 if (!cmsSliceSpace16(3, GridPoints, EstimateTAC, &bp)) {
\r
500 cmsDeleteTransform(bp.hRoundTrip);
\r
507 // Carefully, clamp on CIELab space.
\r
509 cmsBool CMSEXPORT cmsDesaturateLab(cmsCIELab* Lab,
\r
510 double amax, double amin,
\r
511 double bmax, double bmin)
\r
514 // Whole Luma surface to zero
\r
516 if (Lab -> L < 0) {
\r
518 Lab-> L = Lab->a = Lab-> b = 0.0;
\r
522 // Clamp white, DISCARD HIGHLIGHTS. This is done
\r
523 // in such way because icc spec doesn't allow the
\r
524 // use of L>100 as a highlight means.
\r
529 // Check out gamut prism, on a, b faces
\r
531 if (Lab -> a < amin || Lab->a > amax||
\r
532 Lab -> b < bmin || Lab->b > bmax) {
\r
537 // Falls outside a, b limits. Transports to LCh space,
\r
538 // and then do the clipping
\r
541 if (Lab -> a == 0.0) { // Is hue exactly 90?
\r
543 // atan will not work, so clamp here
\r
544 Lab -> b = Lab->b < 0 ? bmin : bmax;
\r
548 cmsLab2LCh(&LCh, Lab);
\r
550 slope = Lab -> b / Lab -> a;
\r
553 // There are 4 zones
\r
555 if ((h >= 0. && h < 45.) ||
\r
556 (h >= 315 && h <= 360.)) {
\r
560 Lab -> b = amax * slope;
\r
563 if (h >= 45. && h < 135.)
\r
567 Lab -> a = bmax / slope;
\r
570 if (h >= 135. && h < 225.) {
\r
573 Lab -> b = amin * slope;
\r
577 if (h >= 225. && h < 315.) {
\r
580 Lab -> a = bmin / slope;
\r
583 cmsSignalError(0, cmsERROR_RANGE, "Invalid angle");
\r