mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-21 18:50: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/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
|
||||||
|
|
|
@ -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
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 "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,37 +386,44 @@ 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,
|
QString filename,
|
||||||
QString filename,
|
std::optional<mtx::crypto::EncryptedFile> encryptedFile,
|
||||||
std::optional<mtx::crypto::EncryptedFile> encryptedFile,
|
QString url,
|
||||||
QString url,
|
QString mimeClass,
|
||||||
QString mimeClass,
|
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();
|
||||||
|
|
||||||
if (encryptedFile)
|
if (encryptedFile)
|
||||||
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,
|
||||||
else if (mimeClass == "audio")
|
encryptedFile,
|
||||||
view_manager_->queueAudioMessage(
|
url,
|
||||||
roomid, filename, encryptedFile, url, mime, dsize, related);
|
mime,
|
||||||
else if (mimeClass == "video")
|
dsize,
|
||||||
view_manager_->queueVideoMessage(
|
dimensions,
|
||||||
roomid, filename, encryptedFile, url, mime, dsize, related);
|
blurhash,
|
||||||
else
|
related);
|
||||||
view_manager_->queueFileMessage(
|
else if (mimeClass == "audio")
|
||||||
roomid, filename, encryptedFile, url, mime, dsize, related);
|
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);
|
connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar);
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)));
|
||||||
|
|
|
@ -142,6 +142,7 @@ public:
|
||||||
Timestamp,
|
Timestamp,
|
||||||
Url,
|
Url,
|
||||||
ThumbnailUrl,
|
ThumbnailUrl,
|
||||||
|
Blurhash,
|
||||||
Filename,
|
Filename,
|
||||||
Filesize,
|
Filesize,
|
||||||
MimeType,
|
MimeType,
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
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