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}
258{
259 if (d->shared->lcmsProfile) {
260 return d->shared->lcmsProfile->isSuitableForInput() && d->shared->profileInfo->value.canCreateCyclicTransform;
261 }
262 return false;
263}
265{
266 if (d->shared->lcmsProfile) {
267 return d->shared->lcmsProfile->isSuitableForWorkspace() && d->shared->profileInfo->value.canCreateCyclicTransform;
268 }
269 return false;
270}
271
272
274{
275 if (d->shared->lcmsProfile) {
276 return d->shared->lcmsProfile->isSuitableForPrinting();
277 }
278 return false;
279}
280
282{
283 if (d->shared->lcmsProfile) {
284 return d->shared->lcmsProfile->isSuitableForDisplay();
285 }
286 return false;
287}
288
290{
291 if (d->shared->lcmsProfile) {
292 return d->shared->lcmsProfile->supportsPerceptual();
293 }
294 return false;
295}
297{
298 if (d->shared->lcmsProfile) {
299 return d->shared->lcmsProfile->supportsSaturation();
300 }
301 return false;
302}
304{
305 if (d->shared->lcmsProfile) {
306 return d->shared->lcmsProfile->supportsAbsolute();
307 }
308 return false;
309}
311{
312 if (d->shared->lcmsProfile) {
313 return d->shared->lcmsProfile->supportsRelative();
314 }
315 return false;
316}
318{
319 if (d->shared->lcmsProfile) {
320 return d->shared->lcmsProfile->hasColorants();
321 }
322 return false;
323}
325{
326 if (d->shared->lcmsProfile)
327 return d->shared->lcmsProfile->hasTRC();
328 return false;
329}
331{
332 if (d->shared->lcmsProfile)
333 return d->shared->lcmsProfile->isLinear();
334 return false;
335}
336QVector <qreal> IccColorProfile::getColorantsXYZ() const
337{
338 if (d->shared->lcmsProfile) {
339 return d->shared->lcmsProfile->getColorantsXYZ();
340 }
341 return QVector<qreal>(9);
342}
343QVector <qreal> IccColorProfile::getColorantsxyY() const
344{
345 if (d->shared->lcmsProfile) {
346 return d->shared->lcmsProfile->getColorantsxyY();
347 }
348 return QVector<qreal>(9);
349}
351{
352 QVector <qreal> d50Dummy(3);
353 d50Dummy << 0.9642 << 1.0000 << 0.8249;
354 if (d->shared->lcmsProfile) {
355 return d->shared->lcmsProfile->getWhitePointXYZ();
356 }
357 return d50Dummy;
358}
360{
361 QVector <qreal> d50Dummy(3);
362 d50Dummy << 0.34773 << 0.35952 << 1.0;
363 if (d->shared->lcmsProfile) {
364 return d->shared->lcmsProfile->getWhitePointxyY();
365 }
366 return d50Dummy;
367}
368QVector <qreal> IccColorProfile::getEstimatedTRC() const
369{
370 QVector <qreal> dummy(3);
371 dummy.fill(2.2);//estimated sRGB trc.
372 if (d->shared->lcmsProfile) {
373 return d->shared->lcmsProfile->getEstimatedTRC();
374 }
375 return dummy;
376}
377
378bool IccColorProfile::compareTRC(TransferCharacteristics characteristics, float error) const
379{
380 if (d->shared->lcmsProfile) {
381 return d->shared->lcmsProfile->compareTRC(characteristics, error);
382 }
383 return false;
384}
385
386void IccColorProfile::linearizeFloatValue(QVector <qreal> & Value) const
387{
388 if (d->shared->lcmsProfile)
389 d->shared->lcmsProfile->LinearizeFloatValue(Value);
390}
392{
393 if (d->shared->lcmsProfile)
394 d->shared->lcmsProfile->DelinearizeFloatValue(Value);
395}
397{
398 if (d->shared->lcmsProfile)
399 d->shared->lcmsProfile->LinearizeFloatValueFast(Value);
400}
402{
403 if (d->shared->lcmsProfile)
404 d->shared->lcmsProfile->DelinearizeFloatValueFast(Value);
405}
406
408{
409 QByteArray dummy;
410 if (d->shared->lcmsProfile) {
411 dummy = d->shared->lcmsProfile->getProfileUniqueId();
412 }
413 return dummy;
414}
415
417{
418 QFile file(fileName());
419 file.open(QIODevice::ReadOnly);
420 QByteArray rawData = file.readAll();
422 file.close();
423 if (init()) {
424 return true;
425 }
426 qWarning() << "Failed to load profile from " << fileName();
427 return false;
428}
429
431{
432 return false;
433}
434
436{
437 if (!d->shared->lcmsProfile) {
438 d->shared->lcmsProfile.reset(new LcmsColorProfileContainer(d->shared->data.data()));
439 }
440 if (d->shared->lcmsProfile->init()) {
441 setName(d->shared->lcmsProfile->name());
442 setInfo(d->shared->lcmsProfile->info());
443 setManufacturer(d->shared->lcmsProfile->manufacturer());
444 setCopyright(d->shared->lcmsProfile->copyright());
445 if (d->shared->lcmsProfile->valid()) {
446 d->shared->profileInfo = Private::LazyProfileInfo([this] () {
447 return d->calculateFloatUIMinMax();
448 });
449 }
450 return true;
451 } else {
452 return false;
453 }
454}
455
457{
458 Q_ASSERT(d->shared->lcmsProfile);
459 return d->shared->lcmsProfile.data();
460}
461
463{
464 const IccColorProfile *rhsIcc = dynamic_cast<const IccColorProfile *>(&rhs);
465 if (rhsIcc) {
466 return d->shared == rhsIcc->d->shared;
467 }
468 return false;
469}
470
472{
473 Q_ASSERT(!d->shared->profileInfo->value.uiMinMaxes.isEmpty());
474 return d->shared->profileInfo->value.uiMinMaxes;
475}
476
479{
482
483 cmsHPROFILE cprofile = shared->lcmsProfile->lcmsProfile();
484 Q_ASSERT(cprofile);
485
486 cmsColorSpaceSignature color_space_sig = cmsGetColorSpace(cprofile);
487 unsigned int num_channels = cmsChannelsOf(color_space_sig);
488 unsigned int color_space_mask = _cmsLCMScolorSpace(color_space_sig);
489
490 Q_ASSERT(num_channels >= 1 && num_channels <= 4); // num_channels==1 is for grayscale, we need to handle it
491 Q_ASSERT(color_space_mask);
492
493 // to try to find the max range of float/doubles for this profile,
494 // pass in min/max int and make the profile convert that
495 // this is far from perfect, we need a better way, if possible to get the "bounds" of a profile
496
497 uint16_t in_min_pixel[4] = {0, 0, 0, 0};
498 uint16_t in_max_pixel[4] = {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
499 qreal out_min_pixel[4] = {0, 0, 0, 0};
500 qreal out_max_pixel[4] = {0, 0, 0, 0};
501
502 cmsHTRANSFORM trans = cmsCreateTransform(
503 cprofile,
504 (COLORSPACE_SH(color_space_mask) | CHANNELS_SH(num_channels) | BYTES_SH(2)),
505 cprofile,
506 (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
507 INTENT_ABSOLUTE_COLORIMETRIC, 0); // does the intent matter in this case?
508 // absolute colorimetric gives bigger bounds with cmyk's Chemical Proof
509
510 if (trans) {
511 cmsDoTransform(trans, in_min_pixel, out_min_pixel, 1);
512 cmsDoTransform(trans, in_max_pixel, out_max_pixel, 1);
513 cmsDeleteTransform(trans);
514 }//else, we'll just default to [0..1] below
515
516 // Some (calibration) profiles may have a weird RGB->XYZ transformation matrix,
517 // which is not invertible. Therefore, such profile cannot be used as
518 // a workspace color profile and we should convert the image to sRGB
519 // right on image loading
520
521 // LCMS doesn't have a separate method for checking if conversion matrix
522 // is invertible, therefore we just try to create a simple transformation,
523 // where the profile is both, input and output. If the transformation
524 // is created successfully, then this profile is probably suitable for
525 // usage as a working color space.
526
527 info.canCreateCyclicTransform = bool(trans);
528
529 ret.resize(num_channels);
530 for (unsigned int i = 0; i < num_channels; ++i) {
531 if (color_space_sig == cmsSigYCbCrData) {
532 // Although YCbCr profiles are essentially LUT-based
533 // (due to the inability of ICC to represent multiple successive
534 // matrix transforms except with BtoD0 tags in V4),
535 // YCbCr is intended to be a roundtrip transform to the
536 // corresponding RGB transform (BT.601, BT.709).
537 // Force enable the full range of values.
538 ret[i].minVal = 0;
539 ret[i].maxVal = 1;
540 } else if (out_min_pixel[i] < out_max_pixel[i]) {
541 ret[i].minVal = out_min_pixel[i];
542 ret[i].maxVal = out_max_pixel[i];
543 } else {
544 // apparently we can't even guarantee that converted_to_double(0x0000) < converted_to_double(0xFFFF)
545 // assume [0..1] in such cases
546 // we need to find a really solid way of determining the bounds of a profile, if possible
547 ret[i].minVal = 0;
548 ret[i].maxVal = 1;
549 }
550 }
551
552 return info;
553}
554
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
bool isSuitableForInput() 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 isSuitableForWorkspace() 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)