matrixion/src/MxcImageProvider.cpp

373 lines
14 KiB
C++
Raw Normal View History

// SPDX-FileCopyrightText: Nheko Contributors
2021-03-05 02:35:15 +03:00
//
// SPDX-License-Identifier: GPL-3.0-or-later
2019-09-07 23:22:07 +03:00
#include "MxcImageProvider.h"
#include <optional>
2020-10-27 19:45:28 +03:00
#include <mtxclient/crypto/client.hpp>
#include <QByteArray>
#include <QCache>
2021-03-17 21:08:17 +03:00
#include <QDir>
#include <QFileInfo>
2021-08-14 18:17:50 +03:00
#include <QPainter>
#include <QPainterPath>
#include <QStandardPaths>
2022-12-13 08:02:07 +03:00
#include <QThreadPool>
#include <QTimer>
2019-12-15 04:56:04 +03:00
#include "Logging.h"
#include "MatrixClient.h"
#include "Utils.h"
2019-09-07 23:22:07 +03:00
QHash<QString, mtx::crypto::EncryptedFile> infos;
2023-06-02 01:24:26 +03:00
MxcImageProvider::MxcImageProvider()
: QQuickAsyncImageProvider()
2022-12-13 08:02:07 +03:00
{
auto timer = new QTimer(this);
2022-12-13 08:05:29 +03:00
timer->setInterval(std::chrono::hours(1));
2022-12-13 08:02:07 +03:00
connect(timer, &QTimer::timeout, this, [] {
QThreadPool::globalInstance()->start([] {
QDir dir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/media_cache",
"",
QDir::SortFlags(QDir::Name | QDir::IgnoreCase),
QDir::Filter::Writable | QDir::Filter::NoDotAndDotDot | QDir::Filter::Files);
auto files = dir.entryInfoList();
2023-10-14 00:28:57 +03:00
for (const auto &fileInfo : std::as_const(files)) {
2022-12-13 08:02:07 +03:00
if (fileInfo.fileTime(QFile::FileTime::FileAccessTime)
.daysTo(QDateTime::currentDateTime()) > 30) {
if (QFile::remove(fileInfo.absoluteFilePath()))
nhlog::net()->debug("Deleted stale media '{}'",
fileInfo.absoluteFilePath().toStdString());
else
nhlog::net()->warn("Failed to delete stale media '{}'",
fileInfo.absoluteFilePath().toStdString());
}
}
});
});
timer->start();
}
QQuickImageResponse *
MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
{
2021-09-18 01:22:33 +03:00
auto id_ = id;
bool crop = true;
double radius = 0;
auto size = requestedSize;
2021-09-18 01:22:33 +03:00
if (requestedSize.width() == 0 && requestedSize.height() == 0)
size = QSize();
2021-09-18 01:22:33 +03:00
auto queryStart = id.lastIndexOf('?');
if (queryStart != -1) {
id_ = id.left(queryStart);
2021-12-28 22:09:08 +03:00
auto query = QStringView(id).mid(queryStart + 1);
2021-09-18 01:22:33 +03:00
auto queryBits = query.split('&');
for (auto b : queryBits) {
2021-12-28 22:09:08 +03:00
if (b == QStringView(u"scale")) {
2021-09-18 01:22:33 +03:00
crop = false;
2021-12-28 22:09:08 +03:00
} else if (b.startsWith(QStringView(u"radius="))) {
2021-09-18 01:22:33 +03:00
radius = b.mid(7).toDouble();
2021-12-28 22:09:08 +03:00
} else if (b.startsWith(u"height=")) {
size.setHeight(b.mid(7).toInt());
size.setWidth(0);
2021-09-18 01:22:33 +03:00
}
2021-08-06 02:45:47 +03:00
}
2021-09-18 01:22:33 +03:00
}
2021-08-06 02:45:47 +03:00
return new MxcImageResponse(id_, crop, radius, size);
}
void
MxcImageProvider::addEncryptionInfo(mtx::crypto::EncryptedFile info)
{
2021-09-18 01:22:33 +03:00
infos.insert(QString::fromStdString(info.url), info);
}
2019-09-07 23:22:07 +03:00
void
2021-12-08 01:58:17 +03:00
MxcImageRunnable::run()
2019-09-07 23:22:07 +03:00
{
2021-09-18 01:22:33 +03:00
MxcImageProvider::download(
m_id,
m_requestedSize,
[this](QString id, QSize, QImage image, QString) {
2021-09-18 01:22:33 +03:00
if (image.isNull()) {
emit error(QStringLiteral("Failed to download image: %1").arg(id));
2021-09-18 01:22:33 +03:00
} else {
2021-12-08 01:58:17 +03:00
emit done(image);
2021-09-18 01:22:33 +03:00
}
2021-12-08 01:58:17 +03:00
this->deleteLater();
2021-09-18 01:22:33 +03:00
},
m_crop,
m_radius);
2021-08-14 18:17:50 +03:00
}
static QImage
clipRadius(QImage img, double radius)
{
2021-09-18 01:22:33 +03:00
QImage out(img.size(), QImage::Format_ARGB32_Premultiplied);
out.fill(Qt::transparent);
2021-08-14 18:17:50 +03:00
2021-09-18 01:22:33 +03:00
QPainter painter(&out);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
2021-08-14 18:17:50 +03:00
2021-09-18 01:22:33 +03:00
QPainterPath ppath;
ppath.addRoundedRect(img.rect(), radius, radius, Qt::SizeMode::RelativeSize);
2021-08-14 18:17:50 +03:00
2021-09-18 01:22:33 +03:00
painter.setClipPath(ppath);
painter.drawImage(img.rect(), img);
2021-08-14 18:17:50 +03:00
2021-09-18 01:22:33 +03:00
return out;
}
2022-12-13 08:02:07 +03:00
static void
possiblyUpdateAccessTime(const QFileInfo &fileInfo)
{
if (fileInfo.fileTime(QFile::FileTime::FileAccessTime).daysTo(QDateTime::currentDateTime()) >
7) {
nhlog::net()->debug("Updating file time for '{}'",
fileInfo.absoluteFilePath().toStdString());
QFile f(fileInfo.absoluteFilePath());
if (!f.open(QIODevice::ReadWrite) ||
!f.setFileTime(QDateTime::currentDateTime(), QFile::FileTime::FileAccessTime)) {
nhlog::net()->warn("Failed to update filetime for '{}'",
fileInfo.absoluteFilePath().toStdString());
}
}
}
void
MxcImageProvider::download(const QString &id,
const QSize &requestedSize,
2021-08-06 02:45:47 +03:00
std::function<void(QString, QSize, QImage, QString)> then,
2021-08-14 18:17:50 +03:00
bool crop,
double radius)
{
if (id.isEmpty()) {
nhlog::net()->warn("Attempted to download image with empty ID");
then(id, QSize{}, QImage{}, QString{});
return;
}
bool cropLocally = false;
if (crop && requestedSize.width() > 96) {
crop = false;
cropLocally = true;
}
2021-09-18 01:22:33 +03:00
std::optional<mtx::crypto::EncryptedFile> encryptionInfo;
auto temp = infos.find("mxc://" + id);
if (temp != infos.end())
encryptionInfo = *temp;
2021-12-09 01:07:13 +03:00
if (requestedSize.isValid() &&
!encryptionInfo
// Protect against synapse not following the spec:
// https://github.com/matrix-org/synapse/issues/5302
&& requestedSize.height() <= 600 && requestedSize.width() <= 800) {
2021-12-28 22:09:08 +03:00
QString fileName = QStringLiteral("%1_%2x%3_%4_radius%5")
2021-09-18 01:22:33 +03:00
.arg(QString::fromUtf8(id.toUtf8().toBase64(
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)))
.arg(requestedSize.width())
.arg(requestedSize.height())
.arg(crop ? "crop" : "scale")
.arg(radius);
2021-09-18 01:22:33 +03:00
QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/media_cache",
fileName);
QDir().mkpath(fileInfo.absolutePath());
if (fileInfo.exists()) {
QImage image = utils::readImageFromFile(fileInfo.absoluteFilePath());
if (!image.isNull()) {
2022-12-13 08:02:07 +03:00
possiblyUpdateAccessTime(fileInfo);
if (requestedSize.width() <= 0) {
image = image.scaledToHeight(requestedSize.height(), Qt::SmoothTransformation);
} else {
image = image.scaled(requestedSize,
cropLocally ? Qt::KeepAspectRatioByExpanding
: Qt::KeepAspectRatio,
Qt::SmoothTransformation);
if (cropLocally) {
image = image.copy((image.width() - requestedSize.width()) / 2,
(image.height() - requestedSize.height()) / 2,
requestedSize.width(),
requestedSize.height());
}
}
2021-09-18 01:22:33 +03:00
if (radius != 0) {
image = clipRadius(std::move(image), radius);
2019-09-07 23:22:07 +03:00
}
2021-09-18 01:22:33 +03:00
if (!image.isNull()) {
then(id, requestedSize, image, fileInfo.absoluteFilePath());
return;
}
}
}
mtx::http::ThumbOpts opts;
opts.mxc_url = "mxc://" + id.toStdString();
2022-10-27 23:53:21 +03:00
opts.width = static_cast<uint16_t>(requestedSize.width() > 0 ? requestedSize.width() : -1);
2022-10-26 02:10:35 +03:00
opts.height =
2022-10-27 23:53:21 +03:00
static_cast<uint16_t>(requestedSize.height() > 0 ? requestedSize.height() : -1);
2022-10-26 02:14:46 +03:00
opts.method = crop ? "crop" : "scale";
2021-09-18 01:22:33 +03:00
http::client()->get_thumbnail(
opts,
[fileInfo, requestedSize, radius, then, id, crop, cropLocally](
const std::string &res, mtx::http::RequestErr err) {
2021-09-18 01:22:33 +03:00
if (err || res.empty()) {
download(id, QSize(), then, crop, radius);
2021-09-18 01:22:33 +03:00
return;
}
auto data = QByteArray(res.data(), (int)res.size());
QImage image = utils::readImage(data);
if (!image.isNull()) {
2022-12-13 08:02:07 +03:00
possiblyUpdateAccessTime(fileInfo);
if (requestedSize.width() <= 0) {
image =
image.scaledToHeight(requestedSize.height(), Qt::SmoothTransformation);
} else {
image = image.scaled(requestedSize,
cropLocally ? Qt::KeepAspectRatioByExpanding
: Qt::KeepAspectRatio,
Qt::SmoothTransformation);
if (cropLocally) {
image = image.copy((image.width() - requestedSize.width()) / 2,
(image.height() - requestedSize.height()) / 2,
requestedSize.width(),
requestedSize.height());
}
}
2021-09-18 01:22:33 +03:00
if (radius != 0) {
image = clipRadius(std::move(image), radius);
}
}
image.setText(QStringLiteral("mxc url"), "mxc://" + id);
2021-09-18 01:22:33 +03:00
if (image.save(fileInfo.absoluteFilePath(), "png"))
nhlog::ui()->debug("Wrote: {}", fileInfo.absoluteFilePath().toStdString());
else
nhlog::ui()->debug("Failed to write: {}",
fileInfo.absoluteFilePath().toStdString());
then(id, requestedSize, image, fileInfo.absoluteFilePath());
});
} else {
try {
QString fileName = QStringLiteral("%1_radius%2")
2021-09-18 01:22:33 +03:00
.arg(QString::fromUtf8(id.toUtf8().toBase64(
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)))
.arg(radius);
QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/media_cache",
fileName);
QDir().mkpath(fileInfo.absolutePath());
if (fileInfo.exists()) {
if (encryptionInfo) {
QFile f(fileInfo.absoluteFilePath());
f.open(QIODevice::ReadOnly);
QByteArray fileData = f.readAll();
auto tempData = mtx::crypto::to_string(
mtx::crypto::decrypt_file(fileData.toStdString(), encryptionInfo.value()));
auto data = QByteArray(tempData.data(), (int)tempData.size());
QImage image = utils::readImage(data);
image.setText(QStringLiteral("mxc url"), "mxc://" + id);
2021-09-18 01:22:33 +03:00
if (!image.isNull()) {
2022-12-13 08:02:07 +03:00
possiblyUpdateAccessTime(fileInfo);
2021-09-18 01:22:33 +03:00
if (radius != 0) {
image = clipRadius(std::move(image), radius);
}
2021-09-18 01:22:33 +03:00
then(id, requestedSize, image, fileInfo.absoluteFilePath());
return;
}
} else {
QImage image = utils::readImageFromFile(fileInfo.absoluteFilePath());
if (!image.isNull()) {
2022-12-13 08:02:07 +03:00
possiblyUpdateAccessTime(fileInfo);
2021-09-18 01:22:33 +03:00
if (radius != 0) {
image = clipRadius(std::move(image), radius);
}
then(id, requestedSize, image, fileInfo.absoluteFilePath());
return;
}
2019-09-07 23:22:07 +03:00
}
2021-09-18 01:22:33 +03:00
}
http::client()->download(
"mxc://" + id.toStdString(),
[fileInfo, requestedSize, then, id, radius, encryptionInfo](
const std::string &res,
const std::string &,
const std::string &originalFilename,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->error("Failed to download {}: {}", id.toStdString(), *err);
then(id, QSize(), {}, QLatin1String(""));
2021-09-18 01:22:33 +03:00
return;
}
auto tempData = res;
QFile f(fileInfo.absoluteFilePath());
if (!f.open(QIODevice::Truncate | QIODevice::WriteOnly)) {
nhlog::net()->error(
"Failed to write {}: {}", id.toStdString(), f.errorString().toStdString());
then(id, QSize(), {}, QLatin1String(""));
2021-09-18 01:22:33 +03:00
return;
}
f.write(tempData.data(), tempData.size());
f.close();
if (encryptionInfo) {
tempData = mtx::crypto::to_string(
mtx::crypto::decrypt_file(tempData, encryptionInfo.value()));
auto data = QByteArray(tempData.data(), (int)tempData.size());
QImage image = utils::readImage(data);
if (radius != 0) {
image = clipRadius(std::move(image), radius);
}
image.setText(QStringLiteral("original filename"),
QString::fromStdString(originalFilename));
image.setText(QStringLiteral("mxc url"), "mxc://" + id);
2021-09-18 01:22:33 +03:00
then(id, requestedSize, image, fileInfo.absoluteFilePath());
return;
}
QImage image = utils::readImageFromFile(fileInfo.absoluteFilePath());
if (radius != 0) {
image = clipRadius(std::move(image), radius);
}
image.setText(QStringLiteral("original filename"),
QString::fromStdString(originalFilename));
image.setText(QStringLiteral("mxc url"), "mxc://" + id);
2021-09-18 01:22:33 +03:00
then(id, requestedSize, image, fileInfo.absoluteFilePath());
});
} catch (std::exception &e) {
nhlog::net()->error("Exception while downloading media: {}", e.what());
2019-09-07 23:22:07 +03:00
}
2021-09-18 01:22:33 +03:00
}
2019-09-07 23:22:07 +03:00
}