File indexing completed on 2024-05-12 04:44:27

0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT
0003 
0004 // First included header is the public header of the class we are testing;
0005 // this forces the header to be self-contained.
0006 #include "rgbcolorspace.h"
0007 // Second, the private implementation.
0008 #include "rgbcolorspace_p.h" // IWYU pragma: keep
0009 
0010 #include "cielchd50values.h"
0011 #include "constpropagatinguniquepointer.h"
0012 #include "helpermath.h"
0013 #include "helperposixmath.h"
0014 #include "lchdouble.h"
0015 #include "rgbcolorspacefactory.h"
0016 #include <lcms2.h>
0017 #include <qbytearray.h>
0018 #include <qcolor.h>
0019 #include <qdatetime.h>
0020 #include <qdir.h>
0021 #include <qfileinfo.h>
0022 #include <qglobal.h>
0023 #include <qlist.h>
0024 #include <qnamespace.h>
0025 #include <qobject.h>
0026 #include <qrgb.h>
0027 #include <qrgba64.h>
0028 #include <qscopedpointer.h>
0029 #include <qsharedpointer.h>
0030 #include <qstringbuilder.h>
0031 #include <qstringliteral.h>
0032 #include <qtemporarydir.h>
0033 #include <qtemporaryfile.h>
0034 #include <qtest.h>
0035 #include <qtestcase.h>
0036 #include <qversionnumber.h>
0037 
0038 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
0039 #include <qstring.h>
0040 #include <qtmetamacros.h>
0041 #else
0042 #include <qobjectdefs.h>
0043 #include <qstring.h>
0044 #include <qvector.h>
0045 #endif
0046 
0047 namespace PerceptualColor
0048 {
0049 class TestRgbColorSpace : public QObject
0050 {
0051     Q_OBJECT
0052 
0053 public:
0054     explicit TestRgbColorSpace(QObject *parent = nullptr)
0055         : QObject(parent)
0056     {
0057     }
0058 
0059 private Q_SLOTS:
0060     void initTestCase()
0061     {
0062         // Called before the first test function is executed
0063     }
0064 
0065     void cleanupTestCase()
0066     {
0067         // Called after the last test function was executed
0068     }
0069 
0070     void init()
0071     {
0072         // Called before each test function is executed
0073     }
0074 
0075     void cleanup()
0076     {
0077         // Called after every test function
0078     }
0079 
0080     void testConstructorDestructorUninitialized()
0081     {
0082         RgbColorSpace myColorSpace;
0083     }
0084 
0085     void testCreateSrgb()
0086     {
0087         QSharedPointer<PerceptualColor::RgbColorSpace> myColorSpace;
0088 
0089         myColorSpace = RgbColorSpace::createSrgb();
0090         QCOMPARE(myColorSpace.isNull(), false);
0091 
0092         QVERIFY(isInRange<qreal>(0, myColorSpace->d_pointer->m_cielabD50BlackpointL, 1));
0093         QVERIFY( //
0094             isInRange<qreal>(99, myColorSpace->d_pointer->m_cielabD50WhitepointL, 100));
0095 
0096         QVERIFY(isInRange<qreal>(0.00, myColorSpace->d_pointer->m_oklabBlackpointL, 0.01));
0097         QVERIFY( //
0098             isInRange<qreal>(0.99, myColorSpace->d_pointer->m_oklabWhitepointL, 1.00));
0099     }
0100 
0101     void testCreateFromFile()
0102     {
0103         QScopedPointer<QTemporaryFile> invalidFile(
0104             // Create a temporary actual file…
0105             QTemporaryFile::createNativeFile(
0106                 // …from the content of this resource:
0107                 QStringLiteral(":/testbed/ascii-abcd.txt")));
0108         if (invalidFile.isNull()) {
0109             throw 0;
0110         }
0111         QScopedPointer<QTemporaryFile> validRgbFile(
0112             // Create a temporary actual file…
0113             QTemporaryFile::createNativeFile(
0114                 // …from the content of this resource:
0115                 QStringLiteral(":/testbed/Compact-ICC-Profiles/Compact-ICC-Profiles/profiles/WideGamutCompat-v4.icc")));
0116         if (validRgbFile.isNull()) {
0117             throw 0;
0118         }
0119         QTemporaryDir existingDirectoryWithoutTrailingSlash;
0120         if (!existingDirectoryWithoutTrailingSlash.isValid()) {
0121             throw 0;
0122         }
0123         if (existingDirectoryWithoutTrailingSlash.path().endsWith(QStringLiteral("/"))) {
0124             throw 0;
0125         }
0126 
0127         QSharedPointer<PerceptualColor::RgbColorSpace> myColorSpace;
0128 
0129         // Invalid file
0130         QVERIFY(QFileInfo::exists(invalidFile->fileName())); // assertion
0131         myColorSpace = RgbColorSpace::createFromFile(invalidFile->fileName());
0132         QCOMPARE(myColorSpace.isNull(), true);
0133 
0134         // Non-existing file/directory name
0135         QString nonexistingfilename = //
0136             QStringLiteral("/nonexistingfilename.txt");
0137         QCOMPARE(QFileInfo::exists(nonexistingfilename), false); // assertion
0138         QCOMPARE(QDir{nonexistingfilename}.exists(), false); // assertion
0139         myColorSpace = RgbColorSpace::createFromFile(nonexistingfilename);
0140         QCOMPARE(myColorSpace.isNull(), true);
0141 
0142         // Existing folder with trailing slash
0143         myColorSpace = RgbColorSpace::createFromFile( //
0144             existingDirectoryWithoutTrailingSlash.path() + QStringLiteral("/"));
0145         QCOMPARE(myColorSpace.isNull(), true);
0146 
0147         // Existing folder without trailing slash
0148         myColorSpace = RgbColorSpace::createFromFile( //
0149             existingDirectoryWithoutTrailingSlash.path());
0150         QCOMPARE(myColorSpace.isNull(), true);
0151 
0152         // Valid RGB profile (should load correctly)
0153         QVERIFY(QFileInfo::exists(validRgbFile->fileName())); // assertion
0154         myColorSpace = RgbColorSpace::createFromFile(validRgbFile->fileName());
0155         QCOMPARE(myColorSpace.isNull(), false);
0156     }
0157 
0158     void testInitialize()
0159     {
0160         QScopedPointer<QTemporaryFile> wideGamutFile(
0161             // Create a temporary actual file…
0162             QTemporaryFile::createNativeFile(
0163                 // …from the content of this resource:
0164                 QStringLiteral(":/testbed/Compact-ICC-Profiles/Compact-ICC-Profiles/profiles/WideGamutCompat-v4.icc")));
0165         if (wideGamutFile.isNull()) {
0166             throw 0;
0167         }
0168 
0169         auto myColorSpace = RgbColorSpace::createFromFile(wideGamutFile->fileName());
0170         QCOMPARE(myColorSpace.isNull(), false); // assertion
0171         // assertion that maximum lightness is out-of-gamut for this profile:
0172         QCOMPARE(myColorSpace->isCielchD50InGamut(LchDouble{100, 0, 0}), false);
0173         QCOMPARE(myColorSpace->isOklchInGamut(LchDouble{1, 0, 0}), false);
0174 
0175         // Actual test:
0176         QVERIFY(isInRange<qreal>(0, myColorSpace->d_pointer->m_cielabD50BlackpointL, 1));
0177         QVERIFY( //
0178             isInRange<qreal>(99, myColorSpace->d_pointer->m_cielabD50WhitepointL, 100));
0179 
0180         QVERIFY(isInRange<qreal>(0.00, myColorSpace->d_pointer->m_oklabBlackpointL, 0.01));
0181         QVERIFY( //
0182             isInRange<qreal>(0.99, myColorSpace->d_pointer->m_oklabWhitepointL, 1.00));
0183     }
0184 
0185     void testReduceCielchD50ChromaToFitIntoGamut()
0186     {
0187         QScopedPointer<QTemporaryFile> wideGamutFile(
0188             // Create a temporary actual file…
0189             QTemporaryFile::createNativeFile(
0190                 // …from the content of this resource:
0191                 QStringLiteral(":/testbed/Compact-ICC-Profiles/Compact-ICC-Profiles/profiles/WideGamutCompat-v4.icc")));
0192         if (wideGamutFile.isNull()) {
0193             throw 0;
0194         }
0195 
0196         QSharedPointer<PerceptualColor::RgbColorSpace> myColorSpace;
0197         myColorSpace = RgbColorSpace::createFromFile(wideGamutFile->fileName());
0198         QCOMPARE(myColorSpace.isNull(), false); // assertion
0199         const LchDouble referenceColor{100, 50, 0};
0200         QCOMPARE(myColorSpace->isCielchD50InGamut(referenceColor), false); // assertion
0201         // The value referenceColor is out-of-gamut because WideGamutRGB stops
0202         // just a little bit before the lightness of 100.
0203 
0204         // Now, test how this special situation is handled:
0205         const LchDouble modifiedColor = //
0206             myColorSpace->reduceCielchD50ChromaToFitIntoGamut(referenceColor);
0207         QVERIFY(modifiedColor.c <= referenceColor.c);
0208         QCOMPARE(modifiedColor.h, referenceColor.h);
0209         QVERIFY(isInRange<qreal>(99, modifiedColor.l, 100));
0210         QVERIFY(modifiedColor.l < 100);
0211         QVERIFY(myColorSpace->isCielchD50InGamut(modifiedColor));
0212     }
0213 
0214     void testBugReduceCielchD50ChromaToFitIntoGamut()
0215     {
0216         QScopedPointer<QTemporaryFile> wideGamutFile(
0217             // Create a temporary actual file…
0218             QTemporaryFile::createNativeFile(
0219                 // …from the content of this resource:
0220                 QStringLiteral(":/testbed/Compact-ICC-Profiles/Compact-ICC-Profiles/profiles/WideGamutCompat-v4.icc")));
0221         if (wideGamutFile.isNull()) {
0222             throw 0;
0223         }
0224 
0225         // This test looks for a bug that was seen during development
0226         // phase. When using WideGamutRGB and raising the lightness
0227         // slider up to 100%: Bug behaviour: the color switches
0228         // to 0% lightness. Expected behaviour: the color has almost
0229         // 100% lightness.
0230         auto myColorSpace = RgbColorSpace::createFromFile(wideGamutFile->fileName());
0231         LchDouble temp{100, 50, 0};
0232         QVERIFY(myColorSpace->reduceCielchD50ChromaToFitIntoGamut(temp).l > 95);
0233     }
0234 
0235     void testReduceOklabChromaToFitIntoGamut()
0236     {
0237         QScopedPointer<QTemporaryFile> wideGamutFile(
0238             // Create a temporary actual file…
0239             QTemporaryFile::createNativeFile(
0240                 // …from the content of this resource:
0241                 QStringLiteral(":/testbed/Compact-ICC-Profiles/Compact-ICC-Profiles/profiles/WideGamutCompat-v4.icc")));
0242         if (wideGamutFile.isNull()) {
0243             throw 0;
0244         }
0245 
0246         QSharedPointer<PerceptualColor::RgbColorSpace> myColorSpace;
0247         myColorSpace = RgbColorSpace::createFromFile(wideGamutFile->fileName());
0248         QCOMPARE(myColorSpace.isNull(), false); // assertion
0249         const LchDouble referenceColor{1, 0.151189, 359.374};
0250         QCOMPARE(myColorSpace->isOklchInGamut(referenceColor), false); // assertion
0251         // The value referenceColor is out-of-gamut because WideGamutRGB stops
0252         // just a little bit before the lightness of 100.
0253 
0254         // Now, test how this special situation is handled:
0255         const LchDouble modifiedColor = //
0256             myColorSpace->reduceOklchChromaToFitIntoGamut(referenceColor);
0257         QVERIFY(modifiedColor.c <= referenceColor.c);
0258         QCOMPARE(modifiedColor.h, referenceColor.h);
0259         QVERIFY(isInRange<qreal>(0.99, modifiedColor.l, 1));
0260         QVERIFY(modifiedColor.l < 1);
0261         QVERIFY(myColorSpace->isOklchInGamut(modifiedColor));
0262     }
0263 
0264     void testBugReduceOklabChromaToFitIntoGamut()
0265     {
0266         QScopedPointer<QTemporaryFile> wideGamutFile(
0267             // Create a temporary actual file…
0268             QTemporaryFile::createNativeFile(
0269                 // …from the content of this resource:
0270                 QStringLiteral(":/testbed/Compact-ICC-Profiles/Compact-ICC-Profiles/profiles/WideGamutCompat-v4.icc")));
0271         if (wideGamutFile.isNull()) {
0272             throw 0;
0273         }
0274 
0275         // This test looks for a bug that was seen during development
0276         // phase. When using WideGamutRGB and raising the lightness
0277         // slider up to 100%: Bug behaviour: the color switches
0278         // to 0% lightness. Expected behaviour: the color has almost
0279         // 100% lightness.
0280         auto myColorSpace = RgbColorSpace::createFromFile(wideGamutFile->fileName());
0281         const LchDouble temp{1, 0.151189, 359.374};
0282         QVERIFY(myColorSpace->reduceOklchChromaToFitIntoGamut(temp).l > 0.95);
0283     }
0284 
0285     void testDeleteTransformThatIsNull()
0286     {
0287         cmsHTRANSFORM myTransform = nullptr;
0288         RgbColorSpacePrivate::deleteTransform(&myTransform);
0289         QCOMPARE(myTransform, nullptr);
0290     }
0291 
0292     void testDeleteTransformThatIsValid()
0293     {
0294         // Initialization
0295 
0296         cmsHPROFILE myProfile = cmsCreate_sRGBProfile();
0297         QVERIFY(myProfile != nullptr); // assertion
0298         cmsHTRANSFORM myTransform = cmsCreateTransform(
0299             // Create a transform function and get a handle to this function:
0300             myProfile, // input profile handle
0301             TYPE_RGB_16, // output buffer format
0302             myProfile, // output profile handle
0303             TYPE_RGB_16, // output buffer format
0304             INTENT_ABSOLUTE_COLORIMETRIC, // rendering intent
0305             cmsFLAGS_NOCACHE // flags
0306         );
0307         QVERIFY(myTransform != nullptr); // assertion
0308 
0309         // Do the actual unit test
0310         RgbColorSpacePrivate::deleteTransform(&myTransform);
0311         QCOMPARE(myTransform, nullptr);
0312 
0313         // Clean-up
0314         if (myTransform != nullptr) {
0315             cmsDeleteTransform(myTransform);
0316         }
0317         if (myProfile != nullptr) {
0318             cmsCloseProfile(myProfile);
0319         }
0320     }
0321 
0322     void testProperties()
0323     {
0324         QScopedPointer<QTemporaryFile> wideGamutFile(
0325             // Create a temporary actual file…
0326             QTemporaryFile::createNativeFile(
0327                 // …from the content of this resource:
0328                 QStringLiteral(":/testbed/Compact-ICC-Profiles/Compact-ICC-Profiles/profiles/WideGamutCompat-v4.icc")));
0329         if (wideGamutFile.isNull()) {
0330             throw 0;
0331         }
0332 
0333         auto srgb = RgbColorSpace::createSrgb();
0334         QCOMPARE(srgb.isNull(), false); // assertion
0335         auto widegamutrgb = RgbColorSpace::createFromFile( //
0336             wideGamutFile->fileName());
0337         QCOMPARE(widegamutrgb.isNull(), false); // assertion
0338 
0339         // Start testing
0340 
0341         QCOMPARE(srgb->profileAbsoluteFilePath(), QString());
0342         QVERIFY( //
0343             widegamutrgb->profileAbsoluteFilePath().endsWith( //
0344                 wideGamutFile->fileName()));
0345 
0346         QCOMPARE(srgb->profileClass(), //
0347                  cmsProfileClassSignature::cmsSigDisplayClass);
0348         QCOMPARE(widegamutrgb->profileClass(), //
0349                  cmsProfileClassSignature::cmsSigDisplayClass);
0350 
0351         QCOMPARE(srgb->profileColorModel(), //
0352                  cmsColorSpaceSignature::cmsSigRgbData);
0353         QCOMPARE(widegamutrgb->profileColorModel(), //
0354                  cmsColorSpaceSignature::cmsSigRgbData);
0355 
0356         QCOMPARE(srgb->profileCopyright(), //
0357                  QStringLiteral("No copyright, use freely"));
0358         // No non-localized test data for widgegamutrgb
0359 
0360         QCOMPARE(srgb->profileCreationDateTime().isNull(), true);
0361         QCOMPARE(widegamutrgb->profileCreationDateTime(), //
0362                  QDateTime(QDate(2021, 04, 27), QTime(10, 27, 00), Qt::UTC));
0363 
0364         QVERIFY(srgb->profileFileSize() == -1);
0365         QCOMPARE(widegamutrgb->profileFileSize(), 464);
0366 
0367         QCOMPARE(srgb->profileHasMatrixShaper(), true);
0368 
0369         // No external test data for srgb profile
0370         QCOMPARE(widegamutrgb->profileIccVersion(), QVersionNumber(4, 2));
0371 
0372         // No external test data for srgb profile
0373         QCOMPARE(widegamutrgb->profileManufacturer(), QString());
0374 
0375         QVERIFY(isInRange<double>(0,
0376                                   widegamutrgb->profileMaximumCielchD50Chroma(), //
0377                                   CielchD50Values::maximumChroma));
0378 
0379         // The test for profileModel is missing, because
0380         // we have currently no external test data against which
0381         // we could test.
0382 
0383         // The test for profileName is missing, because
0384         // we have currently no external test data against which
0385         // we could test.
0386 
0387         // According to the ICC specification v4.4, only two color models
0388         // are allowed as PCS (for all profile classes except the device link
0389         // class):
0390         const QList validPcsModels = {cmsColorSpaceSignature::cmsSigLabData, //
0391                                       cmsColorSpaceSignature::cmsSigXYZData};
0392         // We have no reference data for the PCS color model of these
0393         // profiles. So instead, we test if it’s one of the allowed
0394         // values as described in the ICC specification.
0395         QVERIFY(validPcsModels.contains(srgb->profilePcsColorModel()));
0396         QVERIFY(validPcsModels.contains(widegamutrgb->profilePcsColorModel()));
0397     }
0398 
0399     void testProfileMaximumCielchD50Chroma()
0400     {
0401         auto temp = RgbColorSpace::createSrgb();
0402         LchDouble color;
0403 
0404         // Test if profileMaximumCielchD50Chroma is big enough
0405         qreal precisionDegreeMaxSrgbChroma = //
0406             0.1 / 360 * 2 * pi * temp->profileMaximumCielchD50Chroma();
0407         color.c = temp->profileMaximumCielchD50Chroma();
0408         qreal hue = 0;
0409         qreal lightness;
0410         qreal cielabPresicion = 0.1;
0411         while (hue <= 360) {
0412             color.h = hue;
0413             lightness = 0;
0414             while (lightness <= 100) {
0415                 color.l = lightness;
0416                 QVERIFY2(!temp->isCielchD50InGamut(color), "Test if profileMaximumCielchD50Chroma is big enough");
0417                 lightness += cielabPresicion;
0418             }
0419             hue += precisionDegreeMaxSrgbChroma;
0420         }
0421 
0422         // Test if profileMaximumCielchD50Chroma is as small as possible
0423         color.c = temp->profileMaximumCielchD50Chroma() * 0.97;
0424         bool inGamutValueFound = false;
0425         hue = 0;
0426         while (hue <= 360) {
0427             color.h = hue;
0428             lightness = 0;
0429             while (lightness <= 100) {
0430                 color.l = lightness;
0431                 if (temp->isCielchD50InGamut(color)) {
0432                     inGamutValueFound = true;
0433                     break;
0434                 }
0435                 lightness += cielabPresicion;
0436             }
0437             if (inGamutValueFound) {
0438                 break;
0439             }
0440             hue += precisionDegreeMaxSrgbChroma;
0441         }
0442         QVERIFY2(inGamutValueFound, //
0443                  "Test if profileMaximumCielchD50Chroma is as small as possible");
0444     }
0445 
0446     void testProfileMaximumOklchChroma()
0447     {
0448         auto temp = RgbColorSpace::createSrgb();
0449         LchDouble color;
0450 
0451         // Test if profileMaximumOklchChroma is big enough
0452         qreal precisionDegreeMaxSrgbChroma = //
0453             0.1 / 360 * 2 * pi * temp->profileMaximumOklchChroma() * 100;
0454         color.c = temp->profileMaximumOklchChroma();
0455         qreal hue = 0;
0456         qreal lightness;
0457         qreal oklabPrecision = 0.001;
0458         while (hue <= 360) {
0459             color.h = hue;
0460             lightness = 0;
0461             while (lightness <= 1) {
0462                 color.l = lightness;
0463                 QVERIFY2(!temp->isOklchInGamut(color), "Test if profileMaximumOklchChroma is big enough");
0464                 lightness += oklabPrecision;
0465             }
0466             hue += precisionDegreeMaxSrgbChroma;
0467         }
0468 
0469         // Test if profileMaximumOklchChroma is as small as possible
0470         color.c = temp->profileMaximumOklchChroma() * 0.97;
0471         bool inGamutValueFound = false;
0472         hue = 0;
0473         while (hue <= 360) {
0474             color.h = hue;
0475             lightness = 0;
0476             while (lightness <= 1) {
0477                 color.l = lightness;
0478                 if (temp->isOklchInGamut(color)) {
0479                     inGamutValueFound = true;
0480                     break;
0481                 }
0482                 lightness += oklabPrecision;
0483             }
0484             if (inGamutValueFound) {
0485                 break;
0486             }
0487             hue += precisionDegreeMaxSrgbChroma;
0488         }
0489         QVERIFY2(inGamutValueFound, //
0490                  "Test if profileMaximumOklchChroma is as small as possible");
0491     }
0492 
0493     void testToCielchD50Double()
0494     {
0495         QSharedPointer<PerceptualColor::RgbColorSpace> myColorSpace =
0496             // Create sRGB which is pretty much standard.
0497             PerceptualColor::RgbColorSpaceFactory::createSrgb();
0498 
0499         // Testing
0500         const QRgba64 white = QColor{255, 255, 255}.rgba64();
0501         const auto convertedWhite = myColorSpace->toCielchD50Double(white);
0502         const auto bufferWhiteL = QStringLiteral("convertedWhite.l: %1") //
0503                                       .arg(convertedWhite.l, 0, 'e') //
0504                                       .toUtf8();
0505         QVERIFY2(convertedWhite.l >= 99, bufferWhiteL.constData());
0506         QVERIFY2(convertedWhite.l <= 100, bufferWhiteL.constData());
0507         const auto bufferWhiteC = QStringLiteral("convertedWhite.c: %1") //
0508                                       .arg(convertedWhite.c, 0, 'e') //
0509                                       .toUtf8();
0510         QVERIFY2(convertedWhite.c >= -1, bufferWhiteC.constData());
0511         QVERIFY2(convertedWhite.c <= 1, bufferWhiteC.constData());
0512         // No test for hue because it would be meaningless.
0513 
0514         // Testing
0515         const QRgba64 black = QColor{0, 0, 0}.rgba64();
0516         const auto convertedBlack = myColorSpace->toCielchD50Double(black);
0517         const auto bufferBlackL = QStringLiteral("convertedBlack.l: %1") //
0518                                       .arg(convertedBlack.l, 0, 'e') //
0519                                       .toUtf8();
0520         QVERIFY2(convertedBlack.l >= 0, bufferBlackL.constData());
0521         QVERIFY2(convertedBlack.l <= 1, bufferBlackL.constData());
0522         const auto bufferBlackC = QStringLiteral("convertedBlack.c: %1") //
0523                                       .arg(convertedBlack.c, 0, 'e') //
0524                                       .toUtf8();
0525         QVERIFY2(convertedBlack.c >= -1, bufferBlackC.constData());
0526         QVERIFY2(convertedBlack.c <= 1, bufferBlackC.constData());
0527         // No test for hue because it would be meaningless.
0528     }
0529 
0530     void testToQRgbForce()
0531     {
0532         QSharedPointer<PerceptualColor::RgbColorSpace> myColorSpace =
0533             // Create sRGB which is pretty much standard.
0534             PerceptualColor::RgbColorSpaceFactory::createSrgb();
0535 
0536         // Variables
0537         LchDouble color;
0538 
0539         // In-gamut colors should work:
0540         color.l = 50;
0541         color.c = 20;
0542         color.h = 10;
0543         auto result = myColorSpace->fromCielchD50ToQRgbBound(color);
0544         QCOMPARE(qAlpha(result), 255); // opaque
0545 
0546         // Out-of-gamut colors should work:
0547         color.l = 100;
0548         color.c = 200;
0549         color.h = 10;
0550         result = myColorSpace->fromCielchD50ToQRgbBound(color);
0551         QCOMPARE(qAlpha(result), 255); // opaque
0552 
0553         // Out-of-boundary colors should work:
0554         color.l = 200;
0555         color.c = 300;
0556         color.h = 400;
0557         result = myColorSpace->fromCielchD50ToQRgbBound(color);
0558         QCOMPARE(qAlpha(result), 255); // opaque
0559     }
0560 
0561     void testIsCielchD50InGamut()
0562     {
0563         QSharedPointer<PerceptualColor::RgbColorSpace> myColorSpace =
0564             // Create sRGB which is pretty much standard.
0565             PerceptualColor::RgbColorSpaceFactory::createSrgb();
0566 
0567         // Variables
0568         LchDouble color;
0569 
0570         // In-gamut colors should work:
0571         color.l = 50;
0572         color.c = 20;
0573         color.h = 10;
0574         QCOMPARE(myColorSpace->isCielchD50InGamut(color), true);
0575 
0576         // Out-of-gamut colors should work:
0577         color.l = 100;
0578         color.c = 200;
0579         color.h = 10;
0580         QCOMPARE(myColorSpace->isCielchD50InGamut(color), false);
0581 
0582         // Out-of-boundary colors should work:
0583         color.l = 200;
0584         color.c = 300;
0585         color.h = 400;
0586         QCOMPARE(myColorSpace->isCielchD50InGamut(color), false);
0587     }
0588 
0589     void testIsOklchInGamut()
0590     {
0591         QSharedPointer<PerceptualColor::RgbColorSpace> myColorSpace =
0592             // Create sRGB which is pretty much standard.
0593             PerceptualColor::RgbColorSpaceFactory::createSrgb();
0594 
0595         // Variables
0596         LchDouble color;
0597 
0598         // In-gamut colors should work:
0599         color.l = 0.5;
0600         color.c = 0.10;
0601         color.h = 10;
0602         QCOMPARE(myColorSpace->isOklchInGamut(color), true);
0603 
0604         // Out-of-gamut colors should work:
0605         color.l = 1;
0606         color.c = 0.3;
0607         color.h = 10;
0608         QCOMPARE(myColorSpace->isOklchInGamut(color), false);
0609 
0610         // Out-of-boundary colors should work:
0611         color.l = 200;
0612         color.c = 300;
0613         color.h = 400;
0614         QCOMPARE(myColorSpace->isOklchInGamut(color), false);
0615     }
0616 
0617     void testIsCielabD50InGamut()
0618     {
0619         QSharedPointer<PerceptualColor::RgbColorSpace> myColorSpace =
0620             // Create sRGB which is pretty much standard.
0621             PerceptualColor::RgbColorSpaceFactory::createSrgb();
0622 
0623         // Variables
0624         cmsCIELab color;
0625 
0626         // In-gamut colors should work:
0627         color.L = 50;
0628         color.a = 10;
0629         color.b = 10;
0630         QCOMPARE(myColorSpace->isCielabD50InGamut(color), true);
0631 
0632         // Out-of-gamut colors should work:
0633         color.L = 100;
0634         color.a = 100;
0635         color.b = 100;
0636         QCOMPARE(myColorSpace->isCielabD50InGamut(color), false);
0637 
0638         // Out-of-boundary colors should work:
0639         color.L = 200;
0640         color.a = 300;
0641         color.b = 300;
0642         QCOMPARE(myColorSpace->isCielabD50InGamut(color), false);
0643     }
0644 
0645     void testToQRgbOrTransparent()
0646     {
0647         QSharedPointer<PerceptualColor::RgbColorSpace> myColorSpace =
0648             // Create sRGB which is pretty much standard.
0649             PerceptualColor::RgbColorSpaceFactory::createSrgb();
0650 
0651         // Variables
0652         cmsCIELab color;
0653 
0654         // In-gamut colors should work:
0655         color.L = 50;
0656         color.a = 10;
0657         color.b = 10;
0658         QVERIFY(qAlpha(myColorSpace->fromCielabD50ToQRgbOrTransparent(color)) == 255);
0659 
0660         // Out-of-gamut colors should work:
0661         color.L = 100;
0662         color.a = 100;
0663         color.b = 100;
0664         QVERIFY(qAlpha(myColorSpace->fromCielabD50ToQRgbOrTransparent(color)) == 0);
0665 
0666         // Out-of-boundary colors should work:
0667         color.L = 200;
0668         color.a = 300;
0669         color.b = 300;
0670         QVERIFY(qAlpha(myColorSpace->fromCielabD50ToQRgbOrTransparent(color)) == 0);
0671     }
0672 
0673     // The following unit tests are a little bit special. They do not
0674     // actually test the functionality of getInformationFromProfile()
0675     // but rather if its character encoding converting approach works
0676     // reliable in all situations.
0677     //
0678     // LittleCMS returns wchar_t. This type might have different sizes,
0679     // depending on the  operating system either 16 bit or 32 bit.
0680     // LittleCMS does not specify the encoding in its documentation for
0681     // cmsGetProfileInfo() as of LittleCMS 2.9. It only says “Strings are
0682     // returned as wide chars.” So this is likely either UTF-16 or UTF-32.
0683     // According to github.com/mm2/Little-CMS/issues/180#issue-421837278
0684     // it is even UTF-16 when the size of wchar_t is 32 bit. And according
0685     // to github.com/mm2/Little-CMS/issues/180#issuecomment-1007490587
0686     // in LittleCMS versions after 2.13 it might be UTF-32 when the size
0687     // of wchar_t is 32 bit. So the behaviour of LittleCMS changes between
0688     // various versions. Conclusion: It’s either UTF-16 or UTF-32, but we
0689     // never know which it is and have to be prepared for all possible
0690     // combinations between UTF-16/UTF-32 and a wchar_t size of
0691     // 16 bit/32 bit.
0692     //
0693     // The code of getInformationFromProfile() relies on
0694     // QString::fromWCharArray() to handle also these non-standard encoding
0695     // situations, which it seems to do well, but this is unfortunately
0696     // not documented.
0697     //
0698     // Those unit tests can only test the behaviour for the wchar_t size
0699     // of the system on which it’s running. But for this wchar_t size
0700     // we test it well…
0701 
0702     void testGetInformationFromProfile1()
0703     {
0704         // Test UTF-16 single-code-unit codepoint
0705         // (identical to testing UTF-32 code points below U+10000)
0706         wchar_t *buffer = new wchar_t[2]; // Allocate the buffer
0707         // Initialize the buffer an UTF-16 encoding of “✂” who’s code point
0708         // is U+2702 and who’s UTF-16 representation is 0x2702,
0709         // followed by a null character.
0710         *(buffer + 0) = 0x2702;
0711         *(buffer + 1) = 0;
0712         const QString result = QString::fromWCharArray(
0713             // Convert to string with these parameters:
0714             buffer, // read from this buffer
0715             -1 // read until the first null element
0716         );
0717         // Free allocated memory of the buffer
0718         delete[] buffer;
0719         // Test if the resulting QString has valid data:
0720         QVERIFY(result.isValidUtf16());
0721         // Test if the count of UTF-16 code units is as expected:
0722         QCOMPARE(result.size(), 1);
0723         // Test if the content is exactly 1 code point (excluding
0724         // the null termination)
0725         QCOMPARE(result.toUcs4().size(), 1);
0726         // Test if the code point is correctly recognized:
0727         QCOMPARE(result.toUcs4().at(0), 0x2702);
0728     }
0729 
0730     void testGetInformationFromProfile2()
0731     {
0732         // Test UTF-16 surrogate pair
0733         wchar_t *buffer = new wchar_t[3]; // Allocate the buffer
0734         // Initialize the buffer an UTF-16 encoding of “🖌” who’s code point
0735         // is U+1F58C and who’s UTF-16 representation is 0xD83D 0xDD8C,
0736         // followed by a null character.
0737         *(buffer + 0) = 0xD83D;
0738         *(buffer + 1) = 0xDD8C;
0739         *(buffer + 2) = 0;
0740         const QString result = QString::fromWCharArray(
0741             // Convert to string with these parameters:
0742             buffer, // read from this buffer
0743             -1 // read until the first null element
0744         );
0745         // Free allocated memory of the buffer
0746         delete[] buffer;
0747         // Test if the resulting QString has valid data:
0748         QVERIFY(result.isValidUtf16());
0749         // Test if the count of UTF-16 code units is as expected:
0750         QCOMPARE(result.size(), 2);
0751         // Test if the content is exactly 1 code point (excluding
0752         // the null termination)
0753         QCOMPARE(result.toUcs4().size(), 1);
0754         // Test if the code point is correctly recognized:
0755         QCOMPARE(result.toUcs4().at(0), 0x1F58C);
0756     }
0757 
0758     void testGetInformationFromProfile3()
0759     {
0760         if constexpr (sizeof(wchar_t) >= 4) {
0761             // This test makes only sense when wchar_t has 32 bit (4 bytes).
0762 
0763             // Test UTF-32 value beyond U+10000
0764             wchar_t *buffer = new wchar_t[2]; // Allocate the buffer
0765             // Initialize the buffer an UTF-32 encoding of “🖌” who’s code point
0766             // is U+1F58C and who’s UTF-32 representation is 0x1F58C,
0767             // followed by a null character.
0768             *(buffer + 0) = 0x1F58C;
0769             *(buffer + 1) = 0;
0770             const QString result = QString::fromWCharArray(
0771                 // Convert to string with these parameters:
0772                 buffer, // read from this buffer
0773                 -1 // read until the first null element
0774             );
0775             // Free allocated memory of the buffer
0776             delete[] buffer;
0777             // Test if the resulting QString has valid data:
0778             QVERIFY(result.isValidUtf16());
0779             // Test if the count of UTF-16 code units is as expected:
0780             QCOMPARE(result.size(), 2);
0781             // Test if the content is exactly 1 code point (excluding
0782             // the null termination)
0783             QCOMPARE(result.toUcs4().size(), 1);
0784             // Test if the code point is correctly recognized:
0785             QCOMPARE(result.toUcs4().at(0), 0x1F58C);
0786         }
0787     }
0788 };
0789 
0790 } // namespace PerceptualColor
0791 
0792 QTEST_MAIN(PerceptualColor::TestRgbColorSpace)
0793 
0794 // The following “include” is necessary because we do not use a header file:
0795 #include "testrgbcolorspace.moc"