Browse Source

Decode and prepare bounded group avatar images outside the GUI thread

master
evgeny 6 days ago
parent
commit
2f9dd6a27b
  1. 119
      tools/chatgui/src/avatars/groupavatarmanager.cpp
  2. 34
      tools/chatgui/src/avatars/groupavatarmanager.h

119
tools/chatgui/src/avatars/groupavatarmanager.cpp

@ -0,0 +1,119 @@
#include "groupavatarmanager.h"
#include "../../db/db_manager.h"
#include "../../transport/gui_bridge.h"
#include <QCoreApplication>
#include <QBuffer>
#include <QCryptographicHash>
#include <QImageReader>
#include <QMetaObject>
#include <QRandomGenerator>
#include <QRunnable>
#include <cstring>
extern "C" {
#include "chat/group_avatar.h"
#include "member_files/protocol.h"
#include "mem.h"
#include "debug_config.h"
}
static GroupAvatarManager* s_groupAvatars;
static QString text(const char* value) { return QCoreApplication::translate("ChatGui", value); }
GroupAvatarManager::GroupAvatarManager(DbManager* db, QObject* parent) : QObject(parent), m_db(db) {
s_groupAvatars = this; m_pool.setMaxThreadCount(1);
}
GroupAvatarManager::~GroupAvatarManager() { s_groupAvatars = nullptr; m_pool.waitForDone(); }
GroupAvatarManager* GroupAvatarManager::instance() { return s_groupAvatars; }
QImage GroupAvatarManager::currentImage(const QString& channel) const {
auto* image = m_cache.object(m_current.value(channel)); return image ? *image : QImage();
}
QImage GroupAvatarManager::image(const QString& channel) {
if (!m_db || !m_db->isOpen()) return {};
for (const auto& row : m_db->getChannels()) if (row.channelId == channel) return image(channel, row.avatarVersion, row.avatarHash);
return {};
}
QImage GroupAvatarManager::image(const QString& channel, quint64 version, const QByteArray& hash) {
if (!version) { m_current.remove(channel); return {}; }
const QString key = channel + ':' + QString::number(version) + ':' + QString::fromLatin1(hash.toHex()); m_current[channel] = key;
if (auto* cached = m_cache.object(key)) return *cached;
if (m_loading.contains(key) || m_invalid.contains(key) || !m_db || !m_db->isOpen()) return {};
const QByteArray bytes = m_db->getChannelAvatar(channel); m_loading.insert(key);
m_pool.start(QRunnable::create([this, key, bytes, hash, channel] {
QImage image; QString error;
if (bytes.isEmpty() || bytes.size() > GROUP_AVATAR_MAX || hash.size() != 32 ||
QCryptographicHash::hash(bytes, QCryptographicHash::Sha256) != hash) error = QStringLiteral("invalid length/hash");
else {
QBuffer buffer; buffer.setData(bytes); buffer.open(QIODevice::ReadOnly); QImageReader reader(&buffer, "jpeg");
QSize size = reader.size();
if (!size.isValid() || size.width() > 128 || size.height() > 128 || size.width() != size.height()) error = QStringLiteral("invalid dimensions");
else { image = reader.read(); if (image.isNull()) error = reader.errorString(); }
}
QMetaObject::invokeMethod(this, [this, key, image, error, channel, bytes] {
m_loading.remove(key);
if (image.isNull()) {
// Пустое изображение — подписанное удаление, а не ошибка декодирования.
if (!bytes.isEmpty()) DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: decode failed channel=%s reason=%s", qPrintable(channel), qPrintable(error));
m_invalid.insert(key);
} else m_cache.insert(key, new QImage(image), qMax(1, int(image.sizeInBytes() / 1024)));
if (m_current.value(channel) == key) emit changed(channel);
}, Qt::QueuedConnection);
}));
return {};
}
void GroupAvatarManager::upload(const QString& channel, const QString& source) {
if (!gui_bridge_is_uasync_ready()) { emit status(channel, text("Chat is stopped")); return; }
quint64 generation = ++m_generation[channel]; emit status(channel, text("Preparing group avatar…"));
m_pool.start(QRunnable::create([this, channel, source, generation] {
QImageReader reader(source); reader.setAutoTransform(true); QImage image; QByteArray encoded; QString error;
QSize size = reader.size();
if (!size.isValid() || qint64(size.width()) * size.height() > 16 * 1024 * 1024 || reader.format() == "svg")
error = QStringLiteral("image exceeds 16 megapixels or has an unsupported format");
else {
image = reader.read();
if (image.isNull()) error = reader.errorString();
else {
int side = qMin(image.width(), image.height());
image = image.copy((image.width() - side) / 2, (image.height() - side) / 2, side, side).convertToFormat(QImage::Format_RGB32);
for (int dimension : {128, 96, 64}) {
const QImage thumbnail = image.scaled(dimension, dimension, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
for (int quality : {85, 70, 50, 35}) {
encoded.clear(); QBuffer buffer(&encoded); buffer.open(QIODevice::WriteOnly);
if (!thumbnail.save(&buffer, "JPEG", quality)) { error = QStringLiteral("JPEG encoder unavailable"); break; }
if (encoded.size() <= GROUP_AVATAR_MAX) break;
}
if (!error.isEmpty() || encoded.size() <= GROUP_AVATAR_MAX) break;
}
if (encoded.size() > GROUP_AVATAR_MAX) error = QStringLiteral("cannot fit image in 16 KiB");
}
}
QMetaObject::invokeMethod(this, [this, channel, generation, encoded, error] {
if (m_generation.value(channel) != generation) return;
if (!error.isEmpty()) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: preparation failed channel=%s reason=%s", qPrintable(channel), qPrintable(error));
emit status(channel, text("Cannot prepare avatar: %1").arg(error)); return;
}
publish(channel, encoded, generation);
}, Qt::QueuedConnection);
}));
}
void GroupAvatarManager::remove(const QString& channel) { publish(channel, {}, ++m_generation[channel]); }
void GroupAvatarManager::publish(const QString& channel, const QByteArray& bytes, quint64 generation) {
if (!gui_bridge_is_uasync_ready() || !channel.toULongLong() || bytes.size() > GROUP_AVATAR_MAX) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: GUI publication unavailable channel=%s", qPrintable(channel));
emit status(channel, text("Chat is stopped")); return;
}
auto* q = (group_avatar_gui_request*)u_calloc(1, sizeof(group_avatar_gui_request));
if (!q) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: GUI publication allocation failed"); emit status(channel, text("Cannot update group avatar")); return; }
q->inst = gui_bridge_get_inst(); q->group = channel.toULongLong(); q->size = bytes.size();
do { q->id = QRandomGenerator::global()->generate64(); } while (!q->id || m_requests.contains(q->id));
memcpy(q->data, bytes.constData(), bytes.size()); m_requests[q->id] = {channel, generation};
emit status(channel, text("Saving group avatar…")); gui_bridge_post_uasync_fn(group_avatar_gui_set, q);
}
void GroupAvatarManager::onResult(const uint8_t* bytes, int length) {
if (length != 17 || !m_requests.contains(mf_get64(bytes))) return;
const auto request = m_requests.take(mf_get64(bytes));
if (request.first.toULongLong() != mf_get64(bytes + 8) || m_generation.value(request.first) != request.second) return;
emit status(request.first, bytes[16] ? text("Cannot update group avatar") : text("Group avatar updated"));
}

