6#include "rgbcolorspace.h"
8#include "rgbcolorspace_p.h"
10#include "absolutecolor.h"
11#include "constpropagatingrawpointer.h"
12#include "constpropagatinguniquepointer.h"
13#include "genericcolor.h"
14#include "helperconstants.h"
15#include "helpermath.h"
16#include "helperqttypes.h"
17#include "initializetranslation.h"
18#include "iohandlerfactory.h"
22#include <qbytearray.h>
24#include <qcoreapplication.h>
28#include <qnamespace.h>
30#include <qsharedpointer.h>
31#include <qstringliteral.h>
34#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
35#include <qcontainerfwd.h>
38#include <qstringlist.h>
55RgbColorSpace::RgbColorSpace(
QObject *parent)
57 , d_pointer(new RgbColorSpacePrivate(this))
86 cmsHPROFILE srgb = cmsCreate_sRGBProfile();
87 const bool success = result->d_pointer->initialize(srgb);
88 cmsCloseProfile(srgb);
101 std::optional<QStringList>());
104 result->d_pointer->m_profileCreationDateTime =
QDateTime();
106 result->d_pointer->m_profileManufacturer = tr(
"LittleCMS");
107 result->d_pointer->m_profileModel =
QString();
109 result->d_pointer->m_profileName = tr(
"sRGB color space");
110 result->d_pointer->m_profileMaximumCielchD50Chroma = 132;
160 constexpr auto myContextID =
nullptr;
163 cmsIOHANDLER *myIOHandler =
164 IOHandlerFactory::createReadOnly(myContextID, fileName);
165 if (myIOHandler ==
nullptr) {
170 cmsHPROFILE myProfileHandle =
171 cmsOpenProfileFromIOhandlerTHR(myContextID, myIOHandler);
172 if (myProfileHandle ==
nullptr) {
184 newObject->d_pointer->m_profileAbsoluteFilePath =
186 newObject->d_pointer->m_profileFileSize = myFileInfo.
size();
187 const bool success = newObject->d_pointer->initialize(myProfileHandle);
190 cmsCloseProfile(myProfileHandle);
227bool RgbColorSpacePrivate::initialize(cmsHPROFILE rgbProfileHandle)
229 constexpr auto renderingIntent = INTENT_ABSOLUTE_COLORIMETRIC;
231 m_profileClass = cmsGetDeviceClass(rgbProfileHandle);
232 m_profileColorModel = cmsGetColorSpace(rgbProfileHandle);
233 m_profileCopyright = profileInformation(rgbProfileHandle,
235 m_profileCreationDateTime =
236 profileCreationDateTime(rgbProfileHandle);
237 const bool inputUsesCLUT = cmsIsCLUT(rgbProfileHandle,
240 const bool outputUsesCLUT = cmsIsCLUT(rgbProfileHandle,
242 LCMS_USED_AS_OUTPUT);
247 m_profileHasClut = inputUsesCLUT || outputUsesCLUT;
248 m_profileHasMatrixShaper = cmsIsMatrixShaper(rgbProfileHandle);
249 m_profileIccVersion = profileIccVersion(rgbProfileHandle);
250 m_profileManufacturer = profileInformation(rgbProfileHandle,
251 cmsInfoManufacturer);
252 m_profileModel = profileInformation(rgbProfileHandle,
254 m_profileName = profileInformation(rgbProfileHandle,
256 m_profilePcsColorModel = cmsGetPCS(rgbProfileHandle);
257 m_profileTagSignatures = profileTagSignatures(rgbProfileHandle);
287 if (m_profileTagSignatures.contains(QStringLiteral(
"vcgt"))) {
293 cmsHPROFILE cielabD50ProfileHandle = cmsCreateLab4Profile(
307 constexpr auto flags = cmsFLAGS_NOCACHE;
308 m_transformCielabD50ToRgbHandle = cmsCreateTransform(
310 cielabD50ProfileHandle,
316 m_transformCielabD50ToRgb16Handle = cmsCreateTransform(
318 cielabD50ProfileHandle,
324 m_transformRgbToCielabD50Handle = cmsCreateTransform(
328 cielabD50ProfileHandle,
333 cmsCloseProfile(cielabD50ProfileHandle);
338 if ((m_transformCielabD50ToRgbHandle ==
nullptr)
339 || (m_transformCielabD50ToRgb16Handle ==
nullptr)
340 || (m_transformRgbToCielabD50Handle ==
nullptr)
350 GenericColor candidate;
351 candidate.second = 0;
354 while (!q_pointer->isCielchD50InGamut(candidate)) {
355 candidate.first += gamutPrecisionCielab;
356 if (candidate.first >= 100) {
360 m_cielabD50BlackpointL = candidate.first;
361 candidate.first = 100;
362 while (!q_pointer->isCielchD50InGamut(candidate)) {
363 candidate.first -= gamutPrecisionCielab;
364 if (candidate.first <= m_cielabD50BlackpointL) {
368 m_cielabD50WhitepointL = candidate.first;
371 while (!q_pointer->isOklchInGamut(candidate)) {
372 candidate.first += gamutPrecisionOklab;
373 if (candidate.first >= 1) {
377 m_oklabBlackpointL = candidate.first;
379 while (!q_pointer->isOklchInGamut(candidate)) {
380 candidate.first -= gamutPrecisionOklab;
381 if (candidate.first <= m_oklabBlackpointL) {
385 m_oklabWhitepointL = candidate.first;
389 m_profileMaximumCielchD50Chroma = detectMaximumCielchD50Chroma();
390 m_profileMaximumOklchChroma = detectMaximumOklchChroma();
396RgbColorSpace::~RgbColorSpace() noexcept
398 RgbColorSpacePrivate::deleteTransform(
399 &d_pointer->m_transformCielabD50ToRgb16Handle);
400 RgbColorSpacePrivate::deleteTransform(
401 &d_pointer->m_transformCielabD50ToRgbHandle);
402 RgbColorSpacePrivate::deleteTransform(
403 &d_pointer->m_transformRgbToCielabD50Handle);
410RgbColorSpacePrivate::RgbColorSpacePrivate(RgbColorSpace *backLink)
411 : q_pointer(backLink)
429void RgbColorSpacePrivate::deleteTransform(cmsHTRANSFORM *transformHandle)
431 if ((*transformHandle) !=
nullptr) {
432 cmsDeleteTransform(*transformHandle);
433 (*transformHandle) =
nullptr;
439QString RgbColorSpace::profileAbsoluteFilePath()
const
441 return d_pointer->m_profileAbsoluteFilePath;
446cmsProfileClassSignature RgbColorSpace::profileClass()
const
448 return d_pointer->m_profileClass;
453cmsColorSpaceSignature RgbColorSpace::profileColorModel()
const
455 return d_pointer->m_profileColorModel;
460QString RgbColorSpace::profileCopyright()
const
462 return d_pointer->m_profileCopyright;
467QDateTime RgbColorSpace::profileCreationDateTime()
const
469 return d_pointer->m_profileCreationDateTime;
474qint64 RgbColorSpace::profileFileSize()
const
476 return d_pointer->m_profileFileSize;
481bool RgbColorSpace::profileHasClut()
const
483 return d_pointer->m_profileHasClut;
488bool RgbColorSpace::profileHasMatrixShaper()
const
490 return d_pointer->m_profileHasMatrixShaper;
497 return d_pointer->m_profileIccVersion;
502QString RgbColorSpace::profileManufacturer()
const
504 return d_pointer->m_profileManufacturer;
509double RgbColorSpace::profileMaximumCielchD50Chroma()
const
511 return d_pointer->m_profileMaximumCielchD50Chroma;
516double RgbColorSpace::profileMaximumOklchChroma()
const
518 return d_pointer->m_profileMaximumOklchChroma;
523QString RgbColorSpace::profileModel()
const
525 return d_pointer->m_profileModel;
530QString RgbColorSpace::profileName()
const
532 return d_pointer->m_profileName;
537cmsColorSpaceSignature RgbColorSpace::profilePcsColorModel()
const
539 return d_pointer->m_profilePcsColorModel;
544QStringList RgbColorSpace::profileTagSignatures()
const
546 return d_pointer->m_profileTagSignatures;
559QString RgbColorSpacePrivate::profileInformation(cmsHPROFILE profileHandle, cmsInfoType infoType)
570 languageCode =
list.
at(0).toUtf8();
575 if (languageCode.
size() != 2) {
578 languageCode = QByteArrayLiteral(
"en");
620 const cmsUInt32Number resultLength = cmsGetProfileInfo(
637 const cmsUInt32Number bufferLength = resultLength + 1;
656 wchar_t *buffer =
new wchar_t[bufferLength];
658 for (cmsUInt32Number i = 0; i < bufferLength; ++i) {
678 *(buffer + (bufferLength - 1)) = 0;
746QVersionNumber RgbColorSpacePrivate::profileIccVersion(cmsHPROFILE profileHandle)
767 cmsGetProfileVersion(profileHandle),
779QDateTime RgbColorSpacePrivate::profileCreationDateTime(cmsHPROFILE profileHandle)
782 const bool success = cmsGetHeaderCreationDateTime(profileHandle, &myDateTime);
787 const QDate myDate(myDateTime.tm_year + 1900,
788 myDateTime.tm_mon + 1,
794 const QTime myTime(myDateTime.tm_hour,
796 qBound(0, myDateTime.tm_sec, 59));
813QStringList RgbColorSpacePrivate::profileTagSignatures(cmsHPROFILE profileHandle)
815 const cmsInt32Number count = cmsGetTagCount(profileHandle);
821 const cmsUInt32Number countUnsigned =
static_cast<cmsUInt32Number
>(count);
822 using underlyingType = std::underlying_type<cmsTagSignature>::type;
823 for (cmsUInt32Number i = 0; i < countUnsigned; ++i) {
824 const underlyingType value = cmsGetTagSignature(profileHandle, i);
828 byteArray.
append(
static_cast<char>((value >> 24) & 0xFF));
829 byteArray.
append(
static_cast<char>((value >> 16) & 0xFF));
830 byteArray.
append(
static_cast<char>((value >> 8) & 0xFF));
831 byteArray.
append(
static_cast<char>(value & 0xFF));
851PerceptualColor::GenericColor RgbColorSpace::reduceCielchD50ChromaToFitIntoGamut(
const PerceptualColor::GenericColor &cielchD50color)
const
853 GenericColor referenceColor = cielchD50color;
856 normalizePolar360(referenceColor.second, referenceColor.third);
859 referenceColor.second = qMin<
decltype(referenceColor.second)>(
860 referenceColor.second,
861 profileMaximumCielchD50Chroma());
862 referenceColor.first = qBound(d_pointer->m_cielabD50BlackpointL,
863 referenceColor.first,
864 d_pointer->m_cielabD50WhitepointL);
867 if (isCielchD50InGamut(referenceColor)) {
868 return referenceColor;
875 GenericColor lowerChroma{referenceColor.first, 0, referenceColor.third};
876 if (!isCielchD50InGamut(lowerChroma)) {
881 referenceColor.first = d_pointer->m_cielabD50BlackpointL;
882 lowerChroma.first = d_pointer->m_cielabD50BlackpointL;
886 constexpr bool quickApproximate =
true;
887 if constexpr (quickApproximate) {
889 GenericColor upperChroma{referenceColor};
893 while (upperChroma.second - lowerChroma.second > gamutPrecisionCielab) {
896 temp.second = ((lowerChroma.second + upperChroma.second) / 2);
897 if (isCielchD50InGamut(temp)) {
907 temp = referenceColor;
908 while (temp.second > 0) {
909 if (isCielchD50InGamut(temp)) {
912 temp.second -= gamutPrecisionCielab;
915 if (temp.second < 0) {
935PerceptualColor::GenericColor RgbColorSpace::reduceOklchChromaToFitIntoGamut(
const PerceptualColor::GenericColor &oklchColor)
const
937 GenericColor referenceColor = oklchColor;
940 normalizePolar360(referenceColor.second, referenceColor.third);
943 referenceColor.second = qMin<
decltype(referenceColor.second)>(
944 referenceColor.second,
945 profileMaximumOklchChroma());
946 referenceColor.first = qBound(d_pointer->m_oklabBlackpointL,
947 referenceColor.first,
948 d_pointer->m_oklabWhitepointL);
951 if (isOklchInGamut(referenceColor)) {
952 return referenceColor;
959 GenericColor lowerChroma{referenceColor.first, 0, referenceColor.third};
960 if (!isOklchInGamut(lowerChroma)) {
965 referenceColor.first = d_pointer->m_oklabBlackpointL;
966 lowerChroma.first = d_pointer->m_oklabBlackpointL;
970 constexpr bool quickApproximate =
true;
971 if constexpr (quickApproximate) {
973 GenericColor upperChroma{referenceColor};
977 while (upperChroma.second - lowerChroma.second > gamutPrecisionOklab) {
980 temp.second = ((lowerChroma.second + upperChroma.second) / 2);
981 if (isOklchInGamut(temp)) {
991 temp = referenceColor;
992 while (temp.second > 0) {
993 if (isOklchInGamut(temp)) {
996 temp.second -= gamutPrecisionOklab;
999 if (temp.second < 0) {
1016cmsCIELab RgbColorSpace::toCielabD50(
const QRgba64 rgbColor)
const
1018 constexpr qreal maximum =
1019 std::numeric_limits<
decltype(rgbColor.
red())>::max();
1020 const double my_rgb[]{rgbColor.
red() / maximum,
1021 rgbColor.
green() / maximum,
1022 rgbColor.
blue() / maximum};
1023 cmsCIELab cielabD50;
1024 cmsDoTransform(d_pointer->m_transformRgbToCielabD50Handle,
1029 if (cielabD50.L < 0) {
1047PerceptualColor::GenericColor RgbColorSpace::toCielchD50(
const QRgba64 rgbColor)
const
1049 constexpr qreal maximum =
1050 std::numeric_limits<
decltype(rgbColor.
red())>::max();
1051 const double my_rgb[]{rgbColor.
red() / maximum,
1052 rgbColor.
green() / maximum,
1053 rgbColor.
blue() / maximum};
1054 cmsCIELab cielabD50;
1055 cmsDoTransform(d_pointer->m_transformRgbToCielabD50Handle,
1060 if (cielabD50.L < 0) {
1064 cmsCIELCh cielchD50;
1065 cmsLab2LCh(&cielchD50,
1068 return GenericColor{cielchD50.L, cielchD50.C, cielchD50.h};
1083cmsCIELab RgbColorSpace::fromLchToCmsCIELab(
const GenericColor &lch)
1085 const cmsCIELCh myCmsCieLch = lch.reinterpretAsLchToCmscielch();
1105QRgb RgbColorSpace::fromCielchD50ToQRgbBound(
const GenericColor &cielchD50)
const
1107 const auto cielabD50 = fromLchToCmsCIELab(cielchD50);
1108 cmsUInt16Number rgb_int[3];
1109 cmsDoTransform(d_pointer->m_transformCielabD50ToRgb16Handle,
1114 constexpr qreal channelMaximumQReal =
1115 std::numeric_limits<cmsUInt16Number>::max();
1116 constexpr quint8 rgbMaximum = 255;
1117 return qRgb(qRound(rgb_int[0] / channelMaximumQReal * rgbMaximum),
1118 qRound(rgb_int[1] / channelMaximumQReal * rgbMaximum),
1119 qRound(rgb_int[2] / channelMaximumQReal * rgbMaximum));
1126bool RgbColorSpace::isCielchD50InGamut(
const GenericColor &lch)
const
1128 if (!isInRange<
decltype(lch.first)>(0, lch.first, 100)) {
1131 if (!isInRange<
decltype(lch.first)>(
1132 (-1) * d_pointer->m_profileMaximumCielchD50Chroma,
1134 d_pointer->m_profileMaximumCielchD50Chroma
1138 const auto cielabD50 = fromLchToCmsCIELab(lch);
1139 return qAlpha(fromCielabD50ToQRgbOrTransparent(cielabD50)) != 0;
1146bool RgbColorSpace::isOklchInGamut(
const GenericColor &lch)
const
1148 if (!isInRange<
decltype(lch.first)>(0, lch.first, 1)) {
1151 if (!isInRange<
decltype(lch.first)>(
1152 (-1) * d_pointer->m_profileMaximumOklchChroma,
1154 d_pointer->m_profileMaximumOklchChroma
1158 const auto oklab = AbsoluteColor::fromPolarToCartesian(GenericColor(lch));
1159 const auto xyzD65 = AbsoluteColor::fromOklabToXyzD65(oklab);
1160 const auto xyzD50 = AbsoluteColor::fromXyzD65ToXyzD50(xyzD65);
1161 const auto cielabD50 = AbsoluteColor::fromXyzD50ToCielabD50(xyzD50);
1162 const auto cielabD50cms = cielabD50.reinterpretAsLabToCmscielab();
1163 const auto rgb = fromCielabD50ToQRgbOrTransparent(cielabD50cms);
1164 return (qAlpha(rgb) != 0);
1171bool RgbColorSpace::isCielabD50InGamut(
const cmsCIELab &lab)
const
1173 if (!isInRange<
decltype(lab.L)>(0, lab.L, 100)) {
1176 const auto chromaSquare = lab.a * lab.a + lab.b * lab.b;
1177 const auto maximumChromaSquare = qPow(d_pointer->m_profileMaximumCielchD50Chroma, 2);
1178 if (chromaSquare > maximumChromaSquare) {
1181 return qAlpha(fromCielabD50ToQRgbOrTransparent(lab)) != 0;
1198QRgb RgbColorSpace::fromCielabD50ToQRgbOrTransparent(
const cmsCIELab &lab)
const
1200 constexpr QRgb transparentValue = 0;
1201 static_assert(qAlpha(transparentValue) == 0,
1202 "The alpha value of a transparent QRgb must be 0.");
1207 d_pointer->m_transformCielabD50ToRgbHandle,
1214 const bool colorIsValid =
1215 isInRange<double>(0, rgb[0], 1)
1216 && isInRange<double>(0, rgb[1], 1)
1217 && isInRange<double>(0, rgb[2], 1);
1218 if (!colorIsValid) {
1219 return transparentValue;
1223 cmsCIELab roundtripCielabD50;
1226 d_pointer->m_transformRgbToCielabD50Handle,
1228 &roundtripCielabD50,
1231 const qreal actualDeviationSquare =
1232 qPow(lab.L - roundtripCielabD50.L, 2)
1233 + qPow(lab.a - roundtripCielabD50.a, 2)
1234 + qPow(lab.b - roundtripCielabD50.b, 2);
1235 constexpr auto cielabDeviationLimitSquare =
1236 RgbColorSpacePrivate::cielabDeviationLimit
1237 * RgbColorSpacePrivate::cielabDeviationLimit;
1238 const bool actualDeviationIsOkay =
1239 actualDeviationSquare <= cielabDeviationLimitSquare;
1242 if (!actualDeviationIsOkay) {
1243 return transparentValue;
1248 static_cast<QColorFloatType
>(rgb[1]),
1249 static_cast<QColorFloatType
>(rgb[2]));
1261PerceptualColor::GenericColor RgbColorSpace::fromCielchD50ToRgb1(
const PerceptualColor::GenericColor &lch)
const
1263 const auto cielabD50 = fromLchToCmsCIELab(lch);
1267 d_pointer->m_transformCielabD50ToRgbHandle,
1272 return GenericColor(rgb[0], rgb[1], rgb[2]);
1278double RgbColorSpacePrivate::detectMaximumCielchD50Chroma()
const
1282 static_assert(0. + chromaDetectionHuePrecision > 0.);
1283 static_assert(360. + chromaDetectionHuePrecision > 360.);
1289 const auto qColorHue =
static_cast<QColorFloatType
>(
hue / 360.);
1291 result = qMax(result, q_pointer->toCielchD50(color).second);
1292 hue += chromaDetectionHuePrecision;
1294 result = result * chromaDetectionIncrementFactor + cielabDeviationLimit;
1295 return std::min<double>(result, CielchD50Values::maximumChroma);
1301double RgbColorSpacePrivate::detectMaximumOklchChroma()
const
1305 static_assert(0. + chromaDetectionHuePrecision > 0.);
1306 static_assert(360. + chromaDetectionHuePrecision > 360.);
1308 double chromaSquare = 0;
1311 const auto qColorHue =
static_cast<QColorFloatType
>(
hue / 360.);
1313 const auto cielabD50Color = q_pointer->toCielabD50(rgbColor);
1314 const auto cielabD50 = GenericColor(cielabD50Color);
1315 const auto xyzD50 = AbsoluteColor::fromCielabD50ToXyzD50(cielabD50);
1316 const auto xyzD65 = AbsoluteColor::fromXyzD50ToXyzD65(xyzD50);
1317 const auto oklab = AbsoluteColor::fromXyzD65ToOklab(xyzD65);
1318 chromaSquare = qMax(
1320 oklab.second * oklab.second + oklab.third * oklab.third);
1321 hue += chromaDetectionHuePrecision;
1323 const auto result = qSqrt(chromaSquare) * chromaDetectionIncrementFactor
1324 + oklabDeviationLimit;
1325 return std::min<double>(result, OklchValues::maximumChroma);
1337 const cmsUInt32Number intentCount =
1338 cmsGetSupportedIntents(0,
nullptr,
nullptr);
1339 cmsUInt32Number *codeArray =
new cmsUInt32Number[intentCount];
1340 char **descriptionArray =
new char *[intentCount];
1341 cmsGetSupportedIntents(intentCount, codeArray, descriptionArray);
1342 for (cmsUInt32Number i = 0; i < intentCount; ++i) {
1346 delete[] descriptionArray;
KGUIADDONS_EXPORT qreal hue(const QColor &)
QStringView countryCode(QStringView coachNumber)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
The namespace of this library.
const char * versionString()
QByteArray & append(QByteArrayView data)
const char * constData() const const
void reserve(qsizetype size)
qsizetype size() const const
QColor fromHsvF(float h, float s, float v, float a)
QColor fromRgbF(float r, float g, float b, float a)
QRgba64 rgba64() const const
QCoreApplication * instance()
QString absoluteFilePath() const const
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
qsizetype count() const const
void reserve(qsizetype size)
QString name() const const
iterator insert(const Key &key, const T &value)
quint16 blue() const const
quint16 green() const const
quint16 red() const const
QString fromLatin1(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
QString fromWCharArray(const wchar_t *string, qsizetype size)
QString number(double n, char format, int precision)
qsizetype size() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
const_pointer constData() const const
qsizetype size() const const
QVersionNumber fromString(QAnyStringView string, qsizetype *suffixIndex)