File indexing completed on 2024-07-14 08:12:53

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 "rgbcolor.h"
0007 
0008 #include "genericcolor.h"
0009 #include <qcolor.h>
0010 #include <qglobal.h>
0011 #include <qnamespace.h>
0012 #include <qobject.h>
0013 #include <qscopedpointer.h>
0014 #include <qtest.h>
0015 #include <qtestcase.h>
0016 #include <utility>
0017 
0018 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
0019 #include <qtmetamacros.h>
0020 #else
0021 #include <qobjectdefs.h>
0022 #include <qstring.h>
0023 #endif
0024 
0025 namespace PerceptualColor
0026 {
0027 class TestRgbColor : public QObject
0028 {
0029     Q_OBJECT
0030 
0031 public:
0032     explicit TestRgbColor(QObject *parent = nullptr)
0033         : QObject(parent)
0034     {
0035     }
0036 
0037 private:
0038     template<typename T>
0039     bool isAlmostEqual(T first, T second, T tolerance = 3)
0040     {
0041         return qAbs<T>(first - second) < tolerance;
0042     }
0043 
0044 private Q_SLOTS:
0045     void initTestCase()
0046     {
0047         // Called before the first test function is executed
0048     }
0049 
0050     void cleanupTestCase()
0051     {
0052         // Called after the last test function was executed
0053     }
0054 
0055     void init()
0056     {
0057         // Called before each test function is executed
0058     }
0059 
0060     void cleanup()
0061     {
0062         // Called after every test function
0063     }
0064 
0065     void testConstructorDestructorStatic()
0066     {
0067         RgbColor myColor;
0068     }
0069 
0070     void testConstructorDestructorDynamic()
0071     {
0072         QScopedPointer<RgbColor> myPointer{new RgbColor()};
0073     }
0074 
0075     void testDefaultConstructor()
0076     {
0077         RgbColor myColor;
0078         QCOMPARE(myColor.rgbQColor.isValid(), false);
0079     }
0080 
0081     void testCopyConstructorUninitialized()
0082     {
0083         RgbColor myColor1;
0084         RgbColor myColor2(myColor1);
0085         QCOMPARE(myColor2.hsl, myColor1.hsl);
0086         QCOMPARE(myColor2.hsv, myColor1.hsv);
0087         QCOMPARE(myColor2.hwb, myColor1.hwb);
0088         QCOMPARE(myColor2.rgb255, myColor1.rgb255);
0089         QCOMPARE(myColor2.rgbQColor, myColor1.rgbQColor);
0090     }
0091 
0092     void testCopyConstructor()
0093     {
0094         RgbColor myColor1 = RgbColor::fromRgb255(GenericColor{1, 2, 3});
0095         RgbColor myColor2(myColor1);
0096         QCOMPARE(myColor2.hsl, myColor1.hsl);
0097         QCOMPARE(myColor2.hsv, myColor1.hsv);
0098         QCOMPARE(myColor2.hwb, myColor1.hwb);
0099         QCOMPARE(myColor2.rgb255, myColor1.rgb255);
0100         QCOMPARE(myColor2.rgbQColor, myColor1.rgbQColor);
0101     }
0102 
0103     void testCopyAssignmentUninitialized()
0104     {
0105         RgbColor myColor1;
0106         RgbColor myColor2;
0107         myColor2 = myColor1;
0108         QCOMPARE(myColor2.hsl, myColor1.hsl);
0109         QCOMPARE(myColor2.hsv, myColor1.hsv);
0110         QCOMPARE(myColor2.hwb, myColor1.hwb);
0111         QCOMPARE(myColor2.rgb255, myColor1.rgb255);
0112         QCOMPARE(myColor2.rgbQColor, myColor1.rgbQColor);
0113     }
0114 
0115     void testCopyAssignment()
0116     {
0117         RgbColor myColor1 = RgbColor::fromRgb255(GenericColor{4, 5, 6});
0118         RgbColor myColor2 = RgbColor::fromRgb255(GenericColor{7, 8, 9});
0119         Q_UNUSED(myColor2);
0120         myColor2 = myColor1;
0121         QCOMPARE(myColor2.hsl, myColor1.hsl);
0122         QCOMPARE(myColor2.hsv, myColor1.hsv);
0123         QCOMPARE(myColor2.hwb, myColor1.hwb);
0124         QCOMPARE(myColor2.rgb255, myColor1.rgb255);
0125         QCOMPARE(myColor2.rgbQColor, myColor1.rgbQColor);
0126     }
0127 
0128     void testMoveConstructorUninitialized()
0129     {
0130         RgbColor myReference;
0131         RgbColor myColor1 = myReference;
0132         RgbColor myColor2(std::move(myColor1));
0133         QCOMPARE(myColor2.hsl, myColor1.hsl);
0134         QCOMPARE(myColor2.hsv, myColor1.hsv);
0135         QCOMPARE(myColor2.hwb, myColor1.hwb);
0136         QCOMPARE(myColor2.rgb255, myColor1.rgb255);
0137         QCOMPARE(myColor2.rgbQColor, myColor1.rgbQColor);
0138     }
0139 
0140     void testMoveConstructor()
0141     {
0142         RgbColor myReference = RgbColor::fromRgbQColor(Qt::yellow);
0143         RgbColor myColor1 = myReference;
0144         RgbColor myColor2(std::move(myColor1));
0145         QCOMPARE(myColor2.hsl, myColor1.hsl);
0146         QCOMPARE(myColor2.hsv, myColor1.hsv);
0147         QCOMPARE(myColor2.hwb, myColor1.hwb);
0148         QCOMPARE(myColor2.rgb255, myColor1.rgb255);
0149         QCOMPARE(myColor2.rgbQColor, myColor1.rgbQColor);
0150     }
0151 
0152     void testMoveAssignmentUninitialized()
0153     {
0154         RgbColor myReference;
0155         RgbColor myColor1 = myReference;
0156         RgbColor myColor2;
0157         myColor2 = std::move(myColor1);
0158         QCOMPARE(myColor2.hsl, myColor1.hsl);
0159         QCOMPARE(myColor2.hsv, myColor1.hsv);
0160         QCOMPARE(myColor2.hwb, myColor1.hwb);
0161         QCOMPARE(myColor2.rgb255, myColor1.rgb255);
0162         QCOMPARE(myColor2.rgbQColor, myColor1.rgbQColor);
0163     }
0164 
0165     void testMoveAssignment()
0166     {
0167         RgbColor myReference = RgbColor::fromRgbQColor(Qt::red);
0168         RgbColor myColor1 = myReference;
0169         RgbColor myColor2;
0170         myColor2 = std::move(myColor1);
0171         QCOMPARE(myColor2.hsl, myColor1.hsl);
0172         QCOMPARE(myColor2.hsv, myColor1.hsv);
0173         QCOMPARE(myColor2.hwb, myColor1.hwb);
0174         QCOMPARE(myColor2.rgb255, myColor1.rgb255);
0175         QCOMPARE(myColor2.rgbQColor, myColor1.rgbQColor);
0176     }
0177 
0178     void testFromRgb()
0179     {
0180         RgbColor myColor1 = RgbColor::fromRgb255(GenericColor{113, 53, 23});
0181         QCOMPARE(myColor1.rgbQColor, QColor::fromRgb(113, 53, 23));
0182     }
0183 
0184     void testFromRgbQColor()
0185     {
0186         RgbColor myColor1 = RgbColor::fromRgbQColor(Qt::yellow);
0187         QCOMPARE(myColor1.rgbQColor, Qt::yellow);
0188     }
0189 
0190     void testRgbHue()
0191     {
0192         // The hue of the RGB-based HSV, HSL and HBW is identical.
0193         RgbColor value;
0194 
0195         value = RgbColor::fromHsl(GenericColor{150, 40, 30});
0196         QCOMPARE(value.hsl.first, 150);
0197         QCOMPARE(value.hsv.first, 150);
0198 
0199         value = RgbColor::fromHsv(GenericColor{150, 40, 30});
0200         QCOMPARE(value.hsl.first, 150);
0201         QCOMPARE(value.hsv.first, 150);
0202     }
0203 
0204     void testRgbHueOnGrayAxis()
0205     {
0206         // The hue of the RGB-based HSV, HSL and HBW is identical,
0207         // even when the value is on the gray axis.
0208         RgbColor value;
0209 
0210         value = RgbColor::fromHsl(GenericColor{150, 0, 50});
0211         QCOMPARE(value.hsl.first, 150);
0212         QCOMPARE(value.hsv.first, 150);
0213         QCOMPARE(value.hwb.first, 150);
0214 
0215         value = RgbColor::fromHsv(GenericColor{150, 0, 50});
0216         QCOMPARE(value.hsl.first, 150);
0217         QCOMPARE(value.hsv.first, 150);
0218         QCOMPARE(value.hwb.first, 150);
0219 
0220         value = RgbColor::fromHwb(GenericColor{150, 50, 50});
0221         // Sum of w and b is 100.
0222         QCOMPARE(value.hsl.first, 150);
0223         QCOMPARE(value.hsv.first, 150);
0224         QCOMPARE(value.hwb.first, 150);
0225 
0226         value = RgbColor::fromHwb(GenericColor{150, 70, 70});
0227         // Sum of w and b is more than 100.
0228         QCOMPARE(value.hsl.first, 150);
0229         QCOMPARE(value.hsv.first, 150);
0230         QCOMPARE(value.hwb.first, 150);
0231 
0232         value = RgbColor::fromRgb255(GenericColor{120, 120, 120});
0233         // An RGB value on the gray axis does not provide any information
0234         // about the hue. We can reasonably expect a standard value: 0°.
0235         QCOMPARE(value.hsl.first, 0);
0236         QCOMPARE(value.hsv.first, 0);
0237         QCOMPARE(value.hwb.first, 0);
0238     }
0239 
0240     void testHueFromRgbToLchSaturationContinuityWhite()
0241     {
0242         // LCH-hue values can be arbitrary when the color is on the gray axis.
0243         // For usability reasons, we should have nevertheless meaningful
0244         // hue values.
0245     }
0246 
0247     void testFromHsl()
0248     {
0249         const RgbColor value = RgbColor::fromHsl(GenericColor{100, 60, 30});
0250 
0251         QVERIFY(isAlmostEqual<double>(value.hsl.first, 100));
0252         QVERIFY(isAlmostEqual<double>(value.hsl.second, 60));
0253         QVERIFY(isAlmostEqual<double>(value.hsl.third, 30));
0254 
0255         QVERIFY(isAlmostEqual<double>(value.hsv.first, 100));
0256         QVERIFY(isAlmostEqual<double>(value.hsv.second, 75));
0257         QVERIFY(isAlmostEqual<double>(value.hsv.third, 48));
0258 
0259         QVERIFY(isAlmostEqual<double>(value.hwb.first, 100));
0260         QVERIFY(isAlmostEqual<double>(value.hwb.second, 12));
0261         QVERIFY(isAlmostEqual<double>(value.hwb.third, 52));
0262 
0263         QVERIFY(isAlmostEqual<double>(value.rgb255.first, 61));
0264         QVERIFY(isAlmostEqual<double>(value.rgb255.second, 122));
0265         QVERIFY(isAlmostEqual<double>(value.rgb255.third, 31));
0266     }
0267 
0268     void testFromHsv()
0269     {
0270         const RgbColor value = RgbColor::fromHsv(GenericColor{100, 60, 30});
0271 
0272         QVERIFY(isAlmostEqual<double>(value.hsl.first, 100));
0273         QVERIFY(isAlmostEqual<double>(value.hsl.second, 43));
0274         QVERIFY(isAlmostEqual<double>(value.hsl.third, 21));
0275 
0276         QVERIFY(isAlmostEqual<double>(value.hsv.first, 100));
0277         QVERIFY(isAlmostEqual<double>(value.hsv.second, 60));
0278         QVERIFY(isAlmostEqual<double>(value.hsv.third, 30));
0279 
0280         QVERIFY(isAlmostEqual<double>(value.hwb.first, 100));
0281         QVERIFY(isAlmostEqual<double>(value.hwb.second, 12));
0282         QVERIFY(isAlmostEqual<double>(value.hwb.third, 70));
0283 
0284         QVERIFY(isAlmostEqual<double>(value.rgb255.first, 45));
0285         QVERIFY(isAlmostEqual<double>(value.rgb255.second, 76));
0286         QVERIFY(isAlmostEqual<double>(value.rgb255.third, 30));
0287     }
0288 
0289     void testSaturationSynchronizationForBlackFromHsv()
0290     {
0291         // HSV-saturation and HSL-saturation are different. However, when
0292         // the color is black, changing any of these two saturation types
0293         // does not modify the color. But near to the blackpoint,
0294         // HSV-saturation and HSL-saturation behave very similar,
0295         // while they become more different the larger we get away
0296         // from the blackpoint. Therefore, it seems somewhat logical
0297         // that both are synchronized if (and only if) the color is black.
0298 
0299         RgbColor value;
0300 
0301         value = RgbColor::fromHsv(GenericColor{150, 100, 0});
0302         QVERIFY(isAlmostEqual<double>(value.hsl.second, 100));
0303 
0304         value = RgbColor::fromHsv(GenericColor{150, 60, 0});
0305         QVERIFY(isAlmostEqual<double>(value.hsl.second, 60));
0306 
0307         value = RgbColor::fromHsv(GenericColor{150, 30, 0});
0308         QVERIFY(isAlmostEqual<double>(value.hsl.second, 30));
0309 
0310         value = RgbColor::fromHsv(GenericColor{150, 0, 0});
0311         QVERIFY(isAlmostEqual<double>(value.hsl.second, 0));
0312     }
0313 
0314     void testSaturationSynchronizationForBlackFromHsl()
0315     {
0316         // HSV-saturation and HSL-saturation are different. However, when
0317         // the color is black, changing any of these two saturation types
0318         // does not modify the color. But near to the blackpoint,
0319         // HSV-saturation and HSL-saturation behave very similar,
0320         // while they become more different the larger we get away
0321         // from the blackpoint. Therefore, it seems somewhat logical
0322         // that both are synchronized if (and only if) the color is black.
0323 
0324         RgbColor value;
0325 
0326         value = RgbColor::fromHsl(GenericColor{150, 100, 0});
0327         QVERIFY(isAlmostEqual<double>(value.hsv.second, 100));
0328 
0329         value = RgbColor::fromHsl(GenericColor{150, 60, 0});
0330         QVERIFY(isAlmostEqual<double>(value.hsv.second, 60));
0331 
0332         value = RgbColor::fromHsl(GenericColor{150, 30, 0});
0333         QVERIFY(isAlmostEqual<double>(value.hsv.second, 30));
0334 
0335         value = RgbColor::fromHsl(GenericColor{150, 0, 0});
0336         QVERIFY(isAlmostEqual<double>(value.hsv.second, 0));
0337     }
0338 
0339     void testSaturationSynchronizationForBlackFromOther()
0340     {
0341         // HSV-saturation and HSL-saturation are different. However, when
0342         // the color is black, changing any of these two saturation types
0343         // does not modify the color. When converting from a color format
0344         // different from HSV and HSL, there is no information about the
0345         // saturation, so the saturation could be anything within the valid
0346         // range. But for usability, it is better that for all conversions
0347         // of black, we get always the same saturation value. Given that
0348         // for white, we want always 0% for different reasons, and that all
0349         // the gray axis between black and white has also a saturation of 0%
0350         // for both saturation types, it seems natural to use also 0% for
0351         // black, so that the whole gray axis has a uniform saturation
0352         // value.
0353 
0354         RgbColor value;
0355 
0356         constexpr int saturationOfBlackColor = 0;
0357 
0358         value = RgbColor::fromRgb255(GenericColor{0, 0, 0});
0359         QVERIFY(isAlmostEqual<double>(value.hsv.second, saturationOfBlackColor));
0360         QVERIFY(isAlmostEqual<double>(value.hsl.second, saturationOfBlackColor));
0361 
0362         value = RgbColor::fromHwb(GenericColor{320, 0, 100});
0363         QVERIFY(isAlmostEqual<double>(value.hsv.second, saturationOfBlackColor));
0364         QVERIFY(isAlmostEqual<double>(value.hsl.second, saturationOfBlackColor));
0365     }
0366 
0367     void testHslSaturationForWhite()
0368     {
0369         // For white, the HSV-saturation is necessarily 0%, while the
0370         // HSL-saturation might have any valid values (0%–100%). It is better
0371         // for usability to get in any situation always the same value for
0372         // HSL-saturation for white color. It seems natural to choose 0% as
0373         // a standard value, because this is synchronized with HSV-saturation,
0374         // and especially because the whole gray axis between black and white
0375         // has 0% as saturation anyway, so it is nice to have a uniform
0376         // saturation value for the whole gray axis (including black and
0377         // white).
0378 
0379         RgbColor value;
0380 
0381         constexpr int saturationOfWhiteColor = 0;
0382 
0383         value = RgbColor::fromHsl(GenericColor{320, 50, 100});
0384         // Expect a non-standard value because original values
0385         // should never be changed.
0386         QVERIFY(isAlmostEqual<double>(value.hsl.second, 50));
0387 
0388         // All other original color formats should give the standard
0389         // HSL-saturation for white:
0390 
0391         value = RgbColor::fromRgb255(GenericColor{255, 255, 255});
0392         // Expect a non-standard value because original values
0393         // should never be changed.
0394         QVERIFY(isAlmostEqual<double>(value.hsl.second, saturationOfWhiteColor));
0395 
0396         value = RgbColor::fromHsv(GenericColor{320, 0, 100});
0397         // Expect a non-standard value because original values
0398         // should never be changed.
0399         QVERIFY(isAlmostEqual<double>(value.hsl.second, saturationOfWhiteColor));
0400 
0401         value = RgbColor::fromHwb(GenericColor{320, 100, 0});
0402         // Expect a non-standard value because original values
0403         // should never be changed.
0404         QVERIFY(isAlmostEqual<double>(value.hsl.second, saturationOfWhiteColor));
0405     }
0406 
0407     void testFromHwb()
0408     {
0409         const RgbColor value = RgbColor::fromHwb(GenericColor{100, 60, 30});
0410 
0411         QVERIFY(isAlmostEqual<double>(value.hsl.first, 100));
0412         QVERIFY(isAlmostEqual<double>(value.hsl.second, 14));
0413         QVERIFY(isAlmostEqual<double>(value.hsl.third, 65)); //
0414 
0415         QVERIFY(isAlmostEqual<double>(value.hsv.first, 100));
0416         QVERIFY(isAlmostEqual<double>(value.hsv.second, 15));
0417         QVERIFY(isAlmostEqual<double>(value.hsv.third, 70)); //
0418 
0419         QVERIFY(isAlmostEqual<double>(value.hwb.first, 100));
0420         QVERIFY(isAlmostEqual<double>(value.hwb.second, 60));
0421         QVERIFY(isAlmostEqual<double>(value.hwb.third, 30)); //
0422 
0423         QVERIFY(isAlmostEqual<double>(value.rgb255.first, 162));
0424         QVERIFY(isAlmostEqual<double>(value.rgb255.second, 179));
0425         QVERIFY(isAlmostEqual<double>(value.rgb255.third, 153)); //
0426     }
0427 
0428     void testFromHwbDenormalized()
0429     {
0430         const RgbColor value = RgbColor::fromHwb(GenericColor{100, 70, 70});
0431         // The sum of w and b is greater than 100. This is denormalized.
0432 
0433         QVERIFY(isAlmostEqual<double>(value.hsl.first, 100));
0434         QVERIFY(isAlmostEqual<double>(value.hsl.second, 0));
0435         QVERIFY(isAlmostEqual<double>(value.hsl.third, 50)); //
0436 
0437         QVERIFY(isAlmostEqual<double>(value.hsv.first, 100));
0438         QVERIFY(isAlmostEqual<double>(value.hsv.second, 0));
0439         QVERIFY(isAlmostEqual<double>(value.hsv.third, 50)); //
0440 
0441         QVERIFY(isAlmostEqual<double>(value.hwb.first, 100));
0442         QVERIFY(isAlmostEqual<double>(value.hwb.second, 70));
0443         QVERIFY(isAlmostEqual<double>(value.hwb.third, 70)); //
0444 
0445         QVERIFY(isAlmostEqual<double>(value.rgb255.first, 128));
0446         QVERIFY(isAlmostEqual<double>(value.rgb255.second, 128));
0447         QVERIFY(isAlmostEqual<double>(value.rgb255.third, 128)); //
0448     }
0449 
0450     void testEquality()
0451     {
0452         RgbColor myColor1 = RgbColor::fromRgb255(GenericColor{1, 2, 3});
0453         RgbColor myColor2 = RgbColor::fromRgb255(GenericColor{1, 2, 3});
0454         QVERIFY(myColor1 == myColor2);
0455         myColor2.rgb255.first += 1;
0456         QVERIFY(!(myColor1 == myColor2));
0457     }
0458 };
0459 
0460 } // namespace PerceptualColor
0461 
0462 QTEST_MAIN(PerceptualColor::TestRgbColor)
0463 
0464 // The following “include” is necessary because we do not use a header file:
0465 #include "testrgbcolor.moc"