File indexing completed on 2024-05-12 04:44:35
0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com> 0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT 0003 0004 // Own headers 0005 // First the interface, which forces the header to be self-contained. 0006 #include "rgbcolorspace.h" 0007 // Second, the private implementation. 0008 #include "rgbcolorspace_p.h" // IWYU pragma: associated 0009 0010 #include "absolutecolor.h" 0011 #include "constpropagatingrawpointer.h" 0012 #include "constpropagatinguniquepointer.h" 0013 #include "genericcolor.h" 0014 #include "helperconstants.h" 0015 #include "helperconversion.h" 0016 #include "helpermath.h" 0017 #include "helperqttypes.h" 0018 #include "initializetranslation.h" 0019 #include "iohandlerfactory.h" 0020 #include "lchdouble.h" 0021 #include <algorithm> 0022 #include <limits> 0023 #include <optional> 0024 #include <qbytearray.h> 0025 #include <qcolor.h> 0026 #include <qcoreapplication.h> 0027 #include <qfileinfo.h> 0028 #include <qlocale.h> 0029 #include <qmath.h> 0030 #include <qnamespace.h> 0031 #include <qrgba64.h> 0032 #include <qsharedpointer.h> 0033 #include <qstringliteral.h> 0034 0035 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) 0036 #include <qcontainerfwd.h> 0037 #include <qlist.h> 0038 #else 0039 #include <qstringlist.h> 0040 #endif 0041 0042 // Include the type “tm” as defined in the C standard (time.h), as LittleCMS 0043 // expects, preventing IWYU < 0.19 to produce false-positives. 0044 #include <time.h> // IWYU pragma: keep 0045 // IWYU pragma: no_include <bits/types/struct_tm.h> 0046 0047 namespace PerceptualColor 0048 { 0049 /** @internal 0050 * 0051 * @brief Constructor 0052 * 0053 * @attention Creates an uninitialised object. You have to call 0054 * @ref RgbColorSpacePrivate::initialize() <em>successfully</em> 0055 * before actually use object. */ 0056 RgbColorSpace::RgbColorSpace(QObject *parent) 0057 : QObject(parent) 0058 , d_pointer(new RgbColorSpacePrivate(this)) 0059 { 0060 } 0061 0062 /** @brief Create an sRGB color space object. 0063 * 0064 * This is build-in, no external ICC file is used. 0065 * 0066 * @pre This function is called from the main thread. 0067 * 0068 * @returns A shared pointer to the newly created color space object. 0069 * 0070 * @sa @ref RgbColorSpaceFactory::createSrgb() 0071 * 0072 * @internal 0073 * 0074 * @note This function has to be called from the main thread because 0075 * <a href="https://doc.qt.io/qt-6/qobject.html#tr">it is not save to use 0076 * <tt>QObject::tr()</tt> while a new translation is loaded into 0077 * QCoreApplication</a>, which should happen within the main thread. Therefore, 0078 * if this function is also called within the main thread, we can use 0079 * QObject::tr() safely because there will be not be executed simultaneously 0080 * with loading a translation. */ 0081 QSharedPointer<PerceptualColor::RgbColorSpace> RgbColorSpace::createSrgb() 0082 { 0083 // Create an invalid object: 0084 QSharedPointer<PerceptualColor::RgbColorSpace> result{new RgbColorSpace()}; 0085 0086 // Transform it into a valid object: 0087 cmsHPROFILE srgb = cmsCreate_sRGBProfile(); // Use build-in profile 0088 const bool success = result->d_pointer->initialize(srgb); 0089 cmsCloseProfile(srgb); 0090 0091 if (!success) { 0092 // This should never fail. If it fails anyway, that’s a 0093 // programming error and we throw an exception. 0094 throw 0; 0095 } 0096 0097 initializeTranslation(QCoreApplication::instance(), 0098 // An empty std::optional means: If in initialization 0099 // had been done yet, repeat this initialization. 0100 // If not, do a new initialization now with default 0101 // values. 0102 std::optional<QStringList>()); 0103 0104 // Fine-tuning (and localization) for this build-in profile: 0105 result->d_pointer->m_profileCreationDateTime = QDateTime(); 0106 /*: @item Manufacturer information for the built-in sRGB color. */ 0107 result->d_pointer->m_profileManufacturer = tr("LittleCMS"); 0108 result->d_pointer->m_profileModel = QString(); 0109 /*: @item Name of the built-in sRGB color space. */ 0110 result->d_pointer->m_profileName = tr("sRGB color space"); 0111 result->d_pointer->m_profileMaximumCielchD50Chroma = 132; 0112 0113 // Return: 0114 return result; 0115 } 0116 0117 /** @brief Try to create a color space object for a given ICC file. 0118 * 0119 * @note This function may fail to create the color space object when it 0120 * cannot open the given file, or when the file cannot be interpreted. 0121 * 0122 * @pre This function is called from the main thread. 0123 * 0124 * @param fileName The file name. See <tt>QFile</tt> documentation 0125 * for what are valid file names. The file is only used during the 0126 * execution of this function and it is closed again at the end of 0127 * this function. The created object does not need the file anymore, 0128 * because all necessary information has already been loaded into 0129 * memory. Accepted are most RGB-based ICC profiles up to version 4. 0130 * 0131 * @returns A shared pointer to a newly created color space object on success. 0132 * A shared pointer to <tt>nullptr</tt> on fail. 0133 * 0134 * @sa @ref RgbColorSpaceFactory::createFromFile() 0135 * 0136 * @internal 0137 * 0138 * @todo The value for @ref profileMaximumCielchD50Chroma should be the actual maximum 0139 * chroma value of the profile, and not a fallback default value as currently. 0140 * 0141 * @note Currently, there is no function that loads a profile from a memory 0142 * buffer instead of a file. However it would easily be possible to implement 0143 * this if necessary, because LittleCMS allows loading from a memory buffer. 0144 * 0145 * @note While it is not strictly necessary to call this function within 0146 * the main thread, we put it nevertheless as precondition because of 0147 * consistency with @ref createSrgb(). 0148 * 0149 * @note The new <a href="https://www.color.org/iccmax/index.xalter">version 5 0150 * (iccMax)</a> is <em>not</em> accepted. <a href="https://www.littlecms.com/"> 0151 * LittleCMS</a> does not support ICC version 5, but only 0152 * up to version 4. The ICC organization itself provides 0153 * a <a href="https://github.com/InternationalColorConsortium/DemoIccMAX">demo 0154 * implementation</a>, but this does not seem to be a complete color 0155 * management system. */ 0156 QSharedPointer<PerceptualColor::RgbColorSpace> RgbColorSpace::createFromFile(const QString &fileName) 0157 { 0158 // TODO xxx Only accept Display Class profiles 0159 0160 // Definitions 0161 constexpr auto myContextID = nullptr; 0162 0163 // Create an IO handler for the file 0164 cmsIOHANDLER *myIOHandler = // 0165 IOHandlerFactory::createReadOnly(myContextID, fileName); 0166 if (myIOHandler == nullptr) { 0167 return nullptr; 0168 } 0169 0170 // Create a handle to a LittleCMS profile representation 0171 cmsHPROFILE myProfileHandle = // 0172 cmsOpenProfileFromIOhandlerTHR(myContextID, myIOHandler); 0173 if (myProfileHandle == nullptr) { 0174 // If cmsOpenProfileFromIOhandlerTHR fails to create a profile 0175 // handle, it deletes the IO handler. Therefore, we do not 0176 // have to delete the underlying IO handler manually. 0177 return nullptr; 0178 } 0179 0180 // Create an invalid object: 0181 QSharedPointer<PerceptualColor::RgbColorSpace> newObject{new RgbColorSpace()}; 0182 0183 // Try to transform it into a valid object: 0184 const QFileInfo myFileInfo{fileName}; 0185 newObject->d_pointer->m_profileAbsoluteFilePath = // 0186 myFileInfo.absoluteFilePath(); 0187 newObject->d_pointer->m_profileFileSize = myFileInfo.size(); 0188 const bool success = newObject->d_pointer->initialize(myProfileHandle); 0189 0190 // Clean up 0191 cmsCloseProfile(myProfileHandle); // Also deletes the underlying IO handler 0192 0193 // Return 0194 if (success) { 0195 return newObject; 0196 } 0197 return nullptr; 0198 } 0199 0200 /** @brief Basic initialization. 0201 * 0202 * This function is meant to be called when constructing the object. 0203 * 0204 * @param rgbProfileHandle Handle for the RGB profile 0205 * 0206 * @pre rgbProfileHandle is valid. 0207 * 0208 * @returns <tt>true</tt> on success. <tt>false</tt> otherwise (for example 0209 * when it’s not an RGB profile but an CMYK profile). When <tt>false</tt> 0210 * is returned, the object is still in an undefined state; it cannot 0211 * be used, but only be destroyed. This should happen as soon as 0212 * possible to reduce memory usage. 0213 * 0214 * @note rgbProfileHandle is <em>not</em> deleted in this function. 0215 * Remember to delete it manually. 0216 * 0217 * @internal 0218 * 0219 * @todo LUT profiles should be detected and refused, as the actual diagram 0220 * results are currently bad. (LUT profiles for RGB are not common among 0221 * the usual standard profile files. But they might be more common among 0222 * individually calibrated monitors?) 0223 * 0224 * @todo This function is used in @ref RgbColorSpace::createSrgb() 0225 * and @ref RgbColorSpace::createFromFile(), but some of the initialization 0226 * is changed afterwards (file name, file size, profile name, maximum chroma). 0227 * Is it possible to find a more elegant design? */ 0228 bool RgbColorSpacePrivate::initialize(cmsHPROFILE rgbProfileHandle) 0229 { 0230 constexpr auto renderingIntent = INTENT_ABSOLUTE_COLORIMETRIC; 0231 0232 m_profileClass = cmsGetDeviceClass(rgbProfileHandle); 0233 m_profileColorModel = cmsGetColorSpace(rgbProfileHandle); 0234 m_profileCopyright = getInformationFromProfile(rgbProfileHandle, // 0235 cmsInfoCopyright); 0236 m_profileCreationDateTime = // 0237 getCreationDateTimeFromProfile(rgbProfileHandle); 0238 const bool inputUsesCLUT = cmsIsCLUT(rgbProfileHandle, // 0239 renderingIntent, // 0240 LCMS_USED_AS_INPUT); 0241 const bool outputUsesCLUT = cmsIsCLUT(rgbProfileHandle, // 0242 renderingIntent, // 0243 LCMS_USED_AS_OUTPUT); 0244 // There is a third value, LCMS_USED_AS_PROOF. This value seem to return 0245 // always true, even for the sRGB built-in profile. Not sure if this is 0246 // a bug? Anyway, as we do not actually use the profile in proof mode, 0247 // we can discard this information. 0248 m_profileHasClut = inputUsesCLUT || outputUsesCLUT; 0249 m_profileHasMatrixShaper = cmsIsMatrixShaper(rgbProfileHandle); 0250 m_profileIccVersion = getIccVersionFromProfile(rgbProfileHandle); 0251 m_profileManufacturer = getInformationFromProfile(rgbProfileHandle, // 0252 cmsInfoManufacturer); 0253 m_profileModel = getInformationFromProfile(rgbProfileHandle, // 0254 cmsInfoModel); 0255 m_profileName = getInformationFromProfile(rgbProfileHandle, // 0256 cmsInfoDescription); 0257 m_profilePcsColorModel = cmsGetPCS(rgbProfileHandle); 0258 0259 { 0260 // Create an ICC v4 profile object for the CielabD50 color space. 0261 cmsHPROFILE cielabD50ProfileHandle = cmsCreateLab4Profile( 0262 // nullptr means: Default white point (D50) 0263 // TODO Does this make sense? sRGB, for example, has 0264 // D65 as whitepoint… 0265 nullptr); 0266 0267 // Create the transforms. 0268 // We use the flag cmsFLAGS_NOCACHE which disables the 1-pixel-cache 0269 // which is normally used in the transforms. We do this because 0270 // transforms that use the 1-pixel-cache are not thread-safe. And 0271 // disabling it should not have negative impacts as we usually work 0272 // with gradients, so anyway it is not likely to have two consecutive 0273 // pixels with the same color, which is the only situation where the 0274 // 1-pixel-cache makes processing faster. 0275 constexpr auto flags = cmsFLAGS_NOCACHE; 0276 m_transformCielabD50ToRgbHandle = cmsCreateTransform( 0277 // Create a transform function and get a handle to this function: 0278 cielabD50ProfileHandle, // input profile handle 0279 TYPE_Lab_DBL, // input buffer format 0280 rgbProfileHandle, // output profile handle 0281 TYPE_RGB_DBL, // output buffer format 0282 renderingIntent, 0283 flags); 0284 m_transformCielabD50ToRgb16Handle = cmsCreateTransform( 0285 // Create a transform function and get a handle to this function: 0286 cielabD50ProfileHandle, // input profile handle 0287 TYPE_Lab_DBL, // input buffer format 0288 rgbProfileHandle, // output profile handle 0289 TYPE_RGB_16, // output buffer format 0290 renderingIntent, 0291 flags); 0292 m_transformRgbToCielabD50Handle = cmsCreateTransform( 0293 // Create a transform function and get a handle to this function: 0294 rgbProfileHandle, // input profile handle 0295 TYPE_RGB_DBL, // input buffer format 0296 cielabD50ProfileHandle, // output profile handle 0297 TYPE_Lab_DBL, // output buffer format 0298 renderingIntent, 0299 flags); 0300 // It is mandatory to close the profiles to prevent memory leaks: 0301 cmsCloseProfile(cielabD50ProfileHandle); 0302 } 0303 0304 // After having closed the profiles, we can now return 0305 // (if appropriate) without having memory leaks: 0306 if ((m_transformCielabD50ToRgbHandle == nullptr) // 0307 || (m_transformCielabD50ToRgb16Handle == nullptr) // 0308 || (m_transformRgbToCielabD50Handle == nullptr) // 0309 ) { 0310 return false; 0311 } 0312 0313 // Maximum chroma: 0314 // TODO Detect an appropriate value for m_profileMaximumCielchD50Chroma. 0315 0316 // Find blackpoint and whitepoint. 0317 // For CielabD50 make sure that: 0 <= blackbpoint < whitepoint <= 100 0318 LchDouble candidate; 0319 candidate.c = 0; 0320 candidate.h = 0; 0321 candidate.l = 0; 0322 while (!q_pointer->isCielchD50InGamut(candidate)) { 0323 candidate.l += gamutPrecisionCielab; 0324 if (candidate.l >= 100) { 0325 return false; 0326 } 0327 } 0328 m_cielabD50BlackpointL = candidate.l; 0329 candidate.l = 100; 0330 while (!q_pointer->isCielchD50InGamut(candidate)) { 0331 candidate.l -= gamutPrecisionCielab; 0332 if (candidate.l <= m_cielabD50BlackpointL) { 0333 return false; 0334 } 0335 } 0336 m_cielabD50WhitepointL = candidate.l; 0337 // For Oklab make sure that: 0 <= blackbpoint < whitepoint <= 1 0338 candidate.l = 0; 0339 while (!q_pointer->isOklchInGamut(candidate)) { 0340 candidate.l += gamutPrecisionOklab; 0341 if (candidate.l >= 1) { 0342 return false; 0343 } 0344 } 0345 m_oklabBlackpointL = candidate.l; 0346 candidate.l = 1; 0347 while (!q_pointer->isOklchInGamut(candidate)) { 0348 candidate.l -= gamutPrecisionOklab; 0349 if (candidate.l <= m_oklabBlackpointL) { 0350 return false; 0351 } 0352 } 0353 m_oklabWhitepointL = candidate.l; 0354 0355 // Now, calculate the properties who’s calculation depends on a fully 0356 // initialized object. 0357 m_profileMaximumCielchD50Chroma = detectMaximumCielchD50Chroma(); 0358 m_profileMaximumOklchChroma = detectMaximumOklchChroma(); 0359 0360 return true; 0361 } 0362 0363 /** @brief Destructor */ 0364 RgbColorSpace::~RgbColorSpace() noexcept 0365 { 0366 RgbColorSpacePrivate::deleteTransform( // 0367 &d_pointer->m_transformCielabD50ToRgb16Handle); 0368 RgbColorSpacePrivate::deleteTransform( // 0369 &d_pointer->m_transformCielabD50ToRgbHandle); 0370 RgbColorSpacePrivate::deleteTransform( // 0371 &d_pointer->m_transformRgbToCielabD50Handle); 0372 } 0373 0374 /** @brief Constructor 0375 * 0376 * @param backLink Pointer to the object from which <em>this</em> object 0377 * is the private implementation. */ 0378 RgbColorSpacePrivate::RgbColorSpacePrivate(RgbColorSpace *backLink) 0379 : q_pointer(backLink) 0380 { 0381 } 0382 0383 /** @brief Convenience function for deleting LittleCMS transforms 0384 * 0385 * <tt>cmsDeleteTransform()</tt> is not comfortable. Calling it on a 0386 * <tt>nullptr</tt> crashes. If called on a valid handle, it does not 0387 * reset the handle to <tt>nullptr</tt>. Calling it again on the now 0388 * invalid handle crashes. This convenience function can be used instead 0389 * of <tt>cmsDeleteTransform()</tt>: It provides some more comfort, 0390 * by adding support for <tt>nullptr</tt> checks. 0391 * 0392 * @param transformHandle handle of the transform 0393 * 0394 * @post If the handle is <tt>nullptr</tt>, nothing happens. Otherwise, 0395 * <tt>cmsDeleteTransform()</tt> is called, and afterwards the handle is set 0396 * to <tt>nullptr</tt>. */ 0397 void RgbColorSpacePrivate::deleteTransform(cmsHTRANSFORM *transformHandle) 0398 { 0399 if ((*transformHandle) != nullptr) { 0400 cmsDeleteTransform(*transformHandle); 0401 (*transformHandle) = nullptr; 0402 } 0403 } 0404 0405 // No documentation here (documentation of properties 0406 // and its getters are in the header) 0407 QString RgbColorSpace::profileAbsoluteFilePath() const 0408 { 0409 return d_pointer->m_profileAbsoluteFilePath; 0410 } 0411 0412 // No documentation here (documentation of properties 0413 // and its getters are in the header) 0414 cmsProfileClassSignature RgbColorSpace::profileClass() const 0415 { 0416 return d_pointer->m_profileClass; 0417 } 0418 0419 // No documentation here (documentation of properties 0420 // and its getters are in the header) 0421 cmsColorSpaceSignature RgbColorSpace::profileColorModel() const 0422 { 0423 return d_pointer->m_profileColorModel; 0424 } 0425 0426 // No documentation here (documentation of properties 0427 // and its getters are in the header) 0428 QString RgbColorSpace::profileCopyright() const 0429 { 0430 return d_pointer->m_profileCopyright; 0431 } 0432 0433 // No documentation here (documentation of properties 0434 // and its getters are in the header) 0435 QDateTime RgbColorSpace::profileCreationDateTime() const 0436 { 0437 return d_pointer->m_profileCreationDateTime; 0438 } 0439 0440 // No documentation here (documentation of properties 0441 // and its getters are in the header) 0442 qint64 RgbColorSpace::profileFileSize() const 0443 { 0444 return d_pointer->m_profileFileSize; 0445 } 0446 0447 // No documentation here (documentation of properties 0448 // and its getters are in the header) 0449 bool RgbColorSpace::profileHasClut() const 0450 { 0451 return d_pointer->m_profileHasClut; 0452 } 0453 0454 // No documentation here (documentation of properties 0455 // and its getters are in the header) 0456 bool RgbColorSpace::profileHasMatrixShaper() const 0457 { 0458 return d_pointer->m_profileHasMatrixShaper; 0459 } 0460 0461 // No documentation here (documentation of properties 0462 // and its getters are in the header) 0463 QVersionNumber RgbColorSpace::profileIccVersion() const 0464 { 0465 return d_pointer->m_profileIccVersion; 0466 } 0467 0468 // No documentation here (documentation of properties 0469 // and its getters are in the header) 0470 QString RgbColorSpace::profileManufacturer() const 0471 { 0472 return d_pointer->m_profileManufacturer; 0473 } 0474 0475 // No documentation here (documentation of properties 0476 // and its getters are in the header) 0477 double RgbColorSpace::profileMaximumCielchD50Chroma() const 0478 { 0479 return d_pointer->m_profileMaximumCielchD50Chroma; 0480 } 0481 0482 // No documentation here (documentation of properties 0483 // and its getters are in the header) 0484 double RgbColorSpace::profileMaximumOklchChroma() const 0485 { 0486 return d_pointer->m_profileMaximumOklchChroma; 0487 } 0488 0489 // No documentation here (documentation of properties 0490 // and its getters are in the header) 0491 QString RgbColorSpace::profileModel() const 0492 { 0493 return d_pointer->m_profileModel; 0494 } 0495 0496 // No documentation here (documentation of properties 0497 // and its getters are in the header) 0498 QString RgbColorSpace::profileName() const 0499 { 0500 return d_pointer->m_profileName; 0501 } 0502 0503 // No documentation here (documentation of properties 0504 // and its getters are in the header) 0505 cmsColorSpaceSignature RgbColorSpace::profilePcsColorModel() const 0506 { 0507 return d_pointer->m_profilePcsColorModel; 0508 } 0509 0510 /** @brief Get information from an ICC profile via LittleCMS 0511 * 0512 * @param profileHandle handle to the ICC profile in which will be searched 0513 * @param infoType the type of information that is searched 0514 * @returns A QString with the information. It searches the 0515 * information in the current locale (language code and country code as 0516 * provided currently by <tt>QLocale</tt>). If the information is not 0517 * available in this locale, LittleCMS silently falls back to another available 0518 * localization. Note that the returned <tt>QString</tt> might be empty if the 0519 * requested information is not available in the ICC profile. */ 0520 QString RgbColorSpacePrivate::getInformationFromProfile(cmsHPROFILE profileHandle, cmsInfoType infoType) 0521 { 0522 QByteArray languageCode; 0523 QByteArray countryCode; 0524 // Update languageCode and countryCode to the actual locale (if possible) 0525 const QStringList list = QLocale().name().split(QStringLiteral(u"_")); 0526 // The list of locale codes should be ASCII only. 0527 // Therefore QString::toUtf8() should return ASCII-only valid results. 0528 // (We do not know what character encoding LittleCMS expects, 0529 // but ASCII seems a safe choice.) 0530 if (list.count() == 2) { 0531 languageCode = list.at(0).toUtf8(); 0532 countryCode = list.at(1).toUtf8(); 0533 } 0534 // Fallback for missing (empty) values to the default value recommended 0535 // by LittleCMS documentation: “en” and “US”. 0536 if (languageCode.size() != 2) { 0537 // Encoding of C++ string literals is UTF8 (we have static_assert 0538 // for this): 0539 languageCode = QByteArrayLiteral("en"); 0540 } 0541 if (countryCode.size() != 2) { 0542 // Encoding of C++ string literals is UTF8 (we have a static_assert 0543 // for this): 0544 countryCode = QByteArrayLiteral("US"); 0545 } 0546 // NOTE Since LittleCMS ≥ 2.16, cmsNoLanguage and cmsNoCountry could be 0547 // used instead of "en" and "US" and would return simply the first language 0548 // in the profile, but that seems less predictable and less reliably than 0549 // "en" and "US". 0550 // 0551 // NOTE Do only v4 profiles provide internationalization, while v2 profiles 0552 // don’t? This seems to be implied in LittleCMS documentation: 0553 // 0554 // “Since 2.16, a special setting for the lenguage and country allows 0555 // to access the unicode variant on V2 profiles. 0556 // 0557 // For the language and country: 0558 // 0559 // cmsV2Unicode 0560 // 0561 // Many V2 profiles have this field empty or filled with bogus values. 0562 // Previous versions of Little CMS were ignoring it, but with 0563 // this additional setting, correct V2 profiles with two variants 0564 // can be honored now. By default, the ASCII variant is returned on 0565 // V2 profiles unless you specify this special setting. If you decide 0566 // to use it, check the result for empty strings and if this is the 0567 // case, repeat reading by using the normal path.” 0568 // 0569 // So maybe v2 profiles have just one ASCII and one Unicode string, and 0570 // that’s all? If so, our approach seems fine: Our locale will be honored 0571 // on v4 profiles, and it will be ignored on v2 profiles because we do not 0572 // use cmsV2Unicode. This seems a wise choice, because otherwise we would 0573 // need different code paths for v2 and v4 profiles, which would be even 0574 // even more complex than the current code, and still potentially return 0575 // “bogus values” (as LittleCMS the documentation states), so the result 0576 // would be worse than the current code. 0577 0578 // Calculate the expected maximum size of the return value that we have 0579 // to provide for cmsGetProfileInfo later on in order to return an 0580 // actual value. 0581 const cmsUInt32Number resultLength = cmsGetProfileInfo( 0582 // Profile in which we search: 0583 profileHandle, 0584 // The type of information we search: 0585 infoType, 0586 // The preferred language in which we want to get the information: 0587 languageCode.constData(), 0588 // The preferred country for which we want to get the information: 0589 countryCode.constData(), 0590 // Do not actually provide the information, 0591 // just return the required buffer size: 0592 nullptr, 0593 // Do not actually provide the information, 0594 // just return the required buffer size: 0595 0); 0596 // For the actual buffer size, increment by 1. This helps us to 0597 // guarantee a null-terminated string later on. 0598 const cmsUInt32Number bufferLength = resultLength + 1; 0599 0600 // NOTE According to the documentation, it seems that cmsGetProfileInfo() 0601 // calculates the buffer length in bytes and not in wchar_t. However, 0602 // the documentation (as of LittleCMS 2.9) is not clear about the 0603 // used encoding, and the buffer type must be wchar_t anyway, and 0604 // wchar_t might have different sizes (either 16 bit or 32 bit) on 0605 // different systems, and LittleCMS’ treatment of this situation is 0606 // not well documented. Therefore, we interpret the buffer length 0607 // as number of necessary wchart_t, which creates a greater buffer, 0608 // which might possibly be waste of space, but it’s just a little bit 0609 // of text, so that’s not so much space that is wasted finally. 0610 0611 // TODO For security reasons (you never know what surprise a foreign ICC 0612 // file might have for us), it would be better to have a maximum 0613 // length for the buffer, so that insane big buffer will not be 0614 // actually created, and instead an empty string is returned. 0615 0616 // Allocate the buffer 0617 wchar_t *buffer = new wchar_t[bufferLength]; 0618 // Initialize the buffer with 0 0619 for (cmsUInt32Number i = 0; i < bufferLength; ++i) { 0620 *(buffer + i) = 0; 0621 } 0622 0623 // Write the actual information to the buffer 0624 cmsGetProfileInfo( 0625 // profile in which we search 0626 profileHandle, 0627 // the type of information we search 0628 infoType, 0629 // the preferred language in which we want to get the information 0630 languageCode.constData(), 0631 // the preferred country for which we want to get the information 0632 countryCode.constData(), 0633 // the buffer into which the requested information will be written 0634 buffer, 0635 // the buffer size as previously calculated by cmsGetProfileInfo 0636 resultLength); 0637 // Make absolutely sure the buffer is null-terminated by marking its last 0638 // element (the one that was the +1 "extra" element) as null. 0639 *(buffer + (bufferLength - 1)) = 0; 0640 0641 // Create a QString() from the from the buffer 0642 // 0643 // cmsGetProfileInfo returns often strings that are smaller than the 0644 // previously calculated buffer size. But we had initialized the buffer 0645 // with null, so actually we get a null-terminated string even if LittleCMS 0646 // would not provide the final null. So we read only up to the first null 0647 // value. 0648 // 0649 // LittleCMS returns wchar_t. This type might have different sizes: 0650 // Depending on the operating system either 16 bit or 32 bit. 0651 // LittleCMS does not specify the encoding in its documentation for 0652 // cmsGetProfileInfo() as of LittleCMS 2.9. It only says “Strings are 0653 // returned as wide chars.” So this is likely either UTF-16 or UTF-32. 0654 // According to github.com/mm2/Little-CMS/issues/180#issue-421837278 0655 // it is even UTF-16 when the size of wchar_t is 32 bit! And according 0656 // to github.com/mm2/Little-CMS/issues/180#issuecomment-1007490587 0657 // in LittleCMS versions after 2.13 it might be UTF-32 when the size 0658 // of wchar_t is 32 bit. So the behaviour of LittleCMS changes between 0659 // various versions. Conclusion: It’s either UTF-16 or UTF-32, but we 0660 // never know which it is and have to be prepared for all possible 0661 // combinations between UTF-16/UTF-32 and a wchar_t size of 0662 // 16 bit/32 bit. 0663 // 0664 // QString::fromWCharArray can create a QString from this data. It 0665 // accepts arrays of wchar_t. As Qt’s documentation of 0666 // QString::fromWCharArray() says: 0667 // 0668 // “If wchar is 4 bytes, the string is interpreted as UCS-4, 0669 // if wchar is 2 bytes it is interpreted as UTF-16.” 0670 // 0671 // However, apparently this is not exact: When wchar is 4 bytes, 0672 // surrogate pairs in the code unit array are interpreted like UTF-16: 0673 // The surrogate pair is recognized as such, which is not strictly 0674 // UTF-32 conform, but enhances the compatibility. Single surrogates 0675 // cannot be interpreted correctly, but there will be no crash: 0676 // QString::fromWCharArray will continue to read, also the part 0677 // after the first UTF error. So QString::fromWCharArray is quite 0678 // error-tolerant, which is great as we do not exactly know the 0679 // encoding of the buffer that LittleCMS returns. However, this is 0680 // undocumented behaviour of QString::fromWCharArray which means 0681 // it could change over time. Therefore, in the unit tests of this 0682 // class, we test if QString::fromWCharArray actually behaves as we want. 0683 // 0684 // NOTE Instead of cmsGetProfileInfo(), we could also use 0685 // cmsGetProfileInfoUTF8() which returns directly an UTF-8 encoded 0686 // string. We were no longer required to guess the encoding, but we 0687 // would have a return value in a well-defined encoding. However, 0688 // this would also require LittleCMS ≥ 2.16, and we would still 0689 // need the buffer. 0690 const QString result = QString::fromWCharArray( 0691 // Convert to string with these parameters: 0692 buffer, // read from this buffer 0693 -1 // read until the first null element 0694 ); 0695 0696 // Free allocated memory of the buffer 0697 delete[] buffer; 0698 0699 // Return 0700 return result; 0701 } 0702 0703 /** @brief Get ICC version from profile via LittleCMS 0704 * 0705 * @param profileHandle handle to the ICC profile 0706 * @returns The version number of the ICC format used in the profile. */ 0707 QVersionNumber RgbColorSpacePrivate::getIccVersionFromProfile(cmsHPROFILE profileHandle) 0708 { 0709 // cmsGetProfileVersion returns a floating point number. Apparently 0710 // the digits before the decimal separator are the major version, 0711 // and the digits after the decimal separator are the minor version. 0712 // So, the version number strings “2.1” (major version 2, minor version 1) 0713 // and “2.10” (major version 2, minor version 10) both get the same 0714 // representation as floating point number 2.1 because floating 0715 // point numbers do not have memory about how many trailing zeros 0716 // exist. So we have to assume minor versions higher than 9 are not 0717 // supported by cmsGetProfileVersion anyway. A positive side effect 0718 // of this assumption is that is makes the conversion to QVersionNumber 0719 // easier: We use a fixed width of exactly one digit for the 0720 // part after the decimal separator. This makes also sure that 0721 // the floating point number 2 is interpreted as “2.0” (and not 0722 // simply as “2”). 0723 0724 // QString::number() ignores the locale and uses always a “.” 0725 // as separator, which is exactly what we need to create 0726 // a QVersionNumber from. 0727 const QString versionString = QString::number( // 0728 cmsGetProfileVersion(profileHandle), // floating point 0729 'f', // use normal rendering format (no exponents) 0730 1 // number of digits after the decimal point 0731 ); 0732 return QVersionNumber::fromString(versionString); 0733 } 0734 0735 /** @brief Date and time of creation of a profile via LittleCMS 0736 * 0737 * @param profileHandle handle to the ICC profile 0738 * @returns Date and time of creation of the profile, if available. An invalid 0739 * date and time otherwise. */ 0740 QDateTime RgbColorSpacePrivate::getCreationDateTimeFromProfile(cmsHPROFILE profileHandle) 0741 { 0742 tm myDateTime; // The type “tm” as defined in C (time.h), as LittleCMS expects. 0743 const bool success = cmsGetHeaderCreationDateTime(profileHandle, &myDateTime); 0744 if (!success) { 0745 // Return invalid QDateTime object 0746 return QDateTime(); 0747 } 0748 const QDate myDate(myDateTime.tm_year + 1900, // tm_year means: years since 1900 0749 myDateTime.tm_mon + 1, // tm_mon ranges fromm 0 to 11 0750 myDateTime.tm_mday // tm_mday ranges from 1 to 31 0751 ); 0752 // “tm” allows seconds higher than 59: It allows up to 60 seconds: The 0753 // “supplement” second is for leap seconds. However, QTime does not 0754 // accept seconds beyond 59. Therefore, this has to be corrected: 0755 const QTime myTime(myDateTime.tm_hour, // 0756 myDateTime.tm_min, // 0757 qBound(0, myDateTime.tm_sec, 59)); 0758 return QDateTime( 0759 // Date: 0760 myDate, 0761 // Time: 0762 myTime, 0763 // Assuming UTC for the QDateTime because it’s the only choice 0764 // that will not change arbitrary. 0765 Qt::TimeSpec::UTC); 0766 } 0767 0768 /** @brief Reduces the chroma until the color fits into the gamut. 0769 * 0770 * It always preserves the hue. It preservers the lightness whenever 0771 * possible. 0772 * 0773 * @note In some cases with very curvy color spaces, the nearest in-gamut 0774 * color (with the same lightness and hue) might be at <em>higher</em> 0775 * chroma. As this function always <em>reduces</em> the chroma, 0776 * in this case the result is not the nearest in-gamut color. 0777 * 0778 * @param cielchD50color The color that will be adapted. 0779 * 0780 * @returns An @ref isCielchD50InGamut color. */ 0781 PerceptualColor::LchDouble RgbColorSpace::reduceCielchD50ChromaToFitIntoGamut(const PerceptualColor::LchDouble &cielchD50color) const 0782 { 0783 LchDouble referenceColor = cielchD50color; 0784 0785 // Normalize the LCH coordinates 0786 normalizePolar360(referenceColor.c, referenceColor.h); 0787 0788 // Bound to valid range: 0789 referenceColor.c = qMin<decltype(referenceColor.c)>( // 0790 referenceColor.c, // 0791 profileMaximumCielchD50Chroma()); 0792 referenceColor.l = qBound(d_pointer->m_cielabD50BlackpointL, // 0793 referenceColor.l, // 0794 d_pointer->m_cielabD50WhitepointL); 0795 0796 // Test special case: If we are yet in-gamut… 0797 if (isCielchD50InGamut(referenceColor)) { 0798 return referenceColor; 0799 } 0800 0801 // Now we know: We are out-of-gamut. 0802 LchDouble temp; 0803 0804 // Create an in-gamut point on the gray axis: 0805 LchDouble lowerChroma{referenceColor.l, 0, referenceColor.h}; 0806 if (!isCielchD50InGamut(lowerChroma)) { 0807 // This is quite strange because every point between the blackpoint 0808 // and the whitepoint on the gray axis should be in-gamut on 0809 // normally shaped gamuts. But as we never know, we need a fallback, 0810 // which is guaranteed to be in-gamut: 0811 referenceColor.l = d_pointer->m_cielabD50BlackpointL; 0812 lowerChroma.l = d_pointer->m_cielabD50BlackpointL; 0813 } 0814 // TODO Decide which one of the algorithms provides with the “if constexpr” 0815 // will be used (and remove the other one). 0816 constexpr bool quickApproximate = true; 0817 if constexpr (quickApproximate) { 0818 // Do a quick-approximate search: 0819 LchDouble upperChroma{referenceColor}; 0820 // Now we know for sure that lowerChroma is in-gamut 0821 // and upperChroma is out-of-gamut… 0822 temp = upperChroma; 0823 while (upperChroma.c - lowerChroma.c > gamutPrecisionCielab) { 0824 // Our test candidate is half the way between lowerChroma 0825 // and upperChroma: 0826 temp.c = ((lowerChroma.c + upperChroma.c) / 2); 0827 if (isCielchD50InGamut(temp)) { 0828 lowerChroma = temp; 0829 } else { 0830 upperChroma = temp; 0831 } 0832 } 0833 return lowerChroma; 0834 0835 } else { 0836 // Do a slow-thorough search: 0837 temp = referenceColor; 0838 while (temp.c > 0) { 0839 if (isCielchD50InGamut(temp)) { 0840 break; 0841 } else { 0842 temp.c -= gamutPrecisionCielab; 0843 } 0844 } 0845 if (temp.c < 0) { 0846 temp.c = 0; 0847 } 0848 return temp; 0849 } 0850 } 0851 0852 /** @brief Reduces the chroma until the color fits into the gamut. 0853 * 0854 * It always preserves the hue. It preservers the lightness whenever 0855 * possible. 0856 * 0857 * @note In some cases with very curvy color spaces, the nearest in-gamut 0858 * color (with the same lightness and hue) might be at <em>higher</em> 0859 * chroma. As this function always <em>reduces</em> the chroma, 0860 * in this case the result is not the nearest in-gamut color. 0861 * 0862 * @param oklchColor The color that will be adapted. 0863 * 0864 * @returns An @ref isOklchInGamut color. */ 0865 PerceptualColor::LchDouble RgbColorSpace::reduceOklchChromaToFitIntoGamut(const PerceptualColor::LchDouble &oklchColor) const 0866 { 0867 LchDouble referenceColor = oklchColor; 0868 0869 // Normalize the LCH coordinates 0870 normalizePolar360(referenceColor.c, referenceColor.h); 0871 0872 // Bound to valid range: 0873 referenceColor.c = qMin<decltype(referenceColor.c)>( // 0874 referenceColor.c, // 0875 profileMaximumOklchChroma()); 0876 referenceColor.l = qBound(d_pointer->m_oklabBlackpointL, 0877 referenceColor.l, // 0878 d_pointer->m_oklabWhitepointL); 0879 0880 // Test special case: If we are yet in-gamut… 0881 if (isOklchInGamut(referenceColor)) { 0882 return referenceColor; 0883 } 0884 0885 // Now we know: We are out-of-gamut. 0886 LchDouble temp; 0887 0888 // Create an in-gamut point on the gray axis: 0889 LchDouble lowerChroma{referenceColor.l, 0, referenceColor.h}; 0890 if (!isOklchInGamut(lowerChroma)) { 0891 // This is quite strange because every point between the blackpoint 0892 // and the whitepoint on the gray axis should be in-gamut on 0893 // normally shaped gamuts. But as we never know, we need a fallback, 0894 // which is guaranteed to be in-gamut: 0895 referenceColor.l = d_pointer->m_oklabBlackpointL; 0896 lowerChroma.l = d_pointer->m_oklabBlackpointL; 0897 } 0898 // TODO Decide which one of the algorithms provides with the “if constexpr” 0899 // will be used (and remove the other one). 0900 constexpr bool quickApproximate = true; 0901 if constexpr (quickApproximate) { 0902 // Do a quick-approximate search: 0903 LchDouble upperChroma{referenceColor}; 0904 // Now we know for sure that lowerChroma is in-gamut 0905 // and upperChroma is out-of-gamut… 0906 temp = upperChroma; 0907 while (upperChroma.c - lowerChroma.c > gamutPrecisionOklab) { 0908 // Our test candidate is half the way between lowerChroma 0909 // and upperChroma: 0910 temp.c = ((lowerChroma.c + upperChroma.c) / 2); 0911 if (isOklchInGamut(temp)) { 0912 lowerChroma = temp; 0913 } else { 0914 upperChroma = temp; 0915 } 0916 } 0917 return lowerChroma; 0918 0919 } else { 0920 // Do a slow-thorough search: 0921 temp = referenceColor; 0922 while (temp.c > 0) { 0923 if (isOklchInGamut(temp)) { 0924 break; 0925 } else { 0926 temp.c -= gamutPrecisionOklab; 0927 } 0928 } 0929 if (temp.c < 0) { 0930 temp.c = 0; 0931 } 0932 return temp; 0933 } 0934 } 0935 0936 /** @brief Conversion to CIELab. 0937 * 0938 * @param rgbColor The original color. 0939 * @returns The corresponding (opaque) CIELab color. 0940 * 0941 * @note By definition, each RGB color in a given color space is an in-gamut 0942 * color in this very same color space. Nevertheless, because of rounding 0943 * errors, when converting colors that are near to the outer hull of the 0944 * gamut/color space, than @ref isCielabD50InGamut() might return <tt>false</tt> for 0945 * a return value of <em>this</em> function. */ 0946 cmsCIELab RgbColorSpace::toCielabD50(const QRgba64 rgbColor) const 0947 { 0948 constexpr qreal maximum = // 0949 std::numeric_limits<decltype(rgbColor.red())>::max(); 0950 const double my_rgb[]{rgbColor.red() / maximum, // 0951 rgbColor.green() / maximum, // 0952 rgbColor.blue() / maximum}; 0953 cmsCIELab cielabD50; 0954 cmsDoTransform(d_pointer->m_transformRgbToCielabD50Handle, // handle to transform 0955 &my_rgb, // input 0956 &cielabD50, // output 0957 1 // convert exactly 1 value 0958 ); 0959 if (cielabD50.L < 0) { 0960 // Workaround for https://github.com/mm2/Little-CMS/issues/395 0961 cielabD50.L = 0; 0962 } 0963 return cielabD50; 0964 } 0965 0966 /** @brief Conversion to CIELCh-D50. 0967 * 0968 * @param rgbColor The original color. 0969 * @returns The corresponding (opaque) CIELCh-D50 color. 0970 * 0971 * @note By definition, each RGB color in a given color space is an in-gamut 0972 * color in this very same color space. Nevertheless, because of rounding 0973 * errors, when converting colors that are near to the outer hull of the 0974 * gamut/color space, than @ref isCielchD50InGamut() might return <tt>false</tt> for 0975 * a return value of <em>this</em> function. */ 0976 PerceptualColor::LchDouble RgbColorSpace::toCielchD50Double(const QRgba64 rgbColor) const 0977 { 0978 constexpr qreal maximum = // 0979 std::numeric_limits<decltype(rgbColor.red())>::max(); 0980 const double my_rgb[]{rgbColor.red() / maximum, // 0981 rgbColor.green() / maximum, // 0982 rgbColor.blue() / maximum}; 0983 cmsCIELab cielabD50; 0984 cmsDoTransform(d_pointer->m_transformRgbToCielabD50Handle, // handle to transform 0985 &my_rgb, // input 0986 &cielabD50, // output 0987 1 // convert exactly 1 value 0988 ); 0989 if (cielabD50.L < 0) { 0990 // Workaround for https://github.com/mm2/Little-CMS/issues/395 0991 cielabD50.L = 0; 0992 } 0993 cmsCIELCh cielchD50; 0994 cmsLab2LCh(&cielchD50, // output 0995 &cielabD50 // input 0996 ); 0997 return LchDouble{cielchD50.L, cielchD50.C, cielchD50.h}; 0998 } 0999 1000 /** @brief Conversion to QRgb. 1001 * 1002 * @param lch The original color. 1003 * 1004 * @returns If the original color is in-gamut, the corresponding 1005 * (opaque) in-range RGB value. If the original color is out-of-gamut, 1006 * a more or less similar (opaque) in-range RGB value. 1007 * 1008 * @note There is no guarantee <em>which</em> specific algorithm is used 1009 * to fit out-of-gamut colors into the gamut. 1010 * 1011 * @sa @ref fromCielabD50ToQRgbOrTransparent */ 1012 QRgb RgbColorSpace::fromCielchD50ToQRgbBound(const LchDouble &lch) const 1013 { 1014 const cmsCIELCh myCmsCieLch = toCmsLch(lch); 1015 cmsCIELab lab; // uses cmsFloat64Number internally 1016 cmsLCh2Lab(&lab, // output 1017 &myCmsCieLch // input 1018 ); 1019 cmsUInt16Number rgb_int[3]; 1020 cmsDoTransform(d_pointer->m_transformCielabD50ToRgb16Handle, // transform 1021 &lab, // input 1022 rgb_int, // output 1023 1 // number of values to convert 1024 ); 1025 constexpr qreal channelMaximumQReal = // 1026 std::numeric_limits<cmsUInt16Number>::max(); 1027 constexpr quint8 rgbMaximum = 255; 1028 return qRgb(qRound(rgb_int[0] / channelMaximumQReal * rgbMaximum), // 1029 qRound(rgb_int[1] / channelMaximumQReal * rgbMaximum), // 1030 qRound(rgb_int[2] / channelMaximumQReal * rgbMaximum)); 1031 } 1032 1033 /** @brief Check if a color is within the gamut. 1034 * @param lch the color 1035 * @returns <tt>true</tt> if the color is in the gamut. 1036 * <tt>false</tt> otherwise. */ 1037 bool RgbColorSpace::isCielchD50InGamut(const LchDouble &lch) const 1038 { 1039 if (!isInRange<decltype(lch.l)>(0, lch.l, 100)) { 1040 return false; 1041 } 1042 if (!isInRange<decltype(lch.l)>( // 1043 (-1) * d_pointer->m_profileMaximumCielchD50Chroma, // 1044 lch.c, // 1045 d_pointer->m_profileMaximumCielchD50Chroma // 1046 )) { 1047 return false; 1048 } 1049 cmsCIELab lab; // uses cmsFloat64Number internally 1050 const cmsCIELCh myCmsCieLch = toCmsLch(lch); 1051 cmsLCh2Lab(&lab, &myCmsCieLch); 1052 return qAlpha(fromCielabD50ToQRgbOrTransparent(lab)) != 0; 1053 } 1054 1055 /** @brief Check if a color is within the gamut. 1056 * @param lch the color 1057 * @returns <tt>true</tt> if the color is in the gamut. 1058 * <tt>false</tt> otherwise. */ 1059 bool RgbColorSpace::isOklchInGamut(const LchDouble &lch) const 1060 { 1061 if (!isInRange<decltype(lch.l)>(0, lch.l, 1)) { 1062 return false; 1063 } 1064 if (!isInRange<decltype(lch.l)>( // 1065 (-1) * d_pointer->m_profileMaximumOklchChroma, // 1066 lch.c, // 1067 d_pointer->m_profileMaximumOklchChroma // 1068 )) { 1069 return false; 1070 } 1071 const auto oklab = AbsoluteColor::fromPolarToCartesian(GenericColor(lch)); 1072 const auto xyzD65 = AbsoluteColor::fromOklabToXyzD65(oklab); 1073 const auto xyzD50 = AbsoluteColor::fromXyzD65ToXyzD50(xyzD65); 1074 const auto cielabD50 = AbsoluteColor::fromXyzD50ToCielabD50(xyzD50); 1075 const auto cielabD50cms = cielabD50.reinterpretAsLabToCmscielab(); 1076 const auto rgb = fromCielabD50ToQRgbOrTransparent(cielabD50cms); 1077 return (qAlpha(rgb) != 0); 1078 } 1079 1080 /** @brief Check if a color is within the gamut. 1081 * @param lab the color 1082 * @returns <tt>true</tt> if the color is in the gamut. 1083 * <tt>false</tt> otherwise. */ 1084 bool RgbColorSpace::isCielabD50InGamut(const cmsCIELab &lab) const 1085 { 1086 if (!isInRange<decltype(lab.L)>(0, lab.L, 100)) { 1087 return false; 1088 } 1089 const auto chromaSquare = lab.a * lab.a + lab.b * lab.b; 1090 const auto maximumChromaSquare = qPow(d_pointer->m_profileMaximumCielchD50Chroma, 2); 1091 if (chromaSquare > maximumChromaSquare) { 1092 return false; 1093 } 1094 return qAlpha(fromCielabD50ToQRgbOrTransparent(lab)) != 0; 1095 } 1096 1097 /** @brief Conversion to QRgb. 1098 * 1099 * @pre 1100 * - Input Lightness: 0 ≤ lightness ≤ 100 1101 * @pre 1102 * - Input Chroma: - @ref RgbColorSpace::profileMaximumCielchD50Chroma ≤ chroma ≤ 1103 * @ref RgbColorSpace::profileMaximumCielchD50Chroma 1104 * 1105 * @param lab the original color 1106 * 1107 * @returns The corresponding opaque color if the original color is in-gamut. 1108 * A transparent color otherwise. 1109 * 1110 * @sa @ref fromCielchD50ToQRgbBound */ 1111 QRgb RgbColorSpace::fromCielabD50ToQRgbOrTransparent(const cmsCIELab &lab) const 1112 { 1113 constexpr QRgb transparentValue = 0; 1114 static_assert(qAlpha(transparentValue) == 0, // 1115 "The alpha value of a transparent QRgb must be 0."); 1116 1117 double rgb[3]; 1118 cmsDoTransform( 1119 // Parameters: 1120 d_pointer->m_transformCielabD50ToRgbHandle, // handle to transform function 1121 &lab, // input 1122 &rgb, // output 1123 1 // convert exactly 1 value 1124 ); 1125 1126 // Detect if valid: 1127 const bool colorIsValid = // 1128 isInRange<double>(0, rgb[0], 1) // 1129 && isInRange<double>(0, rgb[1], 1) // 1130 && isInRange<double>(0, rgb[2], 1); 1131 if (!colorIsValid) { 1132 return transparentValue; 1133 } 1134 1135 // Detect deviation: 1136 cmsCIELab roundtripCielabD50; 1137 cmsDoTransform( 1138 // Parameters: 1139 d_pointer->m_transformRgbToCielabD50Handle, // handle to transform function 1140 &rgb, // input 1141 &roundtripCielabD50, // output 1142 1 // convert exactly 1 value 1143 ); 1144 const qreal actualDeviationSquare = // 1145 qPow(lab.L - roundtripCielabD50.L, 2) // 1146 + qPow(lab.a - roundtripCielabD50.a, 2) // 1147 + qPow(lab.b - roundtripCielabD50.b, 2); 1148 constexpr auto cielabDeviationLimitSquare = // 1149 RgbColorSpacePrivate::cielabDeviationLimit // 1150 * RgbColorSpacePrivate::cielabDeviationLimit; 1151 const bool actualDeviationIsOkay = // 1152 actualDeviationSquare <= cielabDeviationLimitSquare; 1153 1154 // If deviation is too big, return a transparent color. 1155 if (!actualDeviationIsOkay) { 1156 return transparentValue; 1157 } 1158 1159 // If in-gamut, return an opaque color. 1160 QColor temp = QColor::fromRgbF(static_cast<QColorFloatType>(rgb[0]), // 1161 static_cast<QColorFloatType>(rgb[1]), // 1162 static_cast<QColorFloatType>(rgb[2])); 1163 return temp.rgb(); 1164 } 1165 1166 /** @brief Conversion to RGB. 1167 * 1168 * @param lch The original color. 1169 * 1170 * @returns If the original color is in-gamut, it returns the corresponding 1171 * in-range RGB color. If the original color is out-of-gamut, it returns an 1172 * RGB value which might be in-range or out-of range. The RGB value range 1173 * is [0, 1]. */ 1174 PerceptualColor::GenericColor RgbColorSpace::fromCielchD50ToRgb1(const PerceptualColor::LchDouble &lch) const 1175 { 1176 const cmsCIELCh myCmsCieLch = toCmsLch(lch); 1177 cmsCIELab lab; // uses cmsFloat64Number internally 1178 cmsLCh2Lab(&lab, // output 1179 &myCmsCieLch // input 1180 ); 1181 double rgb[3]; 1182 cmsDoTransform( 1183 // Parameters: 1184 d_pointer->m_transformCielabD50ToRgbHandle, // handle to transform function 1185 &lab, // input 1186 &rgb, // output 1187 1 // convert exactly 1 value 1188 ); 1189 return GenericColor(rgb[0], rgb[1], rgb[2]); 1190 } 1191 1192 /** @brief Calculation of @ref RgbColorSpace::profileMaximumCielchD50Chroma 1193 * 1194 * @returns Calculation of @ref RgbColorSpace::profileMaximumCielchD50Chroma */ 1195 double RgbColorSpacePrivate::detectMaximumCielchD50Chroma() const 1196 { 1197 // Make sure chromaDetectionHuePrecision is big enough to make a difference 1198 // when being added to floating point variable “hue” used in loop later. 1199 static_assert(0. + chromaDetectionHuePrecision > 0.); 1200 static_assert(360. + chromaDetectionHuePrecision > 360.); 1201 1202 // Implementation 1203 double result = 0; 1204 double hue = 0; 1205 while (hue < 360) { 1206 const auto qColorHue = static_cast<QColorFloatType>(hue / 360.); 1207 const auto color = QColor::fromHsvF(qColorHue, 1, 1).rgba64(); 1208 result = qMax(result, q_pointer->toCielchD50Double(color).c); 1209 hue += chromaDetectionHuePrecision; 1210 } 1211 result = result * chromaDetectionIncrementFactor + cielabDeviationLimit; 1212 return std::min<double>(result, CielchD50Values::maximumChroma); 1213 } 1214 1215 /** @brief Calculation of @ref RgbColorSpace::profileMaximumOklchChroma 1216 * 1217 * @returns Calculation of @ref RgbColorSpace::profileMaximumOklchChroma */ 1218 double RgbColorSpacePrivate::detectMaximumOklchChroma() const 1219 { 1220 // Make sure chromaDetectionHuePrecision is big enough to make a difference 1221 // when being added to floating point variable “hue” used in loop later. 1222 static_assert(0. + chromaDetectionHuePrecision > 0.); 1223 static_assert(360. + chromaDetectionHuePrecision > 360.); 1224 1225 double chromaSquare = 0; 1226 double hue = 0; 1227 while (hue < 360) { 1228 const auto qColorHue = static_cast<QColorFloatType>(hue / 360.); 1229 const auto rgbColor = QColor::fromHsvF(qColorHue, 1, 1).rgba64(); 1230 const auto cielabD50Color = q_pointer->toCielabD50(rgbColor); 1231 const auto cielabD50 = GenericColor(cielabD50Color); 1232 const auto xyzD50 = AbsoluteColor::fromCielabD50ToXyzD50(cielabD50); 1233 const auto xyzD65 = AbsoluteColor::fromXyzD50ToXyzD65(xyzD50); 1234 const auto oklab = AbsoluteColor::fromXyzD65ToOklab(xyzD65); 1235 chromaSquare = qMax( // 1236 chromaSquare, // 1237 oklab.second * oklab.second + oklab.third * oklab.third); 1238 hue += chromaDetectionHuePrecision; 1239 } 1240 const auto result = qSqrt(chromaSquare) * chromaDetectionIncrementFactor // 1241 + oklabDeviationLimit; 1242 return std::min<double>(result, OklchValues::maximumChroma); 1243 } 1244 1245 /** @brief Gets the rendering intents supported by the LittleCMS library. 1246 * 1247 * @returns The rendering intents supported by the LittleCMS library. 1248 * 1249 * @note Do not use this function. Instead, use @ref intentList. */ 1250 QMap<cmsUInt32Number, QString> RgbColorSpacePrivate::getIntentList() 1251 { 1252 // TODO xxx Actually use this (for translation, for example), or remove it… 1253 QMap<cmsUInt32Number, QString> result; 1254 const cmsUInt32Number intentCount = // 1255 cmsGetSupportedIntents(0, nullptr, nullptr); 1256 cmsUInt32Number *codeArray = new cmsUInt32Number[intentCount]; 1257 char **descriptionArray = new char *[intentCount]; 1258 cmsGetSupportedIntents(intentCount, codeArray, descriptionArray); 1259 for (cmsUInt32Number i = 0; i < intentCount; ++i) { 1260 result.insert(codeArray[i], QString::fromUtf8(descriptionArray[i])); 1261 } 1262 delete[] codeArray; 1263 delete[] descriptionArray; 1264 return result; 1265 } 1266 1267 } // namespace PerceptualColor