|
| 1 | +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. |
| 2 | +// |
| 3 | +// SPDX-License-Identifier: GPL-3.0-or-later |
| 4 | + |
| 5 | +#include <QTest> |
| 6 | + |
| 7 | +#include "blurhash.hpp" |
| 8 | + |
| 9 | +#include <vector> |
| 10 | + |
| 11 | +namespace { |
| 12 | + |
| 13 | +std::vector<unsigned char> solidImage(size_t width, size_t height, |
| 14 | + unsigned char r, unsigned char g, unsigned char b) |
| 15 | +{ |
| 16 | + std::vector<unsigned char> image(width * height * 3, 0); |
| 17 | + for (size_t i = 0; i < width * height; ++i) { |
| 18 | + image[i * 3 + 0] = r; |
| 19 | + image[i * 3 + 1] = g; |
| 20 | + image[i * 3 + 2] = b; |
| 21 | + } |
| 22 | + return image; |
| 23 | +} |
| 24 | + |
| 25 | +// blurhash layout: 1 (components) + 1 (maxAC) + 4 (DC) + 2 bytes per AC component |
| 26 | +size_t expectedHashLength(int cx, int cy) |
| 27 | +{ |
| 28 | + return size_t(1 + 1 + 4 + (cx * cy - 1) * 2); |
| 29 | +} |
| 30 | +} |
| 31 | + |
| 32 | +class TestBlurhash : public QObject |
| 33 | +{ |
| 34 | + Q_OBJECT |
| 35 | +private slots: |
| 36 | + void decodeEmptyStringReturnsEmpty(); |
| 37 | + void decodeInvalidHashReturnsEmpty(); |
| 38 | + void encodeRejectsInvalidInput(); |
| 39 | + void encodedHashHasExpectedLength(); |
| 40 | + void decodedImageHasExpectedDimensions(); |
| 41 | + void roundTripPreservesAverageColor(); |
| 42 | + void decodeWithFourBytesPerPixel(); |
| 43 | + void decodeTooShortHashReturnsEmpty(); |
| 44 | + void decodeWrongSizeForComponentsReturnsEmpty(); |
| 45 | +}; |
| 46 | + |
| 47 | +void TestBlurhash::decodeEmptyStringReturnsEmpty() |
| 48 | +{ |
| 49 | + const auto img = blurhash::decode(std::string_view{}, 8, 8); |
| 50 | + QVERIFY(img.image.empty()); |
| 51 | + QCOMPARE(img.width, size_t(0)); |
| 52 | + QCOMPARE(img.height, size_t(0)); |
| 53 | +} |
| 54 | + |
| 55 | +void TestBlurhash::decodeInvalidHashReturnsEmpty() |
| 56 | +{ |
| 57 | + const auto img = blurhash::decode(std::string_view("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"), 8, 8); |
| 58 | + QVERIFY(img.image.empty()); |
| 59 | +} |
| 60 | + |
| 61 | +void TestBlurhash::encodeRejectsInvalidInput() |
| 62 | +{ |
| 63 | + std::vector<unsigned char> image = solidImage(4, 4, 255, 0, 0); |
| 64 | + QVERIFY(blurhash::encode(image.data(), 0, 4, 1, 1).empty()); // width 0 |
| 65 | + QVERIFY(blurhash::encode(nullptr, 4, 4, 1, 1).empty()); // null image |
| 66 | + QVERIFY(blurhash::encode(image.data(), 4, 4, 0, 1).empty()); // components_x < 1 |
| 67 | + QVERIFY(blurhash::encode(image.data(), 4, 4, 1, 10).empty()); // components_y > 9 |
| 68 | +} |
| 69 | + |
| 70 | +void TestBlurhash::encodedHashHasExpectedLength() |
| 71 | +{ |
| 72 | + std::vector<unsigned char> image = solidImage(8, 8, 128, 64, 200); |
| 73 | + QCOMPARE(blurhash::encode(image.data(), 8, 8, 1, 1).size(), size_t(6)); // no AC components |
| 74 | + QCOMPARE(blurhash::encode(image.data(), 8, 8, 1, 3).size(), expectedHashLength(1, 3)); // 10 |
| 75 | + QCOMPARE(blurhash::encode(image.data(), 8, 8, 2, 2).size(), expectedHashLength(2, 2)); // 12 |
| 76 | + QCOMPARE(blurhash::encode(image.data(), 8, 8, 4, 4).size(), expectedHashLength(4, 4)); // 36 |
| 77 | +} |
| 78 | + |
| 79 | +void TestBlurhash::decodedImageHasExpectedDimensions() |
| 80 | +{ |
| 81 | + constexpr size_t width = 16, height = 16; |
| 82 | + std::vector<unsigned char> image = solidImage(width, height, 128, 64, 200); |
| 83 | + const std::string hash = blurhash::encode(image.data(), width, height, 2, 2); |
| 84 | + QVERIFY(!hash.empty()); |
| 85 | + |
| 86 | + const auto decoded = blurhash::decode(hash, width, height); |
| 87 | + QCOMPARE(decoded.width, width); |
| 88 | + QCOMPARE(decoded.height, height); |
| 89 | + QCOMPARE(decoded.image.size(), width * height * 3); |
| 90 | +} |
| 91 | + |
| 92 | +void TestBlurhash::roundTripPreservesAverageColor() |
| 93 | +{ |
| 94 | + constexpr size_t width = 16, height = 16; |
| 95 | + const unsigned char r = 128, g = 64, b = 200; |
| 96 | + std::vector<unsigned char> image = solidImage(width, height, r, g, b); |
| 97 | + |
| 98 | + const std::string hash = blurhash::encode(image.data(), width, height, 3, 3); |
| 99 | + QVERIFY(!hash.empty()); |
| 100 | + |
| 101 | + const auto decoded = blurhash::decode(hash, width, height); |
| 102 | + QCOMPARE(decoded.image.size(), width * height * 3); |
| 103 | + |
| 104 | + long rs = 0, gs = 0, bs = 0; |
| 105 | + for (size_t i = 0; i < width * height; ++i) { |
| 106 | + rs += decoded.image[i * 3 + 0]; |
| 107 | + gs += decoded.image[i * 3 + 1]; |
| 108 | + bs += decoded.image[i * 3 + 2]; |
| 109 | + } |
| 110 | + const long n = long(width * height); |
| 111 | + const long avgR = rs / n, avgG = gs / n, avgB = bs / n; |
| 112 | + // blurhash is lossy per-pixel, but the average (DC) stays close to the source color |
| 113 | + QVERIFY(avgR >= long(r) - 15 && avgR <= long(r) + 15); |
| 114 | + QVERIFY(avgG >= long(g) - 15 && avgG <= long(g) + 15); |
| 115 | + QVERIFY(avgB >= long(b) - 15 && avgB <= long(b) + 15); |
| 116 | +} |
| 117 | + |
| 118 | +void TestBlurhash::decodeWithFourBytesPerPixel() |
| 119 | +{ |
| 120 | + constexpr size_t width = 16, height = 16; |
| 121 | + std::vector<unsigned char> image = solidImage(width, height, 128, 64, 200); |
| 122 | + const std::string hash = blurhash::encode(image.data(), width, height, 3, 3); |
| 123 | + QVERIFY(!hash.empty()); |
| 124 | + |
| 125 | + const auto decoded = blurhash::decode(hash, width, height, 4); |
| 126 | + QCOMPARE(decoded.width, width); |
| 127 | + QCOMPARE(decoded.height, height); |
| 128 | + // buffer should be width * height * 4 (not 3) |
| 129 | + QCOMPARE(decoded.image.size(), width * height * 4); |
| 130 | + // first 3 bytes of each pixel should still be the color channels |
| 131 | + QVERIFY(decoded.image[0] != 255 || decoded.image[1] != 255 || decoded.image[2] != 255); |
| 132 | +} |
| 133 | + |
| 134 | +void TestBlurhash::decodeTooShortHashReturnsEmpty() |
| 135 | +{ |
| 136 | + // hash must be >= 10 chars (1 + 1 + 4 + at least 4 for 2x2 components - 1 AC = 2 bytes -> 8, but 10 is the minimum check) |
| 137 | + const auto img = blurhash::decode(std::string_view("short"), 8, 8); |
| 138 | + QVERIFY(img.image.empty()); |
| 139 | +} |
| 140 | + |
| 141 | +void TestBlurhash::decodeWrongSizeForComponentsReturnsEmpty() |
| 142 | +{ |
| 143 | + // 1 component char says 4x4 = 16 components -> needs 1+1+4+(16-1)*2 = 36 chars |
| 144 | + // but we'll give only 12 chars |
| 145 | + const auto img = blurhash::decode(std::string_view("LFE.}?a]a]a]a]a]a]a]"), 8, 8); |
| 146 | + // this hash has wrong length for its declared components -> should return empty |
| 147 | + QVERIFY(img.image.empty()); |
| 148 | +} |
| 149 | + |
| 150 | +QTEST_MAIN(TestBlurhash) |
| 151 | +#include "blurhashtest.moc" |
0 commit comments