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 <kis_assert.h>
22
24
25#include <KisLazyStorage.h>
26#include <KisLazyValueWrapper.h>
27
28
32
38 : d(new Private)
39{
40 d->rawData = rawData;
41}
42
46
48{
49 return d->rawData;
50}
51
53{
54 d->rawData = rawData;
55}
56
60
64
70
72
73 struct Shared {
74 QScopedPointer<IccColorProfile::Data> data;
75 QScopedPointer<LcmsColorProfileContainer> lcmsProfile;
76 LazyProfileInfo profileInfo = LazyProfileInfo(LazyProfileInfo::init_value_tag{}, {});
77
79 : data(new IccColorProfile::Data())
80 {
81 }
82 };
83
85 : shared(QSharedPointer<Shared>::create())
86 {
87 }
89
91};
92
97
98IccColorProfile::IccColorProfile(const QByteArray &rawData)
99 : KoColorProfile(QString()), d(new Private)
100{
102 init();
103}
104
106 const ColorPrimaries colorPrimariesType,
107 const TransferCharacteristics transferFunction)
108: KoColorProfile(QString()), d(new Private)
109{
111 (!colorants.isEmpty() || colorPrimariesType != PRIMARIES_UNSPECIFIED)
112 && transferFunction != TRC_UNSPECIFIED);
113
114 cmsCIExyY whitePoint;
115
116 QVector<double> modifiedColorants = colorants;
117
118 KoColorProfile::colorantsForType(colorPrimariesType, modifiedColorants);
119
120 if (modifiedColorants.size()>=2) {
121 whitePoint.x = modifiedColorants[0];
122 whitePoint.y = modifiedColorants[1];
123 whitePoint.Y = 1.0;
124 }
125
126 cmsToneCurve *mainCurve = LcmsColorProfileContainer::transferFunction(transferFunction);
127
128 cmsCIExyYTRIPLE primaries;
129
130 if (modifiedColorants.size()>2 && modifiedColorants.size() <= 8) {
131 primaries = {{modifiedColorants[2], modifiedColorants[3], 1.0},
132 {modifiedColorants[4], modifiedColorants[5], 1.0},
133 {modifiedColorants[6], modifiedColorants[7], 1.0}};
134 }
135
136 cmsHPROFILE iccProfile = nullptr;
137
138 if (colorants.size() == 2) {
139 iccProfile = cmsCreateGrayProfile(&whitePoint, mainCurve);
140 } else /*if (colorants.size()>2 || colorPrimariesType != 2)*/ {
141 // generate rgb profile.
142 cmsToneCurve *curve[3];
143 curve[0] = curve[1] = curve[2] = mainCurve;
144 iccProfile = cmsCreateRGBProfile(&whitePoint, &primaries, curve);
145 }
146
147 KIS_ASSERT(iccProfile);
148
150 name.append("Krita");
151 name.append(KoColorProfile::getColorPrimariesName(colorPrimariesType));
152 name.append(KoColorProfile::getTransferCharacteristicName(transferFunction));
153
154 cmsCIEXYZ media_blackpoint = {0.0, 0.0, 0.0};
155 cmsWriteTag (iccProfile, cmsSigMediaBlackPointTag, &media_blackpoint);
156
157 //set the color profile info on the iccProfile;
158 cmsMLU *mlu;
159 mlu = cmsMLUalloc (NULL, 1);
160 cmsMLUsetASCII (mlu, "en", "US", name.join(" ").toLatin1());
161 cmsWriteTag (iccProfile, cmsSigProfileDescriptionTag, mlu);
162 cmsMLUfree (mlu);
163 mlu = cmsMLUalloc (NULL, 1);
164 cmsMLUsetASCII (mlu, "en", "US", QString("Profile generated by Krita, Public domain.").toLatin1());
165 cmsWriteTag(iccProfile, cmsSigCopyrightTag, mlu);
166 cmsMLUfree (mlu);
167
168 setCharacteristics(colorPrimariesType, transferFunction);
169
171
173 cmsCloseProfile(iccProfile);
174 setFileName(name.join(" ").split(" ").join("-")+".icc");
175 init();
176}
177
179 : KoColorProfile(rhs)
180 , d(new Private(*rhs.d))
181{
182 Q_ASSERT(d->shared);
183}
184
186{
187 Q_ASSERT(d->shared);
188}
189
191{
192 return new IccColorProfile(*this);
193}
194
195QByteArray IccColorProfile::rawData() const
196{
197 return d->shared->data->rawData();
198}
199
200void IccColorProfile::setRawData(const QByteArray &rawData)
201{
202 d->shared->data->setRawData(rawData);
203}
204
206{
207 if (d->shared->lcmsProfile) {
208 return d->shared->lcmsProfile->valid();
209 }
210 return false;
211}
213{
214 if (d->shared->lcmsProfile) {
215 return d->shared->lcmsProfile->version();
216 }
217 return 0.0;
218}
219
221{
222 QString model;
223
224 switch (d->shared->lcmsProfile->colorSpaceSignature()) {
225 case cmsSigRgbData:
226 model = "RGBA";
227 break;
228 case cmsSigLabData:
229 model = "LABA";
230 break;
231 case cmsSigCmykData:
232 model = "CMYKA";
233 break;
234 case cmsSigGrayData:
235 model = "GRAYA";
236 break;
237 case cmsSigXYZData:
238 model = "XYZA";
239 break;
240 case cmsSigYCbCrData:
241 model = "YCbCrA";
242 break;
243 default:
244 // In theory we should be able to interpret the colorspace signature as a 4 char array...
245 model = QString();
246 }
247
248 return model;
249}
251{
252 if (d->shared->lcmsProfile) {
253 return d->shared->lcmsProfile->isSuitableForOutput() && d->shared->profileInfo->value.canCreateCyclicTransform;
254 }
255 return false;
256}
257
259{
260 if (d->shared->lcmsProfile) {
261 return d->shared->lcmsProfile->isSuitableForPrinting();
262 }
263 return false;
264}
265
267{
268 if (d->shared->lcmsProfile) {
269 return d->shared->lcmsProfile->isSuitableForDisplay();
270 }
271 return false;
272}
273
275{
276 if (d->shared->lcmsProfile) {
277 return d->shared->lcmsProfile->supportsPerceptual();
278 }
279 return false;
280}
282{
283 if (d->shared->lcmsProfile) {
284 return d->shared->lcmsProfile->supportsSaturation();
285 }
286 return false;
287}
289{
290 if (d->shared->lcmsProfile) {
291 return d->shared->lcmsProfile->supportsAbsolute();
292 }
293 return false;
294}
296{
297 if (d->shared->lcmsProfile) {
298 return d->shared->lcmsProfile->supportsRelative();
299 }
300 return false;
301}
303{
304 if (d->shared->lcmsProfile) {
305 return d->shared->lcmsProfile->hasColorants();
306 }
307 return false;
308}
310{
311 if (d->shared->lcmsProfile)
312 return d->shared->lcmsProfile->hasTRC();
313 return false;
314}
316{
317 if (d->shared->lcmsProfile)
318 return d->shared->lcmsProfile->isLinear();
319 return false;
320}
321QVector <qreal> IccColorProfile::getColorantsXYZ() const
322{
323 if (d->shared->lcmsProfile) {
324 return d->shared->lcmsProfile->getColorantsXYZ();
325 }
326 return QVector<qreal>(9);
327}
328QVector <qreal> IccColorProfile::getColorantsxyY() const
329{
330 if (d->shared->lcmsProfile) {
331 return d->shared->lcmsProfile->getColorantsxyY();
332 }
333 return QVector<qreal>(9);
334}
336{
337 QVector <qreal> d50Dummy(3);
338 d50Dummy << 0.9642 << 1.0000 << 0.8249;
339 if (d->shared->lcmsProfile) {
340 return d->shared->lcmsProfile->getWhitePointXYZ();
341 }
342 return d50Dummy;
343}
345{
346 QVector <qreal> d50Dummy(3);
347 d50Dummy << 0.34773 << 0.35952 << 1.0;
348 if (d->shared->lcmsProfile) {
349 return d->shared->lcmsProfile->getWhitePointxyY();
350 }
351 return d50Dummy;
352}
353QVector <qreal> IccColorProfile::getEstimatedTRC() const
354{
355 QVector <qreal> dummy(3);
356 dummy.fill(2.2);//estimated sRGB trc.
357 if (d->shared->lcmsProfile) {
358 return d->shared->lcmsProfile->getEstimatedTRC();
359 }
360 return dummy;
361}
362
363bool IccColorProfile::compareTRC(TransferCharacteristics characteristics, float error) const
364{
365 if (d->shared->lcmsProfile) {
366 return d->shared->lcmsProfile->compareTRC(characteristics, error);
367 }
368 return false;
369}
370
371void IccColorProfile::linearizeFloatValue(QVector <qreal> & Value) const
372{
373 if (d->shared->lcmsProfile)
374 d->shared->lcmsProfile->LinearizeFloatValue(Value);
375}
377{
378 if (d->shared->lcmsProfile)
379 d->shared->lcmsProfile->DelinearizeFloatValue(Value);
380}
382{
383 if (d->shared->lcmsProfile)
384 d->shared->lcmsProfile->LinearizeFloatValueFast(Value);
385}
387{
388 if (d->shared->lcmsProfile)
389 d->shared->lcmsProfile->DelinearizeFloatValueFast(Value);
390}
391
393{
394 QByteArray dummy;
395 if (d->shared->lcmsProfile) {
396 dummy = d->shared->lcmsProfile->getProfileUniqueId();
397 }
398 return dummy;
399}
400
402{
403 QFile file(fileName());
404 file.open(QIODevice::ReadOnly);
405 QByteArray rawData = file.readAll();
407 file.close();
408 if (init()) {
409 return true;
410 }
411 qWarning() << "Failed to load profile from " << fileName();
412 return false;
413}
414
416{
417 return false;
418}
419
421{
422 if (!d->shared->lcmsProfile) {
423 d->shared->lcmsProfile.reset(new LcmsColorProfileContainer(d->shared->data.data()));
424 }
425 if (d->shared->lcmsProfile->init()) {
426 setName(d->shared->lcmsProfile->name());
427 setInfo(d->shared->lcmsProfile->info());
428 setManufacturer(d->shared->lcmsProfile->manufacturer());
429 setCopyright(d->shared->lcmsProfile->copyright());
430 if (d->shared->lcmsProfile->valid()) {
431 d->shared->profileInfo = Private::LazyProfileInfo([this] () {
432 return d->calculateFloatUIMinMax();
433 });
434 }
435 return true;
436 } else {
437 return false;
438 }
439}
440
442{
443 Q_ASSERT(d->shared->lcmsProfile);
444 return d->shared->lcmsProfile.data();
445}
446
448{
449 const IccColorProfile *rhsIcc = dynamic_cast<const IccColorProfile *>(&rhs);
450 if (rhsIcc) {
451 return d->shared == rhsIcc->d->shared;
452 }
453 return false;
454}
455
457{
458 Q_ASSERT(!d->shared->profileInfo->value.uiMinMaxes.isEmpty());
459 return d->shared->profileInfo->value.uiMinMaxes;
460}
461
464{
467
468 cmsHPROFILE cprofile = shared->lcmsProfile->lcmsProfile();
469 Q_ASSERT(cprofile);
470
471 cmsColorSpaceSignature color_space_sig = cmsGetColorSpace(cprofile);
472 unsigned int num_channels = cmsChannelsOf(color_space_sig);
473 unsigned int color_space_mask = _cmsLCMScolorSpace(color_space_sig);
474
475 Q_ASSERT(num_channels >= 1 && num_channels <= 4); // num_channels==1 is for grayscale, we need to handle it
476 Q_ASSERT(color_space_mask);
477
478 // to try to find the max range of float/doubles for this profile,
479 // pass in min/max int and make the profile convert that
480 // this is far from perfect, we need a better way, if possible to get the "bounds" of a profile
481
482 uint16_t in_min_pixel[4] = {0, 0, 0, 0};
483 uint16_t in_max_pixel[4] = {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
484 qreal out_min_pixel[4] = {0, 0, 0, 0};
485 qreal out_max_pixel[4] = {0, 0, 0, 0};
486
487 cmsHTRANSFORM trans = cmsCreateTransform(
488 cprofile,
489 (COLORSPACE_SH(color_space_mask) | CHANNELS_SH(num_channels) | BYTES_SH(2)),
490 cprofile,
491 (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
492 INTENT_ABSOLUTE_COLORIMETRIC, 0); // does the intent matter in this case?
493 // absolute colorimetric gives bigger bounds with cmyk's Chemical Proof
494
495 if (trans) {
496 cmsDoTransform(trans, in_min_pixel, out_min_pixel, 1);
497 cmsDoTransform(trans, in_max_pixel, out_max_pixel, 1);
498 cmsDeleteTransform(trans);
499 }//else, we'll just default to [0..1] below
500
501 // Some (calibration) profiles may have a weird RGB->XYZ transformation matrix,
502 // which is not invertible. Therefore, such profile cannot be used as
503 // a workspace color profile and we should convert the image to sRGB
504 // right on image loading
505
506 // LCMS doesn't have a separate method for checking if conversion matrix
507 // is invertible, therefore we just try to create a simple transformation,
508 // where the profile is both, input and output. If the transformation
509 // is created successfully, then this profile is probably suitable for
510 // usage as a working color space.
511
512 info.canCreateCyclicTransform = bool(trans);
513
514 ret.resize(num_channels);
515 for (unsigned int i = 0; i < num_channels; ++i) {
516 if (color_space_sig == cmsSigYCbCrData) {
517 // Although YCbCr profiles are essentially LUT-based
518 // (due to the inability of ICC to represent multiple successive
519 // matrix transforms except with BtoD0 tags in V4),
520 // YCbCr is intended to be a roundtrip transform to the
521 // corresponding RGB transform (BT.601, BT.709).
522 // Force enable the full range of values.
523 ret[i].minVal = 0;
524 ret[i].maxVal = 1;
525 } else if (out_min_pixel[i] < out_max_pixel[i]) {
526 ret[i].minVal = out_min_pixel[i];
527 ret[i].maxVal = out_max_pixel[i];
528 } else {
529 // apparently we can't even guarantee that converted_to_double(0x0000) < converted_to_double(0xFFFF)
530 // assume [0..1] in such cases
531 // we need to find a really solid way of determining the bounds of a profile, if possible
532 ret[i].minVal = 0;
533 ret[i].maxVal = 1;
534 }
535 }
536
537 return info;
538}
539
ColorPrimaries
The colorPrimaries enum Enum of colorants, follows ITU H.273 for values 0 to 255, and has extra known...
@ PRIMARIES_UNSPECIFIED
TransferCharacteristics
The transferCharacteristics enum Enum of transfer characteristics, follows ITU H.273 for values 0 to ...
QScopedPointer< Private > const d
void setRawData(const QByteArray &)
bool isSuitableForDisplay() const override
virtual bool save()
void linearizeFloatValueFast(QVector< qreal > &Value) const override
QVector< qreal > getColorantsxyY() const override
void setRawData(const QByteArray &rawData)
bool operator==(const KoColorProfile &) const override
~IccColorProfile() override
LcmsColorProfileContainer * asLcms() const
QVector< qreal > getColorantsXYZ() const override
bool supportsRelative() const override
bool hasColorants() const override
bool supportsPerceptual() const override
bool isSuitableForPrinting() const override
QVector< qreal > getWhitePointXYZ() 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
QVector< qreal > getWhitePointxyY() const override
bool compareTRC(TransferCharacteristics characteristics, float error) const override
bool supportsAbsolute() const override
QVector< qreal > getEstimatedTRC() const override
static cmsToneCurve * transferFunction(TransferCharacteristics transferFunction)
static QByteArray lcmsProfileToByteArray(const cmsHPROFILE profile)
#define KIS_ASSERT(cond)
Definition kis_assert.h:33
#define INTENT_ABSOLUTE_COLORIMETRIC
Definition kis_global.h:106
QVector< KoChannelInfo::DoubleRange > uiMinMaxes
QScopedPointer< LcmsColorProfileContainer > lcmsProfile
QScopedPointer< IccColorProfile::Data > data
ProfileInfo calculateFloatUIMinMax() const
QSharedPointer< Shared > shared
KisLazyStorage< KisLazyValueWrapper< ProfileInfo >, std::function< ProfileInfo()> > LazyProfileInfo
static void colorantsForType(ColorPrimaries primaries, QVector< double > &colorants)
colorantsForPrimaries fills a QVector<float> with the xy values of the whitepoint and red,...
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)
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)