File indexing completed on 2025-02-16 10:41:56
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 "chromahueimageparameters.h" 0007 0008 #include "asyncimagerendercallback.h" 0009 #include "helpermath.h" 0010 #include "lchdouble.h" 0011 #include "rgbcolorspacefactory.h" 0012 #include <qbenchmark.h> 0013 #include <qcolor.h> 0014 #include <qglobal.h> 0015 #include <qimage.h> 0016 #include <qobject.h> 0017 #include <qsharedpointer.h> 0018 #include <qsize.h> 0019 #include <qtest.h> 0020 #include <qtestcase.h> 0021 #include <qtestdata.h> 0022 #include <qvariant.h> 0023 #include <rgbcolorspace.h> 0024 0025 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) 0026 #include <qtmetamacros.h> 0027 #else 0028 #include <qobjectdefs.h> 0029 #include <qstring.h> 0030 #endif 0031 0032 namespace PerceptualColor 0033 { 0034 class Mockup : public AsyncImageRenderCallback 0035 { 0036 public: 0037 virtual bool shouldAbort() const override; 0038 virtual void deliverInterlacingPass(const QImage &image, const QVariant ¶meters, const InterlacingState state) override; 0039 QImage lastDeliveredImage() const; 0040 QVariant lastDeliveredParameters() const; 0041 0042 private: 0043 QImage m_lastDeliveredImage; 0044 QVariant m_lastDeliveredParameters; 0045 }; 0046 0047 bool Mockup::shouldAbort() const 0048 { 0049 return false; 0050 } 0051 0052 void Mockup::deliverInterlacingPass(const QImage &image, const QVariant ¶meters, const InterlacingState state) 0053 { 0054 Q_UNUSED(state) 0055 m_lastDeliveredImage = image; 0056 m_lastDeliveredParameters = parameters; 0057 } 0058 0059 QImage Mockup::lastDeliveredImage() const 0060 { 0061 return m_lastDeliveredImage; 0062 } 0063 0064 QVariant Mockup::lastDeliveredParameters() const 0065 { 0066 return m_lastDeliveredParameters; 0067 } 0068 0069 class TestChromaHueImageParameters : public QObject 0070 { 0071 Q_OBJECT 0072 0073 public: 0074 explicit TestChromaHueImageParameters(QObject *parent = nullptr) 0075 : QObject(parent) 0076 { 0077 } 0078 0079 private Q_SLOTS: 0080 void initTestCase() 0081 { 0082 // Called before the first test function is executed 0083 } 0084 0085 void cleanupTestCase() 0086 { 0087 // Called after the last test function was executed 0088 } 0089 0090 void init() 0091 { 0092 // Called before each test function is executed 0093 } 0094 0095 void cleanup() 0096 { 0097 // Called after every test function 0098 } 0099 0100 void testConstructorDestructor() 0101 { 0102 ChromaHueImageParameters test; 0103 } 0104 0105 void testCopyConstructorAndEqualUnequal() 0106 { 0107 ChromaHueImageParameters test; 0108 test.borderPhysical = 1; 0109 test.devicePixelRatioF = 3; 0110 test.imageSizePhysical = 4; 0111 test.lightness = 5; 0112 test.rgbColorSpace = RgbColorSpaceFactory::createSrgb(); 0113 0114 auto copy = test; 0115 0116 QCOMPARE(copy, test); 0117 QVERIFY(!(test != copy)); 0118 QVERIFY(test == copy); 0119 0120 copy.lightness = 30; 0121 0122 QVERIFY(test != copy); 0123 QVERIFY(!(test == copy)); 0124 } 0125 0126 void testImageSizeNew() 0127 { 0128 ChromaHueImageParameters testProperties; 0129 testProperties.rgbColorSpace = RgbColorSpaceFactory::createSrgb(); 0130 Mockup myMockup; 0131 0132 // Test especially small values, that might make special 0133 // problems in the algorithm (division by zero, offset by 1…) 0134 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0135 QCOMPARE(myMockup.lastDeliveredImage().size(), QSize(0, 0)); 0136 0137 testProperties.imageSizePhysical = 1; 0138 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0139 QCOMPARE(myMockup.lastDeliveredImage().size(), QSize(1, 1)); 0140 0141 testProperties.imageSizePhysical = 2; 0142 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0143 QCOMPARE(myMockup.lastDeliveredImage().size(), QSize(2, 2)); 0144 0145 testProperties.imageSizePhysical = 3; 0146 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0147 QCOMPARE(myMockup.lastDeliveredImage().size(), QSize(3, 3)); 0148 0149 testProperties.imageSizePhysical = 4; 0150 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0151 QCOMPARE(myMockup.lastDeliveredImage().size(), QSize(4, 4)); 0152 0153 testProperties.imageSizePhysical = 5; 0154 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0155 QCOMPARE(myMockup.lastDeliveredImage().size(), QSize(5, 5)); 0156 0157 // Test a normal size value 0158 testProperties.imageSizePhysical = 500; 0159 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0160 QCOMPARE(myMockup.lastDeliveredImage().size(), QSize(500, 500)); 0161 } 0162 0163 void testDevicePixelRatioF() 0164 { 0165 ChromaHueImageParameters testProperties; 0166 testProperties.rgbColorSpace = RgbColorSpaceFactory::createSrgb(); 0167 Mockup myMockup; 0168 testProperties.imageSizePhysical = 100; 0169 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0170 // Image size is as described. 0171 QCOMPARE(myMockup.lastDeliveredImage().size(), QSize(100, 100)); 0172 // Default devicePixelRatioF is 1 0173 QCOMPARE(myMockup.lastDeliveredImage().devicePixelRatio(), 1); 0174 // Testing with a (non-integer) scale factor 0175 testProperties.devicePixelRatioF = 1.5; 0176 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0177 // Image size remains unchanged. 0178 QCOMPARE(myMockup.lastDeliveredImage().size(), QSize(100, 100)); 0179 // Default devicePixelRatioF is 1.5 0180 QCOMPARE(myMockup.lastDeliveredImage().devicePixelRatio(), 1.5); 0181 } 0182 0183 void testCornerCases() 0184 { 0185 ChromaHueImageParameters testProperties; 0186 testProperties.rgbColorSpace = RgbColorSpaceFactory::createSrgb(); 0187 Mockup myMockup; 0188 // Set a non-zero image size: 0189 testProperties.imageSizePhysical = 50; 0190 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0191 QVERIFY2(!myMockup.lastDeliveredImage().isNull(), 0192 "Verify that there is no crash " 0193 "and that the returned image is not null."); 0194 testProperties.borderPhysical = -10; 0195 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0196 QVERIFY2(!myMockup.lastDeliveredImage().isNull(), 0197 "Verify that there is no crash " 0198 "and that the returned image is not null."); 0199 testProperties.borderPhysical = 10; 0200 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0201 QVERIFY2(!myMockup.lastDeliveredImage().isNull(), 0202 "Verify that there is no crash " 0203 "and that the returned image is not null."); 0204 testProperties.borderPhysical = 25; 0205 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0206 QVERIFY2(!myMockup.lastDeliveredImage().isNull(), 0207 "Verify that there is no crash " 0208 "and that the returned image is not null."); 0209 testProperties.borderPhysical = 100; 0210 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0211 QVERIFY2(!myMockup.lastDeliveredImage().isNull(), 0212 "Verify that there is no crash " 0213 "and that the returned image is not null."); 0214 testProperties.borderPhysical = 5; 0215 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0216 QVERIFY2(!myMockup.lastDeliveredImage().isNull(), 0217 "Verify that there is no crash " 0218 "and that the returned image is not null."); 0219 testProperties.lightness = -10; 0220 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0221 QVERIFY2(!myMockup.lastDeliveredImage().isNull(), 0222 "Verify that there is no crash " 0223 "and that the returned image is not null."); 0224 testProperties.lightness = 0; 0225 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0226 QVERIFY2(!myMockup.lastDeliveredImage().isNull(), 0227 "Verify that there is no crash " 0228 "and that the returned image is not null."); 0229 testProperties.lightness = 50; 0230 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0231 QVERIFY2(!myMockup.lastDeliveredImage().isNull(), 0232 "Verify that there is no crash " 0233 "and that the returned image is not null."); 0234 testProperties.lightness = 100; 0235 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0236 QVERIFY2(!myMockup.lastDeliveredImage().isNull(), 0237 "Verify that there is no crash " 0238 "and that the returned image is not null."); 0239 testProperties.lightness = 150; 0240 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0241 QVERIFY2(!myMockup.lastDeliveredImage().isNull(), 0242 "Verify that there is no crash " 0243 "and that the returned image is not null."); 0244 } 0245 0246 void testVeryBigBorder() 0247 { 0248 Mockup myMockup; 0249 constexpr int myImageSize = 51; 0250 ChromaHueImageParameters testProperties; 0251 testProperties.rgbColorSpace = RgbColorSpaceFactory::createSrgb(); 0252 // Set a non-zero image size: 0253 testProperties.imageSizePhysical = myImageSize; 0254 // Set a border that is bigger than half of the image size: 0255 testProperties.borderPhysical = myImageSize / 2 + 1; 0256 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0257 // The border is so big that the hole image should be transparent. 0258 for (int x = 0; x < myImageSize; ++x) { 0259 for (int y = 0; y < myImageSize; ++y) { 0260 QCOMPARE( // 0261 myMockup.lastDeliveredImage().pixelColor(x, y).alpha(), // 0262 0); 0263 } 0264 } 0265 } 0266 0267 void testSetLightness_data() 0268 { 0269 QTest::addColumn<qreal>("lightness"); 0270 QTest::newRow("10") << 10.; 0271 QTest::newRow("20") << 20.; 0272 QTest::newRow("30") << 30.; 0273 QTest::newRow("40") << 40.; 0274 QTest::newRow("50") << 50.; 0275 QTest::newRow("60") << 60.; 0276 QTest::newRow("70") << 70.; 0277 QTest::newRow("80") << 80.; 0278 QTest::newRow("90") << 90.; 0279 } 0280 0281 void testSetLightness() 0282 { 0283 Mockup myMockup; 0284 QFETCH(qreal, lightness); 0285 constexpr int imageSize = 20; 0286 ChromaHueImageParameters testProperties; 0287 testProperties.rgbColorSpace = RgbColorSpaceFactory::createSrgb(); 0288 // Set a non-zero image size: 0289 testProperties.imageSizePhysical = imageSize; 0290 testProperties.lightness = lightness; 0291 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0292 // Test the lightness. We are using QColor’s simple (non-color-managed) 0293 // lightness property. Therefore, we allow a tolerance up to 10%. 0294 const double gamutImageLightnessInPercent = // 0295 myMockup // 0296 .lastDeliveredImage() // 0297 .pixelColor(imageSize / 2, imageSize / 2) // 0298 .lightnessF() // 0299 * 100; 0300 constexpr double tolerance = 2; 0301 const bool lightnessIsCorrect = PerceptualColor::isInRange( // 0302 lightness - tolerance, // 0303 gamutImageLightnessInPercent, // 0304 lightness + tolerance); 0305 QVERIFY2(lightnessIsCorrect, // 0306 "Verify that the correct lightness is applied. " 0307 "(10% tolerance is allowed.)"); 0308 } 0309 0310 void testSetLightnessInvalid() 0311 { 0312 // Make sure that calling setLightness with invalid values 0313 // does not crash. 0314 ChromaHueImageParameters testProperties; 0315 testProperties.rgbColorSpace = RgbColorSpaceFactory::createSrgb(); 0316 Mockup myMockup; 0317 testProperties.imageSizePhysical = 20; // Set a non-zero image size 0318 testProperties.lightness = 0; 0319 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0320 Q_UNUSED(myMockup.lastDeliveredImage()) 0321 testProperties.lightness = 1; 0322 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0323 Q_UNUSED(myMockup.lastDeliveredImage()) 0324 testProperties.lightness = 2; 0325 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0326 Q_UNUSED(myMockup.lastDeliveredImage()) 0327 testProperties.lightness = -10; 0328 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0329 Q_UNUSED(myMockup.lastDeliveredImage()) 0330 testProperties.lightness = -1000; 0331 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0332 Q_UNUSED(myMockup.lastDeliveredImage()) 0333 testProperties.lightness = 100; 0334 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0335 Q_UNUSED(myMockup.lastDeliveredImage()) 0336 testProperties.lightness = 110; 0337 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0338 Q_UNUSED(myMockup.lastDeliveredImage()) 0339 testProperties.lightness = 250; 0340 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0341 Q_UNUSED(myMockup.lastDeliveredImage()) 0342 } 0343 0344 void testSizeBorderCombinations() 0345 { 0346 // Make sure this code does not crash. 0347 ChromaHueImageParameters testProperties; 0348 testProperties.rgbColorSpace = RgbColorSpaceFactory::createSrgb(); 0349 Mockup myMockup; 0350 // Set a non-zero image size: 0351 testProperties.imageSizePhysical = 20; 0352 // Set exactly the half of image size as border: 0353 testProperties.borderPhysical = 10; 0354 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0355 Q_UNUSED(myMockup.lastDeliveredImage()) 0356 } 0357 0358 void testDevicePixelRatioFForExtremeCases() 0359 { 0360 ChromaHueImageParameters testProperties; 0361 testProperties.rgbColorSpace = RgbColorSpaceFactory::createSrgb(); 0362 Mockup myMockup; 0363 // Testing with a (non-integer) scale factor 0364 testProperties.devicePixelRatioF = 1.5; 0365 // Test with fully transparent image (here, the border is too big 0366 // for the given image size) 0367 testProperties.imageSizePhysical = 20; 0368 testProperties.borderPhysical = 30; 0369 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0370 QCOMPARE(myMockup.lastDeliveredImage().devicePixelRatio(), 1.5); 0371 } 0372 0373 void testIfGamutIsCenteredCorrectlyOnOddSize() 0374 { 0375 ChromaHueImageParameters testProperties; 0376 testProperties.rgbColorSpace = RgbColorSpaceFactory::createSrgb(); 0377 Mockup myMockup; 0378 testProperties.borderPhysical = 0; 0379 testProperties.lightness = 50; 0380 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0381 constexpr int oddSize = 101; 0382 testProperties.imageSizePhysical = oddSize; // an odd number 0383 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0384 constexpr int positionAtCenter = (oddSize - 1) / 2; 0385 const qreal chromaAtCenter = // 0386 testProperties // 0387 .rgbColorSpace // 0388 ->toCielchD50Double( // 0389 myMockup 0390 .lastDeliveredImage() // 0391 .pixelColor(positionAtCenter, positionAtCenter) // 0392 .rgba64()) // 0393 .c; 0394 for (int x = positionAtCenter - 2; x <= positionAtCenter + 2; ++x) { 0395 for (int y = positionAtCenter - 2; y <= positionAtCenter + 2; ++y) { 0396 if ((x == positionAtCenter) && (y == positionAtCenter)) { 0397 continue; 0398 } 0399 const qreal chromaAround = // 0400 testProperties // 0401 .rgbColorSpace // 0402 ->toCielchD50Double(myMockup // 0403 .lastDeliveredImage() // 0404 .pixelColor(x, y) // 0405 .rgba64()) // 0406 .c; 0407 QVERIFY2(chromaAtCenter < chromaAround, 0408 "The chroma of the pixel at the center of the image " 0409 "is lower than the chroma of any of the pixels " 0410 "around."); 0411 } 0412 } 0413 } 0414 0415 void testIfGamutIsCenteredCorrectlyOnEvenSize() 0416 { 0417 ChromaHueImageParameters testProperties; 0418 testProperties.rgbColorSpace = RgbColorSpaceFactory::createSrgb(); 0419 Mockup myMockup; 0420 testProperties.borderPhysical = 0; 0421 testProperties.lightness = 50; 0422 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0423 constexpr int evenSize = 100; 0424 testProperties.imageSizePhysical = evenSize; // an even number 0425 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0426 constexpr int positionAtCenter2 = evenSize / 2; 0427 constexpr int positionAtCenter1 = positionAtCenter2 - 1; 0428 const qreal chromaAtCenterA = // 0429 testProperties // 0430 .rgbColorSpace // 0431 ->toCielchD50Double( // 0432 myMockup // 0433 .lastDeliveredImage() // 0434 .pixelColor(positionAtCenter1, positionAtCenter1) // 0435 .rgba64()) 0436 .c; 0437 const qreal chromaAtCenterB = // 0438 testProperties // 0439 .rgbColorSpace // 0440 ->toCielchD50Double( // 0441 myMockup // 0442 .lastDeliveredImage() // 0443 .pixelColor(positionAtCenter1, positionAtCenter2) // 0444 .rgba64()) 0445 .c; 0446 const qreal chromaAtCenterC = // 0447 testProperties // 0448 .rgbColorSpace // 0449 ->toCielchD50Double( // 0450 myMockup // 0451 .lastDeliveredImage() // 0452 .pixelColor(positionAtCenter2, positionAtCenter1) // 0453 .rgba64()) 0454 .c; 0455 const qreal chromaAtCenterD = // 0456 testProperties // 0457 .rgbColorSpace // 0458 ->toCielchD50Double( // 0459 myMockup // 0460 .lastDeliveredImage() // 0461 .pixelColor(positionAtCenter2, positionAtCenter2) // 0462 .rgba64()) 0463 .c; 0464 const qreal maximumChromaAtCenter = qMax( // 0465 qMax(chromaAtCenterA, chromaAtCenterB), // 0466 qMax(chromaAtCenterC, chromaAtCenterD) // 0467 ); 0468 for (int x = positionAtCenter1 - 2; x <= positionAtCenter2 + 2; ++x) { 0469 for (int y = positionAtCenter1 - 2; // 0470 y <= positionAtCenter2 + 2; // 0471 ++y // 0472 ) { 0473 if (isInRange(positionAtCenter1, x, positionAtCenter2) // 0474 && isInRange(positionAtCenter1, y, positionAtCenter2)) { 0475 continue; 0476 } 0477 const qreal chromaAround = // 0478 testProperties // 0479 .rgbColorSpace // 0480 ->toCielchD50Double(myMockup // 0481 .lastDeliveredImage() // 0482 .pixelColor(x, y) // 0483 .rgba64()) // 0484 .c; 0485 QVERIFY2(maximumChromaAtCenter < chromaAround, 0486 "The chroma of the pixels at the center of the image " 0487 "is lower than the chroma of any of the pixels " 0488 "around."); 0489 } 0490 } 0491 } 0492 0493 void benchmarkGetImage() 0494 { 0495 ChromaHueImageParameters testProperties; 0496 testProperties.rgbColorSpace = RgbColorSpaceFactory::createSrgb(); 0497 Mockup myMockup; 0498 testProperties.borderPhysical = 0; 0499 testProperties.lightness = 50; 0500 testProperties.imageSizePhysical = 1000; // an even number 0501 testProperties.render(QVariant::fromValue(testProperties), myMockup); 0502 QBENCHMARK { 0503 testProperties.lightness = 51; 0504 testProperties.render(QVariant::fromValue(testProperties), // 0505 myMockup); 0506 testProperties.lightness = 50; 0507 testProperties.render(QVariant::fromValue(testProperties), // 0508 myMockup); 0509 } 0510 } 0511 }; 0512 0513 } // namespace PerceptualColor 0514 0515 QTEST_MAIN(PerceptualColor::TestChromaHueImageParameters) 0516 0517 // The following “include” is necessary because we do not use a header file: 0518 #include "testchromahueimageparameters.moc"