diff --git a/components/images/CachingImage.qml b/components/images/CachingImage.qml index 5c5f8bbb..1e932dba 100644 --- a/components/images/CachingImage.qml +++ b/components/images/CachingImage.qml @@ -1,28 +1,17 @@ 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: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); } } diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index ccb5f0c4..367d9e98 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -184,8 +184,10 @@ Item { source: Players.getArtUrl(Players.active) asynchronous: true fillMode: Image.PreserveAspectCrop - sourceSize.width: width - sourceSize.height: height + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); + } MouseArea { anchors.fill: parent diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index b8e78abd..a14e5e38 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Shapes +import Quickshell import Caelestia.Config import Caelestia.Services import qs.components @@ -109,8 +110,10 @@ Item { source: Players.getArtUrl(Players.active) asynchronous: true fillMode: Image.PreserveAspectCrop - sourceSize.width: width - sourceSize.height: height + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); + } } } diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index 58be068d..d09a894c 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -1,4 +1,5 @@ import QtQuick +import Quickshell import Caelestia.Config import Caelestia.Models import qs.components @@ -63,11 +64,13 @@ Item { } CachingImage { + anchors.fill: parent path: root.modelData.path smooth: !root.PathView.view.moving - cache: true - - anchors.fill: parent + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(image.implicitWidth * dpr, image.implicitHeight * dpr); + } } } diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml index 05c9daaf..c55333c7 100644 --- a/modules/lock/Media.qml +++ b/modules/lock/Media.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts +import Quickshell import Caelestia.Config import qs.components import qs.components.effects @@ -22,8 +23,10 @@ Item { asynchronous: true fillMode: Image.PreserveAspectCrop - sourceSize.width: width - sourceSize.height: height + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); + } layer.enabled: true layer.effect: OpacityMask { diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml index bf827579..2254dfe0 100644 --- a/modules/lock/NotifDock.qml +++ b/modules/lock/NotifDock.qml @@ -52,7 +52,7 @@ ColumnLayout { asynchronous: true source: Paths.absolutePath(Config.paths.lockNoNotifsPic) fillMode: Image.PreserveAspectFit - sourceSize.width: clipRect.width * 0.8 + sourceSize.width: clipRect.width * 0.8 * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1) layer.enabled: true layer.effect: Colouriser { diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 0a5b6d73..971b921a 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -73,8 +73,10 @@ StyledRect { Image { source: Qt.resolvedUrl(root.image) fillMode: Image.PreserveAspectCrop - sourceSize.width: TokenConfig.sizes.notifs.image - sourceSize.height: TokenConfig.sizes.notifs.image + sourceSize: { + const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1); + return Qt.size(size, size); + } cache: false asynchronous: true width: TokenConfig.sizes.notifs.image diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 70f58af0..fe6fd056 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -126,8 +126,10 @@ StyledRect { anchors.fill: parent source: Qt.resolvedUrl(root.modelData.image) fillMode: Image.PreserveAspectCrop - sourceSize.width: TokenConfig.sizes.notifs.image - sourceSize.height: TokenConfig.sizes.notifs.image + sourceSize: { + const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1); + return Qt.size(size, size); + } cache: false asynchronous: true } diff --git a/modules/session/Content.qml b/modules/session/Content.qml index bf16b148..e1ba2809 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -48,7 +48,7 @@ Column { AnimatedImage { width: Tokens.sizes.session.button height: Tokens.sizes.session.button - sourceSize.width: width + sourceSize.width: width * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1) playing: visible asynchronous: true diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index bf978ec0..4509ce26 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts +import Quickshell import Quickshell.Widgets import Caelestia.Config import qs.components @@ -98,7 +99,7 @@ Item { asynchronous: true source: Paths.absolutePath(Config.paths.noNotifsPic) fillMode: Image.PreserveAspectFit - sourceSize.width: clipRect.width * 0.8 + sourceSize.width: clipRect.width * 0.8 * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1) layer.enabled: true layer.effect: Colouriser { diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index 57ee06b4..2920af74 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -87,8 +87,10 @@ StyledRect { Image { source: Qt.resolvedUrl(root.image) fillMode: Image.PreserveAspectCrop - sourceSize.width: TokenConfig.sizes.notifs.image - sourceSize.height: TokenConfig.sizes.notifs.image + sourceSize: { + const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1); + return Qt.size(size, size); + } cache: false asynchronous: true width: TokenConfig.sizes.notifs.image diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt index 6f4ca980..6e73f4fe 100644 --- a/plugin/src/Caelestia/CMakeLists.txt +++ b/plugin/src/Caelestia/CMakeLists.txt @@ -79,3 +79,4 @@ add_subdirectory(Internal) add_subdirectory(Models) add_subdirectory(Services) add_subdirectory(Blobs) +add_subdirectory(Images) diff --git a/plugin/src/Caelestia/Images/CMakeLists.txt b/plugin/src/Caelestia/Images/CMakeLists.txt new file mode 100644 index 00000000..d869667d --- /dev/null +++ b/plugin/src/Caelestia/Images/CMakeLists.txt @@ -0,0 +1,11 @@ +qml_module(caelestia-images + URI Caelestia.Images + SOURCES + cachingimageprovider.cpp + imagecacher.cpp + iutils.cpp + LIBRARIES + Qt::Gui + Qt::Quick + Qt::Concurrent +) diff --git a/plugin/src/Caelestia/Images/cachingimageprovider.cpp b/plugin/src/Caelestia/Images/cachingimageprovider.cpp new file mode 100644 index 00000000..768e05da --- /dev/null +++ b/plugin/src/Caelestia/Images/cachingimageprovider.cpp @@ -0,0 +1,120 @@ +#include "cachingimageprovider.hpp" + +#include "imagecacher.hpp" + +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(lcCProv, "caelestia.images.cacheprovider", QtInfoMsg) + +namespace caelestia::images { + +namespace { + +class CachingImageResponse final : public QQuickImageResponse, public QRunnable { +public: + CachingImageResponse(const QString& id, const QSize& requestedSize, ImageCacher::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; + } + + QSize size = m_requestedSize; + const bool needsW = size.width() <= 0; + const bool needsH = size.height() <= 0; + + // If both dimensions are missing, return the original directly + if (needsW && needsH) { + qCDebug(lcCProv).noquote() << "Given source size is invalid, returning original:" << path; + m_image = QImage(path); + if (m_image.isNull()) { + m_error = QStringLiteral("Failed to decode source: ") + path; + qCWarning(lcCProv).noquote() << m_error; + } + return; + } + + // If one dimension is missing, derive it from the source aspect ratio + if (needsW || needsH) { + const QImageReader sourceReader(path); + const QSize sourceSize = sourceReader.size(); + if (!sourceSize.isValid() || sourceSize.isEmpty()) { + m_error = QStringLiteral("Could not determine source size for: ") + path; + qCWarning(lcCProv).noquote() << m_error; + return; + } + + if (needsW) + size.setWidth(qRound(size.height() * sourceSize.width() / static_cast(sourceSize.height()))); + else + size.setHeight(qRound(size.width() * sourceSize.height() / static_cast(sourceSize.width()))); + } + + // Try to use cached image + const auto cachePath = ImageCacher::cachePathFor(path, size, m_fillMode); + if (!cachePath.isEmpty()) { + QImageReader cacheReader(cachePath); + if (cacheReader.canRead()) { + m_image = cacheReader.read(); + if (!m_image.isNull()) + return; + } + } + + // Schedule cache job (this call will return the original image, but later ones will use cache) + ImageCacher::instance()->schedule(path, cachePath, size, m_fillMode); + + m_image = QImage(path); + if (m_image.isNull()) { + m_error = QStringLiteral("Failed to decode source: ") + path; + qCWarning(lcCProv).noquote() << m_error; + } + } + + QString m_id; + QSize m_requestedSize; + ImageCacher::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 diff --git a/plugin/src/Caelestia/Images/cachingimageprovider.hpp b/plugin/src/Caelestia/Images/cachingimageprovider.hpp new file mode 100644 index 00000000..97082c76 --- /dev/null +++ b/plugin/src/Caelestia/Images/cachingimageprovider.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "imagecacher.hpp" + +#include + +namespace caelestia::images { + +class CachingImageProvider : public QQuickAsyncImageProvider { +public: + using FillMode = ImageCacher::FillMode; + + explicit CachingImageProvider(FillMode fillMode); + + QQuickImageResponse* requestImageResponse(const QString& id, const QSize& requestedSize) override; + +private: + FillMode m_fillMode; +}; + +} // namespace caelestia::images diff --git a/plugin/src/Caelestia/Images/imagecacher.cpp b/plugin/src/Caelestia/Images/imagecacher.cpp new file mode 100644 index 00000000..dfef8f0b --- /dev/null +++ b/plugin/src/Caelestia/Images/imagecacher.cpp @@ -0,0 +1,159 @@ +#include "imagecacher.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(lcCacher, "caelestia.images.cacher", QtInfoMsg) + +namespace caelestia::images { + +namespace { + +QString sha256sum(const QString& path) { + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + qCWarning(lcCacher).noquote() << "sha256sum: failed to open" << path; + return {}; + } + + QCryptographicHash hash(QCryptographicHash::Sha256); + hash.addData(&file); + file.close(); + + return hash.result().toHex(); +} + +QString fillSuffix(ImageCacher::FillMode fillMode) { + switch (fillMode) { + case ImageCacher::FillMode::Crop: + return QStringLiteral("crop"); + case ImageCacher::FillMode::Fit: + return QStringLiteral("fit"); + default: + return QStringLiteral("stretch"); + } +} + +} // namespace + +const QString& ImageCacher::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 ImageCacher::cachePathFor(const QString& sourcePath, const QSize& size, FillMode fillMode) { + const QString sha = sha256sum(sourcePath); + if (sha.isEmpty()) + return {}; + + const QString filename = + QStringLiteral("%1@%2x%3-%4.png") + .arg(sha, QString::number(size.width()), QString::number(size.height()), fillSuffix(fillMode)); + + return cacheDir() + QLatin1Char('/') + filename; +} + +ImageCacher* ImageCacher::instance() { + static ImageCacher s_instance; + return &s_instance; +} + +ImageCacher::ImageCacher(QObject* parent) + : QObject(parent) {} + +void ImageCacher::schedule(const QString& sourcePath, const QSize& size, FillMode fillMode) { + schedule(sourcePath, cachePathFor(sourcePath, size, fillMode), size, fillMode); +} + +void ImageCacher::schedule(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode) { + if (cachePath.isEmpty()) + return; + + { + QMutexLocker locker(&m_mutex); + if (m_inflight.contains(cachePath)) + return; + m_inflight.insert(cachePath); + } + + QThreadPool::globalInstance()->start([this, sourcePath, cachePath, size, fillMode]() { + runJob(sourcePath, cachePath, size, fillMode); + QMutexLocker locker(&m_mutex); + m_inflight.remove(cachePath); + }); +} + +void ImageCacher::runJob(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode) { + if (QFile::exists(cachePath)) { + return; + } + + QImage image(sourcePath); + if (image.isNull()) { + qCWarning(lcCacher).noquote() << "Failed to decode source" << sourcePath; + return; + } + + Qt::AspectRatioMode scaleMode; + switch (fillMode) { + case FillMode::Crop: + scaleMode = Qt::KeepAspectRatioByExpanding; + break; + case FillMode::Fit: + scaleMode = Qt::KeepAspectRatio; + break; + case FillMode::Stretch: + scaleMode = Qt::IgnoreAspectRatio; + break; + } + + image.convertTo(QImage::Format_ARGB32); + image = image.scaled(size, scaleMode, Qt::SmoothTransformation); + + if (image.isNull()) { + qCWarning(lcCacher).noquote() << "Failed to scale" << sourcePath; + return; + } + + QImage canvas; + if (fillMode == FillMode::Stretch) { + canvas = image; + } else { + canvas = QImage(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(); + } + + const QString parent = QFileInfo(cachePath).absolutePath(); + if (!QDir().mkpath(parent)) { + qCWarning(lcCacher).noquote() << "Failed to create cache dir" << parent; + return; + } + + QSaveFile saveFile(cachePath); + if (!saveFile.open(QIODevice::WriteOnly) || !canvas.save(&saveFile, "PNG") || !saveFile.commit()) { + qCWarning( + lcCacher, "Failed to save to %s: %s", qUtf8Printable(cachePath), qUtf8Printable(saveFile.errorString())); + return; + } + + qCDebug(lcCacher).noquote() << "Saved to" << cachePath; +} + +} // namespace caelestia::images diff --git a/plugin/src/Caelestia/Images/imagecacher.hpp b/plugin/src/Caelestia/Images/imagecacher.hpp new file mode 100644 index 00000000..79608419 --- /dev/null +++ b/plugin/src/Caelestia/Images/imagecacher.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace caelestia::images { + +class ImageCacher : public QObject { + Q_OBJECT + +public: + enum class FillMode { + Crop, + Fit, + Stretch, + }; + + static ImageCacher* instance(); + + static const QString& cacheDir(); + static QString cachePathFor(const QString& sourcePath, const QSize& size, FillMode fillMode); + + void schedule(const QString& sourcePath, const QSize& size, FillMode fillMode); + void schedule(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode); + +private: + explicit ImageCacher(QObject* parent = nullptr); + + static void runJob(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode); + + QMutex m_mutex; + QSet m_inflight; +}; + +} // namespace caelestia::images diff --git a/plugin/src/Caelestia/Images/iutils.cpp b/plugin/src/Caelestia/Images/iutils.cpp new file mode 100644 index 00000000..996f93dd --- /dev/null +++ b/plugin/src/Caelestia/Images/iutils.cpp @@ -0,0 +1,42 @@ +#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) { + if (path.isEmpty()) + return QUrl(); + + 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 diff --git a/plugin/src/Caelestia/Images/iutils.hpp b/plugin/src/Caelestia/Images/iutils.hpp new file mode 100644 index 00000000..2187c8d0 --- /dev/null +++ b/plugin/src/Caelestia/Images/iutils.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +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 diff --git a/plugin/src/Caelestia/Internal/CMakeLists.txt b/plugin/src/Caelestia/Internal/CMakeLists.txt index bc4a6948..f4bbc5fd 100644 --- a/plugin/src/Caelestia/Internal/CMakeLists.txt +++ b/plugin/src/Caelestia/Internal/CMakeLists.txt @@ -1,19 +1,17 @@ qml_module(caelestia-internal URI Caelestia.Internal SOURCES - arcgauge.hpp arcgauge.cpp - cachingimagemanager.hpp cachingimagemanager.cpp - circularbuffer.hpp circularbuffer.cpp - circularindicatormanager.hpp circularindicatormanager.cpp - hyprdevices.hpp hyprdevices.cpp - hyprextras.hpp hyprextras.cpp - logindmanager.hpp logindmanager.cpp - sparklineitem.hpp sparklineitem.cpp - visualiserbars.hpp visualiserbars.cpp + arcgauge.cpp + circularbuffer.cpp + circularindicatormanager.cpp + hyprdevices.cpp + hyprextras.cpp + logindmanager.cpp + sparklineitem.cpp + visualiserbars.cpp LIBRARIES Qt::Gui Qt::Quick - Qt::Concurrent Qt::Network Qt::DBus ) diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp deleted file mode 100644 index 46152d2a..00000000 --- a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp +++ /dev/null @@ -1,213 +0,0 @@ -#include "cachingimagemanager.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -Q_LOGGING_CATEGORY(lcCim, "caelestia.internal.cim", QtInfoMsg) - -namespace caelestia::internal { - -qreal CachingImageManager::effectiveScale() const { - if (m_item && m_item->window()) { - return m_item->window()->devicePixelRatio(); - } - - return 1.0; -} - -QSize CachingImageManager::effectiveSize() const { - if (!m_item) { - return QSize(); - } - - const qreal scale = effectiveScale(); - const QSize size = QSizeF(m_item->width() * scale, m_item->height() * scale).toSize(); - m_item->setProperty("sourceSize", size); - return size; -} - -QQuickItem* CachingImageManager::item() const { - return m_item; -} - -void CachingImageManager::setItem(QQuickItem* item) { - if (m_item == item) { - return; - } - - if (m_widthConn) { - disconnect(m_widthConn); - } - if (m_heightConn) { - disconnect(m_heightConn); - } - - m_item = item; - emit itemChanged(); - - if (item) { - m_widthConn = connect(item, &QQuickItem::widthChanged, this, [this]() { - updateSource(); - }); - m_heightConn = connect(item, &QQuickItem::heightChanged, this, [this]() { - updateSource(); - }); - updateSource(); - } -} - -QUrl CachingImageManager::cacheDir() const { - return m_cacheDir; -} - -void CachingImageManager::setCacheDir(const QUrl& cacheDir) { - if (m_cacheDir == cacheDir) { - return; - } - - m_cacheDir = cacheDir; - if (!m_cacheDir.path().endsWith("/")) { - m_cacheDir.setPath(m_cacheDir.path() + "/"); - } - emit cacheDirChanged(); -} - -QString CachingImageManager::path() const { - return m_path; -} - -void CachingImageManager::setPath(const QString& path) { - if (m_path == path) { - return; - } - - m_path = path; - emit pathChanged(); - - if (!path.isEmpty()) { - updateSource(path); - } -} - -void CachingImageManager::updateSource() { - updateSource(m_path); -} - -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; - - QtConcurrent::run(&CachingImageManager::sha256sum, path).then(this, [path, this](const QString& sha) { - if (m_path != path) { - return; - } - - const QSize size = effectiveSize(); - - if (!m_item || !size.width() || !size.height()) { - return; - } - - const QString fillMode = m_item->property("fillMode").toString(); - // clang-format off - const QString filename = QString("%1@%2x%3-%4.png") - .arg(sha).arg(size.width()).arg(size.height()) - .arg(fillMode == "PreserveAspectCrop" ? "crop" : fillMode == "PreserveAspectFit" ? "fit" : "stretch"); - // clang-format on - - const QUrl cache = m_cacheDir.resolved(QUrl(filename)); - if (m_cachePath == cache) { - return; - } - - m_cachePath = cache; - emit cachePathChanged(); - - if (!cache.isLocalFile()) { - qCWarning(lcCim) << "updateSource: cachePath" << cache << "is not a local file"; - return; - } - - const QImageReader reader(cache.toLocalFile()); - if (reader.canRead()) { - m_item->setProperty("source", cache); - } else { - m_item->setProperty("source", QUrl::fromLocalFile(path)); - createCache(path, cache.toLocalFile(), fillMode, size); - } - - // Clear current running sha if same - if (m_shaPath == path) { - m_shaPath = QString(); - } - }); -} - -QUrl CachingImageManager::cachePath() const { - return m_cachePath; -} - -void CachingImageManager::createCache( - const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const { - QThreadPool::globalInstance()->start([path, cache, fillMode, size] { - QImage image(path); - - if (image.isNull()) { - qCWarning(lcCim) << "createCache: failed to read" << path; - return; - } - - 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)) { - qCWarning(lcCim) << "createCache: failed to save to" << cache; - } - }); -} - -QString CachingImageManager::sha256sum(const QString& path) { - QFile file(path); - if (!file.open(QIODevice::ReadOnly)) { - qCWarning(lcCim) << "sha256sum: failed to open" << path; - return ""; - } - - QCryptographicHash hash(QCryptographicHash::Sha256); - hash.addData(&file); - file.close(); - - return hash.result().toHex(); -} - -} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp b/plugin/src/Caelestia/Internal/cachingimagemanager.hpp deleted file mode 100644 index 1b707414..00000000 --- a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -namespace caelestia::internal { - -class CachingImageManager : public QObject { - Q_OBJECT - QML_ELEMENT - - Q_PROPERTY(QQuickItem* item READ item WRITE setItem NOTIFY itemChanged REQUIRED) - Q_PROPERTY(QUrl cacheDir READ cacheDir WRITE setCacheDir NOTIFY cacheDirChanged REQUIRED) - - Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) - Q_PROPERTY(QUrl cachePath READ cachePath NOTIFY cachePathChanged) - -public: - explicit CachingImageManager(QObject* parent = nullptr) - : QObject(parent) {} - - [[nodiscard]] QQuickItem* item() const; - void setItem(QQuickItem* item); - - [[nodiscard]] QUrl cacheDir() const; - void setCacheDir(const QUrl& cacheDir); - - [[nodiscard]] QString path() const; - void setPath(const QString& path); - - [[nodiscard]] QUrl cachePath() const; - - Q_INVOKABLE void updateSource(); - Q_INVOKABLE void updateSource(const QString& path); - -signals: - void itemChanged(); - void cacheDirChanged(); - - void pathChanged(); - void cachePathChanged(); - void usingCacheChanged(); - -private: - QString m_shaPath; - - QPointer m_item; - QUrl m_cacheDir; - - QString m_path; - QUrl m_cachePath; - - QMetaObject::Connection m_widthConn; - QMetaObject::Connection m_heightConn; - - [[nodiscard]] qreal effectiveScale() const; - [[nodiscard]] QSize effectiveSize() const; - - void createCache(const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const; - [[nodiscard]] static QString sha256sum(const QString& path); -}; - -} // namespace caelestia::internal