feat: use image provider for caching
This commit is contained in:
parent
53dd959f5e
commit
411e013ea0
7 changed files with 282 additions and 19 deletions
|
|
@ -1,28 +1,14 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Caelestia.Internal
|
||||
import qs.utils
|
||||
import Caelestia.Images
|
||||
|
||||
Image {
|
||||
id: root
|
||||
|
||||
property alias path: manager.path
|
||||
property string path
|
||||
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
|
||||
Connections {
|
||||
function onDevicePixelRatioChanged(): void {
|
||||
manager.updateSource();
|
||||
}
|
||||
|
||||
target: QsWindow.window
|
||||
}
|
||||
|
||||
CachingImageManager {
|
||||
id: manager
|
||||
|
||||
item: root
|
||||
cacheDir: Qt.resolvedUrl(Paths.imagecache)
|
||||
}
|
||||
source: IUtils.urlForPath(path, fillMode)
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,3 +79,4 @@ add_subdirectory(Internal)
|
|||
add_subdirectory(Models)
|
||||
add_subdirectory(Services)
|
||||
add_subdirectory(Blobs)
|
||||
add_subdirectory(Images)
|
||||
|
|
|
|||
10
plugin/src/Caelestia/Images/CMakeLists.txt
Normal file
10
plugin/src/Caelestia/Images/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
qml_module(caelestia-images
|
||||
URI Caelestia.Images
|
||||
SOURCES
|
||||
cachingimageprovider.cpp
|
||||
iutils.cpp
|
||||
LIBRARIES
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
Qt::Concurrent
|
||||
)
|
||||
180
plugin/src/Caelestia/Images/cachingimageprovider.cpp
Normal file
180
plugin/src/Caelestia/Images/cachingimageprovider.cpp
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
#include "cachingimageprovider.hpp"
|
||||
|
||||
#include <qcryptographichash.h>
|
||||
#include <qdir.h>
|
||||
#include <qfile.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qimage.h>
|
||||
#include <qimagereader.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qpainter.h>
|
||||
#include <qrunnable.h>
|
||||
#include <qthreadpool.h>
|
||||
|
||||
Q_LOGGING_CATEGORY(lcCProv, "caelestia.images.cacheprovider", QtInfoMsg)
|
||||
|
||||
namespace caelestia::images {
|
||||
|
||||
namespace {
|
||||
|
||||
const QString& cacheDir() {
|
||||
static const QString s_dir = [] {
|
||||
QString cache = qEnvironmentVariable("XDG_CACHE_HOME");
|
||||
if (cache.isEmpty())
|
||||
cache = QDir::homePath() + QStringLiteral("/.cache");
|
||||
return cache + QStringLiteral("/caelestia/imagecache");
|
||||
}();
|
||||
return s_dir;
|
||||
}
|
||||
|
||||
QString sha256sum(const QString& path) {
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qCWarning(lcCProv).noquote() << "sha256sum: failed to open" << path;
|
||||
return {};
|
||||
}
|
||||
|
||||
QCryptographicHash hash(QCryptographicHash::Sha256);
|
||||
hash.addData(&file);
|
||||
file.close();
|
||||
|
||||
return hash.result().toHex();
|
||||
}
|
||||
|
||||
QString fillSuffix(CachingImageProvider::FillMode fillMode) {
|
||||
switch (fillMode) {
|
||||
case CachingImageProvider::FillMode::Crop:
|
||||
return QStringLiteral("crop");
|
||||
case CachingImageProvider::FillMode::Fit:
|
||||
return QStringLiteral("fit");
|
||||
default:
|
||||
return QStringLiteral("stretch");
|
||||
}
|
||||
}
|
||||
|
||||
class CachingImageResponse final : public QQuickImageResponse, public QRunnable {
|
||||
public:
|
||||
CachingImageResponse(const QString& id, const QSize& requestedSize, CachingImageProvider::FillMode fillMode)
|
||||
: m_id(id)
|
||||
, m_requestedSize(requestedSize)
|
||||
, m_fillMode(fillMode) {
|
||||
setAutoDelete(false);
|
||||
}
|
||||
|
||||
[[nodiscard]] QQuickTextureFactory* textureFactory() const override {
|
||||
return QQuickTextureFactory::textureFactoryForImage(m_image);
|
||||
}
|
||||
|
||||
[[nodiscard]] QString errorString() const override { return m_error; }
|
||||
|
||||
void run() override {
|
||||
process();
|
||||
emit finished();
|
||||
}
|
||||
|
||||
private:
|
||||
void process() {
|
||||
QString path = QString::fromUtf8(m_id.toUtf8().percentDecoded());
|
||||
if (!path.startsWith(QLatin1Char('/')))
|
||||
path.prepend(QLatin1Char('/'));
|
||||
|
||||
if (!QFileInfo::exists(path)) {
|
||||
m_error = QStringLiteral("Source file does not exist: ") + path;
|
||||
qCWarning(lcCProv).noquote() << m_error;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get size from requested size, or the source's original size
|
||||
QSize size = m_requestedSize;
|
||||
if (size.width() <= 0 || size.height() <= 0) {
|
||||
const QImageReader reader(path);
|
||||
size = reader.size();
|
||||
if (!size.isValid() || size.isEmpty()) {
|
||||
m_error = QStringLiteral("Could not determine size for: ") + path;
|
||||
qCWarning(lcCProv).noquote() << m_error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const QString sha = sha256sum(path);
|
||||
if (sha.isEmpty()) {
|
||||
m_error = QStringLiteral("Failed to hash: ") + path;
|
||||
return;
|
||||
}
|
||||
|
||||
// clang-format off
|
||||
const QString filename = QStringLiteral("%1@%2x%3-%4.png")
|
||||
.arg(sha).arg(size.width()).arg(size.height()).arg(fillSuffix(m_fillMode));
|
||||
// clang-format on
|
||||
const QString cache = cacheDir() + QLatin1Char('/') + filename;
|
||||
|
||||
// Check cache, if it already exists, set and return
|
||||
QImageReader cacheReader(cache);
|
||||
if (cacheReader.canRead()) {
|
||||
m_image = cacheReader.read();
|
||||
if (!m_image.isNull())
|
||||
return;
|
||||
}
|
||||
|
||||
QImage image(path);
|
||||
if (image.isNull()) {
|
||||
m_error = QStringLiteral("Failed to decode: ") + path;
|
||||
qCWarning(lcCProv).noquote() << m_error;
|
||||
return;
|
||||
}
|
||||
|
||||
image.convertTo(QImage::Format_ARGB32);
|
||||
|
||||
// Scale to requested size
|
||||
switch (m_fillMode) {
|
||||
case CachingImageProvider::FillMode::Crop:
|
||||
image = image.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
|
||||
break;
|
||||
case CachingImageProvider::FillMode::Fit:
|
||||
image = image.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
break;
|
||||
case CachingImageProvider::FillMode::Stretch:
|
||||
image = image.scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
break;
|
||||
}
|
||||
|
||||
if (m_fillMode == CachingImageProvider::FillMode::Stretch) {
|
||||
m_image = image;
|
||||
} else {
|
||||
// Crop or fit
|
||||
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();
|
||||
|
||||
m_image = canvas;
|
||||
}
|
||||
|
||||
const QString parent = QFileInfo(cache).absolutePath();
|
||||
if (QDir().mkpath(parent) && m_image.save(cache))
|
||||
qCDebug(lcCProv).noquote() << "Saved to" << cache;
|
||||
else
|
||||
qCWarning(lcCProv).noquote() << "Failed to save to" << cache;
|
||||
}
|
||||
|
||||
QString m_id;
|
||||
QSize m_requestedSize;
|
||||
CachingImageProvider::FillMode m_fillMode;
|
||||
QImage m_image;
|
||||
QString m_error;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
CachingImageProvider::CachingImageProvider(FillMode fillMode)
|
||||
: m_fillMode(fillMode) {}
|
||||
|
||||
QQuickImageResponse* CachingImageProvider::requestImageResponse(const QString& id, const QSize& requestedSize) {
|
||||
auto* const response = new CachingImageResponse(id, requestedSize, m_fillMode);
|
||||
QThreadPool::globalInstance()->start(response);
|
||||
return response;
|
||||
}
|
||||
|
||||
} // namespace caelestia::images
|
||||
23
plugin/src/Caelestia/Images/cachingimageprovider.hpp
Normal file
23
plugin/src/Caelestia/Images/cachingimageprovider.hpp
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#pragma once
|
||||
|
||||
#include <qquickimageprovider.h>
|
||||
|
||||
namespace caelestia::images {
|
||||
|
||||
class CachingImageProvider : public QQuickAsyncImageProvider {
|
||||
public:
|
||||
enum class FillMode {
|
||||
Crop,
|
||||
Fit,
|
||||
Stretch
|
||||
};
|
||||
|
||||
explicit CachingImageProvider(FillMode fillMode);
|
||||
|
||||
QQuickImageResponse* requestImageResponse(const QString& id, const QSize& requestedSize) override;
|
||||
|
||||
private:
|
||||
FillMode m_fillMode;
|
||||
};
|
||||
|
||||
} // namespace caelestia::images
|
||||
39
plugin/src/Caelestia/Images/iutils.cpp
Normal file
39
plugin/src/Caelestia/Images/iutils.cpp
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#include "iutils.hpp"
|
||||
|
||||
#include "cachingimageprovider.hpp"
|
||||
|
||||
namespace caelestia::images {
|
||||
|
||||
IUtils* IUtils::create(QQmlEngine* engine, QJSEngine* jsEngine) {
|
||||
Q_UNUSED(jsEngine);
|
||||
|
||||
engine->addImageProvider(QStringLiteral("ccache"), new CachingImageProvider(CachingImageProvider::FillMode::Crop));
|
||||
engine->addImageProvider(QStringLiteral("fcache"), new CachingImageProvider(CachingImageProvider::FillMode::Fit));
|
||||
engine->addImageProvider(
|
||||
QStringLiteral("scache"), new CachingImageProvider(CachingImageProvider::FillMode::Stretch));
|
||||
|
||||
return new IUtils(engine);
|
||||
}
|
||||
|
||||
QUrl IUtils::urlForPath(const QString& path, int fillMode) {
|
||||
QString prefix;
|
||||
switch (fillMode) {
|
||||
case 1: // Image.PreserveAspectFit
|
||||
prefix = QStringLiteral("fcache");
|
||||
break;
|
||||
case 2: // Image.PreserveAspectCrop
|
||||
prefix = QStringLiteral("ccache");
|
||||
break;
|
||||
default: // Image.Stretch or any other ones
|
||||
prefix = QStringLiteral("scache");
|
||||
break;
|
||||
}
|
||||
|
||||
QUrl url;
|
||||
url.setScheme(QStringLiteral("image"));
|
||||
url.setHost(prefix);
|
||||
url.setPath(path.startsWith(QLatin1Char('/')) ? path : QLatin1Char('/') + path);
|
||||
return url;
|
||||
}
|
||||
|
||||
} // namespace caelestia::images
|
||||
24
plugin/src/Caelestia/Images/iutils.hpp
Normal file
24
plugin/src/Caelestia/Images/iutils.hpp
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qurl.h>
|
||||
|
||||
namespace caelestia::images {
|
||||
|
||||
class IUtils : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
public:
|
||||
static IUtils* create(QQmlEngine* engine, QJSEngine* jsEngine);
|
||||
|
||||
Q_INVOKABLE static QUrl urlForPath(const QString& path, int fillMode);
|
||||
|
||||
private:
|
||||
explicit IUtils(QObject* parent = nullptr)
|
||||
: QObject(parent) {};
|
||||
};
|
||||
|
||||
} // namespace caelestia::images
|
||||
Loading…
Add table
Reference in a new issue