1 /* ============================================================
2 *
3 * This file is a part of digiKam project
4 * https://www.digikam.org
5 *
6 * Date : 2005-11-18
7 * Description : a class to apply ICC color correction to image.
8 *
9 * Copyright (C) 2005-2006 by F.J. Cruz <fj dot cruz at supercable dot es>
10 * Copyright (C) 2009 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
11 * Copyright (C) 2005-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
12 *
13 * This program is free software; you can redistribute it
14 * and/or modify it under the terms of the GNU General
15 * Public License as published by the Free Software Foundation;
16 * either version 2, or (at your option)
17 * any later version.
18 *
19 * This program is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * ============================================================ */
25
26 #include "icctransform.h"
27 #include "digikam-lcms.h"
28
29 // Qt includes
30
31 #include <QDataStream>
32 #include <QFile>
33 #include <QImage>
34 #include <QVarLengthArray>
35
36 // KDE includes
37
38 #include <kconfiggroup.h>
39
40 // Local includes
41
42 #include "digikam_debug.h"
43 #include "dimgloaderobserver.h"
44
45 namespace Digikam
46 {
47
48 class Q_DECL_HIDDEN TransformDescription
49 {
50 public:
51
TransformDescription()52 TransformDescription()
53 : inputFormat (0),
54 outputFormat (0),
55 intent (INTENT_PERCEPTUAL),
56 transformFlags (0),
57 proofIntent (INTENT_ABSOLUTE_COLORIMETRIC)
58 {
59 }
60
operator ==(const TransformDescription & other) const61 bool operator==(const TransformDescription& other) const
62 {
63 return (
64 (inputProfile == other.inputProfile) &&
65 (inputFormat == other.inputFormat) &&
66 (outputProfile == other.outputProfile) &&
67 (outputFormat == other.outputFormat) &&
68 (intent == other.intent) &&
69 (transformFlags == other.transformFlags) &&
70 (proofProfile == other.proofProfile) &&
71 (proofIntent == other.proofIntent)
72 );
73 }
74
75 public:
76
77 IccProfile inputProfile;
78 int inputFormat;
79 IccProfile outputProfile;
80 int outputFormat;
81 int intent;
82 int transformFlags;
83 IccProfile proofProfile;
84 int proofIntent;
85 };
86
87 class Q_DECL_HIDDEN IccTransform::Private : public QSharedData
88 {
89 public:
90
Private()91 explicit Private()
92 : intent (IccTransform::Perceptual),
93 proofIntent (IccTransform::AbsoluteColorimetric),
94 useBPC (false),
95 checkGamut (false),
96 doNotEmbed (false),
97 checkGamutColor(QColor(126, 255, 255)),
98 handle (nullptr)
99 {
100 }
101
Private(const Private & other)102 explicit Private(const Private& other)
103 : QSharedData(other),
104 handle (nullptr)
105 {
106 operator=(other);
107 }
108
operator =(const Private & other)109 Private& operator=(const Private& other)
110 {
111 // Attention: This is sensitive. Add any new members here.
112 // We can't use the default operator= because of handle.
113
114 intent = other.intent;
115 proofIntent = other.proofIntent;
116 useBPC = other.useBPC;
117 checkGamut = other.checkGamut;
118 doNotEmbed = other.doNotEmbed;
119 checkGamutColor = other.checkGamutColor;
120
121 embeddedProfile = other.embeddedProfile;
122 inputProfile = other.inputProfile;
123 outputProfile = other.outputProfile;
124 proofProfile = other.proofProfile;
125 builtinProfile = other.builtinProfile;
126
127 close();
128 handle = nullptr;
129 currentDescription = TransformDescription();
130
131 return *this;
132 }
133
~Private()134 ~Private()
135 {
136 close();
137 }
138
close()139 void close()
140 {
141 if (handle)
142 {
143 currentDescription = TransformDescription();
144 LcmsLock lock;
145 dkCmsDeleteTransform(handle);
146 handle = nullptr;
147 }
148 }
149
sRGB()150 IccProfile& sRGB()
151 {
152 if (builtinProfile.isNull())
153 {
154 builtinProfile = IccProfile::sRGB();
155 }
156
157 return builtinProfile;
158 }
159
effectiveInputProfile()160 IccProfile& effectiveInputProfile()
161 {
162 if (!embeddedProfile.isNull())
163 {
164 return embeddedProfile;
165 }
166 else if (!inputProfile.isNull())
167 {
168 return inputProfile;
169 }
170 else
171 {
172 return sRGB();
173 }
174 }
175
effectiveInputProfileConst() const176 IccProfile effectiveInputProfileConst() const
177 {
178 if (!embeddedProfile.isNull())
179 {
180 return embeddedProfile;
181 }
182 else if (!inputProfile.isNull())
183 {
184 return inputProfile;
185 }
186 else
187 {
188 return IccProfile::sRGB();
189 }
190 }
191
192 public:
193
194 IccTransform::RenderingIntent intent;
195 IccTransform::RenderingIntent proofIntent;
196 bool useBPC;
197 bool checkGamut;
198 bool doNotEmbed;
199 QColor checkGamutColor;
200
201 IccProfile embeddedProfile;
202 IccProfile inputProfile;
203 IccProfile outputProfile;
204 IccProfile proofProfile;
205 IccProfile builtinProfile;
206
207 cmsHTRANSFORM handle;
208 TransformDescription currentDescription;
209 };
210
IccTransform()211 IccTransform::IccTransform()
212 : d(new Private)
213 {
214 }
215
IccTransform(const IccTransform & other)216 IccTransform::IccTransform(const IccTransform& other)
217 : d(other.d)
218 {
219 }
220
operator =(const IccTransform & other)221 IccTransform& IccTransform::operator=(const IccTransform& other)
222 {
223 d = other.d;
224
225 return *this;
226 }
227
~IccTransform()228 IccTransform::~IccTransform()
229 {
230 // close() is done in ~Private
231 }
232
init()233 void IccTransform::init()
234 {
235 LcmsLock lock;
236 dkCmsErrorAction(LCMS_ERROR_SHOW);
237 }
238
setInputProfile(const IccProfile & profile)239 void IccTransform::setInputProfile(const IccProfile& profile)
240 {
241 if (profile == d->inputProfile)
242 {
243 return;
244 }
245
246 close();
247 d->inputProfile = profile;
248 }
249
setEmbeddedProfile(const DImg & image)250 void IccTransform::setEmbeddedProfile(const DImg& image)
251 {
252 IccProfile profile = image.getIccProfile();
253
254 if (profile == d->embeddedProfile)
255 {
256 return;
257 }
258
259 close();
260 d->embeddedProfile = profile;
261 }
262
setOutputProfile(const IccProfile & profile)263 void IccTransform::setOutputProfile(const IccProfile& profile)
264 {
265 if (profile == d->outputProfile)
266 {
267 return;
268 }
269
270 close();
271 d->outputProfile = profile;
272 }
273
setProofProfile(const IccProfile & profile)274 void IccTransform::setProofProfile(const IccProfile& profile)
275 {
276 if (profile == d->proofProfile)
277 {
278 return;
279 }
280
281 close();
282 d->proofProfile = profile;
283 }
284
embeddedProfile() const285 IccProfile IccTransform::embeddedProfile() const
286 {
287 return d->embeddedProfile;
288 }
289
inputProfile() const290 IccProfile IccTransform::inputProfile() const
291 {
292 return d->inputProfile;
293 }
294
outputProfile() const295 IccProfile IccTransform::outputProfile() const
296 {
297 return d->outputProfile;
298 }
299
proofProfile() const300 IccProfile IccTransform::proofProfile() const
301 {
302 return d->proofProfile;
303 }
304
effectiveInputProfile() const305 IccProfile IccTransform::effectiveInputProfile() const
306 {
307 return d->effectiveInputProfileConst();
308 }
309
setIntent(RenderingIntent intent)310 void IccTransform::setIntent(RenderingIntent intent)
311 {
312 if (intent == d->intent)
313 {
314 return;
315 }
316
317 d->intent = intent;
318 close();
319 }
320
setProofIntent(RenderingIntent intent)321 void IccTransform::setProofIntent(RenderingIntent intent)
322 {
323 if (intent == d->proofIntent)
324 {
325 return;
326 }
327
328 d->proofIntent = intent;
329 close();
330 }
331
setUseBlackPointCompensation(bool useBPC)332 void IccTransform::setUseBlackPointCompensation(bool useBPC)
333 {
334 if (d->useBPC == useBPC)
335 {
336 return;
337 }
338
339 close();
340 d->useBPC = useBPC;
341 }
342
setCheckGamut(bool checkGamut)343 void IccTransform::setCheckGamut(bool checkGamut)
344 {
345 if (d->checkGamut == checkGamut)
346 {
347 return;
348 }
349
350 close();
351 d->checkGamut = checkGamut;
352 }
353
setCheckGamutMaskColor(const QColor & color)354 void IccTransform::setCheckGamutMaskColor(const QColor& color)
355 {
356 d->checkGamutColor = color;
357 }
358
intent() const359 IccTransform::RenderingIntent IccTransform::intent() const
360 {
361 return d->intent;
362 }
363
proofIntent() const364 IccTransform::RenderingIntent IccTransform::proofIntent() const
365 {
366 return d->proofIntent;
367 }
368
isUsingBlackPointCompensation() const369 bool IccTransform::isUsingBlackPointCompensation() const
370 {
371 return d->useBPC;
372 }
373
isCheckingGamut() const374 bool IccTransform::isCheckingGamut() const
375 {
376 return d->checkGamut;
377 }
378
checkGamutMaskColor() const379 QColor IccTransform::checkGamutMaskColor() const
380 {
381 return d->checkGamutColor;
382 }
383
setDoNotEmbedOutputProfile(bool doNotEmbed)384 void IccTransform::setDoNotEmbedOutputProfile(bool doNotEmbed)
385 {
386 d->doNotEmbed = doNotEmbed;
387 }
388
389 /*
390 void IccTransform::readFromConfig()
391 {
392 KSharedConfig::Ptr config = KSharedConfig::openConfig();
393 KConfigGroup group = config->group(QString("Color Management"));
394
395 int intent = group.readEntry("RenderingIntent", 0);
396 bool useBPC = group.readEntry("BPCAlgorithm", false);
397
398 setIntent(intent);
399 setUseBlackPointCompensation(useBPC);
400 }
401 */
402
willHaveEffect()403 bool IccTransform::willHaveEffect()
404 {
405 if (d->outputProfile.isNull())
406 {
407 return false;
408 }
409
410 return !d->effectiveInputProfile().isSameProfileAs(d->outputProfile);
411 }
412
renderingIntentToLcmsIntent(IccTransform::RenderingIntent intent)413 static int renderingIntentToLcmsIntent(IccTransform::RenderingIntent intent)
414 {
415 switch (intent)
416 {
417 case IccTransform::Perceptual:
418 return INTENT_PERCEPTUAL;
419
420 case IccTransform::RelativeColorimetric:
421 return INTENT_RELATIVE_COLORIMETRIC;
422
423 case IccTransform::Saturation:
424 return INTENT_SATURATION;
425
426 case IccTransform::AbsoluteColorimetric:
427 return INTENT_ABSOLUTE_COLORIMETRIC;
428
429 default:
430 return INTENT_PERCEPTUAL;
431 }
432 }
433
getDescription(const DImg & image)434 TransformDescription IccTransform::getDescription(const DImg& image)
435 {
436 TransformDescription description;
437
438 description.inputProfile = d->effectiveInputProfile();
439 description.outputProfile = d->outputProfile;
440 description.intent = renderingIntentToLcmsIntent(d->intent);
441
442 if (d->useBPC)
443 {
444 description.transformFlags |= cmsFLAGS_WHITEBLACKCOMPENSATION;
445 }
446
447 LcmsLock lock;
448
449 // Do not use TYPE_BGR_ - this implies 3 bytes per pixel, but even if !image.hasAlpha(),
450 // our image data has 4 bytes per pixel with the fourth byte filled with 0xFF.
451
452 if (image.sixteenBit())
453 {
454 /*
455 switch (dkCmsGetColorSpace(description.inputProfile))
456 {
457 case icSigGrayData:
458 description.inputFormat = TYPE_GRAYA_16;
459 break;
460 case icSigCmykData:
461 description.inputFormat = TYPE_CMYK_16;
462 break;
463 default:
464 description.inputFormat = TYPE_BGRA_16;
465 }
466 */
467
468 // A Dimg is always BGRA, converted by the loader
469
470 description.inputFormat = TYPE_BGRA_16;
471 description.outputFormat = TYPE_BGRA_16;
472 }
473 else
474 {
475 description.inputFormat = TYPE_BGRA_8;
476 description.outputFormat = TYPE_BGRA_8;
477 }
478
479 return description;
480 }
481
getDescription(const QImage &)482 TransformDescription IccTransform::getDescription(const QImage&)
483 {
484 TransformDescription description;
485
486 description.inputProfile = d->effectiveInputProfile();
487 description.outputProfile = d->outputProfile;
488 description.intent = renderingIntentToLcmsIntent(d->intent);
489
490 if (d->useBPC)
491 {
492 description.transformFlags |= cmsFLAGS_WHITEBLACKCOMPENSATION;
493 }
494
495 description.inputFormat = TYPE_BGRA_8;
496 description.outputFormat = TYPE_BGRA_8;
497
498 return description;
499 }
500
getProofingDescription(const DImg & image)501 TransformDescription IccTransform::getProofingDescription(const DImg& image)
502 {
503 TransformDescription description = getDescription(image);
504
505 description.proofProfile = d->proofProfile;
506 description.proofIntent = renderingIntentToLcmsIntent(d->proofIntent);
507
508 description.transformFlags |= cmsFLAGS_SOFTPROOFING;
509
510 if (d->checkGamut)
511 {
512 dkCmsSetAlarmCodes(d->checkGamutColor.red(), d->checkGamutColor.green(), d->checkGamutColor.blue());
513 description.transformFlags |= cmsFLAGS_GAMUTCHECK;
514 }
515
516 return description;
517 }
518
open(TransformDescription & description)519 bool IccTransform::open(TransformDescription& description)
520 {
521 if (d->handle)
522 {
523 if (d->currentDescription == description)
524 {
525 return true;
526 }
527 else
528 {
529 close();
530 }
531 }
532
533 d->currentDescription = description;
534
535 LcmsLock lock;
536 d->handle = dkCmsCreateTransform(description.inputProfile,
537 description.inputFormat,
538 description.outputProfile,
539 description.outputFormat,
540 description.intent,
541 description.transformFlags);
542
543 if (!d->handle)
544 {
545 qCDebug(DIGIKAM_DIMG_LOG) << "LCMS internal error: cannot create a color transform instance";
546 return false;
547 }
548
549 return true;
550 }
551
openProofing(TransformDescription & description)552 bool IccTransform::openProofing(TransformDescription& description)
553 {
554 if (d->handle)
555 {
556 if (d->currentDescription == description)
557 {
558 return true;
559 }
560 else
561 {
562 close();
563 }
564 }
565
566 d->currentDescription = description;
567
568 LcmsLock lock;
569 d->handle = dkCmsCreateProofingTransform(description.inputProfile,
570 description.inputFormat,
571 description.outputProfile,
572 description.outputFormat,
573 description.proofProfile,
574 description.intent,
575 description.proofIntent,
576 description.transformFlags);
577
578 if (!d->handle)
579 {
580 qCDebug(DIGIKAM_DIMG_LOG) << "LCMS internal error: cannot create a color transform instance";
581 return false;
582 }
583
584 return true;
585 }
586
checkProfiles()587 bool IccTransform::checkProfiles()
588 {
589 if (!d->effectiveInputProfile().open())
590 {
591 qCDebug(DIGIKAM_DIMG_LOG) << "Cannot open embedded profile";
592 return false;
593 }
594
595 if (!d->outputProfile.open())
596 {
597 qCDebug(DIGIKAM_DIMG_LOG) << "Cannot open output profile";
598 return false;
599 }
600
601 if (!d->proofProfile.isNull())
602 {
603 if (!d->proofProfile.open())
604 {
605 qCDebug(DIGIKAM_DIMG_LOG) << "Cannot open proofing profile";
606 return false;
607 }
608 }
609
610 return true;
611 }
612
apply(DImg & image,DImgLoaderObserver * const observer)613 bool IccTransform::apply(DImg& image, DImgLoaderObserver* const observer)
614 {
615 if (!willHaveEffect())
616 {
617 if (!d->outputProfile.isNull() && !d->doNotEmbed)
618 {
619 image.setIccProfile(d->outputProfile);
620 }
621
622 return true;
623 }
624
625 if (!checkProfiles())
626 {
627 return false;
628 }
629
630 TransformDescription description;
631
632 if (d->proofProfile.isNull())
633 {
634 description = getDescription(image);
635
636 if (!open(description))
637 {
638 return false;
639 }
640 }
641 else
642 {
643 description = getProofingDescription(image);
644
645 if (!openProofing(description))
646 {
647 return false;
648 }
649 }
650
651 if (observer)
652 {
653 observer->progressInfo(0.1F);
654 }
655
656 transform(image, description, observer);
657
658 if (!d->doNotEmbed)
659 {
660 image.setIccProfile(d->outputProfile);
661 }
662
663 // if this was a RAW color image, it is no more
664
665 image.removeAttribute(QLatin1String("uncalibratedColor"));
666
667 return true;
668 }
669
apply(QImage & qimage)670 bool IccTransform::apply(QImage& qimage)
671 {
672 if ((qimage.format() != QImage::Format_RGB32) &&
673 (qimage.format() != QImage::Format_ARGB32) &&
674 (qimage.format() != QImage::Format_ARGB32_Premultiplied))
675 {
676 qCDebug(DIGIKAM_DIMG_LOG) << "Unsupported QImage format" << qimage.format();
677 return false;
678 }
679
680 if (!willHaveEffect())
681 {
682 return true;
683 }
684
685 if (!checkProfiles())
686 {
687 return false;
688 }
689
690 TransformDescription description;
691 description = getDescription(qimage);
692
693 if (!open(description))
694 {
695 return false;
696 }
697
698 transform(qimage, description);
699
700 return true;
701 }
702
transform(DImg & image,const TransformDescription & description,DImgLoaderObserver * const observer)703 void IccTransform::transform(DImg& image, const TransformDescription& description, DImgLoaderObserver* const observer)
704 {
705 const int bytesDepth = image.bytesDepth();
706 const int pixels = image.width() * image.height();
707
708 // convert ten scanlines in a batch
709
710 const int pixelsPerStep = image.width() * 10;
711 uchar* data = image.bits();
712
713 // see dimgloader.cpp, granularity().
714
715 int granularity = 1;
716
717 if (observer)
718 {
719 granularity = (int)((pixels / (20 * 0.9)) / observer->granularity());
720 }
721
722 int checkPoint = pixels;
723
724 // it is safe to use the same input and output buffer if the format is the same
725
726 if (description.inputFormat == description.outputFormat)
727 {
728 for (int p = pixels ; p > 0 ; p -= pixelsPerStep)
729 {
730 int pixelsThisStep = qMin(p, pixelsPerStep);
731 int size = pixelsThisStep * bytesDepth;
732 LcmsLock lock;
733 dkCmsDoTransform(d->handle, data, data, pixelsThisStep);
734 data += size;
735
736 if (observer && (p <= checkPoint))
737 {
738 checkPoint -= granularity;
739 observer->progressInfo(0.1F + 0.9F * (1.0F - float(p) / float(pixels)));
740 }
741 }
742 }
743 else
744 {
745 QVarLengthArray<uchar> buffer(pixelsPerStep * bytesDepth);
746
747 for (int p = pixels ; p > 0 ; p -= pixelsPerStep)
748 {
749 int pixelsThisStep = qMin(p, pixelsPerStep);
750 int size = pixelsThisStep * bytesDepth;
751 LcmsLock lock;
752 memcpy(buffer.data(), data, size);
753 dkCmsDoTransform(d->handle, buffer.data(), data, pixelsThisStep);
754 data += size;
755
756 if (observer && (p <= checkPoint))
757 {
758 checkPoint -= granularity;
759 observer->progressInfo(0.1F + 0.9F * (1.0F - float(p) / float(pixels)));
760 }
761 }
762 }
763 }
764
transform(QImage & image,const TransformDescription &)765 void IccTransform::transform(QImage& image, const TransformDescription&)
766 {
767 const int bytesDepth = 4;
768 const int pixels = image.width() * image.height();
769
770 // convert ten scanlines in a batch
771
772 const int pixelsPerStep = image.width() * 10;
773 uchar* data = image.bits();
774
775 for (int p = pixels ; p > 0 ; p -= pixelsPerStep)
776 {
777 int pixelsThisStep = qMin(p, pixelsPerStep);
778 int size = pixelsThisStep * bytesDepth;
779 LcmsLock lock;
780 dkCmsDoTransform(d->handle, data, data, pixelsThisStep);
781 data += size;
782 }
783 }
784
close()785 void IccTransform::close()
786 {
787 d->close();
788 }
789
790 /*
791 void IccTransform::closeProfiles()
792 {
793 d->inputProfile.close();
794 d->outputProfile.close();
795 d->proofProfile.close();
796 d->embeddedProfile.close();
797 }
798 */
799
800 } // namespace Digikam
801