1 //---------------------------------------------------------------------------------
3 // Little Color Management System
4 // Copyright (c) 1998-2010 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 #define cmsmin(a, b) (((a) < (b)) ? (a) : (b))
31 #define cmsmax(a, b) (((a) > (b)) ? (a) : (b))
33 // This file contains routines for resampling and LUT optimization, black point detection
34 // and black preservation.
36 // Black point detection -------------------------------------------------------------------------
39 // PCS -> PCS round trip transform, always uses relative intent on the device -> pcs
41 cmsHTRANSFORM
CreateRoundtripXForm(cmsHPROFILE hProfile
, cmsUInt32Number nIntent
)
43 cmsContext ContextID
= cmsGetProfileContextID(hProfile
);
44 cmsHPROFILE hLab
= cmsCreateLab4ProfileTHR(ContextID
, NULL
);
46 cmsBool BPC
[4] = { FALSE
, FALSE
, FALSE
, FALSE
};
47 cmsFloat64Number States
[4] = { 1.0, 1.0, 1.0, 1.0 };
48 cmsHPROFILE hProfiles
[4];
49 cmsUInt32Number Intents
[4];
51 hProfiles
[0] = hLab
; hProfiles
[1] = hProfile
; hProfiles
[2] = hProfile
; hProfiles
[3] = hLab
;
52 Intents
[0] = INTENT_RELATIVE_COLORIMETRIC
; Intents
[1] = nIntent
; Intents
[2] = INTENT_RELATIVE_COLORIMETRIC
; Intents
[3] = INTENT_RELATIVE_COLORIMETRIC
;
54 xform
= cmsCreateExtendedTransform(ContextID
, 4, hProfiles
, BPC
, Intents
,
55 States
, NULL
, 0, TYPE_Lab_DBL
, TYPE_Lab_DBL
, cmsFLAGS_NOCACHE
|cmsFLAGS_NOOPTIMIZE
);
57 cmsCloseProfile(hLab
);
61 // Use darker colorants to obtain black point. This works in the relative colorimetric intent and
62 // assumes more ink results in darker colors. No ink limit is assumed.
64 cmsBool
BlackPointAsDarkerColorant(cmsHPROFILE hInput
,
65 cmsUInt32Number Intent
,
66 cmsCIEXYZ
* BlackPoint
,
67 cmsUInt32Number dwFlags
)
69 cmsUInt16Number
*Black
;
71 cmsColorSpaceSignature Space
;
72 cmsUInt32Number nChannels
;
73 cmsUInt32Number dwFormat
;
77 cmsContext ContextID
= cmsGetProfileContextID(hInput
);
79 // If the profile does not support input direction, assume Black point 0
80 if (!cmsIsIntentSupported(hInput
, Intent
, LCMS_USED_AS_INPUT
)) {
82 BlackPoint
-> X
= BlackPoint
->Y
= BlackPoint
-> Z
= 0.0;
86 // Create a formatter which has n channels and floating point
87 dwFormat
= cmsFormatterForColorspaceOfProfile(hInput
, 2, FALSE
);
89 // Try to get black by using black colorant
90 Space
= cmsGetColorSpace(hInput
);
92 // This function returns darker colorant in 16 bits for several spaces
93 if (!_cmsEndPointsBySpace(Space
, NULL
, &Black
, &nChannels
)) {
95 BlackPoint
-> X
= BlackPoint
->Y
= BlackPoint
-> Z
= 0.0;
99 if (nChannels
!= T_CHANNELS(dwFormat
)) {
100 BlackPoint
-> X
= BlackPoint
->Y
= BlackPoint
-> Z
= 0.0;
104 // Lab will be used as the output space, but lab2 will avoid recursion
105 hLab
= cmsCreateLab2ProfileTHR(ContextID
, NULL
);
107 BlackPoint
-> X
= BlackPoint
->Y
= BlackPoint
-> Z
= 0.0;
111 // Create the transform
112 xform
= cmsCreateTransformTHR(ContextID
, hInput
, dwFormat
,
113 hLab
, TYPE_Lab_DBL
, Intent
, cmsFLAGS_NOOPTIMIZE
|cmsFLAGS_NOCACHE
);
114 cmsCloseProfile(hLab
);
118 // Something went wrong. Get rid of open resources and return zero as black
119 BlackPoint
-> X
= BlackPoint
->Y
= BlackPoint
-> Z
= 0.0;
123 // Convert black to Lab
124 cmsDoTransform(xform
, Black
, &Lab
, 1);
126 // Force it to be neutral, clip to max. L* of 50
128 if (Lab
.L
> 50) Lab
.L
= 50;
130 // Free the resources
131 cmsDeleteTransform(xform
);
133 // Convert from Lab (which is now clipped) to XYZ.
134 cmsLab2XYZ(NULL
, &BlackXYZ
, &Lab
);
136 if (BlackPoint
!= NULL
)
137 *BlackPoint
= BlackXYZ
;
141 cmsUNUSED_PARAMETER(dwFlags
);
144 // Get a black point of output CMYK profile, discounting any ink-limiting embedded
145 // in the profile. For doing that, we use perceptual intent in input direction:
146 // Lab (0, 0, 0) -> [Perceptual] Profile -> CMYK -> [Rel. colorimetric] Profile -> Lab
148 cmsBool
BlackPointUsingPerceptualBlack(cmsCIEXYZ
* BlackPoint
, cmsHPROFILE hProfile
)
150 cmsHTRANSFORM hRoundTrip
;
151 cmsCIELab LabIn
, LabOut
;
154 // Is the intent supported by the profile?
155 if (!cmsIsIntentSupported(hProfile
, INTENT_PERCEPTUAL
, LCMS_USED_AS_INPUT
)) {
157 BlackPoint
-> X
= BlackPoint
->Y
= BlackPoint
-> Z
= 0.0;
161 hRoundTrip
= CreateRoundtripXForm(hProfile
, INTENT_PERCEPTUAL
);
162 if (hRoundTrip
== NULL
) {
163 BlackPoint
-> X
= BlackPoint
->Y
= BlackPoint
-> Z
= 0.0;
167 LabIn
.L
= LabIn
.a
= LabIn
.b
= 0;
168 cmsDoTransform(hRoundTrip
, &LabIn
, &LabOut
, 1);
170 // Clip Lab to reasonable limits
171 if (LabOut
.L
> 50) LabOut
.L
= 50;
172 LabOut
.a
= LabOut
.b
= 0;
174 cmsDeleteTransform(hRoundTrip
);
177 cmsLab2XYZ(NULL
, &BlackXYZ
, &LabOut
);
179 if (BlackPoint
!= NULL
)
180 *BlackPoint
= BlackXYZ
;
185 // This function shouldn't exist at all -- there is such quantity of broken
186 // profiles on black point tag, that we must somehow fix chromaticity to
187 // avoid huge tint when doing Black point compensation. This function does
188 // just that. There is a special flag for using black point tag, but turned
189 // off by default because it is bogus on most profiles. The detection algorithm
190 // involves to turn BP to neutral and to use only L component.
191 cmsBool CMSEXPORT
cmsDetectBlackPoint(cmsCIEXYZ
* BlackPoint
, cmsHPROFILE hProfile
, cmsUInt32Number Intent
, cmsUInt32Number dwFlags
)
193 cmsProfileClassSignature devClass
;
195 // Make sure the device class is adequate
196 devClass
= cmsGetDeviceClass(hProfile
);
197 if (devClass
== cmsSigLinkClass
||
198 devClass
== cmsSigAbstractClass
||
199 devClass
== cmsSigNamedColorClass
) {
200 BlackPoint
-> X
= BlackPoint
->Y
= BlackPoint
-> Z
= 0.0;
204 // Make sure intent is adequate
205 if (Intent
!= INTENT_PERCEPTUAL
&&
206 Intent
!= INTENT_RELATIVE_COLORIMETRIC
&&
207 Intent
!= INTENT_SATURATION
) {
208 BlackPoint
-> X
= BlackPoint
->Y
= BlackPoint
-> Z
= 0.0;
212 // v4 + perceptual & saturation intents does have its own black point, and it is
213 // well specified enough to use it. Black point tag is deprecated in V4.
214 if ((cmsGetEncodedICCversion(hProfile
) >= 0x4000000) &&
215 (Intent
== INTENT_PERCEPTUAL
|| Intent
== INTENT_SATURATION
)) {
217 // Matrix shaper share MRC & perceptual intents
218 if (cmsIsMatrixShaper(hProfile
))
219 return BlackPointAsDarkerColorant(hProfile
, INTENT_RELATIVE_COLORIMETRIC
, BlackPoint
, 0);
221 // Get Perceptual black out of v4 profiles. That is fixed for perceptual & saturation intents
222 BlackPoint
-> X
= cmsPERCEPTUAL_BLACK_X
;
223 BlackPoint
-> Y
= cmsPERCEPTUAL_BLACK_Y
;
224 BlackPoint
-> Z
= cmsPERCEPTUAL_BLACK_Z
;
230 #ifdef CMS_USE_PROFILE_BLACK_POINT_TAG
232 // v2, v4 rel/abs colorimetric
233 if (cmsIsTag(hProfile
, cmsSigMediaBlackPointTag
) &&
234 Intent
== INTENT_RELATIVE_COLORIMETRIC
) {
236 cmsCIEXYZ
*BlackPtr
, BlackXYZ
, UntrustedBlackPoint
, TrustedBlackPoint
, MediaWhite
;
239 // If black point is specified, then use it,
241 BlackPtr
= cmsReadTag(hProfile
, cmsSigMediaBlackPointTag
);
242 if (BlackPtr
!= NULL
) {
244 BlackXYZ
= *BlackPtr
;
245 _cmsReadMediaWhitePoint(&MediaWhite
, hProfile
);
247 // Black point is absolute XYZ, so adapt to D50 to get PCS value
248 cmsAdaptToIlluminant(&UntrustedBlackPoint
, &MediaWhite
, cmsD50_XYZ(), &BlackXYZ
);
250 // Force a=b=0 to get rid of any chroma
251 cmsXYZ2Lab(NULL
, &Lab
, &UntrustedBlackPoint
);
253 if (Lab
.L
> 50) Lab
.L
= 50; // Clip to L* <= 50
254 cmsLab2XYZ(NULL
, &TrustedBlackPoint
, &Lab
);
256 if (BlackPoint
!= NULL
)
257 *BlackPoint
= TrustedBlackPoint
;
264 // That is about v2 profiles.
266 // If output profile, discount ink-limiting and that's all
267 if (Intent
== INTENT_RELATIVE_COLORIMETRIC
&&
268 (cmsGetDeviceClass(hProfile
) == cmsSigOutputClass
) &&
269 (cmsGetColorSpace(hProfile
) == cmsSigCmykData
))
270 return BlackPointUsingPerceptualBlack(BlackPoint
, hProfile
);
272 // Nope, compute BP using current intent.
273 return BlackPointAsDarkerColorant(hProfile
, Intent
, BlackPoint
, dwFlags
);
278 // ---------------------------------------------------------------------------------------------------------
280 // Least Squares Fit of a Quadratic Curve to Data
281 // http://www.personal.psu.edu/jhm/f90/lectures/lsq2.html
284 cmsFloat64Number
RootOfLeastSquaresFitQuadraticCurve(int n
, cmsFloat64Number x
[], cmsFloat64Number y
[])
286 double sum_x
= 0, sum_x2
= 0, sum_x3
= 0, sum_x4
= 0;
287 double sum_y
= 0, sum_yx
= 0, sum_yx2
= 0;
295 for (i
=0; i
< n
; i
++) {
303 sum_x4
+= xn
*xn
*xn
*xn
;
310 _cmsVEC3init(&m
.v
[0], n
, sum_x
, sum_x2
);
311 _cmsVEC3init(&m
.v
[1], sum_x
, sum_x2
, sum_x3
);
312 _cmsVEC3init(&m
.v
[2], sum_x2
, sum_x3
, sum_x4
);
314 _cmsVEC3init(&v
, sum_y
, sum_yx
, sum_yx2
);
316 if (!_cmsMAT3solve(&res
, &m
, &v
)) return 0;
323 if (fabs(a
) < 1.0E-10) {
325 return cmsmin(0, cmsmax(50, -c
/b
));
329 d
= b
*b
- 4.0 * a
* c
;
335 double rt
= (-b
+ sqrt(d
)) / (2.0 * a
);
337 return cmsmax(0, cmsmin(50, rt
));
345 cmsBool IsMonotonic(int n, const cmsFloat64Number Table[])
348 cmsFloat64Number last;
352 for (i = n-2; i >= 0; --i) {
366 // Calculates the black point of a destination profile.
367 // This algorithm comes from the Adobe paper disclosing its black point compensation method.
368 cmsBool CMSEXPORT
cmsDetectDestinationBlackPoint(cmsCIEXYZ
* BlackPoint
, cmsHPROFILE hProfile
, cmsUInt32Number Intent
, cmsUInt32Number dwFlags
)
370 cmsColorSpaceSignature ColorSpace
;
371 cmsHTRANSFORM hRoundTrip
= NULL
;
372 cmsCIELab InitialLab
, destLab
, Lab
;
373 cmsFloat64Number inRamp
[256], outRamp
[256];
374 cmsFloat64Number MinL
, MaxL
;
375 cmsBool NearlyStraightMidrange
= TRUE
;
376 cmsFloat64Number yRamp
[256];
377 cmsFloat64Number x
[256], y
[256];
378 cmsFloat64Number lo
, hi
;
380 cmsProfileClassSignature devClass
;
382 // Make sure the device class is adequate
383 devClass
= cmsGetDeviceClass(hProfile
);
384 if (devClass
== cmsSigLinkClass
||
385 devClass
== cmsSigAbstractClass
||
386 devClass
== cmsSigNamedColorClass
) {
387 BlackPoint
-> X
= BlackPoint
->Y
= BlackPoint
-> Z
= 0.0;
391 // Make sure intent is adequate
392 if (Intent
!= INTENT_PERCEPTUAL
&&
393 Intent
!= INTENT_RELATIVE_COLORIMETRIC
&&
394 Intent
!= INTENT_SATURATION
) {
395 BlackPoint
-> X
= BlackPoint
->Y
= BlackPoint
-> Z
= 0.0;
400 // v4 + perceptual & saturation intents does have its own black point, and it is
401 // well specified enough to use it. Black point tag is deprecated in V4.
402 if ((cmsGetEncodedICCversion(hProfile
) >= 0x4000000) &&
403 (Intent
== INTENT_PERCEPTUAL
|| Intent
== INTENT_SATURATION
)) {
405 // Matrix shaper share MRC & perceptual intents
406 if (cmsIsMatrixShaper(hProfile
))
407 return BlackPointAsDarkerColorant(hProfile
, INTENT_RELATIVE_COLORIMETRIC
, BlackPoint
, 0);
409 // Get Perceptual black out of v4 profiles. That is fixed for perceptual & saturation intents
410 BlackPoint
-> X
= cmsPERCEPTUAL_BLACK_X
;
411 BlackPoint
-> Y
= cmsPERCEPTUAL_BLACK_Y
;
412 BlackPoint
-> Z
= cmsPERCEPTUAL_BLACK_Z
;
417 // Check if the profile is lut based and gray, rgb or cmyk (7.2 in Adobe's document)
418 ColorSpace
= cmsGetColorSpace(hProfile
);
419 if (!cmsIsCLUT(hProfile
, Intent
, LCMS_USED_AS_OUTPUT
) ||
420 (ColorSpace
!= cmsSigGrayData
&&
421 ColorSpace
!= cmsSigRgbData
&&
422 ColorSpace
!= cmsSigCmykData
)) {
424 // In this case, handle as input case
425 return cmsDetectBlackPoint(BlackPoint
, hProfile
, Intent
, dwFlags
);
428 // It is one of the valid cases!, use Adobe algorithm
431 // Set a first guess, that should work on good profiles.
432 if (Intent
== INTENT_RELATIVE_COLORIMETRIC
) {
436 // calculate initial Lab as source black point
437 if (!cmsDetectBlackPoint(&IniXYZ
, hProfile
, Intent
, dwFlags
)) {
441 // convert the XYZ to lab
442 cmsXYZ2Lab(NULL
, &InitialLab
, &IniXYZ
);
446 // set the initial Lab to zero, that should be the black point for perceptual and saturation
456 // Create a roundtrip. Define a Transform BT for all x in L*a*b*
457 hRoundTrip
= CreateRoundtripXForm(hProfile
, Intent
);
458 if (hRoundTrip
== NULL
) return FALSE
;
462 for (l
=0; l
< 256; l
++) {
464 Lab
.L
= (cmsFloat64Number
) (l
* 100.0) / 255.0;
465 Lab
.a
= cmsmin(50, cmsmax(-50, InitialLab
.a
));
466 Lab
.b
= cmsmin(50, cmsmax(-50, InitialLab
.b
));
468 cmsDoTransform(hRoundTrip
, &Lab
, &destLab
, 1);
471 outRamp
[l
] = destLab
.L
;
475 for (l
= 254; l
> 0; --l
) {
476 outRamp
[l
] = cmsmin(outRamp
[l
], outRamp
[l
+1]);
480 if (! (outRamp
[0] < outRamp
[255])) {
482 cmsDeleteTransform(hRoundTrip
);
483 BlackPoint
-> X
= BlackPoint
->Y
= BlackPoint
-> Z
= 0.0;
488 // Test for mid range straight (only on relative colorimetric)
490 NearlyStraightMidrange
= TRUE
;
491 MinL
= outRamp
[0]; MaxL
= outRamp
[255];
492 if (Intent
== INTENT_RELATIVE_COLORIMETRIC
) {
494 for (l
=0; l
< 256; l
++) {
496 if (! ((inRamp
[l
] <= MinL
+ 0.2 * (MaxL
- MinL
) ) ||
497 (fabs(inRamp
[l
] - outRamp
[l
]) < 4.0 )))
498 NearlyStraightMidrange
= FALSE
;
501 // If the mid range is straight (as determined above) then the
502 // DestinationBlackPoint shall be the same as initialLab.
503 // Otherwise, the DestinationBlackPoint shall be determined
504 // using curve fitting.
506 if (NearlyStraightMidrange
) {
508 cmsLab2XYZ(NULL
, BlackPoint
, &InitialLab
);
509 cmsDeleteTransform(hRoundTrip
);
515 // curve fitting: The round-trip curve normally looks like a nearly constant section at the black point,
516 // with a corner and a nearly straight line to the white point.
518 for (l
=0; l
< 256; l
++) {
520 yRamp
[l
] = (outRamp
[l
] - MinL
) / (MaxL
- MinL
);
523 // find the black point using the least squares error quadratic curve fitting
525 if (Intent
== INTENT_RELATIVE_COLORIMETRIC
) {
531 // Perceptual and saturation
536 // Capture shadow points for the fitting.
538 for (l
=0; l
< 256; l
++) {
540 cmsFloat64Number ff
= yRamp
[l
];
542 if (ff
>= lo
&& ff
< hi
) {
550 // No suitable points
552 cmsDeleteTransform(hRoundTrip
);
553 BlackPoint
-> X
= BlackPoint
->Y
= BlackPoint
-> Z
= 0.0;
558 // fit and get the vertex of quadratic curve
559 Lab
.L
= RootOfLeastSquaresFitQuadraticCurve(n
, x
, y
);
561 if (Lab
.L
< 0.0) { // clip to zero L* if the vertex is negative
565 Lab
.a
= InitialLab
.a
;
566 Lab
.b
= InitialLab
.b
;
568 cmsLab2XYZ(NULL
, BlackPoint
, &Lab
);
570 cmsDeleteTransform(hRoundTrip
);