File indexing completed on 2024-05-19 04:45:39

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 &parameters, 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 &parameters, 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"