|
|
|
@@ -1,5 +1,7 @@
|
|
|
|
|
#include "cachingimagemanager.hpp"
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
|
|
|
|
|
#include <QtQuick/qquickwindow.h>
|
|
|
|
|
#include <qcryptographichash.h>
|
|
|
|
|
#include <qdir.h>
|
|
|
|
@@ -7,10 +9,27 @@
|
|
|
|
|
#include <qfuturewatcher.h>
|
|
|
|
|
#include <qimagereader.h>
|
|
|
|
|
#include <qpainter.h>
|
|
|
|
|
#include <qthread.h>
|
|
|
|
|
#include <qthreadpool.h>
|
|
|
|
|
#include <qtconcurrentrun.h>
|
|
|
|
|
|
|
|
|
|
namespace ZShell::internal {
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
QThreadPool* imageIoPool() {
|
|
|
|
|
static QThreadPool pool;
|
|
|
|
|
static const bool init = [] {
|
|
|
|
|
pool.setMaxThreadCount(std::max(2, QThread::idealThreadCount() / 2));
|
|
|
|
|
return true;
|
|
|
|
|
}();
|
|
|
|
|
(void)init;
|
|
|
|
|
|
|
|
|
|
return &pool;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
qreal CachingImageManager::effectiveScale() const {
|
|
|
|
|
if (m_item && m_item->window()) {
|
|
|
|
|
return m_item->window()->devicePixelRatio();
|
|
|
|
@@ -101,14 +120,13 @@ void CachingImageManager::updateSource() {
|
|
|
|
|
|
|
|
|
|
void CachingImageManager::updateSource(const QString& path) {
|
|
|
|
|
if (path.isEmpty() || path == m_shaPath) {
|
|
|
|
|
// Path is empty or already calculating sha for path
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_shaPath = path;
|
|
|
|
|
|
|
|
|
|
const auto future =
|
|
|
|
|
QtConcurrent::run(&CachingImageManager::sha256sum, path);
|
|
|
|
|
QtConcurrent::run(imageIoPool(), &CachingImageManager::sha256sum, path);
|
|
|
|
|
|
|
|
|
|
const auto watcher = new QFutureWatcher<QString>(this);
|
|
|
|
|
|
|
|
|
@@ -118,7 +136,16 @@ void CachingImageManager::updateSource(const QString& path) {
|
|
|
|
|
this,
|
|
|
|
|
[watcher, path, this]() {
|
|
|
|
|
if (m_path != path) {
|
|
|
|
|
// Object is destroyed or path has changed, ignore
|
|
|
|
|
watcher->deleteLater();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QString key = watcher->result();
|
|
|
|
|
|
|
|
|
|
if (key.isEmpty()) {
|
|
|
|
|
if (m_shaPath == path) {
|
|
|
|
|
m_shaPath = QString();
|
|
|
|
|
}
|
|
|
|
|
watcher->deleteLater();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@@ -126,19 +153,29 @@ void CachingImageManager::updateSource(const QString& path) {
|
|
|
|
|
const QSize size = effectiveSize();
|
|
|
|
|
|
|
|
|
|
if (!m_item || !size.width() || !size.height()) {
|
|
|
|
|
if (m_shaPath == path) {
|
|
|
|
|
m_shaPath = QString();
|
|
|
|
|
}
|
|
|
|
|
watcher->deleteLater();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QString fillMode = m_item->property("fillMode").toString();
|
|
|
|
|
// clang-format off
|
|
|
|
|
const QString filename = QString("%1@%2x%3-%4.png")
|
|
|
|
|
.arg(watcher->result()).arg(size.width()).arg(size.height())
|
|
|
|
|
.arg(fillMode == "PreserveAspectCrop" ? "crop" : fillMode == "PreserveAspectFit" ? "fit" : "stretch");
|
|
|
|
|
// clang-format on
|
|
|
|
|
const QString filename =
|
|
|
|
|
QString("%1@%2x%3-%4.png")
|
|
|
|
|
.arg(key)
|
|
|
|
|
.arg(size.width())
|
|
|
|
|
.arg(size.height())
|
|
|
|
|
.arg(
|
|
|
|
|
fillMode == "PreserveAspectCrop" ? "crop"
|
|
|
|
|
: fillMode == "PreserveAspectFit" ? "fit"
|
|
|
|
|
: "stretch");
|
|
|
|
|
|
|
|
|
|
const QUrl cache = m_cacheDir.resolved(QUrl(filename));
|
|
|
|
|
if (m_cachePath == cache) {
|
|
|
|
|
if (m_shaPath == path) {
|
|
|
|
|
m_shaPath = QString();
|
|
|
|
|
}
|
|
|
|
|
watcher->deleteLater();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@@ -149,6 +186,9 @@ void CachingImageManager::updateSource(const QString& path) {
|
|
|
|
|
if (!cache.isLocalFile()) {
|
|
|
|
|
qWarning() << "CachingImageManager::updateSource: cachePath"
|
|
|
|
|
<< cache << "is not a local file";
|
|
|
|
|
if (m_shaPath == path) {
|
|
|
|
|
m_shaPath = QString();
|
|
|
|
|
}
|
|
|
|
|
watcher->deleteLater();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@@ -161,7 +201,6 @@ void CachingImageManager::updateSource(const QString& path) {
|
|
|
|
|
createCache(path, cache.toLocalFile(), fillMode, size);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear current running sha if same
|
|
|
|
|
if (m_shaPath == path) {
|
|
|
|
|
m_shaPath = QString();
|
|
|
|
|
}
|
|
|
|
@@ -170,7 +209,6 @@ void CachingImageManager::updateSource(const QString& path) {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
watcher->setFuture(future);
|
|
|
|
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QUrl CachingImageManager::cachePath() const {
|
|
|
|
@@ -182,61 +220,105 @@ void CachingImageManager::createCache(
|
|
|
|
|
const QString& cache,
|
|
|
|
|
const QString& fillMode,
|
|
|
|
|
const QSize& size) const {
|
|
|
|
|
QThreadPool::globalInstance()->start([path, cache, fillMode, size] {
|
|
|
|
|
QImage image(path);
|
|
|
|
|
auto* watcher =
|
|
|
|
|
new QFutureWatcher<bool>(const_cast<CachingImageManager*>(this));
|
|
|
|
|
|
|
|
|
|
if (image.isNull()) {
|
|
|
|
|
qWarning() << "CachingImageManager::createCache: failed to read"
|
|
|
|
|
<< path;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
connect(
|
|
|
|
|
watcher,
|
|
|
|
|
&QFutureWatcher<bool>::finished,
|
|
|
|
|
this,
|
|
|
|
|
[this, watcher, cache, path]() {
|
|
|
|
|
if (watcher->result() && m_item && m_path == path &&
|
|
|
|
|
m_cachePath.toLocalFile() == cache) {
|
|
|
|
|
m_item->setProperty("source", QUrl::fromLocalFile(cache));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
image.convertTo(QImage::Format_ARGB32);
|
|
|
|
|
watcher->deleteLater();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (fillMode == "PreserveAspectCrop") {
|
|
|
|
|
image = image.scaled(
|
|
|
|
|
size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
|
|
|
|
|
} else if (fillMode == "PreserveAspectFit") {
|
|
|
|
|
image = image.scaled(
|
|
|
|
|
size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
|
|
|
} else {
|
|
|
|
|
image = image.scaled(
|
|
|
|
|
size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
|
|
|
|
}
|
|
|
|
|
const auto future =
|
|
|
|
|
QtConcurrent::run(
|
|
|
|
|
imageIoPool(), [path, cache, fillMode, size]() -> bool {
|
|
|
|
|
QImageReader reader(path);
|
|
|
|
|
|
|
|
|
|
if (fillMode == "PreserveAspectCrop" ||
|
|
|
|
|
fillMode == "PreserveAspectFit") {
|
|
|
|
|
QImage canvas(size, QImage::Format_ARGB32);
|
|
|
|
|
canvas.fill(Qt::transparent);
|
|
|
|
|
if (reader.supportsOption(QImageIOHandler::ScaledSize)) {
|
|
|
|
|
const QSize full = reader.size();
|
|
|
|
|
if (full.isValid() && !full.isEmpty()) {
|
|
|
|
|
const QSize hint = size * 2;
|
|
|
|
|
QSize scaled = full;
|
|
|
|
|
scaled.scale(hint, Qt::KeepAspectRatioByExpanding);
|
|
|
|
|
scaled = QSize(
|
|
|
|
|
std::min(scaled.width(), full.width()),
|
|
|
|
|
std::min(scaled.height(), full.height()));
|
|
|
|
|
reader.setScaledSize(scaled);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QPainter painter(&canvas);
|
|
|
|
|
painter.drawImage(
|
|
|
|
|
(size.width() - image.width()) / 2,
|
|
|
|
|
(size.height() - image.height()) / 2,
|
|
|
|
|
image);
|
|
|
|
|
painter.end();
|
|
|
|
|
QImage image = reader.read();
|
|
|
|
|
|
|
|
|
|
image = canvas;
|
|
|
|
|
}
|
|
|
|
|
if (image.isNull()) {
|
|
|
|
|
qWarning()
|
|
|
|
|
<< "CachingImageManager::createCache: failed to read"
|
|
|
|
|
<< path << reader.errorString();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QString parent = QFileInfo(cache).absolutePath();
|
|
|
|
|
if (!QDir().mkpath(parent) || !image.save(cache)) {
|
|
|
|
|
qWarning() << "CachingImageManager::createCache: failed to save to"
|
|
|
|
|
<< cache;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
image.convertTo(QImage::Format_ARGB32);
|
|
|
|
|
|
|
|
|
|
if (fillMode == "PreserveAspectCrop") {
|
|
|
|
|
image = image.scaled(
|
|
|
|
|
size,
|
|
|
|
|
Qt::KeepAspectRatioByExpanding,
|
|
|
|
|
Qt::SmoothTransformation);
|
|
|
|
|
} else if (fillMode == "PreserveAspectFit") {
|
|
|
|
|
image = image.scaled(
|
|
|
|
|
size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
|
|
|
} else {
|
|
|
|
|
image = image.scaled(
|
|
|
|
|
size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fillMode == "PreserveAspectCrop" ||
|
|
|
|
|
fillMode == "PreserveAspectFit") {
|
|
|
|
|
QImage canvas(size, QImage::Format_ARGB32);
|
|
|
|
|
canvas.fill(Qt::transparent);
|
|
|
|
|
|
|
|
|
|
QPainter painter(&canvas);
|
|
|
|
|
painter.drawImage(
|
|
|
|
|
(size.width() - image.width()) / 2,
|
|
|
|
|
(size.height() - image.height()) / 2,
|
|
|
|
|
image);
|
|
|
|
|
painter.end();
|
|
|
|
|
|
|
|
|
|
image = canvas;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QString parent = QFileInfo(cache).absolutePath();
|
|
|
|
|
if (!QDir().mkpath(parent) || !image.save(cache)) {
|
|
|
|
|
qWarning()
|
|
|
|
|
<< "CachingImageManager::createCache: failed to save to"
|
|
|
|
|
<< cache;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
watcher->setFuture(future);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString CachingImageManager::sha256sum(const QString& path) {
|
|
|
|
|
QFile file(path);
|
|
|
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
|
|
|
|
qWarning() << "CachingImageManager::sha256sum: failed to open" << path;
|
|
|
|
|
return "";
|
|
|
|
|
const QFileInfo info(path);
|
|
|
|
|
|
|
|
|
|
if (!info.exists() || !info.isFile()) {
|
|
|
|
|
qWarning() << "CachingImageManager::sha256sum: failed to stat" << path;
|
|
|
|
|
return QString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QCryptographicHash hash(QCryptographicHash::Sha256);
|
|
|
|
|
hash.addData(&file);
|
|
|
|
|
file.close();
|
|
|
|
|
hash.addData(path.toUtf8());
|
|
|
|
|
hash.addData(QByteArray::number(info.size()));
|
|
|
|
|
hash.addData(QByteArray::number(info.lastModified().toMSecsSinceEpoch()));
|
|
|
|
|
|
|
|
|
|
return hash.result().toHex();
|
|
|
|
|
}
|
|
|
|
|