mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-24 20:18:53 +03:00
Initial decoder
This commit is contained in:
commit
12e2d26a60
7 changed files with 2094 additions and 0 deletions
14
.clang-format
Normal file
14
.clang-format
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
Language: Cpp
|
||||
Standard: Cpp11
|
||||
AccessModifierOffset: -8
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveAssignments: true
|
||||
AllowShortFunctionsOnASingleLine: true
|
||||
BasedOnStyle: Mozilla
|
||||
ColumnLimit: 100
|
||||
IndentCaseLabels: false
|
||||
IndentWidth: 8
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
PointerAlignment: Right
|
||||
Cpp11BracedListStyle: true
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
*.swp
|
||||
.exrc
|
||||
|
||||
build*/
|
||||
*.bmp
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "subprojects/doctest"]
|
||||
path = subprojects/doctest
|
||||
url = https://github.com/onqtam/doctest.git
|
389
blurhash.cpp
Normal file
389
blurhash.cpp
Normal file
|
@ -0,0 +1,389 @@
|
|||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
#ifdef DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#include "stb_image_write.h"
|
||||
#include <doctest.h>
|
||||
#endif
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
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 int(std::max(0., std::min(82., std::floor(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;
|
||||
}
|
||||
friend Color operator/(Color lhs, float rhs) { return (lhs /= rhs); }
|
||||
Color &operator+=(const Color &rhs)
|
||||
{
|
||||
r += rhs.r;
|
||||
g += rhs.g;
|
||||
b += rhs.b;
|
||||
return *this;
|
||||
}
|
||||
friend Color operator+(Color lhs, const Color &rhs) { return (lhs += rhs); }
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
namespace blurhash {
|
||||
struct Image
|
||||
{
|
||||
size_t width, height;
|
||||
std::vector<unsigned char> image; // pixels rgb
|
||||
};
|
||||
|
||||
Image
|
||||
decode(std::string_view blurhash, size_t width, size_t height)
|
||||
{
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
i.height = height;
|
||||
i.width = width;
|
||||
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
#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);
|
||||
}
|
||||
#endif
|
16
meson.build
Normal file
16
meson.build
Normal file
|
@ -0,0 +1,16 @@
|
|||
project('blurhash', 'cpp',
|
||||
version : '0.1',
|
||||
default_options : ['warning_level=3', 'cpp_std=c++17'])
|
||||
|
||||
static_library('blurhash',
|
||||
'blurhash.cpp',
|
||||
install : true)
|
||||
|
||||
doctest_proj = subproject('doctest')
|
||||
doctest_dep = doctest_proj.get_variable('doctest_dep')
|
||||
tests = executable('blurhash-tests',
|
||||
'blurhash.cpp',
|
||||
cpp_args : '-DDOCTEST_CONFIG_IMPLEMENT_WITH_MAIN',
|
||||
dependencies: doctest_dep,
|
||||
install : false)
|
||||
test('blurhash-tests', tests)
|
1666
stb_image_write.h
Normal file
1666
stb_image_write.h
Normal file
File diff suppressed because it is too large
Load diff
1
subprojects/doctest
Submodule
1
subprojects/doctest
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit a82821424d4826bfa891b4afef6767f447012774
|
Loading…
Reference in a new issue