1 | /* |
2 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
3 | * |
4 | * This code is free software; you can redistribute it and/or modify it |
5 | * under the terms of the GNU General Public License version 2 only, as |
6 | * published by the Free Software Foundation. Oracle designates this |
7 | * particular file as subject to the "Classpath" exception as provided |
8 | * by Oracle in the LICENSE file that accompanied this code. |
9 | * |
10 | * This code is distributed in the hope that it will be useful, but WITHOUT |
11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
13 | * version 2 for more details (a copy is included in the LICENSE file that |
14 | * accompanied this code). |
15 | * |
16 | * You should have received a copy of the GNU General Public License version |
17 | * 2 along with this work; if not, write to the Free Software Foundation, |
18 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
19 | * |
20 | * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
21 | * or visit www.oracle.com if you need additional information or have any |
22 | * questions. |
23 | */ |
24 | |
25 | // This file is available under and governed by the GNU General Public |
26 | // License version 2 only, as published by the Free Software Foundation. |
27 | // However, the following notice accompanied the original version of this |
28 | // file: |
29 | // |
30 | //--------------------------------------------------------------------------------- |
31 | // |
32 | // Little Color Management System |
33 | // Copyright (c) 1998-2017 Marti Maria Saguer |
34 | // |
35 | // Permission is hereby granted, free of charge, to any person obtaining |
36 | // a copy of this software and associated documentation files (the "Software"), |
37 | // to deal in the Software without restriction, including without limitation |
38 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, |
39 | // and/or sell copies of the Software, and to permit persons to whom the Software |
40 | // is furnished to do so, subject to the following conditions: |
41 | // |
42 | // The above copyright notice and this permission notice shall be included in |
43 | // all copies or substantial portions of the Software. |
44 | // |
45 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
46 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO |
47 | // THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
48 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
49 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
50 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
51 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
52 | // |
53 | //--------------------------------------------------------------------------------- |
54 | // |
55 | |
56 | #include "lcms2_internal.h" |
57 | |
58 | |
59 | // Auxiliary: append a Lab identity after the given sequence of profiles |
60 | // and return the transform. Lab profile is closed, rest of profiles are kept open. |
61 | cmsHTRANSFORM _cmsChain2Lab(cmsContext ContextID, |
62 | cmsUInt32Number nProfiles, |
63 | cmsUInt32Number InputFormat, |
64 | cmsUInt32Number OutputFormat, |
65 | const cmsUInt32Number Intents[], |
66 | const cmsHPROFILE hProfiles[], |
67 | const cmsBool BPC[], |
68 | const cmsFloat64Number AdaptationStates[], |
69 | cmsUInt32Number dwFlags) |
70 | { |
71 | cmsHTRANSFORM xform; |
72 | cmsHPROFILE hLab; |
73 | cmsHPROFILE ProfileList[256]; |
74 | cmsBool BPCList[256]; |
75 | cmsFloat64Number AdaptationList[256]; |
76 | cmsUInt32Number IntentList[256]; |
77 | cmsUInt32Number i; |
78 | |
79 | // This is a rather big number and there is no need of dynamic memory |
80 | // since we are adding a profile, 254 + 1 = 255 and this is the limit |
81 | if (nProfiles > 254) return NULL; |
82 | |
83 | // The output space |
84 | hLab = cmsCreateLab4ProfileTHR(ContextID, NULL); |
85 | if (hLab == NULL) return NULL; |
86 | |
87 | // Create a copy of parameters |
88 | for (i=0; i < nProfiles; i++) { |
89 | |
90 | ProfileList[i] = hProfiles[i]; |
91 | BPCList[i] = BPC[i]; |
92 | AdaptationList[i] = AdaptationStates[i]; |
93 | IntentList[i] = Intents[i]; |
94 | } |
95 | |
96 | // Place Lab identity at chain's end. |
97 | ProfileList[nProfiles] = hLab; |
98 | BPCList[nProfiles] = 0; |
99 | AdaptationList[nProfiles] = 1.0; |
100 | IntentList[nProfiles] = INTENT_RELATIVE_COLORIMETRIC; |
101 | |
102 | // Create the transform |
103 | xform = cmsCreateExtendedTransform(ContextID, nProfiles + 1, ProfileList, |
104 | BPCList, |
105 | IntentList, |
106 | AdaptationList, |
107 | NULL, 0, |
108 | InputFormat, |
109 | OutputFormat, |
110 | dwFlags); |
111 | |
112 | cmsCloseProfile(hLab); |
113 | |
114 | return xform; |
115 | } |
116 | |
117 | |
118 | // Compute K -> L* relationship. Flags may include black point compensation. In this case, |
119 | // the relationship is assumed from the profile with BPC to a black point zero. |
120 | static |
121 | cmsToneCurve* ComputeKToLstar(cmsContext ContextID, |
122 | cmsUInt32Number nPoints, |
123 | cmsUInt32Number nProfiles, |
124 | const cmsUInt32Number Intents[], |
125 | const cmsHPROFILE hProfiles[], |
126 | const cmsBool BPC[], |
127 | const cmsFloat64Number AdaptationStates[], |
128 | cmsUInt32Number dwFlags) |
129 | { |
130 | cmsToneCurve* out = NULL; |
131 | cmsUInt32Number i; |
132 | cmsHTRANSFORM xform; |
133 | cmsCIELab Lab; |
134 | cmsFloat32Number cmyk[4]; |
135 | cmsFloat32Number* SampledPoints; |
136 | |
137 | xform = _cmsChain2Lab(ContextID, nProfiles, TYPE_CMYK_FLT, TYPE_Lab_DBL, Intents, hProfiles, BPC, AdaptationStates, dwFlags); |
138 | if (xform == NULL) return NULL; |
139 | |
140 | SampledPoints = (cmsFloat32Number*) _cmsCalloc(ContextID, nPoints, sizeof(cmsFloat32Number)); |
141 | if (SampledPoints == NULL) goto Error; |
142 | |
143 | for (i=0; i < nPoints; i++) { |
144 | |
145 | cmyk[0] = 0; |
146 | cmyk[1] = 0; |
147 | cmyk[2] = 0; |
148 | cmyk[3] = (cmsFloat32Number) ((i * 100.0) / (nPoints-1)); |
149 | |
150 | cmsDoTransform(xform, cmyk, &Lab, 1); |
151 | SampledPoints[i]= (cmsFloat32Number) (1.0 - Lab.L / 100.0); // Negate K for easier operation |
152 | } |
153 | |
154 | out = cmsBuildTabulatedToneCurveFloat(ContextID, nPoints, SampledPoints); |
155 | |
156 | Error: |
157 | |
158 | cmsDeleteTransform(xform); |
159 | if (SampledPoints) _cmsFree(ContextID, SampledPoints); |
160 | |
161 | return out; |
162 | } |
163 | |
164 | |
165 | // Compute Black tone curve on a CMYK -> CMYK transform. This is done by |
166 | // using the proof direction on both profiles to find K->L* relationship |
167 | // then joining both curves. dwFlags may include black point compensation. |
168 | cmsToneCurve* _cmsBuildKToneCurve(cmsContext ContextID, |
169 | cmsUInt32Number nPoints, |
170 | cmsUInt32Number nProfiles, |
171 | const cmsUInt32Number Intents[], |
172 | const cmsHPROFILE hProfiles[], |
173 | const cmsBool BPC[], |
174 | const cmsFloat64Number AdaptationStates[], |
175 | cmsUInt32Number dwFlags) |
176 | { |
177 | cmsToneCurve *in, *out, *KTone; |
178 | |
179 | // Make sure CMYK -> CMYK |
180 | if (cmsGetColorSpace(hProfiles[0]) != cmsSigCmykData || |
181 | cmsGetColorSpace(hProfiles[nProfiles-1])!= cmsSigCmykData) return NULL; |
182 | |
183 | |
184 | // Make sure last is an output profile |
185 | if (cmsGetDeviceClass(hProfiles[nProfiles - 1]) != cmsSigOutputClass) return NULL; |
186 | |
187 | // Create individual curves. BPC works also as each K to L* is |
188 | // computed as a BPC to zero black point in case of L* |
189 | in = ComputeKToLstar(ContextID, nPoints, nProfiles - 1, Intents, hProfiles, BPC, AdaptationStates, dwFlags); |
190 | if (in == NULL) return NULL; |
191 | |
192 | out = ComputeKToLstar(ContextID, nPoints, 1, |
193 | Intents + (nProfiles - 1), |
194 | &hProfiles [nProfiles - 1], |
195 | BPC + (nProfiles - 1), |
196 | AdaptationStates + (nProfiles - 1), |
197 | dwFlags); |
198 | if (out == NULL) { |
199 | cmsFreeToneCurve(in); |
200 | return NULL; |
201 | } |
202 | |
203 | // Build the relationship. This effectively limits the maximum accuracy to 16 bits, but |
204 | // since this is used on black-preserving LUTs, we are not losing accuracy in any case |
205 | KTone = cmsJoinToneCurve(ContextID, in, out, nPoints); |
206 | |
207 | // Get rid of components |
208 | cmsFreeToneCurve(in); cmsFreeToneCurve(out); |
209 | |
210 | // Something went wrong... |
211 | if (KTone == NULL) return NULL; |
212 | |
213 | // Make sure it is monotonic |
214 | if (!cmsIsToneCurveMonotonic(KTone)) { |
215 | cmsFreeToneCurve(KTone); |
216 | return NULL; |
217 | } |
218 | |
219 | return KTone; |
220 | } |
221 | |
222 | |
223 | // Gamut LUT Creation ----------------------------------------------------------------------------------------- |
224 | |
225 | // Used by gamut & softproofing |
226 | |
227 | typedef struct { |
228 | |
229 | cmsHTRANSFORM hInput; // From whatever input color space. 16 bits to DBL |
230 | cmsHTRANSFORM hForward, hReverse; // Transforms going from Lab to colorant and back |
231 | cmsFloat64Number Thereshold; // The thereshold after which is considered out of gamut |
232 | |
233 | } GAMUTCHAIN; |
234 | |
235 | // This sampler does compute gamut boundaries by comparing original |
236 | // values with a transform going back and forth. Values above ERR_THERESHOLD |
237 | // of maximum are considered out of gamut. |
238 | |
239 | #define ERR_THERESHOLD 5 |
240 | |
241 | |
242 | static |
243 | int GamutSampler(register const cmsUInt16Number In[], register cmsUInt16Number Out[], register void* Cargo) |
244 | { |
245 | GAMUTCHAIN* t = (GAMUTCHAIN* ) Cargo; |
246 | cmsCIELab LabIn1, LabOut1; |
247 | cmsCIELab LabIn2, LabOut2; |
248 | cmsUInt16Number Proof[cmsMAXCHANNELS], Proof2[cmsMAXCHANNELS]; |
249 | cmsFloat64Number dE1, dE2, ErrorRatio; |
250 | |
251 | // Assume in-gamut by default. |
252 | ErrorRatio = 1.0; |
253 | |
254 | // Convert input to Lab |
255 | cmsDoTransform(t -> hInput, In, &LabIn1, 1); |
256 | |
257 | // converts from PCS to colorant. This always |
258 | // does return in-gamut values, |
259 | cmsDoTransform(t -> hForward, &LabIn1, Proof, 1); |
260 | |
261 | // Now, do the inverse, from colorant to PCS. |
262 | cmsDoTransform(t -> hReverse, Proof, &LabOut1, 1); |
263 | |
264 | memmove(&LabIn2, &LabOut1, sizeof(cmsCIELab)); |
265 | |
266 | // Try again, but this time taking Check as input |
267 | cmsDoTransform(t -> hForward, &LabOut1, Proof2, 1); |
268 | cmsDoTransform(t -> hReverse, Proof2, &LabOut2, 1); |
269 | |
270 | // Take difference of direct value |
271 | dE1 = cmsDeltaE(&LabIn1, &LabOut1); |
272 | |
273 | // Take difference of converted value |
274 | dE2 = cmsDeltaE(&LabIn2, &LabOut2); |
275 | |
276 | |
277 | // if dE1 is small and dE2 is small, value is likely to be in gamut |
278 | if (dE1 < t->Thereshold && dE2 < t->Thereshold) |
279 | Out[0] = 0; |
280 | else { |
281 | |
282 | // if dE1 is small and dE2 is big, undefined. Assume in gamut |
283 | if (dE1 < t->Thereshold && dE2 > t->Thereshold) |
284 | Out[0] = 0; |
285 | else |
286 | // dE1 is big and dE2 is small, clearly out of gamut |
287 | if (dE1 > t->Thereshold && dE2 < t->Thereshold) |
288 | Out[0] = (cmsUInt16Number) _cmsQuickFloor((dE1 - t->Thereshold) + .5); |
289 | else { |
290 | |
291 | // dE1 is big and dE2 is also big, could be due to perceptual mapping |
292 | // so take error ratio |
293 | if (dE2 == 0.0) |
294 | ErrorRatio = dE1; |
295 | else |
296 | ErrorRatio = dE1 / dE2; |
297 | |
298 | if (ErrorRatio > t->Thereshold) |
299 | Out[0] = (cmsUInt16Number) _cmsQuickFloor((ErrorRatio - t->Thereshold) + .5); |
300 | else |
301 | Out[0] = 0; |
302 | } |
303 | } |
304 | |
305 | |
306 | return TRUE; |
307 | } |
308 | |
309 | // Does compute a gamut LUT going back and forth across pcs -> relativ. colorimetric intent -> pcs |
310 | // the dE obtained is then annotated on the LUT. Values truly out of gamut are clipped to dE = 0xFFFE |
311 | // and values changed are supposed to be handled by any gamut remapping, so, are out of gamut as well. |
312 | // |
313 | // **WARNING: This algorithm does assume that gamut remapping algorithms does NOT move in-gamut colors, |
314 | // of course, many perceptual and saturation intents does not work in such way, but relativ. ones should. |
315 | |
316 | cmsPipeline* _cmsCreateGamutCheckPipeline(cmsContext ContextID, |
317 | cmsHPROFILE hProfiles[], |
318 | cmsBool BPC[], |
319 | cmsUInt32Number Intents[], |
320 | cmsFloat64Number AdaptationStates[], |
321 | cmsUInt32Number nGamutPCSposition, |
322 | cmsHPROFILE hGamut) |
323 | { |
324 | cmsHPROFILE hLab; |
325 | cmsPipeline* Gamut; |
326 | cmsStage* CLUT; |
327 | cmsUInt32Number dwFormat; |
328 | GAMUTCHAIN Chain; |
329 | cmsUInt32Number nChannels, nGridpoints; |
330 | cmsColorSpaceSignature ColorSpace; |
331 | cmsUInt32Number i; |
332 | cmsHPROFILE ProfileList[256]; |
333 | cmsBool BPCList[256]; |
334 | cmsFloat64Number AdaptationList[256]; |
335 | cmsUInt32Number IntentList[256]; |
336 | |
337 | memset(&Chain, 0, sizeof(GAMUTCHAIN)); |
338 | |
339 | |
340 | if (nGamutPCSposition <= 0 || nGamutPCSposition > 255) { |
341 | cmsSignalError(ContextID, cmsERROR_RANGE, "Wrong position of PCS. 1..255 expected, %d found." , nGamutPCSposition); |
342 | return NULL; |
343 | } |
344 | |
345 | hLab = cmsCreateLab4ProfileTHR(ContextID, NULL); |
346 | if (hLab == NULL) return NULL; |
347 | |
348 | |
349 | // The figure of merit. On matrix-shaper profiles, should be almost zero as |
350 | // the conversion is pretty exact. On LUT based profiles, different resolutions |
351 | // of input and output CLUT may result in differences. |
352 | |
353 | if (cmsIsMatrixShaper(hGamut)) { |
354 | |
355 | Chain.Thereshold = 1.0; |
356 | } |
357 | else { |
358 | Chain.Thereshold = ERR_THERESHOLD; |
359 | } |
360 | |
361 | |
362 | // Create a copy of parameters |
363 | for (i=0; i < nGamutPCSposition; i++) { |
364 | ProfileList[i] = hProfiles[i]; |
365 | BPCList[i] = BPC[i]; |
366 | AdaptationList[i] = AdaptationStates[i]; |
367 | IntentList[i] = Intents[i]; |
368 | } |
369 | |
370 | // Fill Lab identity |
371 | ProfileList[nGamutPCSposition] = hLab; |
372 | BPCList[nGamutPCSposition] = 0; |
373 | AdaptationList[nGamutPCSposition] = 1.0; |
374 | IntentList[nGamutPCSposition] = INTENT_RELATIVE_COLORIMETRIC; |
375 | |
376 | |
377 | ColorSpace = cmsGetColorSpace(hGamut); |
378 | |
379 | nChannels = cmsChannelsOf(ColorSpace); |
380 | nGridpoints = _cmsReasonableGridpointsByColorspace(ColorSpace, cmsFLAGS_HIGHRESPRECALC); |
381 | dwFormat = (CHANNELS_SH(nChannels)|BYTES_SH(2)); |
382 | |
383 | // 16 bits to Lab double |
384 | Chain.hInput = cmsCreateExtendedTransform(ContextID, |
385 | nGamutPCSposition + 1, |
386 | ProfileList, |
387 | BPCList, |
388 | IntentList, |
389 | AdaptationList, |
390 | NULL, 0, |
391 | dwFormat, TYPE_Lab_DBL, |
392 | cmsFLAGS_NOCACHE); |
393 | |
394 | |
395 | // Does create the forward step. Lab double to device |
396 | dwFormat = (CHANNELS_SH(nChannels)|BYTES_SH(2)); |
397 | Chain.hForward = cmsCreateTransformTHR(ContextID, |
398 | hLab, TYPE_Lab_DBL, |
399 | hGamut, dwFormat, |
400 | INTENT_RELATIVE_COLORIMETRIC, |
401 | cmsFLAGS_NOCACHE); |
402 | |
403 | // Does create the backwards step |
404 | Chain.hReverse = cmsCreateTransformTHR(ContextID, hGamut, dwFormat, |
405 | hLab, TYPE_Lab_DBL, |
406 | INTENT_RELATIVE_COLORIMETRIC, |
407 | cmsFLAGS_NOCACHE); |
408 | |
409 | |
410 | // All ok? |
411 | if (Chain.hInput && Chain.hForward && Chain.hReverse) { |
412 | |
413 | // Go on, try to compute gamut LUT from PCS. This consist on a single channel containing |
414 | // dE when doing a transform back and forth on the colorimetric intent. |
415 | |
416 | Gamut = cmsPipelineAlloc(ContextID, 3, 1); |
417 | if (Gamut != NULL) { |
418 | |
419 | CLUT = cmsStageAllocCLut16bit(ContextID, nGridpoints, nChannels, 1, NULL); |
420 | if (!cmsPipelineInsertStage(Gamut, cmsAT_BEGIN, CLUT)) { |
421 | cmsPipelineFree(Gamut); |
422 | Gamut = NULL; |
423 | } |
424 | else { |
425 | cmsStageSampleCLut16bit(CLUT, GamutSampler, (void*) &Chain, 0); |
426 | } |
427 | } |
428 | } |
429 | else |
430 | Gamut = NULL; // Didn't work... |
431 | |
432 | // Free all needed stuff. |
433 | if (Chain.hInput) cmsDeleteTransform(Chain.hInput); |
434 | if (Chain.hForward) cmsDeleteTransform(Chain.hForward); |
435 | if (Chain.hReverse) cmsDeleteTransform(Chain.hReverse); |
436 | if (hLab) cmsCloseProfile(hLab); |
437 | |
438 | // And return computed hull |
439 | return Gamut; |
440 | } |
441 | |
442 | // Total Area Coverage estimation ---------------------------------------------------------------- |
443 | |
444 | typedef struct { |
445 | cmsUInt32Number nOutputChans; |
446 | cmsHTRANSFORM hRoundTrip; |
447 | cmsFloat32Number MaxTAC; |
448 | cmsFloat32Number MaxInput[cmsMAXCHANNELS]; |
449 | |
450 | } cmsTACestimator; |
451 | |
452 | |
453 | // This callback just accounts the maximum ink dropped in the given node. It does not populate any |
454 | // memory, as the destination table is NULL. Its only purpose it to know the global maximum. |
455 | static |
456 | int EstimateTAC(register const cmsUInt16Number In[], register cmsUInt16Number Out[], register void * Cargo) |
457 | { |
458 | cmsTACestimator* bp = (cmsTACestimator*) Cargo; |
459 | cmsFloat32Number RoundTrip[cmsMAXCHANNELS]; |
460 | cmsUInt32Number i; |
461 | cmsFloat32Number Sum; |
462 | |
463 | |
464 | // Evaluate the xform |
465 | cmsDoTransform(bp->hRoundTrip, In, RoundTrip, 1); |
466 | |
467 | // All all amounts of ink |
468 | for (Sum=0, i=0; i < bp ->nOutputChans; i++) |
469 | Sum += RoundTrip[i]; |
470 | |
471 | // If above maximum, keep track of input values |
472 | if (Sum > bp ->MaxTAC) { |
473 | |
474 | bp ->MaxTAC = Sum; |
475 | |
476 | for (i=0; i < bp ->nOutputChans; i++) { |
477 | bp ->MaxInput[i] = In[i]; |
478 | } |
479 | } |
480 | |
481 | return TRUE; |
482 | |
483 | cmsUNUSED_PARAMETER(Out); |
484 | } |
485 | |
486 | |
487 | // Detect Total area coverage of the profile |
488 | cmsFloat64Number CMSEXPORT cmsDetectTAC(cmsHPROFILE hProfile) |
489 | { |
490 | cmsTACestimator bp; |
491 | cmsUInt32Number dwFormatter; |
492 | cmsUInt32Number GridPoints[MAX_INPUT_DIMENSIONS]; |
493 | cmsHPROFILE hLab; |
494 | cmsContext ContextID = cmsGetProfileContextID(hProfile); |
495 | |
496 | // TAC only works on output profiles |
497 | if (cmsGetDeviceClass(hProfile) != cmsSigOutputClass) { |
498 | return 0; |
499 | } |
500 | |
501 | // Create a fake formatter for result |
502 | dwFormatter = cmsFormatterForColorspaceOfProfile(hProfile, 4, TRUE); |
503 | |
504 | bp.nOutputChans = T_CHANNELS(dwFormatter); |
505 | bp.MaxTAC = 0; // Initial TAC is 0 |
506 | |
507 | // for safety |
508 | if (bp.nOutputChans >= cmsMAXCHANNELS) return 0; |
509 | |
510 | hLab = cmsCreateLab4ProfileTHR(ContextID, NULL); |
511 | if (hLab == NULL) return 0; |
512 | // Setup a roundtrip on perceptual intent in output profile for TAC estimation |
513 | bp.hRoundTrip = cmsCreateTransformTHR(ContextID, hLab, TYPE_Lab_16, |
514 | hProfile, dwFormatter, INTENT_PERCEPTUAL, cmsFLAGS_NOOPTIMIZE|cmsFLAGS_NOCACHE); |
515 | |
516 | cmsCloseProfile(hLab); |
517 | if (bp.hRoundTrip == NULL) return 0; |
518 | |
519 | // For L* we only need black and white. For C* we need many points |
520 | GridPoints[0] = 6; |
521 | GridPoints[1] = 74; |
522 | GridPoints[2] = 74; |
523 | |
524 | |
525 | if (!cmsSliceSpace16(3, GridPoints, EstimateTAC, &bp)) { |
526 | bp.MaxTAC = 0; |
527 | } |
528 | |
529 | cmsDeleteTransform(bp.hRoundTrip); |
530 | |
531 | // Results in % |
532 | return bp.MaxTAC; |
533 | } |
534 | |
535 | |
536 | // Carefully, clamp on CIELab space. |
537 | |
538 | cmsBool CMSEXPORT cmsDesaturateLab(cmsCIELab* Lab, |
539 | double amax, double amin, |
540 | double bmax, double bmin) |
541 | { |
542 | |
543 | // Whole Luma surface to zero |
544 | |
545 | if (Lab -> L < 0) { |
546 | |
547 | Lab-> L = Lab->a = Lab-> b = 0.0; |
548 | return FALSE; |
549 | } |
550 | |
551 | // Clamp white, DISCARD HIGHLIGHTS. This is done |
552 | // in such way because icc spec doesn't allow the |
553 | // use of L>100 as a highlight means. |
554 | |
555 | if (Lab->L > 100) |
556 | Lab -> L = 100; |
557 | |
558 | // Check out gamut prism, on a, b faces |
559 | |
560 | if (Lab -> a < amin || Lab->a > amax|| |
561 | Lab -> b < bmin || Lab->b > bmax) { |
562 | |
563 | cmsCIELCh LCh; |
564 | double h, slope; |
565 | |
566 | // Falls outside a, b limits. Transports to LCh space, |
567 | // and then do the clipping |
568 | |
569 | |
570 | if (Lab -> a == 0.0) { // Is hue exactly 90? |
571 | |
572 | // atan will not work, so clamp here |
573 | Lab -> b = Lab->b < 0 ? bmin : bmax; |
574 | return TRUE; |
575 | } |
576 | |
577 | cmsLab2LCh(&LCh, Lab); |
578 | |
579 | slope = Lab -> b / Lab -> a; |
580 | h = LCh.h; |
581 | |
582 | // There are 4 zones |
583 | |
584 | if ((h >= 0. && h < 45.) || |
585 | (h >= 315 && h <= 360.)) { |
586 | |
587 | // clip by amax |
588 | Lab -> a = amax; |
589 | Lab -> b = amax * slope; |
590 | } |
591 | else |
592 | if (h >= 45. && h < 135.) |
593 | { |
594 | // clip by bmax |
595 | Lab -> b = bmax; |
596 | Lab -> a = bmax / slope; |
597 | } |
598 | else |
599 | if (h >= 135. && h < 225.) { |
600 | // clip by amin |
601 | Lab -> a = amin; |
602 | Lab -> b = amin * slope; |
603 | |
604 | } |
605 | else |
606 | if (h >= 225. && h < 315.) { |
607 | // clip by bmin |
608 | Lab -> b = bmin; |
609 | Lab -> a = bmin / slope; |
610 | } |
611 | else { |
612 | cmsSignalError(0, cmsERROR_RANGE, "Invalid angle" ); |
613 | return FALSE; |
614 | } |
615 | |
616 | } |
617 | |
618 | return TRUE; |
619 | } |
620 | |