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
176 if (query.transfer == TRC_ITU_R_BT_2100_0_PQ) {
177 double nits = 80.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}
413QVector <qreal> IccColorProfile::getEstimatedTRC() const
414{
415 QVector <qreal> dummy(3);
416 dummy.fill(2.2);//estimated sRGB trc.
417 if (d->shared->lcmsProfile) {
418 return d->shared->lcmsProfile->getEstimatedTRC();
419 }
420 return dummy;
421}
422
423bool IccColorProfile::compareTRC(TransferCharacteristics characteristics, float error) const
424{
425 if (d->shared->lcmsProfile) {
426 return d->shared->lcmsProfile->compareTRC(characteristics, error);
427 }
428 return false;
429}
430
431void IccColorProfile::linearizeFloatValue(QVector <qreal> & Value) const
432{
433 if (d->shared->lcmsProfile)
434 d->shared->lcmsProfile->LinearizeFloatValue(Value);
435}
437{
438 if (d->shared->lcmsProfile)
439 d->shared->lcmsProfile->DelinearizeFloatValue(Value);
440}
442{
443 if (d->shared->lcmsProfile)
444 d->shared->lcmsProfile->LinearizeFloatValueFast(Value);
445}
447{
448 if (d->shared->lcmsProfile)
449 d->shared->lcmsProfile->DelinearizeFloatValueFast(Value);
450}
451
453{
454 QByteArray dummy;
455 if (d->shared->lcmsProfile) {
456 dummy = d->shared->lcmsProfile->getProfileUniqueId();
457 }
458 return dummy;
459}
460
462{
463 QFile file(fileName());
464 if (file.open(QIODevice::ReadOnly)) {
465 QByteArray rawData = file.readAll();
467 file.close();
468 if (init()) {
469 return true;
470 }
471 }
472 qWarning() << "Failed to load profile from " << fileName();
473 return false;
474}
475
477{
478 return false;
479}
480
482{
483 if (!d->shared->lcmsProfile) {
484 d->shared->lcmsProfile.reset(new LcmsColorProfileContainer(d->shared->data.data()));
485 }
486 if (d->shared->lcmsProfile->init()) {
487 setName(d->shared->lcmsProfile->name());
488 setInfo(d->shared->lcmsProfile->info());
489 setManufacturer(d->shared->lcmsProfile->manufacturer());
490 setCopyright(d->shared->lcmsProfile->copyright());
491 if (d->shared->lcmsProfile->hasCicpValues()) {
492 setCharacteristics(d->shared->lcmsProfile->cicpPrimaries(), d->shared->lcmsProfile->cicpTransfer());
493 }
494 if (d->shared->lcmsProfile->valid()) {
495 d->shared->profileInfo = Private::LazyProfileInfo([this] () {
496 return d->calculateFloatUIMinMax();
497 });
498 }
499 return true;
500 } else {
501 return false;
502 }
503}
504
506{
507 Q_ASSERT(d->shared->lcmsProfile);
508 return d->shared->lcmsProfile.data();
509}
510
512{
513 const IccColorProfile *rhsIcc = dynamic_cast<const IccColorProfile *>(&rhs);
514 if (rhsIcc) {
515 return d->shared == rhsIcc->d->shared;
516 }
517 return false;
518}
519
521{
522 Q_ASSERT(!d->shared->profileInfo->value.uiMinMaxes.isEmpty());
523 return d->shared->profileInfo->value.uiMinMaxes;
524}
525
528{
531
532 cmsHPROFILE cprofile = shared->lcmsProfile->lcmsProfile();
533 Q_ASSERT(cprofile);
534
535 cmsColorSpaceSignature color_space_sig = cmsGetColorSpace(cprofile);
536 unsigned int num_channels = cmsChannelsOf(color_space_sig);
537 unsigned int color_space_mask = _cmsLCMScolorSpace(color_space_sig);
538
539 Q_ASSERT(num_channels >= 1 && num_channels <= 4); // num_channels==1 is for grayscale, we need to handle it
540 Q_ASSERT(color_space_mask);
541
542 // to try to find the max range of float/doubles for this profile,
543 // pass in min/max int and make the profile convert that
544 // this is far from perfect, we need a better way, if possible to get the "bounds" of a profile
545
546 uint16_t in_min_pixel[4] = {0, 0, 0, 0};
547 uint16_t in_max_pixel[4] = {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
548 qreal out_min_pixel[4] = {0, 0, 0, 0};
549 qreal out_max_pixel[4] = {0, 0, 0, 0};
550
551 cmsHTRANSFORM trans = cmsCreateTransform(
552 cprofile,
553 (COLORSPACE_SH(color_space_mask) | CHANNELS_SH(num_channels) | BYTES_SH(2)),
554 cprofile,
555 (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
556 INTENT_ABSOLUTE_COLORIMETRIC, 0); // does the intent matter in this case?
557 // absolute colorimetric gives bigger bounds with cmyk's Chemical Proof
558
559 if (trans) {
560 cmsDoTransform(trans, in_min_pixel, out_min_pixel, 1);
561 cmsDoTransform(trans, in_max_pixel, out_max_pixel, 1);
562 cmsDeleteTransform(trans);
563 }//else, we'll just default to [0..1] below
564
565 // Some (calibration) profiles may have a weird RGB->XYZ transformation matrix,
566 // which is not invertible. Therefore, such profile cannot be used as
567 // a workspace color profile and we should convert the image to sRGB
568 // right on image loading
569
570 // LCMS doesn't have a separate method for checking if conversion matrix
571 // is invertible, therefore we just try to create a simple transformation,
572 // where the profile is both, input and output. If the transformation
573 // is created successfully, then this profile is probably suitable for
574 // usage as a working color space.
575
576 info.canCreateCyclicTransform = bool(trans);
577
578 ret.resize(num_channels);
579 for (unsigned int i = 0; i < num_channels; ++i) {
580 if (color_space_sig == cmsSigYCbCrData) {
581 // Although YCbCr profiles are essentially LUT-based
582 // (due to the inability of ICC to represent multiple successive
583 // matrix transforms except with BtoD0 tags in V4),
584 // YCbCr is intended to be a roundtrip transform to the
585 // corresponding RGB transform (BT.601, BT.709).
586 // Force enable the full range of values.
587 ret[i].minVal = 0;
588 ret[i].maxVal = 1;
589 } else if (out_min_pixel[i] < out_max_pixel[i]) {
590 ret[i].minVal = out_min_pixel[i];
591 ret[i].maxVal = out_max_pixel[i];
592 } else {
593 // apparently we can't even guarantee that converted_to_double(0x0000) < converted_to_double(0xFFFF)
594 // assume [0..1] in such cases
595 // we need to find a really solid way of determining the bounds of a profile, if possible
596 ret[i].minVal = 0;
597 ret[i].maxVal = 1;
598 }
599 }
600
601 return info;
602}
603
@ 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
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
CICP-compatible enum value representing the transfer function.
QList< KoColorimetryUtils::xy > rgbColorants
The desired white point of the profile.
KoColorimetryUtils::xy whitePoint
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)