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