cache icons based on pixel content instead of image string

This commit is contained in:
2026-05-27 13:46:15 +02:00
parent 41c9d9e9b4
commit afa3b0e3c4
3 changed files with 154 additions and 192 deletions
+5 -30
View File
@@ -218,7 +218,7 @@ Singleton {
function onImageChanged(): void { function onImageChanged(): void {
notif.imageSource = notif.notification.image || ""; notif.imageSource = notif.notification.image || "";
notif.image = notif.imageSource; notif.image = notif.imageSource;
notif.cacheImageIfNeeded(); notif.cacheImageIfNeeded(notif.imageSource);
} }
function onResidentChanged(): void { function onResidentChanged(): void {
@@ -283,29 +283,18 @@ Singleton {
} }
property int urgency: NotificationUrgency.Normal property int urgency: NotificationUrgency.Normal
function cacheImageIfNeeded(): void { function cacheImageIfNeeded(source: string): void {
const source = imageSource;
if (!source || cachingImage) if (!source || cachingImage)
return; return;
if (cachedImageSource === source) if (cachedImageSource === source)
return; 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; cachingImage = true;
ZShellIo.saveImage(source, cacheUrl, () => {
ZShellIo.cacheImage(Qt.resolvedUrl(source), Paths.notifimagecache, (path, url) => {
cachedImageSource = source; cachedImageSource = source;
image = cache; image = path;
cachingImage = false; cachingImage = false;
}, () => { }, () => {
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 { function lock(item: Item): void {
locks.add(item); locks.add(item);
} }
+142 -156
View File
@@ -1,17 +1,22 @@
#include "writefile.hpp" #include "writefile.hpp"
#include <QtConcurrent/qtconcurrentrun.h> #include <QtConcurrent/qtconcurrentrun.h>
#include <QtCore/QCryptographicHash>
#include <QtCore/QSaveFile>
#include <QtGui/QImageReader>
#include <QtQuick/qquickimageprovider.h> #include <QtQuick/qquickimageprovider.h>
#include <QtQuick/qquickitemgrabresult.h> #include <QtQuick/qquickitemgrabresult.h>
#include <QtQuick/qquickwindow.h> #include <QtQuick/qquickwindow.h>
#include <qdir.h> #include <QDir>
#include <qfile.h> #include <QFile>
#include <qfileinfo.h> #include <QFileInfo>
#include <qfuturewatcher.h> #include <QFutureWatcher>
#include <qimage.h> #include <QImage>
#include <qjsengine.h> #include <QJSValue>
#include <qqmlengine.h> #include <QQmlEngine>
#include <QSize>
#include <QVariant>
namespace ZShell { namespace ZShell {
@@ -84,54 +89,51 @@ void ZShellIo::saveItem(
&QQuickItemGrabResult::ready, &QQuickItemGrabResult::ready,
this, this,
[grabResult, scaledRect, path, onSaved, onFailed, 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()) { if (scaledRect.isValid()) {
image = image.copy(scaledRect); image = image.copy(scaledRect);
} }
const auto future = QtConcurrent::run([image, path]() {
const QString file = path.toLocalFile(); const QString file = path.toLocalFile();
const QString parent = QFileInfo(file).absolutePath(); 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<bool>(this); auto* watcher = new QFutureWatcher<bool>(this);
auto* engine = qmlEngine(this); auto* engine = qmlEngine(this);
QObject::connect( QObject::connect(watcher, &QFutureWatcher<bool>::finished, this, [=]() {
watcher,
&QFutureWatcher<bool>::finished,
this,
[=]() {
if (watcher->result()) { if (watcher->result()) {
if (onSaved.isCallable() && engine) {
if (onSaved.isCallable()) {
onSaved.call({ onSaved.call({
QJSValue(path.toLocalFile()), engine->toScriptValue(path.toLocalFile()),
engine->toScriptValue(QVariant::fromValue(path)) engine->toScriptValue(path)
}); });
} }
} else { } else {
qWarning() << "ZShellIo::saveItem: failed to save" << path;
qWarning() << "ZShellIo::saveItem: failed to save" if (onFailed.isCallable() && engine) {
<< path;
if (onFailed.isCallable()) {
onFailed.call({ onFailed.call({
engine->toScriptValue(QVariant::fromValue(path)) engine->toScriptValue(path)
}); });
} }
} }
watcher->deleteLater(); watcher->deleteLater();
} });
);
watcher->setFuture(future); watcher->setFuture(future);
} }
@@ -139,106 +141,130 @@ void ZShellIo::saveItem(
} }
// ============================================================ // ============================================================
// saveImage // cacheImage
// ============================================================ // ============================================================
void ZShellIo::saveImage(const QUrl& source, const QUrl& target) { void ZShellIo::cacheImage(const QUrl& source, const QString& cacheDir) {
this->saveImage(source, target, QJSValue(), QJSValue()); this->cacheImage(source, cacheDir, QJSValue(), QJSValue());
} }
void ZShellIo::saveImage(const QUrl& source, const QUrl& target, QJSValue onSaved) { void ZShellIo::cacheImage(const QUrl& source, const QString& cacheDir, QJSValue onSaved) {
this->saveImage(source, target, onSaved, QJSValue()); this->cacheImage(source, cacheDir, onSaved, QJSValue());
} }
void ZShellIo::saveImage( void ZShellIo::cacheImage(
const QUrl& source, const QUrl& source,
const QUrl& target, const QString& cacheDir,
QJSValue onSaved, QJSValue onSaved,
QJSValue onFailed QJSValue onFailed
) { ) {
auto* engine = qmlEngine(this); if (cacheDir.isEmpty()) {
qWarning() << "ZShellIo::cacheImage: cacheDir is empty";
const auto future = QtConcurrent::run([this, source, target]() { return;
return this->saveImageInternal(source, target);
});
auto* watcher = new QFutureWatcher<bool>(this);
QObject::connect(
watcher,
&QFutureWatcher<bool>::finished,
this,
[=]() {
if (watcher->result()) {
if (onSaved.isCallable()) {
onSaved.call({
QJSValue(target.toLocalFile()),
engine->toScriptValue(QVariant::fromValue(target))
});
} }
} else { QImage image;
if (!loadSourceImage(source, image)) {
qWarning() << "ZShellIo::saveImage: failed to save" qWarning() << "ZShellIo::cacheImage: failed to load source image" << source;
<< source auto* engine = qmlEngine(this);
<< "to" if (onFailed.isCallable() && engine) {
<< target;
if (onFailed.isCallable()) {
onFailed.call({ onFailed.call({
engine->toScriptValue(QVariant::fromValue(target)) 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<const char*>(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<QString>(this);
auto* engine = qmlEngine(this);
QObject::connect(watcher, &QFutureWatcher<QString>::finished, this, [=]() {
const QString finalPath = watcher->result();
if (!finalPath.isEmpty()) {
if (onSaved.isCallable() && engine) {
onSaved.call({
engine->toScriptValue(finalPath),
engine->toScriptValue(QUrl::fromLocalFile(finalPath))
});
}
} else {
qWarning() << "ZShellIo::cacheImage: failed to cache" << source;
if (onFailed.isCallable() && engine) {
onFailed.call({
engine->toScriptValue(source),
engine->toScriptValue(cacheDir)
}); });
} }
} }
watcher->deleteLater(); watcher->deleteLater();
} });
);
watcher->setFuture(future); watcher->setFuture(future);
} }
bool ZShellIo::saveImageInternal(const QUrl& source, const QUrl& target) const { // ============================================================
// loadSourceImage
// ============================================================
if (!target.isLocalFile()) { bool ZShellIo::loadSourceImage(const QUrl& source, QImage& image) const {
qWarning() << "ZShellIo::saveImage: target" image = QImage();
<< target
<< "is not a local file";
return false;
}
const QString targetFile = target.toLocalFile();
if (!QDir().mkpath(QFileInfo(targetFile).absolutePath())) {
return false;
}
// ========================================================
// Local file path
// ========================================================
if (source.isLocalFile()) { if (source.isLocalFile()) {
QImageReader reader(source.toLocalFile());
QFile::remove(targetFile); reader.setAutoTransform(true);
image = reader.read();
return QFile::copy( return !image.isNull();
source.toLocalFile(),
targetFile
);
} }
// ========================================================
// image:// provider path
// ========================================================
if (source.scheme() == "image") { if (source.scheme() == "image") {
auto* engine = qmlEngine(const_cast<ZShellIo*>(this)); auto* engine = qmlEngine(const_cast<ZShellIo*>(this));
if (!engine) { if (!engine) {
qWarning() << "ZShellIo::saveImage: no QQmlEngine"; qWarning() << "ZShellIo::loadSourceImage: no QQmlEngine";
return false; return false;
} }
@@ -249,67 +275,41 @@ bool ZShellIo::saveImageInternal(const QUrl& source, const QUrl& target) const {
? source.path().mid(1) ? source.path().mid(1)
: source.path(); : source.path();
auto* providerBase = auto* providerBase = engine->imageProvider(providerId);
engine->imageProvider(providerId);
if (!providerBase) { if (!providerBase) {
qWarning() << "ZShellIo::saveImage: provider not found" qWarning() << "ZShellIo::loadSourceImage: provider not found"
<< providerId; << providerId;
return false; return false;
} }
auto* provider = auto* provider = dynamic_cast<QQuickImageProvider*>(providerBase);
dynamic_cast<QQuickImageProvider*>(providerBase);
if (!provider) { if (!provider) {
qWarning() << "ZShellIo::saveImage: provider is not a QQuickImageProvider" qWarning() << "ZShellIo::loadSourceImage: provider is not a QQuickImageProvider"
<< providerId;
return false;
}
if (!provider) {
qWarning() << "ZShellIo::saveImage: provider not found"
<< providerId; << providerId;
return false; return false;
} }
QSize size; QSize size;
QImage image;
switch (provider->imageType()) { switch (provider->imageType()) {
case QQuickImageProvider::Image: case QQuickImageProvider::Image:
image = provider->requestImage( image = provider->requestImage(imageId, &size, QSize());
imageId,
&size,
QSize()
);
break; break;
case QQuickImageProvider::Pixmap: case QQuickImageProvider::Pixmap:
image = provider->requestPixmap( image = provider->requestPixmap(imageId, &size, QSize()).toImage();
imageId,
&size,
QSize()
).toImage();
break; break;
default: default:
qWarning() << "ZShellIo::saveImage: unsupported provider type"; qWarning() << "ZShellIo::loadSourceImage: unsupported provider type"
<< providerId;
return false; return false;
} }
if (image.isNull()) { return !image.isNull();
qWarning() << "ZShellIo::saveImage: provider returned null image";
return false;
} }
return image.save(targetFile); qWarning() << "ZShellIo::loadSourceImage: unsupported source" << source;
}
qWarning() << "ZShellIo::saveImage: unsupported source"
<< source;
return false; 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 { bool ZShellIo::copyFile(const QUrl& source, const QUrl& target, bool overwrite) const {
if (!source.isLocalFile()) { if (!source.isLocalFile()) {
qWarning() << "ZShellIo::copyFile: source" qWarning() << "ZShellIo::copyFile: source" << source << "is not a local file";
<< source
<< "is not a local file";
return false; return false;
} }
if (!target.isLocalFile()) { if (!target.isLocalFile()) {
qWarning() << "ZShellIo::copyFile: target" qWarning() << "ZShellIo::copyFile: target" << target << "is not a local file";
<< target
<< "is not a local file";
return false; return false;
} }
@@ -337,18 +331,12 @@ bool ZShellIo::copyFile(const QUrl& source, const QUrl& target, bool overwrite)
QFile::remove(target.toLocalFile()); QFile::remove(target.toLocalFile());
} }
return QFile::copy( return QFile::copy(source.toLocalFile(), target.toLocalFile());
source.toLocalFile(),
target.toLocalFile()
);
} }
bool ZShellIo::deleteFile(const QUrl& path) const { bool ZShellIo::deleteFile(const QUrl& path) const {
if (!path.isLocalFile()) { if (!path.isLocalFile()) {
qWarning() << "ZShellIo::deleteFile: path" qWarning() << "ZShellIo::deleteFile: path" << path << "is not a local file";
<< path
<< "is not a local file";
return false; return false;
} }
@@ -356,10 +344,8 @@ bool ZShellIo::deleteFile(const QUrl& path) const {
} }
QString ZShellIo::toLocalFile(const QUrl& url) const { QString ZShellIo::toLocalFile(const QUrl& url) const {
if (!url.isLocalFile()) { if (!url.isLocalFile()) {
qWarning() << "ZShellIo::toLocalFile: given url is not a local file" qWarning() << "ZShellIo::toLocalFile: given url is not a local file" << url;
<< url;
return QString(); return QString();
} }
+8 -7
View File
@@ -1,10 +1,11 @@
#pragma once #pragma once
#include <QtQuick/qquickitem.h> #include <QtQuick/qquickitem.h>
#include <qjsvalue.h> #include <QImage>
#include <qobject.h> #include <QJSValue>
#include <QObject>
#include <qqmlintegration.h> #include <qqmlintegration.h>
#include <qurl.h> #include <QUrl>
namespace ZShell { 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);
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed); 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 cacheImage(const QUrl& source, const QString& cacheDir);
Q_INVOKABLE void saveImage(const QUrl& source, const QUrl& target, QJSValue onSaved); Q_INVOKABLE void cacheImage(const QUrl& source, const QString& cacheDir, 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, QJSValue onSaved, QJSValue onFailed);
// clang-format on // clang-format on
Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const; 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; Q_INVOKABLE QString toLocalFile(const QUrl& url) const;
private: private:
bool saveImageInternal(const QUrl& source, const QUrl& target) const; bool loadSourceImage(const QUrl& source, QImage& image) const;
}; };
} // namespace ZShell } // namespace ZShell