From afa3b0e3c402f7f5c7d27cb1bf48ddf1c54f422a Mon Sep 17 00:00:00 2001 From: zach Date: Wed, 27 May 2026 13:46:15 +0200 Subject: [PATCH] cache icons based on pixel content instead of image string --- Daemons/NotifServer.qml | 35 +---- Plugins/ZShell/writefile.cpp | 296 +++++++++++++++++------------------ Plugins/ZShell/writefile.hpp | 15 +- 3 files changed, 154 insertions(+), 192 deletions(-) diff --git a/Daemons/NotifServer.qml b/Daemons/NotifServer.qml index e5a560f..608ac03 100644 --- a/Daemons/NotifServer.qml +++ b/Daemons/NotifServer.qml @@ -218,7 +218,7 @@ Singleton { function onImageChanged(): void { notif.imageSource = notif.notification.image || ""; notif.image = notif.imageSource; - notif.cacheImageIfNeeded(); + notif.cacheImageIfNeeded(notif.imageSource); } function onResidentChanged(): void { @@ -283,29 +283,18 @@ Singleton { } property int urgency: NotificationUrgency.Normal - function cacheImageIfNeeded(): void { - const source = imageSource; - + function cacheImageIfNeeded(source: string): void { if (!source || cachingImage) return; if (cachedImageSource === source) return; - if (source.startsWith("file:")) { - cachedImageSource = source; - image = source; - return; - } - - const hash = hashForString(source); - const cache = `${Paths.notifimagecache}/${hash}.png`; - const cacheUrl = Qt.resolvedUrl(cache); - cachingImage = true; - ZShellIo.saveImage(source, cacheUrl, () => { + + ZShellIo.cacheImage(Qt.resolvedUrl(source), Paths.notifimagecache, (path, url) => { cachedImageSource = source; - image = cache; + image = path; cachingImage = false; }, () => { cachingImage = false; @@ -321,20 +310,6 @@ Singleton { } } - function hashForString(s: string): string { - let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch; - for (let i = 0; i < s.length; i++) { - ch = s.charCodeAt(i); - h1 = Math.imul(h1 ^ ch, 2654435761); - h2 = Math.imul(h2 ^ ch, 1597334677); - } - h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); - h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); - h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); - h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); - return (h2 >>> 0).toString(16).padStart(8, "0") + (h1 >>> 0).toString(16).padStart(8, "0"); - } - function lock(item: Item): void { locks.add(item); } diff --git a/Plugins/ZShell/writefile.cpp b/Plugins/ZShell/writefile.cpp index 195edd4..504ba86 100644 --- a/Plugins/ZShell/writefile.cpp +++ b/Plugins/ZShell/writefile.cpp @@ -1,17 +1,22 @@ #include "writefile.hpp" #include +#include +#include +#include #include #include #include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include namespace ZShell { @@ -84,54 +89,51 @@ void ZShellIo::saveItem( &QQuickItemGrabResult::ready, this, [grabResult, scaledRect, path, onSaved, onFailed, this]() { + const auto future = QtConcurrent::run([grabResult, scaledRect, path]() { + QImage image = grabResult->image(); - QImage image = grabResult->image(); - - if (scaledRect.isValid()) { - image = image.copy(scaledRect); - } - - const auto future = QtConcurrent::run([image, path]() { + if (scaledRect.isValid()) { + image = image.copy(scaledRect); + } const QString file = path.toLocalFile(); const QString parent = QFileInfo(file).absolutePath(); - return QDir().mkpath(parent) && image.save(file); + QDir().mkpath(parent); + + QSaveFile out(file); + if (!out.open(QIODevice::WriteOnly)) { + return false; + } + + if (!image.save(&out, "PNG")) { + return false; + } + + return out.commit(); }); auto* watcher = new QFutureWatcher(this); auto* engine = qmlEngine(this); - QObject::connect( - watcher, - &QFutureWatcher::finished, - this, - [=]() { - + QObject::connect(watcher, &QFutureWatcher::finished, this, [=]() { if (watcher->result()) { - - if (onSaved.isCallable()) { + if (onSaved.isCallable() && engine) { onSaved.call({ - QJSValue(path.toLocalFile()), - engine->toScriptValue(QVariant::fromValue(path)) + engine->toScriptValue(path.toLocalFile()), + engine->toScriptValue(path) }); } - } else { - - qWarning() << "ZShellIo::saveItem: failed to save" - << path; - - if (onFailed.isCallable()) { + qWarning() << "ZShellIo::saveItem: failed to save" << path; + if (onFailed.isCallable() && engine) { onFailed.call({ - engine->toScriptValue(QVariant::fromValue(path)) + engine->toScriptValue(path) }); } } - watcher->deleteLater(); - } - ); + }); watcher->setFuture(future); } @@ -139,106 +141,130 @@ void ZShellIo::saveItem( } // ============================================================ -// saveImage +// cacheImage // ============================================================ -void ZShellIo::saveImage(const QUrl& source, const QUrl& target) { - this->saveImage(source, target, QJSValue(), QJSValue()); +void ZShellIo::cacheImage(const QUrl& source, const QString& cacheDir) { + this->cacheImage(source, cacheDir, QJSValue(), QJSValue()); } -void ZShellIo::saveImage(const QUrl& source, const QUrl& target, QJSValue onSaved) { - this->saveImage(source, target, onSaved, QJSValue()); +void ZShellIo::cacheImage(const QUrl& source, const QString& cacheDir, QJSValue onSaved) { + this->cacheImage(source, cacheDir, onSaved, QJSValue()); } -void ZShellIo::saveImage( +void ZShellIo::cacheImage( const QUrl& source, - const QUrl& target, + const QString& cacheDir, QJSValue onSaved, QJSValue onFailed ) { - auto* engine = qmlEngine(this); + if (cacheDir.isEmpty()) { + qWarning() << "ZShellIo::cacheImage: cacheDir is empty"; + return; + } - const auto future = QtConcurrent::run([this, source, target]() { - return this->saveImageInternal(source, target); + QImage image; + if (!loadSourceImage(source, image)) { + qWarning() << "ZShellIo::cacheImage: failed to load source image" << source; + auto* engine = qmlEngine(this); + if (onFailed.isCallable() && engine) { + onFailed.call({ + engine->toScriptValue(source), + engine->toScriptValue(cacheDir) + }); + } + return; + } + + const auto future = QtConcurrent::run([image, cacheDir]() -> QString { + if (image.isNull()) { + return QString(); + } + + const QImage normalized = image.convertToFormat(QImage::Format_RGBA8888); + + const QByteArray bytes( + reinterpret_cast(normalized.constBits()), + qsizetype(normalized.sizeInBytes()) + ); + + const QByteArray digest = + QCryptographicHash::hash(bytes, QCryptographicHash::Sha256).toHex(); + + QDir dir(cacheDir); + if (!dir.exists() && !QDir().mkpath(cacheDir)) { + return QString(); + } + + const QString finalPath = dir.filePath(QString::fromLatin1(digest) + ".png"); + + if (QFile::exists(finalPath)) { + return finalPath; + } + + QSaveFile out(finalPath); + if (!out.open(QIODevice::WriteOnly)) { + return QString(); + } + + if (!normalized.save(&out, "PNG")) { + return QString(); + } + + if (!out.commit()) { + return QString(); + } + + return finalPath; }); - auto* watcher = new QFutureWatcher(this); + auto* watcher = new QFutureWatcher(this); + auto* engine = qmlEngine(this); - QObject::connect( - watcher, - &QFutureWatcher::finished, - this, - [=]() { + QObject::connect(watcher, &QFutureWatcher::finished, this, [=]() { + const QString finalPath = watcher->result(); - if (watcher->result()) { - - if (onSaved.isCallable()) { + if (!finalPath.isEmpty()) { + if (onSaved.isCallable() && engine) { onSaved.call({ - QJSValue(target.toLocalFile()), - engine->toScriptValue(QVariant::fromValue(target)) + engine->toScriptValue(finalPath), + engine->toScriptValue(QUrl::fromLocalFile(finalPath)) }); } - } else { - - qWarning() << "ZShellIo::saveImage: failed to save" - << source - << "to" - << target; - - if (onFailed.isCallable()) { + qWarning() << "ZShellIo::cacheImage: failed to cache" << source; + if (onFailed.isCallable() && engine) { onFailed.call({ - engine->toScriptValue(QVariant::fromValue(target)) + engine->toScriptValue(source), + engine->toScriptValue(cacheDir) }); } } watcher->deleteLater(); - } - ); + }); watcher->setFuture(future); } -bool ZShellIo::saveImageInternal(const QUrl& source, const QUrl& target) const { +// ============================================================ +// loadSourceImage +// ============================================================ - if (!target.isLocalFile()) { - qWarning() << "ZShellIo::saveImage: target" - << target - << "is not a local file"; - return false; - } - - const QString targetFile = target.toLocalFile(); - - if (!QDir().mkpath(QFileInfo(targetFile).absolutePath())) { - return false; - } - - // ======================================================== - // Local file path - // ======================================================== +bool ZShellIo::loadSourceImage(const QUrl& source, QImage& image) const { + image = QImage(); if (source.isLocalFile()) { - - QFile::remove(targetFile); - - return QFile::copy( - source.toLocalFile(), - targetFile - ); + QImageReader reader(source.toLocalFile()); + reader.setAutoTransform(true); + image = reader.read(); + return !image.isNull(); } - // ======================================================== - // image:// provider path - // ======================================================== - if (source.scheme() == "image") { - auto* engine = qmlEngine(const_cast(this)); - if (!engine) { - qWarning() << "ZShellIo::saveImage: no QQmlEngine"; + qWarning() << "ZShellIo::loadSourceImage: no QQmlEngine"; return false; } @@ -246,70 +272,44 @@ bool ZShellIo::saveImageInternal(const QUrl& source, const QUrl& target) const { const QString imageId = source.path().startsWith('/') - ? source.path().mid(1) - : source.path(); - - auto* providerBase = - engine->imageProvider(providerId); + ? source.path().mid(1) + : source.path(); + auto* providerBase = engine->imageProvider(providerId); if (!providerBase) { - qWarning() << "ZShellIo::saveImage: provider not found" + qWarning() << "ZShellIo::loadSourceImage: provider not found" << providerId; return false; } - auto* provider = - dynamic_cast(providerBase); - + auto* provider = dynamic_cast(providerBase); if (!provider) { - qWarning() << "ZShellIo::saveImage: provider is not a QQuickImageProvider" - << providerId; - return false; - } - - if (!provider) { - qWarning() << "ZShellIo::saveImage: provider not found" + qWarning() << "ZShellIo::loadSourceImage: provider is not a QQuickImageProvider" << providerId; return false; } QSize size; - QImage image; switch (provider->imageType()) { - case QQuickImageProvider::Image: - image = provider->requestImage( - imageId, - &size, - QSize() - ); + image = provider->requestImage(imageId, &size, QSize()); break; case QQuickImageProvider::Pixmap: - image = provider->requestPixmap( - imageId, - &size, - QSize() - ).toImage(); + image = provider->requestPixmap(imageId, &size, QSize()).toImage(); break; default: - qWarning() << "ZShellIo::saveImage: unsupported provider type"; + qWarning() << "ZShellIo::loadSourceImage: unsupported provider type" + << providerId; return false; } - if (image.isNull()) { - qWarning() << "ZShellIo::saveImage: provider returned null image"; - return false; - } - - return image.save(targetFile); + return !image.isNull(); } - qWarning() << "ZShellIo::saveImage: unsupported source" - << source; - + qWarning() << "ZShellIo::loadSourceImage: unsupported source" << source; return false; } @@ -318,18 +318,12 @@ bool ZShellIo::saveImageInternal(const QUrl& source, const QUrl& target) const { // ============================================================ bool ZShellIo::copyFile(const QUrl& source, const QUrl& target, bool overwrite) const { - if (!source.isLocalFile()) { - qWarning() << "ZShellIo::copyFile: source" - << source - << "is not a local file"; + qWarning() << "ZShellIo::copyFile: source" << source << "is not a local file"; return false; } - if (!target.isLocalFile()) { - qWarning() << "ZShellIo::copyFile: target" - << target - << "is not a local file"; + qWarning() << "ZShellIo::copyFile: target" << target << "is not a local file"; return false; } @@ -337,18 +331,12 @@ bool ZShellIo::copyFile(const QUrl& source, const QUrl& target, bool overwrite) QFile::remove(target.toLocalFile()); } - return QFile::copy( - source.toLocalFile(), - target.toLocalFile() - ); + return QFile::copy(source.toLocalFile(), target.toLocalFile()); } bool ZShellIo::deleteFile(const QUrl& path) const { - if (!path.isLocalFile()) { - qWarning() << "ZShellIo::deleteFile: path" - << path - << "is not a local file"; + qWarning() << "ZShellIo::deleteFile: path" << path << "is not a local file"; return false; } @@ -356,10 +344,8 @@ bool ZShellIo::deleteFile(const QUrl& path) const { } QString ZShellIo::toLocalFile(const QUrl& url) const { - if (!url.isLocalFile()) { - qWarning() << "ZShellIo::toLocalFile: given url is not a local file" - << url; + qWarning() << "ZShellIo::toLocalFile: given url is not a local file" << url; return QString(); } diff --git a/Plugins/ZShell/writefile.hpp b/Plugins/ZShell/writefile.hpp index f518da9..a74d897 100644 --- a/Plugins/ZShell/writefile.hpp +++ b/Plugins/ZShell/writefile.hpp @@ -1,10 +1,11 @@ #pragma once #include -#include -#include +#include +#include +#include #include -#include +#include namespace ZShell { @@ -23,9 +24,9 @@ Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved); Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed); -Q_INVOKABLE void saveImage(const QUrl& source, const QUrl& target); -Q_INVOKABLE void saveImage(const QUrl& source, const QUrl& target, QJSValue onSaved); -Q_INVOKABLE void saveImage(const QUrl& source, const QUrl& target, QJSValue onSaved, QJSValue onFailed); +Q_INVOKABLE void cacheImage(const QUrl& source, const QString& cacheDir); +Q_INVOKABLE void cacheImage(const QUrl& source, const QString& cacheDir, QJSValue onSaved); +Q_INVOKABLE void cacheImage(const QUrl& source, const QString& cacheDir, QJSValue onSaved, QJSValue onFailed); // clang-format on Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const; @@ -33,7 +34,7 @@ Q_INVOKABLE bool deleteFile(const QUrl& path) const; Q_INVOKABLE QString toLocalFile(const QUrl& url) const; private: -bool saveImageInternal(const QUrl& source, const QUrl& target) const; +bool loadSourceImage(const QUrl& source, QImage& image) const; }; } // namespace ZShell