Experimental blurhash implementation (MXC2448)

This commit is contained in:
Nicolas Werner 2020-03-01 19:55:43 +01:00
parent 328a3c7ebd
commit 0fc98b2692
15 changed files with 696 additions and 47 deletions

View file

@ -275,37 +275,40 @@ set(SRC_FILES
src/ui/ThemeManager.cpp src/ui/ThemeManager.cpp
src/AvatarProvider.cpp src/AvatarProvider.cpp
src/BlurhashProvider.cpp
src/Cache.cpp src/Cache.cpp
src/ChatPage.cpp src/ChatPage.cpp
src/CommunitiesListItem.cpp src/ColorImageProvider.cpp
src/CommunitiesList.cpp src/CommunitiesList.cpp
src/CommunitiesListItem.cpp
src/EventAccessors.cpp src/EventAccessors.cpp
src/InviteeItem.cpp src/InviteeItem.cpp
src/LoginPage.cpp
src/Logging.cpp src/Logging.cpp
src/LoginPage.cpp
src/MainWindow.cpp src/MainWindow.cpp
src/MatrixClient.cpp src/MatrixClient.cpp
src/MxcImageProvider.cpp src/MxcImageProvider.cpp
src/ColorImageProvider.cpp
src/QuickSwitcher.cpp
src/Olm.cpp src/Olm.cpp
src/QuickSwitcher.cpp
src/RegisterPage.cpp src/RegisterPage.cpp
src/RoomInfoListItem.cpp src/RoomInfoListItem.cpp
src/RoomList.cpp src/RoomList.cpp
src/SideBarActions.cpp src/SideBarActions.cpp
src/Splitter.cpp src/Splitter.cpp
src/popups/SuggestionsPopup.cpp
src/popups/PopupItem.cpp
src/popups/ReplyPopup.cpp
src/popups/UserMentions.cpp
src/TextInputWidget.cpp src/TextInputWidget.cpp
src/TopRoomBar.cpp src/TopRoomBar.cpp
src/TrayIcon.cpp src/TrayIcon.cpp
src/Utils.cpp
src/UserInfoWidget.cpp src/UserInfoWidget.cpp
src/UserSettingsPage.cpp src/UserSettingsPage.cpp
src/Utils.cpp
src/WelcomePage.cpp src/WelcomePage.cpp
src/popups/PopupItem.cpp
src/popups/ReplyPopup.cpp
src/popups/SuggestionsPopup.cpp
src/popups/UserMentions.cpp
src/main.cpp src/main.cpp
third_party/blurhash/blurhash.cpp
) )
@ -333,7 +336,7 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare( FetchContent_Declare(
MatrixClient MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
GIT_TAG 7fc1d357afaabb134cb6d9c593f94915973d31fa GIT_TAG c1ccd6c6cdaead3ff1c2bf336b719ca45fee2d33
) )
FetchContent_MakeAvailable(MatrixClient) FetchContent_MakeAvailable(MatrixClient)
else() else()
@ -478,28 +481,28 @@ qt5_wrap_cpp(MOC_HEADERS
src/AvatarProvider.h src/AvatarProvider.h
src/Cache_p.h src/Cache_p.h
src/ChatPage.h src/ChatPage.h
src/CommunitiesListItem.h
src/CommunitiesList.h src/CommunitiesList.h
src/CommunitiesListItem.h
src/InviteeItem.h
src/LoginPage.h src/LoginPage.h
src/MainWindow.h src/MainWindow.h
src/MxcImageProvider.h src/MxcImageProvider.h
src/InviteeItem.h
src/QuickSwitcher.h src/QuickSwitcher.h
src/RegisterPage.h src/RegisterPage.h
src/RoomInfoListItem.h src/RoomInfoListItem.h
src/RoomList.h src/RoomList.h
src/SideBarActions.h src/SideBarActions.h
src/Splitter.h src/Splitter.h
src/popups/SuggestionsPopup.h
src/popups/ReplyPopup.h
src/popups/PopupItem.h
src/popups/UserMentions.h
src/TextInputWidget.h src/TextInputWidget.h
src/TopRoomBar.h src/TopRoomBar.h
src/TrayIcon.h src/TrayIcon.h
src/UserInfoWidget.h src/UserInfoWidget.h
src/UserSettingsPage.h src/UserSettingsPage.h
src/WelcomePage.h src/WelcomePage.h
src/popups/PopupItem.h
src/popups/ReplyPopup.h
src/popups/SuggestionsPopup.h
src/popups/UserMentions.h
) )
# #
@ -547,7 +550,7 @@ elseif(WIN32)
else() else()
target_link_libraries (nheko PRIVATE Qt5::DBus) target_link_libraries (nheko PRIVATE Qt5::DBus)
endif() endif()
target_include_directories(nheko PRIVATE src includes) target_include_directories(nheko PRIVATE src includes third_party/blurhash)
target_link_libraries(nheko PRIVATE target_link_libraries(nheko PRIVATE
MatrixClient::MatrixClient MatrixClient::MatrixClient

View file

@ -11,6 +11,20 @@ Item {
height: tooHigh ? timelineRoot.height / 2 : tempHeight height: tooHigh ? timelineRoot.height / 2 : tempHeight
width: tooHigh ? (timelineRoot.height / 2) / model.data.proportionalHeight : tempWidth width: tooHigh ? (timelineRoot.height / 2) / model.data.proportionalHeight : tempWidth
Image {
id: blurhash
anchors.fill: parent
visible: img.status != Image.Ready
source: model.data.blurhash ? ("image://blurhash/" + model.data.blurhash) : ("image://colorimage/:/icons/icons/ui/do-not-disturb-rounded-sign@2x.png?"+colors.buttonText)
asynchronous: true
fillMode: Image.PreserveAspectFit
sourceSize.width: parent.width
sourceSize.height: parent.height
}
Image { Image {
id: img id: img
anchors.fill: parent anchors.fill: parent

42
src/BlurhashProvider.cpp Normal file
View file

@ -0,0 +1,42 @@
#include "BlurhashProvider.h"
#include <algorithm>
#include <QUrl>
#include "blurhash.hpp"
QImage
BlurhashProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize)
{
QSize sz = requestedSize;
if (sz.width() < 1 || sz.height() < 1)
return QImage();
if (size)
*size = sz;
auto decoded = blurhash::decode(
QUrl::fromPercentEncoding(id.toUtf8()).toStdString(), sz.width(), sz.height());
if (decoded.image.empty()) {
*size = QSize();
return QImage();
}
QImage image(sz, QImage::Format_RGB888);
for (int y = 0; y < sz.height(); y++) {
for (int x = 0; x < sz.width(); x++) {
int base = (y * sz.width() + x) * 3;
image.setPixel(x,
y,
qRgb(decoded.image[base],
decoded.image[base + 1],
decoded.image[base + 2]));
}
}
// std::copy(decoded.image.begin(), decoded.image.end(), image.bits());
return image;
}

11
src/BlurhashProvider.h Normal file
View file

@ -0,0 +1,11 @@
#include <QQuickImageProvider>
class BlurhashProvider : public QQuickImageProvider
{
public:
BlurhashProvider()
: QQuickImageProvider(QQuickImageProvider::Image)
{}
QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override;
};

View file

@ -47,6 +47,8 @@
#include "popups/UserMentions.h" #include "popups/UserMentions.h"
#include "timeline/TimelineViewManager.h" #include "timeline/TimelineViewManager.h"
#include "blurhash.hpp"
// TODO: Needs to be updated with an actual secret. // TODO: Needs to be updated with an actual secret.
static const std::string STORAGE_SECRET_KEY("secret"); static const std::string STORAGE_SECRET_KEY("secret");
@ -324,9 +326,25 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
} }
QSize dimensions; QSize dimensions;
if (mimeClass == "image") QString blurhash;
if (mimeClass == "image") {
dimensions = QImageReader(dev.data()).size(); dimensions = QImageReader(dev.data()).size();
QImage img;
img.loadFromData(bin);
std::vector<unsigned char> data;
for (int y = 0; y < img.height(); y++) {
for (int x = 0; x < img.width(); x++) {
auto p = img.pixel(x, y);
data.push_back(static_cast<unsigned char>(qRed(p)));
data.push_back(static_cast<unsigned char>(qGreen(p)));
data.push_back(static_cast<unsigned char>(qBlue(p)));
}
}
blurhash = QString::fromStdString(
blurhash::encode(data.data(), img.width(), img.height(), 4, 3));
}
http::client()->upload( http::client()->upload(
payload, payload,
encryptedFile ? "application/octet-stream" : mime.name().toStdString(), encryptedFile ? "application/octet-stream" : mime.name().toStdString(),
@ -339,6 +357,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
mime = mime.name(), mime = mime.name(),
size = payload.size(), size = payload.size(),
dimensions, dimensions,
blurhash,
related](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) { related](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) {
if (err) { if (err) {
emit uploadFailed( emit uploadFailed(
@ -358,6 +377,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
mime, mime,
size, size,
dimensions, dimensions,
blurhash,
related); related);
}); });
}); });
@ -366,8 +386,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
text_input_->hideUploadSpinner(); text_input_->hideUploadSpinner();
emit showNotification(msg); emit showNotification(msg);
}); });
connect( connect(this,
this,
&ChatPage::mediaUploaded, &ChatPage::mediaUploaded,
this, this,
[this](QString roomid, [this](QString roomid,
@ -378,6 +397,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
QString mime, QString mime,
qint64 dsize, qint64 dsize,
QSize dimensions, QSize dimensions,
QString blurhash,
const std::optional<RelatedInfo> &related) { const std::optional<RelatedInfo> &related) {
text_input_->hideUploadSpinner(); text_input_->hideUploadSpinner();
@ -385,8 +405,15 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
encryptedFile->url = url.toStdString(); encryptedFile->url = url.toStdString();
if (mimeClass == "image") if (mimeClass == "image")
view_manager_->queueImageMessage( view_manager_->queueImageMessage(roomid,
roomid, filename, encryptedFile, url, mime, dsize, dimensions, related); filename,
encryptedFile,
url,
mime,
dsize,
dimensions,
blurhash,
related);
else if (mimeClass == "audio") else if (mimeClass == "audio")
view_manager_->queueAudioMessage( view_manager_->queueAudioMessage(
roomid, filename, encryptedFile, url, mime, dsize, related); roomid, filename, encryptedFile, url, mime, dsize, related);

View file

@ -114,6 +114,7 @@ signals:
const QString &mime, const QString &mime,
qint64 dsize, qint64 dsize,
const QSize &dimensions, const QSize &dimensions,
const QString &blurhash,
const std::optional<RelatedInfo> &related); const std::optional<RelatedInfo> &related);
void contentLoaded(); void contentLoaded();

View file

@ -134,6 +134,20 @@ struct EventThumbnailUrl
} }
}; };
struct EventBlurhash
{
template<class Content>
using blurhash_t = decltype(Content::info.blurhash);
template<class T>
std::string operator()(const mtx::events::Event<T> &e)
{
if constexpr (is_detected<blurhash_t, T>::value) {
return e.content.info.blurhash;
}
return "";
}
};
struct EventFilename struct EventFilename
{ {
template<class T> template<class T>
@ -348,6 +362,11 @@ mtx::accessors::thumbnail_url(const mtx::events::collections::TimelineEvents &ev
return std::visit(EventThumbnailUrl{}, event); return std::visit(EventThumbnailUrl{}, event);
} }
std::string std::string
mtx::accessors::blurhash(const mtx::events::collections::TimelineEvents &event)
{
return std::visit(EventBlurhash{}, event);
}
std::string
mtx::accessors::mimetype(const mtx::events::collections::TimelineEvents &event) mtx::accessors::mimetype(const mtx::events::collections::TimelineEvents &event)
{ {
return std::visit(EventMimeType{}, event); return std::visit(EventMimeType{}, event);

View file

@ -47,6 +47,8 @@ url(const mtx::events::collections::TimelineEvents &event);
std::string std::string
thumbnail_url(const mtx::events::collections::TimelineEvents &event); thumbnail_url(const mtx::events::collections::TimelineEvents &event);
std::string std::string
blurhash(const mtx::events::collections::TimelineEvents &event);
std::string
mimetype(const mtx::events::collections::TimelineEvents &event); mimetype(const mtx::events::collections::TimelineEvents &event);
std::string std::string
in_reply_to_event(const mtx::events::collections::TimelineEvents &event); in_reply_to_event(const mtx::events::collections::TimelineEvents &event);

View file

@ -212,6 +212,7 @@ TimelineModel::roleNames() const
{Timestamp, "timestamp"}, {Timestamp, "timestamp"},
{Url, "url"}, {Url, "url"},
{ThumbnailUrl, "thumbnailUrl"}, {ThumbnailUrl, "thumbnailUrl"},
{Blurhash, "blurhash"},
{Filename, "filename"}, {Filename, "filename"},
{Filesize, "filesize"}, {Filesize, "filesize"},
{MimeType, "mimetype"}, {MimeType, "mimetype"},
@ -296,6 +297,8 @@ TimelineModel::data(const QString &id, int role) const
return QVariant(QString::fromStdString(url(event))); return QVariant(QString::fromStdString(url(event)));
case ThumbnailUrl: case ThumbnailUrl:
return QVariant(QString::fromStdString(thumbnail_url(event))); return QVariant(QString::fromStdString(thumbnail_url(event)));
case Blurhash:
return QVariant(QString::fromStdString(blurhash(event)));
case Filename: case Filename:
return QVariant(QString::fromStdString(filename(event))); return QVariant(QString::fromStdString(filename(event)));
case Filesize: case Filesize:
@ -353,6 +356,7 @@ TimelineModel::data(const QString &id, int role) const
m.insert(names[Timestamp], data(id, static_cast<int>(Timestamp))); m.insert(names[Timestamp], data(id, static_cast<int>(Timestamp)));
m.insert(names[Url], data(id, static_cast<int>(Url))); m.insert(names[Url], data(id, static_cast<int>(Url)));
m.insert(names[ThumbnailUrl], data(id, static_cast<int>(ThumbnailUrl))); m.insert(names[ThumbnailUrl], data(id, static_cast<int>(ThumbnailUrl)));
m.insert(names[Blurhash], data(id, static_cast<int>(Blurhash)));
m.insert(names[Filename], data(id, static_cast<int>(Filename))); m.insert(names[Filename], data(id, static_cast<int>(Filename)));
m.insert(names[Filesize], data(id, static_cast<int>(Filesize))); m.insert(names[Filesize], data(id, static_cast<int>(Filesize)));
m.insert(names[MimeType], data(id, static_cast<int>(MimeType))); m.insert(names[MimeType], data(id, static_cast<int>(MimeType)));

View file

@ -142,6 +142,7 @@ public:
Timestamp, Timestamp,
Url, Url,
ThumbnailUrl, ThumbnailUrl,
Blurhash,
Filename, Filename,
Filesize, Filesize,
MimeType, MimeType,

View file

@ -4,6 +4,7 @@
#include <QPalette> #include <QPalette>
#include <QQmlContext> #include <QQmlContext>
#include "BlurhashProvider.h"
#include "ChatPage.h" #include "ChatPage.h"
#include "ColorImageProvider.h" #include "ColorImageProvider.h"
#include "DelegateChooser.h" #include "DelegateChooser.h"
@ -69,6 +70,7 @@ TimelineViewManager::userColor(QString id, QColor background)
TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettings, QWidget *parent) TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettings, QWidget *parent)
: imgProvider(new MxcImageProvider()) : imgProvider(new MxcImageProvider())
, colorImgProvider(new ColorImageProvider()) , colorImgProvider(new ColorImageProvider())
, blurhashProvider(new BlurhashProvider())
, settings(userSettings) , settings(userSettings)
{ {
qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
@ -99,6 +101,7 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettin
updateColorPalette(); updateColorPalette();
view->engine()->addImageProvider("MxcImage", imgProvider); view->engine()->addImageProvider("MxcImage", imgProvider);
view->engine()->addImageProvider("colorimage", colorImgProvider); view->engine()->addImageProvider("colorimage", colorImgProvider);
view->engine()->addImageProvider("blurhash", blurhashProvider);
view->setSource(QUrl("qrc:///qml/TimelineView.qml")); view->setSource(QUrl("qrc:///qml/TimelineView.qml"));
connect(dynamic_cast<ChatPage *>(parent), connect(dynamic_cast<ChatPage *>(parent),
@ -270,11 +273,13 @@ TimelineViewManager::queueImageMessage(const QString &roomid,
const QString &mime, const QString &mime,
uint64_t dsize, uint64_t dsize,
const QSize &dimensions, const QSize &dimensions,
const QString &blurhash,
const std::optional<RelatedInfo> &related) const std::optional<RelatedInfo> &related)
{ {
mtx::events::msg::Image image; mtx::events::msg::Image image;
image.info.mimetype = mime.toStdString(); image.info.mimetype = mime.toStdString();
image.info.size = dsize; image.info.size = dsize;
image.info.blurhash = blurhash.toStdString();
image.body = filename.toStdString(); image.body = filename.toStdString();
image.url = url.toStdString(); image.url = url.toStdString();
image.info.h = dimensions.height(); image.info.h = dimensions.height();

View file

@ -14,6 +14,7 @@
#include "Utils.h" #include "Utils.h"
class MxcImageProvider; class MxcImageProvider;
class BlurhashProvider;
class ColorImageProvider; class ColorImageProvider;
class UserSettings; class UserSettings;
@ -79,6 +80,7 @@ public slots:
const QString &mime, const QString &mime,
uint64_t dsize, uint64_t dsize,
const QSize &dimensions, const QSize &dimensions,
const QString &blurhash,
const std::optional<RelatedInfo> &related); const std::optional<RelatedInfo> &related);
void queueFileMessage(const QString &roomid, void queueFileMessage(const QString &roomid,
const QString &filename, const QString &filename,
@ -112,6 +114,7 @@ private:
MxcImageProvider *imgProvider; MxcImageProvider *imgProvider;
ColorImageProvider *colorImgProvider; ColorImageProvider *colorImgProvider;
BlurhashProvider *blurhashProvider;
QHash<QString, QSharedPointer<TimelineModel>> models; QHash<QString, QSharedPointer<TimelineModel>> models;
TimelineModel *timeline_ = nullptr; TimelineModel *timeline_ = nullptr;

23
third_party/blurhash/LICENSE vendored Normal file
View file

@ -0,0 +1,23 @@
Boost Software License - Version 1.0 - August 17th, 2003
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

472
third_party/blurhash/blurhash.cpp vendored Normal file
View file

@ -0,0 +1,472 @@
#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)
{
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;
}
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

22
third_party/blurhash/blurhash.hpp vendored Normal file
View file

@ -0,0 +1,22 @@
#pragma once
#include <string>
#include <string_view>
#include <vector>
namespace blurhash {
struct Image
{
size_t width, height;
std::vector<unsigned char> image; // pixels rgb
};
// Decode a blurhash to an image with size width*height
Image
decode(std::string_view blurhash, size_t width, size_t height);
// Encode an image of rgb pixels (without padding) with size width*height into a blurhash with x*y
// components
std::string
encode(unsigned char *image, size_t width, size_t height, int x, int y);
}