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