2 changed files with 153 additions and 0 deletions
@ -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")); |
||||
} |
||||
@ -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…
Reference in new issue