34
tools/chatgui/src/avatars/groupavatarmanager.h

@ -0,0 +1,34 @@
/* SQLite — постоянный кеш; QImage декодируется один раз на подписанную версию. */
#pragma once
#include <QObject>
#include <QImage>
#include <QCache>
#include <QHash>
#include <QSet>
#include <QThreadPool>
class DbManager;
class GroupAvatarManager : public QObject {
Q_OBJECT
public:
explicit GroupAvatarManager(DbManager* db, QObject* parent = nullptr);
~GroupAvatarManager() override;
static GroupAvatarManager* instance();
QImage image(const QString& channel, quint64 version, const QByteArray& hash);
QImage image(const QString& channel);
QImage currentImage(const QString& channel) const;
void upload(const QString& channel, const QString& source);
void remove(const QString& channel);
void onResult(const uint8_t* bytes, int length);
signals:
void changed(const QString& channel);
void status(const QString& channel, const QString& text);
private:
void publish(const QString& channel, const QByteArray& bytes, quint64 generation);
DbManager* m_db;
QThreadPool m_pool;
QCache<QString, QImage> m_cache{32 * 1024};
QHash<QString, QString> m_current;
QHash<QString, quint64> m_generation;
QHash<quint64, QPair<QString, quint64>> m_requests;
QSet<QString> m_loading, m_invalid;
};
Loading…
Cancel
Save