mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-10-30 09:30:47 +03:00
Experimental blurhash implementation (MXC2448)
This commit is contained in:
parent
328a3c7ebd
commit
0fc98b2692
15 changed files with 696 additions and 47 deletions
|
@ -275,37 +275,40 @@ set(SRC_FILES
|
|||
src/ui/ThemeManager.cpp
|
||||
|
||||
src/AvatarProvider.cpp
|
||||
src/BlurhashProvider.cpp
|
||||
src/Cache.cpp
|
||||
src/ChatPage.cpp
|
||||
src/CommunitiesListItem.cpp
|
||||
src/ColorImageProvider.cpp
|
||||
src/CommunitiesList.cpp
|
||||
src/CommunitiesListItem.cpp
|
||||
src/EventAccessors.cpp
|
||||
src/InviteeItem.cpp
|
||||
src/LoginPage.cpp
|
||||
src/Logging.cpp
|
||||
src/LoginPage.cpp
|
||||
src/MainWindow.cpp
|
||||
src/MatrixClient.cpp
|
||||
src/MxcImageProvider.cpp
|
||||
src/ColorImageProvider.cpp
|
||||
src/QuickSwitcher.cpp
|
||||
src/Olm.cpp
|
||||
src/QuickSwitcher.cpp
|
||||
src/RegisterPage.cpp
|
||||
src/RoomInfoListItem.cpp
|
||||
src/RoomList.cpp
|
||||
src/SideBarActions.cpp
|
||||
src/Splitter.cpp
|
||||
src/popups/SuggestionsPopup.cpp
|
||||
src/popups/PopupItem.cpp
|
||||
src/popups/ReplyPopup.cpp
|
||||
src/popups/UserMentions.cpp
|
||||
src/TextInputWidget.cpp
|
||||
src/TopRoomBar.cpp
|
||||
src/TrayIcon.cpp
|
||||
src/Utils.cpp
|
||||
src/UserInfoWidget.cpp
|
||||
src/UserSettingsPage.cpp
|
||||
src/Utils.cpp
|
||||
src/WelcomePage.cpp
|
||||
src/popups/PopupItem.cpp
|
||||
src/popups/ReplyPopup.cpp
|
||||
src/popups/SuggestionsPopup.cpp
|
||||
src/popups/UserMentions.cpp
|
||||
src/main.cpp
|
||||
|
||||
third_party/blurhash/blurhash.cpp
|
||||
)
|
||||
|
||||
|
||||
|
@ -333,7 +336,7 @@ if(USE_BUNDLED_MTXCLIENT)
|
|||
FetchContent_Declare(
|
||||
MatrixClient
|
||||
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
|
||||
GIT_TAG 7fc1d357afaabb134cb6d9c593f94915973d31fa
|
||||
GIT_TAG c1ccd6c6cdaead3ff1c2bf336b719ca45fee2d33
|
||||
)
|
||||
FetchContent_MakeAvailable(MatrixClient)
|
||||
else()
|
||||
|
@ -478,28 +481,28 @@ qt5_wrap_cpp(MOC_HEADERS
|
|||
src/AvatarProvider.h
|
||||
src/Cache_p.h
|
||||
src/ChatPage.h
|
||||
src/CommunitiesListItem.h
|
||||
src/CommunitiesList.h
|
||||
src/CommunitiesListItem.h
|
||||
src/InviteeItem.h
|
||||
src/LoginPage.h
|
||||
src/MainWindow.h
|
||||
src/MxcImageProvider.h
|
||||
src/InviteeItem.h
|
||||
src/QuickSwitcher.h
|
||||
src/RegisterPage.h
|
||||
src/RoomInfoListItem.h
|
||||
src/RoomList.h
|
||||
src/SideBarActions.h
|
||||
src/Splitter.h
|
||||
src/popups/SuggestionsPopup.h
|
||||
src/popups/ReplyPopup.h
|
||||
src/popups/PopupItem.h
|
||||
src/popups/UserMentions.h
|
||||
src/TextInputWidget.h
|
||||
src/TopRoomBar.h
|
||||
src/TrayIcon.h
|
||||
src/UserInfoWidget.h
|
||||
src/UserSettingsPage.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()
|
||||
target_link_libraries (nheko PRIVATE Qt5::DBus)
|
||||
endif()
|
||||
target_include_directories(nheko PRIVATE src includes)
|
||||
target_include_directories(nheko PRIVATE src includes third_party/blurhash)
|
||||
|
||||
target_link_libraries(nheko PRIVATE
|
||||
MatrixClient::MatrixClient
|
||||
|
|
|
@ -11,6 +11,20 @@ Item {
|
|||
height: tooHigh ? timelineRoot.height / 2 : tempHeight
|
||||
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 {
|
||||
id: img
|
||||
anchors.fill: parent
|
||||
|
|
42
src/BlurhashProvider.cpp
Normal file
42
src/BlurhashProvider.cpp
Normal 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
11
src/BlurhashProvider.h
Normal 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;
|
||||
};
|
|
@ -47,6 +47,8 @@
|
|||
#include "popups/UserMentions.h"
|
||||
#include "timeline/TimelineViewManager.h"
|
||||
|
||||
#include "blurhash.hpp"
|
||||
|
||||
// TODO: Needs to be updated with an actual secret.
|
||||
static const std::string STORAGE_SECRET_KEY("secret");
|
||||
|
||||
|
@ -324,9 +326,25 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
|||
}
|
||||
|
||||
QSize dimensions;
|
||||
if (mimeClass == "image")
|
||||
QString blurhash;
|
||||
if (mimeClass == "image") {
|
||||
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(
|
||||
payload,
|
||||
encryptedFile ? "application/octet-stream" : mime.name().toStdString(),
|
||||
|
@ -339,6 +357,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
|||
mime = mime.name(),
|
||||
size = payload.size(),
|
||||
dimensions,
|
||||
blurhash,
|
||||
related](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
emit uploadFailed(
|
||||
|
@ -358,6 +377,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
|||
mime,
|
||||
size,
|
||||
dimensions,
|
||||
blurhash,
|
||||
related);
|
||||
});
|
||||
});
|
||||
|
@ -366,37 +386,44 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
|||
text_input_->hideUploadSpinner();
|
||||
emit showNotification(msg);
|
||||
});
|
||||
connect(
|
||||
this,
|
||||
&ChatPage::mediaUploaded,
|
||||
this,
|
||||
[this](QString roomid,
|
||||
QString filename,
|
||||
std::optional<mtx::crypto::EncryptedFile> encryptedFile,
|
||||
QString url,
|
||||
QString mimeClass,
|
||||
QString mime,
|
||||
qint64 dsize,
|
||||
QSize dimensions,
|
||||
const std::optional<RelatedInfo> &related) {
|
||||
text_input_->hideUploadSpinner();
|
||||
connect(this,
|
||||
&ChatPage::mediaUploaded,
|
||||
this,
|
||||
[this](QString roomid,
|
||||
QString filename,
|
||||
std::optional<mtx::crypto::EncryptedFile> encryptedFile,
|
||||
QString url,
|
||||
QString mimeClass,
|
||||
QString mime,
|
||||
qint64 dsize,
|
||||
QSize dimensions,
|
||||
QString blurhash,
|
||||
const std::optional<RelatedInfo> &related) {
|
||||
text_input_->hideUploadSpinner();
|
||||
|
||||
if (encryptedFile)
|
||||
encryptedFile->url = url.toStdString();
|
||||
if (encryptedFile)
|
||||
encryptedFile->url = url.toStdString();
|
||||
|
||||
if (mimeClass == "image")
|
||||
view_manager_->queueImageMessage(
|
||||
roomid, filename, encryptedFile, url, mime, dsize, dimensions, related);
|
||||
else if (mimeClass == "audio")
|
||||
view_manager_->queueAudioMessage(
|
||||
roomid, filename, encryptedFile, url, mime, dsize, related);
|
||||
else if (mimeClass == "video")
|
||||
view_manager_->queueVideoMessage(
|
||||
roomid, filename, encryptedFile, url, mime, dsize, related);
|
||||
else
|
||||
view_manager_->queueFileMessage(
|
||||
roomid, filename, encryptedFile, url, mime, dsize, related);
|
||||
});
|
||||
if (mimeClass == "image")
|
||||
view_manager_->queueImageMessage(roomid,
|
||||
filename,
|
||||
encryptedFile,
|
||||
url,
|
||||
mime,
|
||||
dsize,
|
||||
dimensions,
|
||||
blurhash,
|
||||
related);
|
||||
else if (mimeClass == "audio")
|
||||
view_manager_->queueAudioMessage(
|
||||
roomid, filename, encryptedFile, url, mime, dsize, related);
|
||||
else if (mimeClass == "video")
|
||||
view_manager_->queueVideoMessage(
|
||||
roomid, filename, encryptedFile, url, mime, dsize, related);
|
||||
else
|
||||
view_manager_->queueFileMessage(
|
||||
roomid, filename, encryptedFile, url, mime, dsize, related);
|
||||
});
|
||||
|
||||
connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar);
|
||||
|
||||
|
|
|
@ -114,6 +114,7 @@ signals:
|
|||
const QString &mime,
|
||||
qint64 dsize,
|
||||
const QSize &dimensions,
|
||||
const QString &blurhash,
|
||||
const std::optional<RelatedInfo> &related);
|
||||
|
||||
void contentLoaded();
|
||||
|
|
|
@ -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
|
||||
{
|
||||
template<class T>
|
||||
|
@ -348,6 +362,11 @@ mtx::accessors::thumbnail_url(const mtx::events::collections::TimelineEvents &ev
|
|||
return std::visit(EventThumbnailUrl{}, event);
|
||||
}
|
||||
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)
|
||||
{
|
||||
return std::visit(EventMimeType{}, event);
|
||||
|
|
|
@ -47,6 +47,8 @@ url(const mtx::events::collections::TimelineEvents &event);
|
|||
std::string
|
||||
thumbnail_url(const mtx::events::collections::TimelineEvents &event);
|
||||
std::string
|
||||
blurhash(const mtx::events::collections::TimelineEvents &event);
|
||||
std::string
|
||||
mimetype(const mtx::events::collections::TimelineEvents &event);
|
||||
std::string
|
||||
in_reply_to_event(const mtx::events::collections::TimelineEvents &event);
|
||||
|
|
|
@ -212,6 +212,7 @@ TimelineModel::roleNames() const
|
|||
{Timestamp, "timestamp"},
|
||||
{Url, "url"},
|
||||
{ThumbnailUrl, "thumbnailUrl"},
|
||||
{Blurhash, "blurhash"},
|
||||
{Filename, "filename"},
|
||||
{Filesize, "filesize"},
|
||||
{MimeType, "mimetype"},
|
||||
|
@ -296,6 +297,8 @@ TimelineModel::data(const QString &id, int role) const
|
|||
return QVariant(QString::fromStdString(url(event)));
|
||||
case ThumbnailUrl:
|
||||
return QVariant(QString::fromStdString(thumbnail_url(event)));
|
||||
case Blurhash:
|
||||
return QVariant(QString::fromStdString(blurhash(event)));
|
||||
case Filename:
|
||||
return QVariant(QString::fromStdString(filename(event)));
|
||||
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[Url], data(id, static_cast<int>(Url)));
|
||||
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[Filesize], data(id, static_cast<int>(Filesize)));
|
||||
m.insert(names[MimeType], data(id, static_cast<int>(MimeType)));
|
||||
|
|
|
@ -142,6 +142,7 @@ public:
|
|||
Timestamp,
|
||||
Url,
|
||||
ThumbnailUrl,
|
||||
Blurhash,
|
||||
Filename,
|
||||
Filesize,
|
||||
MimeType,
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include <QPalette>
|
||||
#include <QQmlContext>
|
||||
|
||||
#include "BlurhashProvider.h"
|
||||
#include "ChatPage.h"
|
||||
#include "ColorImageProvider.h"
|
||||
#include "DelegateChooser.h"
|
||||
|
@ -69,6 +70,7 @@ TimelineViewManager::userColor(QString id, QColor background)
|
|||
TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
||||
: imgProvider(new MxcImageProvider())
|
||||
, colorImgProvider(new ColorImageProvider())
|
||||
, blurhashProvider(new BlurhashProvider())
|
||||
, settings(userSettings)
|
||||
{
|
||||
qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
|
||||
|
@ -99,6 +101,7 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettin
|
|||
updateColorPalette();
|
||||
view->engine()->addImageProvider("MxcImage", imgProvider);
|
||||
view->engine()->addImageProvider("colorimage", colorImgProvider);
|
||||
view->engine()->addImageProvider("blurhash", blurhashProvider);
|
||||
view->setSource(QUrl("qrc:///qml/TimelineView.qml"));
|
||||
|
||||
connect(dynamic_cast<ChatPage *>(parent),
|
||||
|
@ -270,11 +273,13 @@ TimelineViewManager::queueImageMessage(const QString &roomid,
|
|||
const QString &mime,
|
||||
uint64_t dsize,
|
||||
const QSize &dimensions,
|
||||
const QString &blurhash,
|
||||
const std::optional<RelatedInfo> &related)
|
||||
{
|
||||
mtx::events::msg::Image image;
|
||||
image.info.mimetype = mime.toStdString();
|
||||
image.info.size = dsize;
|
||||
image.info.blurhash = blurhash.toStdString();
|
||||
image.body = filename.toStdString();
|
||||
image.url = url.toStdString();
|
||||
image.info.h = dimensions.height();
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
#include "Utils.h"
|
||||
|
||||
class MxcImageProvider;
|
||||
class BlurhashProvider;
|
||||
class ColorImageProvider;
|
||||
class UserSettings;
|
||||
|
||||
|
@ -79,6 +80,7 @@ public slots:
|
|||
const QString &mime,
|
||||
uint64_t dsize,
|
||||
const QSize &dimensions,
|
||||
const QString &blurhash,
|
||||
const std::optional<RelatedInfo> &related);
|
||||
void queueFileMessage(const QString &roomid,
|
||||
const QString &filename,
|
||||
|
@ -112,6 +114,7 @@ private:
|
|||
|
||||
MxcImageProvider *imgProvider;
|
||||
ColorImageProvider *colorImgProvider;
|
||||
BlurhashProvider *blurhashProvider;
|
||||
|
||||
QHash<QString, QSharedPointer<TimelineModel>> models;
|
||||
TimelineModel *timeline_ = nullptr;
|
||||
|
|
23
third_party/blurhash/LICENSE
vendored
Normal file
23
third_party/blurhash/LICENSE
vendored
Normal 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
472
third_party/blurhash/blurhash.cpp
vendored
Normal 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
22
third_party/blurhash/blurhash.hpp
vendored
Normal 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);
|
||||
}
|
Loading…
Reference in a new issue