Krita Source Code Documentation
Loading...
Searching...
No Matches
IccColorProfile.cpp
Go to the documentation of this file.
1/*
2* SPDX-FileCopyrightText: 2007 Cyrille Berger <cberger@cberger.net>
3* SPDX-FileCopyrightText: 2021 Wolthera van Hövell tot Westerflier <griffinvalley@gmail.com>
4*
5* SPDX-License-Identifier: LGPL-2.1-or-later
6*/
7
8#include "IccColorProfile.h"
9
10#include <cmath>
11#include <cstdint>
12#include <limits>
13
14#include <lcms2.h>
15
16#include <QDebug>
17#include <QFile>
18#include <QSharedPointer>
19
20#include <KoColorConversions.h>
21#include <KoColorProfileQuery.h>
22#include <kis_assert.h>
23
26#include "kis_dom_utils.h"
27
28#include <KisLazyStorage.h>
29#include <KisLazyValueWrapper.h>
30
31
35
41 : d(new Private)
42{
43 d->rawData = rawData;
44}
45
49
51{
52 return d->rawData;
53}
54
56{
57 d->rawData = rawData;
58}
59
63
67
73
75
76 struct Shared {
77 QScopedPointer<IccColorProfile::Data> data;
78 QScopedPointer<LcmsColorProfileContainer> lcmsProfile;
79 LazyProfileInfo profileInfo = LazyProfileInfo(LazyProfileInfo::init_value_tag{}, {});
80
82 : data(new IccColorProfile::Data())
83 {
84 }
85 };
86
88 : shared(QSharedPointer<Shared>::create())
89 {
90 }
92
94};
95
100
101IccColorProfile::IccColorProfile(const QByteArray &rawData)
102 : KoColorProfile(QString()), d(new Private)
103{
105 init();
106}
107
109: KoColorProfile(QString()), d(new Private)
110{
112
114
115 cmsCIExyY whitePoint;
116
117 QList<KoColorimetryUtils::xy> modifiedColorants = query.rgbColorants;
118
120
121 KoColorProfile::colorantsForType(query.primaries, wp, modifiedColorants, true);
122
123
124 if (modifiedColorants.size()>=2) {
125 whitePoint.x = wp.x;
126 whitePoint.y = wp.y;
127 whitePoint.Y = 1.0;
128 }
129
130 cmsToneCurve *mainCurve = LcmsColorProfileContainer::transferFunction(query.transfer);
131
132 cmsCIExyYTRIPLE primaries;
133
134 if (modifiedColorants.size()>0 && modifiedColorants.size() <= 3) {
135 primaries = {{modifiedColorants[0].x, modifiedColorants[0].y, 1.0},
136 {modifiedColorants[1].x, modifiedColorants[1].y, 1.0},
137 {modifiedColorants[2].x, modifiedColorants[2].y, 1.0}};
138 }
139
140 cmsHPROFILE iccProfile = nullptr;
141
142 if (query.isGrayscale()) {
143 iccProfile = cmsCreateGrayProfile(&whitePoint, mainCurve);
144 // cleanup
145 cmsFreeToneCurve(mainCurve);
146 } else /*if (colorants.size()>2 || colorPrimariesType != 2)*/ {
147 // generate rgb profile.
148 cmsToneCurve *curve[3];
149 curve[0] = curve[1] = curve[2] = mainCurve;
150 iccProfile = cmsCreateRGBProfile(&whitePoint, &primaries, curve);
151 cmsFreeToneCurve(mainCurve);
152 }
153
154 if (!iccProfile) {
155 qWarning() << "WARNING: LCMS failed to create a profile for the requested parameters";
156 qWarning().nospace() << " transfer function: " << getTransferCharacteristicName(query.transfer) << " (" << query.transfer << ")";
157 qWarning().nospace() << " named primaries:" << getColorPrimariesName(query.primaries) << " (" << query.primaries << ")";
158 qWarning() << " requested colorants:" << query.rgbColorants;
159 // leave the profile in invalid state and return
160 return;
161 }
162
164 name.append("Krita");
167 name.append(QStringLiteral("Rec. 2100"));
168 } else {
170 }
172
173 cmsCIEXYZ media_blackpoint = {0.0, 0.0, 0.0};
174 cmsWriteTag (iccProfile, cmsSigMediaBlackPointTag, &media_blackpoint);
175
177 double nits = query.hdrReferenceWhite? *query.hdrReferenceWhite: 203.0;
181 // Otherwise this is recognised as a matrix shaper...
182 cmsWriteTag(iccProfile, cmsSigRedColorantTag, nullptr);
183 cmsWriteTag(iccProfile, cmsSigGreenColorantTag, nullptr);
184 cmsWriteTag(iccProfile, cmsSigBlueColorantTag, nullptr);
185 cmsWriteTag(iccProfile, cmsSigRedTRCTag, nullptr);
186 cmsWriteTag(iccProfile, cmsSigGreenTRCTag, nullptr);
187 cmsWriteTag(iccProfile, cmsSigBlueTRCTag, nullptr);
188
189 name.append("("+KisDomUtils::toString(nits)+"cd/m²)");
190
191 }
192
193 // we need to not write when primaries are unspecified, because otherwise kocolorprofile's own matching mechanism breaks.
194 if (query.primaries < 256 && query.primaries != PRIMARIES_UNSPECIFIED && query.transfer < 256) {
195 cmsVideoSignalType cicpValues;
196 cicpValues.ColourPrimaries = quint8(query.primaries);
197 cicpValues.TransferCharacteristics = quint8(query.transfer);
198 cicpValues.MatrixCoefficients = 0;
199 cicpValues.VideoFullRangeFlag = 0; // According to the H.273 spec, 0 is the default value.
200 cmsWriteTag (iccProfile, cmsSigcicpTag, &cicpValues);
201 }
202
203 //set the color profile info on the iccProfile;
204 cmsMLU *mlu;
205 mlu = cmsMLUalloc (NULL, 1);
206 cmsMLUsetWide(mlu, "en", "US", name.join(" ").toStdWString().data());
207 cmsWriteTag (iccProfile, cmsSigProfileDescriptionTag, mlu);
208 cmsMLUfree (mlu);
209 mlu = cmsMLUalloc (NULL, 1);
210 cmsMLUsetASCII (mlu, "en", "US", QString("Profile generated by Krita, Public domain.").toLatin1());
211 cmsWriteTag(iccProfile, cmsSigCopyrightTag, mlu);
212 cmsMLUfree (mlu);
213
214 // Still setting this, as it also writes the non H.273 values.
215 if (query.primaries != PRIMARIES_UNSPECIFIED) {
217 }
218
220 cmsCloseProfile(iccProfile);
221
222 setFileName(name.join(" ").remove("(").remove(")").replace("cd/m²", "nits").split(" ").join("-")+".icc");
223 init();
224}
225
227 : KoColorProfile(rhs)
228 , d(new Private(*rhs.d))
229{
230 Q_ASSERT(d->shared);
231}
232
234{
235 Q_ASSERT(d->shared);
236}
237
239{
240 return new IccColorProfile(*this);
241}
242
243QByteArray IccColorProfile::rawData() const
244{
245 return d->shared->data->rawData();
246}
247
248void IccColorProfile::setRawData(const QByteArray &rawData)
249{
250 d->shared->data->setRawData(rawData);
251}
252
254{
255 if (d->shared->lcmsProfile) {
256 return d->shared->lcmsProfile->valid();
257 }
258 return false;
259}
261{
262 if (d->shared->lcmsProfile) {
263 return d->shared->lcmsProfile->version();
264 }
265 return 0.0;
266}
267
269{
270 QString model;
271
272 switch (d->shared->lcmsProfile->colorSpaceSignature()) {
273 case cmsSigRgbData:
274 model = "RGBA";
275 break;
276 case cmsSigLabData:
277 model = "LABA";
278 break;
279 case cmsSigCmykData:
280 model = "CMYKA";
281 break;
282 case cmsSigGrayData:
283 model = "GRAYA";
284 break;
285 case cmsSigXYZData:
286 model = "XYZA";
287 break;
288 case cmsSigYCbCrData:
289 model = "YCbCrA";
290 break;
291 default:
292 // In theory we should be able to interpret the colorspace signature as a 4 char array...
293 model = QString();
294 }
295
296 return model;
297}
299{
300 if (d->shared->lcmsProfile) {
301 return d->shared->lcmsProfile->isSuitableForOutput() && d->shared->profileInfo->value.canCreateCyclicTransform;
302 }
303 return false;
304}
306{
307 if (d->shared->lcmsProfile) {
308 return d->shared->lcmsProfile->isSuitableForInput() && d->shared->profileInfo->value.canCreateCyclicTransform;
309 }
310 return false;
311}
313{
314 if (d->shared->lcmsProfile) {
315 return d->shared->lcmsProfile->isSuitableForWorkspace() && d->shared->profileInfo->value.canCreateCyclicTransform;
316 }
317 return false;
318}
319
320
322{
323 if (d->shared->lcmsProfile) {
324 return d->shared->lcmsProfile->isSuitableForPrinting();
325 }
326 return false;
327}
328
330{
331 if (d->shared->lcmsProfile) {
332 return d->shared->lcmsProfile->isSuitableForDisplay();
333 }
334 return false;
335}
336
338{
339 if (d->shared->lcmsProfile) {
340 return d->shared->lcmsProfile->supportsPerceptual();
341 }
342 return false;
343}
345{
346 if (d->shared->lcmsProfile) {
347 return d->shared->lcmsProfile->supportsSaturation();
348 }
349 return false;
350}
352{
353 if (d->shared->lcmsProfile) {
354 return d->shared->lcmsProfile->supportsAbsolute();
355 }
356 return false;
357}
359{
360 if (d->shared->lcmsProfile) {
361 return d->shared->lcmsProfile->supportsRelative();
362 }
363 return false;
364}
366{
367 if (d->shared->lcmsProfile) {
368 return d->shared->lcmsProfile->hasColorants();
369 }
370 return false;
371}
373{
374 if (d->shared->lcmsProfile)
375 return d->shared->lcmsProfile->hasTRC();
376 return false;
377}
379{
380 if (d->shared->lcmsProfile)
381 return d->shared->lcmsProfile->isLinear();
382 return false;
383}
384QVector <KoColorimetryUtils::XYZ> IccColorProfile::getColorantsXYZ() const
385{
386 if (d->shared->lcmsProfile) {
387 return d->shared->lcmsProfile->getColorantsXYZ();
388 }
390}
392{
393 if (d->shared->lcmsProfile) {
394 return d->shared->lcmsProfile->getColorantsxyY();
395 }
397}
399{
400 KoColorimetryUtils::XYZ d50Dummy {0.9642, 1.0000, 0.8249};
401 if (d->shared->lcmsProfile) {
402 return d->shared->lcmsProfile->getWhitePointXYZ();
403 }
404 return d50Dummy;
405}
407{
408 if (d->shared->lcmsProfile) {
409 return d->shared->lcmsProfile->getWhitePointxyY();
410 }
411 return KoColorimetryUtils::xyY{0.34773, 0.35952, 1.0};
412}
413
414std::optional<double> IccColorProfile::hdrReferenceWhite() const
415{
416 if (d->shared->lcmsProfile) {
417 return d->shared->lcmsProfile->hdrReferenceWhite();
418 }
419 return std::nullopt;
420}
421QVector <qreal> IccColorProfile::getEstimatedTRC() const
422{
423 QVector <qreal> dummy(3);
424 dummy.fill(2.2);//estimated sRGB trc.
425 if (d->shared->lcmsProfile) {
426 return d->shared->lcmsProfile->getEstimatedTRC();
427 }
428 return dummy;
429}
430
431bool IccColorProfile::compareTRC(TransferCharacteristics characteristics, float error) const
432{
433 if (d->shared->lcmsProfile) {
434 return d->shared->lcmsProfile->compareTRC(characteristics, error);
435 }
436 return false;
437}
438
439void IccColorProfile::linearizeFloatValue(QVector <qreal> & Value) const
440{
441 if (d->shared->lcmsProfile)
442 d->shared->lcmsProfile->LinearizeFloatValue(Value);
443}
445{
446 if (d->shared->lcmsProfile)
447 d->shared->lcmsProfile->DelinearizeFloatValue(Value);
448}
450{
451 if (d->shared->lcmsProfile)
452 d->shared->lcmsProfile->LinearizeFloatValueFast(Value);
453}
455{
456 if (d->shared->lcmsProfile)
457 d->shared->lcmsProfile->DelinearizeFloatValueFast(Value);
458}
459
461{
462 QByteArray dummy;
463 if (d->shared->lcmsProfile) {
464 dummy = d->shared->lcmsProfile->getProfileUniqueId();
465 }
466 return dummy;
467}
468
470{
471 QFile file(fileName());
472 if (file.open(QIODevice::ReadOnly)) {
473 QByteArray rawData = file.readAll();
475 file.close();
476 if (init()) {
477 return true;
478 }
479 }
480 qWarning() << "Failed to load profile from " << fileName();
481 return false;
482}
483
485{
486 return false;
487}
488
490{
491 if (!d->shared->lcmsProfile) {
492 d->shared->lcmsProfile.reset(new LcmsColorProfileContainer(d->shared->data.data()));
493 }
494 if (d->shared->lcmsProfile->init()) {
495 setName(d->shared->lcmsProfile->name());
496 setInfo(d->shared->lcmsProfile->info());
497 setManufacturer(d->shared->lcmsProfile->manufacturer());
498 setCopyright(d->shared->lcmsProfile->copyright());
499 if (d->shared->lcmsProfile->hasCicpValues()) {
500 setCharacteristics(d->shared->lcmsProfile->cicpPrimaries(), d->shared->lcmsProfile->cicpTransfer());
501 }
502 if (d->shared->lcmsProfile->valid()) {
503 d->shared->profileInfo = Private::LazyProfileInfo([this] () {
504 return d->calculateFloatUIMinMax();
505 });
506 }
507 return true;
508 } else {
509 return false;
510 }
511}
512
514{
515 Q_ASSERT(d->shared->lcmsProfile);
516 return d->shared->lcmsProfile.data();
517}
518
520{
521 const IccColorProfile *rhsIcc = dynamic_cast<const IccColorProfile *>(&rhs);
522 if (rhsIcc) {
523 return d->shared == rhsIcc->d->shared;
524 }
525 return false;
526}
527
529{
530 Q_ASSERT(!d->shared->profileInfo->value.uiMinMaxes.isEmpty());
531 return d->shared->profileInfo->value.uiMinMaxes;
532}
533
536{
539
540 cmsHPROFILE cprofile = shared->lcmsProfile->lcmsProfile();
541 Q_ASSERT(cprofile);
542
543 cmsColorSpaceSignature color_space_sig = cmsGetColorSpace(cprofile);
544 unsigned int num_channels = cmsChannelsOf(color_space_sig);
545 unsigned int color_space_mask = _cmsLCMScolorSpace(color_space_sig);
546
547 Q_ASSERT(num_channels >= 1 && num_channels <= 4); // num_channels==1 is for grayscale, we need to handle it
548 Q_ASSERT(color_space_mask);
549
550 // to try to find the max range of float/doubles for this profile,
551 // pass in min/max int and make the profile convert that
552 // this is far from perfect, we need a better way, if possible to get the "bounds" of a profile
553
554 uint16_t in_min_pixel[4] = {0, 0, 0, 0};
555 uint16_t in_max_pixel[4] = {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
556 qreal out_min_pixel[4] = {0, 0, 0, 0};
557 qreal out_max_pixel[4] = {0, 0, 0, 0};
558
559 cmsHTRANSFORM trans = cmsCreateTransform(
560 cprofile,
561 (COLORSPACE_SH(color_space_mask) | CHANNELS_SH(num_channels) | BYTES_SH(2)),
562 cprofile,
563 (COLORSPACE_SH(color_space_mask) | FLOAT_SH(1) | CHANNELS_SH(num_channels) | BYTES_SH(0)), //NOTE THAT 'BYTES' FIELD IS SET TO ZERO ON DLB because 8 bytes overflows the bitfield
564 INTENT_ABSOLUTE_COLORIMETRIC, 0); // does the intent matter in this case?
565 // absolute colorimetric gives bigger bounds with cmyk's Chemical Proof
566
567 if (trans) {
568 cmsDoTransform(trans, in_min_pixel, out_min_pixel, 1);
569 cmsDoTransform(trans, in_max_pixel, out_max_pixel, 1);
570 cmsDeleteTransform(trans);
571 }//else, we'll just default to [0..1] below
572
573 // Some (calibration) profiles may have a weird RGB->XYZ transformation matrix,
574 // which is not invertible. Therefore, such profile cannot be used as
575 // a workspace color profile and we should convert the image to sRGB
576 // right on image loading
577
578 // LCMS doesn't have a separate method for checking if conversion matrix
579 // is invertible, therefore we just try to create a simple transformation,
580 // where the profile is both, input and output. If the transformation
581 // is created successfully, then this profile is probably suitable for
582 // usage as a working color space.
583
584 info.canCreateCyclicTransform = bool(trans);
585
586 ret.resize(num_channels);
587 for (unsigned int i = 0; i < num_channels; ++i) {
588 if (color_space_sig == cmsSigYCbCrData) {
589 // Although YCbCr profiles are essentially LUT-based
590 // (due to the inability of ICC to represent multiple successive
591 // matrix transforms except with BtoD0 tags in V4),
592 // YCbCr is intended to be a roundtrip transform to the
593 // corresponding RGB transform (BT.601, BT.709).
594 // Force enable the full range of values.
595 ret[i].minVal = 0;
596 ret[i].maxVal = 1;
597 } else if (out_min_pixel[i] < out_max_pixel[i]) {
598 ret[i].minVal = out_min_pixel[i];
599 ret[i].maxVal = out_max_pixel[i];
600 } else {
601 // apparently we can't even guarantee that converted_to_double(0x0000) < converted_to_double(0xFFFF)
602 // assume [0..1] in such cases
603 // we need to find a really solid way of determining the bounds of a profile, if possible
604 ret[i].minVal = 0;
605 ret[i].maxVal = 1;
606 }
607 }
608
609 return info;
610}
611
@ PRIMARIES_ITU_R_BT_2020_2_AND_2100_0
@ PRIMARIES_UNSPECIFIED
TransferCharacteristics
The transferCharacteristics enum Enum of transfer characteristics, follows ITU H.273 for values 0 to ...
@ TRC_ITU_R_BT_2100_0_HLG
@ TRC_ITU_R_BT_2100_0_PQ
QScopedPointer< Private > const d
void setRawData(const QByteArray &)
bool isSuitableForDisplay() const override
bool isSuitableForInput() const override
virtual bool save()
void linearizeFloatValueFast(QVector< qreal > &Value) const override
KoColorimetryUtils::xyY getWhitePointxyY() const override
void setRawData(const QByteArray &rawData)
bool operator==(const KoColorProfile &) const override
QVector< KoColorimetryUtils::XYZ > getColorantsXYZ() const override
~IccColorProfile() override
LcmsColorProfileContainer * asLcms() const
bool supportsRelative() const override
bool hasColorants() const override
bool isSuitableForWorkspace() const override
bool supportsPerceptual() const override
bool isSuitableForPrinting() const override
bool isLinear() const override
void delinearizeFloatValue(QVector< qreal > &Value) const override
QString colorModelID() const override
QByteArray rawData() const override
void linearizeFloatValue(QVector< qreal > &Value) const override
const QVector< KoChannelInfo::DoubleRange > & getFloatUIMinMax(void) const
bool isSuitableForOutput() const override
std::optional< double > hdrReferenceWhite() const override
hdrReferenceWhite HDR reference white is only available for Perceptual Quantizer profiles that save t...
bool load() override
bool supportsSaturation() const override
bool valid() const override
bool hasTRC() const override
IccColorProfile(const QString &fileName=QString())
float version() const override
QByteArray uniqueId() const override
void delinearizeFloatValueFast(QVector< qreal > &Value) const override
KoColorProfile * clone() const override
QScopedPointer< Private > d
KoColorimetryUtils::XYZ getWhitePointXYZ() const override
bool compareTRC(TransferCharacteristics characteristics, float error) const override
QVector< KoColorimetryUtils::xyY > getColorantsxyY() const override
bool supportsAbsolute() const override
QVector< qreal > getEstimatedTRC() const override
static cmsToneCurve * transferFunction(TransferCharacteristics transferFunction)
static QByteArray lcmsProfileToByteArray(const cmsHPROFILE profile)
static bool setPerceptualQuantizerAToBDummyPipeline(cmsHPROFILE iccProfile, cmsTagSignature tag, ColorPrimaries primaries, double diffuseWhiteNits=80.0)
static bool setDiffuseWhitePerceptualQuantizer(cmsHPROFILE iccProfile, double diffuseWhiteNits=80.0)
setDiffuseWhitePerceptualQuantizer This adds a dictionary with an CRWL entry to the profile.
static bool setPerceptualQuantizerBToADummyPipeline(cmsHPROFILE iccProfile, cmsTagSignature tag, ColorPrimaries primaries, double diffuseWhiteNits=80.0)
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
#define INTENT_ABSOLUTE_COLORIMETRIC
Definition kis_global.h:106
QString toString(const QString &value)
QVector< KoChannelInfo::DoubleRange > uiMinMaxes
QScopedPointer< LcmsColorProfileContainer > lcmsProfile
QScopedPointer< IccColorProfile::Data > data
ProfileInfo calculateFloatUIMinMax() const
QSharedPointer< Shared > shared
KisLazyStorage< KisLazyValueWrapper< ProfileInfo >, std::function< ProfileInfo()> > LazyProfileInfo
The KoColorProfileQuery struct.
TransferCharacteristics transfer
CICP-compatible enum value representing the white point and primaries.
ColorPrimaries primaries
Rgb Primaries of the profile. When empty, this is a query for a greyscale profile.
bool isValid() const
Used for PQ profiles.
QList< KoColorimetryUtils::xy > rgbColorants
The desired white point of the profile.
KoColorimetryUtils::xy whitePoint
std::optional< double > hdrReferenceWhite
CICP-compatible enum value representing the transfer function.
void setFileName(const QString &filename)
TransferCharacteristics characteristics
void setCopyright(const QString &copyright)
static QString getTransferCharacteristicName(TransferCharacteristics curve)
getTransferCharacteristicName
static QString getColorPrimariesName(ColorPrimaries primaries)
getColorPrimariesName
void setName(const QString &name)
static void colorantsForType(ColorPrimaries primaries, KoColorimetryUtils::xy &whitePoint, QList< KoColorimetryUtils::xy > &colorants, const bool prequantized=false)
colorantsForPrimaries fills a QVector<float> with the xy values of the whitepoint and red,...
void setManufacturer(const QString &manufacturer)
void setCharacteristics(ColorPrimaries primaries, TransferCharacteristics curve)
setCharacteristics ideally, we'd read this from the icc profile curve, but that can be tricky,...
void setInfo(const QString &info)