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