File indexing completed on 2024-10-20 04:17:41
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"