diff --git a/src/BlurhashProvider.cpp b/src/BlurhashProvider.cpp
new file mode 100644
index 00000000..a5530a98
--- /dev/null
+++ b/src/BlurhashProvider.cpp
@@ -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;
+}
diff --git a/src/BlurhashProvider.h b/src/BlurhashProvider.h
new file mode 100644
index 00000000..b05fff59
--- /dev/null
+++ b/src/BlurhashProvider.h
@@ -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;
+};
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 89bfd55a..6a7d984c 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -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);
diff --git a/src/ChatPage.h b/src/ChatPage.h
index 8e2e9192..02c19ba7 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -114,6 +114,7 @@ signals:
const QString &mime,
qint64 dsize,
const QSize &dimensions,
+ const QString &blurhash,
const std::optional<RelatedInfo> &related);
void contentLoaded();
diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp
index 20cdb63c..7f28eb46 100644
--- a/src/EventAccessors.cpp
+++ b/src/EventAccessors.cpp
@@ -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);
diff --git a/src/EventAccessors.h b/src/EventAccessors.h
index cf79f68f..c9ac4d00 100644
--- a/src/EventAccessors.h
+++ b/src/EventAccessors.h
@@ -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);
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index cad39bc5..b187a67d 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -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)));
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index f06de5d9..3dc1815f 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -142,6 +142,7 @@ public:
Timestamp,
Url,
ThumbnailUrl,
+ Blurhash,
Filename,
Filesize,
MimeType,
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index a3827501..44e26921 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -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();
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 338101c7..0c516e7f 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -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;
|