diff --git a/third_party/blurhash/blurhash.cpp b/third_party/blurhash/blurhash.cpp
new file mode 100644
index 00000000..cd0a18a4
--- /dev/null
+++ b/third_party/blurhash/blurhash.cpp
@@ -0,0 +1,475 @@
+#include "blurhash.hpp"
+
+#include <algorithm>
+#include <array>
+#include <cassert>
+#include <cmath>
+#include <stdexcept>
+
+#ifndef M_PI
+#define M_PI 3.14159265358979323846
+#endif
+
+#ifdef DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+#include <doctest.h>
+#endif
+
+using namespace std::literals;
+
+namespace {
+constexpr std::array<char, 84> int_to_b83{
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"};
+
+std::string
+leftPad(std::string str, size_t len)
+{
+ if (str.size() >= len)
+ return str;
+ return str.insert(0, len - str.size(), '0');
+}
+
+constexpr std::array<int, 255> b83_to_int = []() constexpr
+{
+ std::array<int, 255> a{};
+
+ for (auto &e : a)
+ e = -1;
+
+ for (int i = 0; i < 83; i++) {
+ a[static_cast<unsigned char>(int_to_b83[i])] = i;
+ }
+
+ return a;
+}
+();
+
+std::string
+encode83(int value)
+{
+ std::string buffer;
+
+ do {
+ buffer += int_to_b83[value % 83];
+ } while ((value = value / 83));
+
+ std::reverse(buffer.begin(), buffer.end());
+ return buffer;
+}
+
+struct Components
+{
+ int x, y;
+};
+
+int
+packComponents(const Components &c)
+{
+ return (c.x - 1) + (c.y - 1) * 9;
+}
+
+Components
+unpackComponents(int c)
+{
+ return {c % 9 + 1, c / 9 + 1};
+}
+
+int
+decode83(std::string_view value)
+{
+ int temp = 0;
+
+ for (char c : value)
+ if (b83_to_int[static_cast<unsigned char>(c)] < 0)
+ throw std::invalid_argument("invalid character in blurhash");
+
+ for (char c : value)
+ temp = temp * 83 + b83_to_int[static_cast<unsigned char>(c)];
+ return temp;
+}
+
+float
+decodeMaxAC(int quantizedMaxAC)
+{
+ return (quantizedMaxAC + 1) / 166.;
+}
+
+float
+decodeMaxAC(std::string_view maxAC)
+{
+ assert(maxAC.size() == 1);
+ return decodeMaxAC(decode83(maxAC));
+}
+
+int
+encodeMaxAC(float maxAC)
+{
+ return std::max(0, std::min(82, int(maxAC * 166 - 0.5)));
+}
+
+float
+srgbToLinear(int value)
+{
+ auto srgbToLinearF = [](float x) {
+ if (x <= 0.0f)
+ return 0.0f;
+ else if (x >= 1.0f)
+ return 1.0f;
+ else if (x < 0.04045f)
+ return x / 12.92f;
+ else
+ return std::pow((x + 0.055f) / 1.055f, 2.4f);
+ };
+
+ return srgbToLinearF(value / 255.f);
+}
+
+int
+linearToSrgb(float value)
+{
+ auto linearToSrgbF = [](float x) -> float {
+ if (x <= 0.0f)
+ return 0.0f;
+ else if (x >= 1.0f)
+ return 1.0f;
+ else if (x < 0.0031308f)
+ return x * 12.92f;
+ else
+ return std::pow(x, 1.0f / 2.4f) * 1.055f - 0.055f;
+ };
+
+ return int(linearToSrgbF(value) * 255.f + 0.5);
+}
+
+struct Color
+{
+ float r, g, b;
+
+ Color &operator*=(float scale)
+ {
+ r *= scale;
+ g *= scale;
+ b *= scale;
+ return *this;
+ }
+ friend Color operator*(Color lhs, float rhs) { return (lhs *= rhs); }
+ Color &operator/=(float scale)
+ {
+ r /= scale;
+ g /= scale;
+ b /= scale;
+ return *this;
+ }
+ Color &operator+=(const Color &rhs)
+ {
+ r += rhs.r;
+ g += rhs.g;
+ b += rhs.b;
+ return *this;
+ }
+};
+
+Color
+decodeDC(int value)
+{
+ const int intR = value >> 16;
+ const int intG = (value >> 8) & 255;
+ const int intB = value & 255;
+ return {srgbToLinear(intR), srgbToLinear(intG), srgbToLinear(intB)};
+}
+
+Color
+decodeDC(std::string_view value)
+{
+ assert(value.size() == 4);
+ return decodeDC(decode83(value));
+}
+
+int
+encodeDC(const Color &c)
+{
+ return (linearToSrgb(c.r) << 16) + (linearToSrgb(c.g) << 8) + linearToSrgb(c.b);
+}
+
+float
+signPow(float value, float exp)
+{
+ return std::copysign(std::pow(std::abs(value), exp), value);
+}
+
+int
+encodeAC(const Color &c, float maximumValue)
+{
+ auto quantR =
+ int(std::max(0., std::min(18., std::floor(signPow(c.r / maximumValue, 0.5) * 9 + 9.5))));
+ auto quantG =
+ int(std::max(0., std::min(18., std::floor(signPow(c.g / maximumValue, 0.5) * 9 + 9.5))));
+ auto quantB =
+ int(std::max(0., std::min(18., std::floor(signPow(c.b / maximumValue, 0.5) * 9 + 9.5))));
+
+ return quantR * 19 * 19 + quantG * 19 + quantB;
+}
+
+Color
+decodeAC(int value, float maximumValue)
+{
+ auto quantR = value / (19 * 19);
+ auto quantG = (value / 19) % 19;
+ auto quantB = value % 19;
+
+ return {signPow((float(quantR) - 9) / 9, 2) * maximumValue,
+ signPow((float(quantG) - 9) / 9, 2) * maximumValue,
+ signPow((float(quantB) - 9) / 9, 2) * maximumValue};
+}
+
+Color
+decodeAC(std::string_view value, float maximumValue)
+{
+ return decodeAC(decode83(value), maximumValue);
+}
+
+Color
+multiplyBasisFunction(Components components, int width, int height, unsigned char *pixels)
+{
+ Color c{};
+ float normalisation = (components.x == 0 && components.y == 0) ? 1 : 2;
+
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ float basis = std::cos(M_PI * components.x * x / float(width)) *
+ std::cos(M_PI * components.y * y / float(height));
+ c.r += basis * srgbToLinear(pixels[3 * x + 0 + y * width * 3]);
+ c.g += basis * srgbToLinear(pixels[3 * x + 1 + y * width * 3]);
+ c.b += basis * srgbToLinear(pixels[3 * x + 2 + y * width * 3]);
+ }
+ }
+
+ float scale = normalisation / (width * height);
+ c *= scale;
+ return c;
+}
+}
+
+namespace blurhash {
+Image
+decode(std::string_view blurhash, size_t width, size_t height, size_t bytesPerPixel)
+{
+ Image i{};
+
+ if (blurhash.size() < 10)
+ return i;
+
+ Components components{};
+ std::vector<Color> values;
+ try {
+ components = unpackComponents(decode83(blurhash.substr(0, 1)));
+
+ if (components.x < 1 || components.y < 1 ||
+ blurhash.size() != size_t(1 + 1 + 4 + (components.x * components.y - 1) * 2))
+ return {};
+
+ auto maxAC = decodeMaxAC(blurhash.substr(1, 1));
+ Color average = decodeDC(blurhash.substr(2, 4));
+
+ values.push_back(average);
+ for (size_t c = 6; c < blurhash.size(); c += 2)
+ values.push_back(decodeAC(blurhash.substr(c, 2), maxAC));
+ } catch (std::invalid_argument &) {
+ return {};
+ }
+
+ i.image.reserve(height * width * 3);
+
+ for (size_t y = 0; y < height; y++) {
+ for (size_t x = 0; x < width; x++) {
+ Color c{};
+
+ for (size_t nx = 0; nx < size_t(components.x); nx++) {
+ for (size_t ny = 0; ny < size_t(components.y); ny++) {
+ float basis =
+ std::cos(M_PI * float(x) * float(nx) / float(width)) *
+ std::cos(M_PI * float(y) * float(ny) / float(height));
+ c += values[nx + ny * components.x] * basis;
+ }
+ }
+
+ i.image.push_back(static_cast<unsigned char>(linearToSrgb(c.r)));
+ i.image.push_back(static_cast<unsigned char>(linearToSrgb(c.g)));
+ i.image.push_back(static_cast<unsigned char>(linearToSrgb(c.b)));
+
+ for (size_t p = 3; p < bytesPerPixel; p++)
+ i.image.push_back(255);
+ }
+ }
+
+ i.height = height;
+ i.width = width;
+
+ return i;
+}
+
+std::string
+encode(unsigned char *image, size_t width, size_t height, int components_x, int components_y)
+{
+ if (width < 1 || height < 1 || components_x < 1 || components_x > 9 || components_y < 1 ||
+ components_y > 9 || !image)
+ return "";
+
+ std::vector<Color> factors;
+ factors.reserve(components_x * components_y);
+ for (int y = 0; y < components_y; y++) {
+ for (int x = 0; x < components_x; x++) {
+ factors.push_back(multiplyBasisFunction({x, y}, width, height, image));
+ }
+ }
+
+ assert(factors.size() > 0);
+
+ auto dc = factors.front();
+ factors.erase(factors.begin());
+
+ std::string h;
+
+ h += leftPad(encode83(packComponents({components_x, components_y})), 1);
+
+ float maximumValue;
+ if (!factors.empty()) {
+ float actualMaximumValue = 0;
+ for (auto ac : factors) {
+ actualMaximumValue = std::max({
+ std::abs(ac.r),
+ std::abs(ac.g),
+ std::abs(ac.b),
+ actualMaximumValue,
+ });
+ }
+
+ int quantisedMaximumValue = encodeMaxAC(actualMaximumValue);
+ maximumValue = ((float)quantisedMaximumValue + 1) / 166;
+ h += leftPad(encode83(quantisedMaximumValue), 1);
+ } else {
+ maximumValue = 1;
+ h += leftPad(encode83(0), 1);
+ }
+
+ h += leftPad(encode83(encodeDC(dc)), 4);
+
+ for (auto ac : factors)
+ h += leftPad(encode83(encodeAC(ac, maximumValue)), 2);
+
+ return h;
+}
+}
+
+#ifdef DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+TEST_CASE("component packing")
+{
+ for (int i = 0; i < 9 * 9; i++)
+ CHECK(packComponents(unpackComponents(i)) == i);
+}
+
+TEST_CASE("encode83")
+{
+ CHECK(encode83(0) == "0");
+
+ CHECK(encode83(packComponents({4, 3})) == "L");
+ CHECK(encode83(packComponents({4, 4})) == "U");
+ CHECK(encode83(packComponents({8, 4})) == "Y");
+ CHECK(encode83(packComponents({2, 1})) == "1");
+}
+
+TEST_CASE("decode83")
+{
+ CHECK(packComponents({4, 3}) == decode83("L"));
+ CHECK(packComponents({4, 4}) == decode83("U"));
+ CHECK(packComponents({8, 4}) == decode83("Y"));
+ CHECK(packComponents({2, 1}) == decode83("1"));
+}
+
+TEST_CASE("maxAC")
+{
+ for (int i = 0; i < 83; i++)
+ CHECK(encodeMaxAC(decodeMaxAC(i)) == i);
+
+ CHECK(std::abs(decodeMaxAC("l"sv) - 0.289157f) < 0.00001f);
+}
+
+TEST_CASE("DC")
+{
+ CHECK(encode83(encodeDC(decodeDC("MF%n"))) == "MF%n"sv);
+ CHECK(encode83(encodeDC(decodeDC("HV6n"))) == "HV6n"sv);
+ CHECK(encode83(encodeDC(decodeDC("F5]+"))) == "F5]+"sv);
+ CHECK(encode83(encodeDC(decodeDC("Pj0^"))) == "Pj0^"sv);
+ CHECK(encode83(encodeDC(decodeDC("O2?U"))) == "O2?U"sv);
+}
+
+TEST_CASE("AC")
+{
+ auto h = "00%#MwS|WCWEM{R*bbWBbH"sv;
+ for (size_t i = 0; i < h.size(); i += 2) {
+ auto s = h.substr(i, 2);
+ const auto maxAC = 0.289157f;
+ CHECK(leftPad(encode83(encodeAC(decodeAC(decode83(s), maxAC), maxAC)), 2) == s);
+ }
+}
+
+TEST_CASE("decode")
+{
+ blurhash::Image i1 = blurhash::decode("LEHV6nWB2yk8pyoJadR*.7kCMdnj", 360, 200);
+ CHECK(i1.width == 360);
+ CHECK(i1.height == 200);
+ CHECK(i1.image.size() == i1.height * i1.width * 3);
+ CHECK(i1.image[0] == 135);
+ CHECK(i1.image[1] == 164);
+ CHECK(i1.image[2] == 177);
+ CHECK(i1.image[10000] == 173);
+ CHECK(i1.image[10001] == 176);
+ CHECK(i1.image[10002] == 163);
+ // stbi_write_bmp("test.bmp", i1.width, i1.height, 3, (void *)i1.image.data());
+
+ i1 = blurhash::decode("LGF5]+Yk^6#M@-5c,1J5@[or[Q6.", 360, 200);
+ CHECK(i1.width == 360);
+ CHECK(i1.height == 200);
+ CHECK(i1.image.size() == i1.height * i1.width * 3);
+ // stbi_write_bmp("test2.bmp", i1.width, i1.height, 3, (void *)i1.image.data());
+
+ // invalid inputs
+ i1 = blurhash::decode(" LGF5]+Yk^6#M@-5c,1J5@[or[Q6.", 360, 200);
+ CHECK(i1.width == 0);
+ CHECK(i1.height == 0);
+ CHECK(i1.image.size() == 0);
+ i1 = blurhash::decode(" LGF5]+Yk^6#M@-5c,1J5@[or[Q6.", 360, 200);
+ CHECK(i1.width == 0);
+ CHECK(i1.height == 0);
+ CHECK(i1.image.size() == 0);
+
+ i1 = blurhash::decode("LGF5]+Yk^6# M@-5c,1J5@[or[Q6.", 360, 200);
+ CHECK(i1.width == 0);
+ CHECK(i1.height == 0);
+ CHECK(i1.image.size() == 0);
+ i1 = blurhash::decode("LGF5]+Yk^6# M@-5c,1J5@[or[Q6.", 360, 200);
+ CHECK(i1.width == 0);
+ CHECK(i1.height == 0);
+ CHECK(i1.image.size() == 0);
+
+ i1 = blurhash::decode("LGF5]+Yk^6# @-5c,1J5@[or[Q6.", 360, 200);
+ CHECK(i1.width == 0);
+ CHECK(i1.height == 0);
+ CHECK(i1.image.size() == 0);
+ i1 = blurhash::decode(" GF5]+Yk^6#M@-5c,1J5@[or[Q6.", 360, 200);
+ CHECK(i1.width == 0);
+ CHECK(i1.height == 0);
+ CHECK(i1.image.size() == 0);
+}
+
+TEST_CASE("encode")
+{
+ CHECK(blurhash::encode(nullptr, 360, 200, 4, 3) == "");
+
+ std::vector<unsigned char> black(360 * 200 * 3, 0);
+ CHECK(blurhash::encode(black.data(), 0, 200, 4, 3) == "");
+ CHECK(blurhash::encode(black.data(), 360, 0, 4, 3) == "");
+ CHECK(blurhash::encode(black.data(), 360, 200, 0, 3) == "");
+ CHECK(blurhash::encode(black.data(), 360, 200, 4, 0) == "");
+ CHECK(blurhash::encode(black.data(), 360, 200, 4, 3) == "L00000fQfQfQfQfQfQfQfQfQfQfQ");
+}
+#endif
|