Merge branch 'main' into feat/m3-revamp

This commit is contained in:
2 * r + 2 * t 2026-04-30 02:13:19 +10:00
commit 91b09253d8
22 changed files with 459 additions and 315 deletions

View file

@ -1,28 +1,17 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Caelestia.Internal import Caelestia.Images
import qs.utils
Image { Image {
id: root id: root
property alias path: manager.path property string path
asynchronous: true asynchronous: true
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
source: IUtils.urlForPath(path, fillMode)
Connections { sourceSize: {
function onDevicePixelRatioChanged(): void { const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
manager.updateSource(); return Qt.size(width * dpr, height * dpr);
}
target: QsWindow.window
}
CachingImageManager {
id: manager
item: root
cacheDir: Qt.resolvedUrl(Paths.imagecache)
} }
} }

View file

@ -184,8 +184,10 @@ Item {
source: Players.getArtUrl(Players.active) source: Players.getArtUrl(Players.active)
asynchronous: true asynchronous: true
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
sourceSize.width: width sourceSize: {
sourceSize.height: height const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
}
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent

View file

@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Effects import QtQuick.Effects
import Quickshell
import M3Shapes import M3Shapes
import Caelestia.Config import Caelestia.Config
import Caelestia.Services import Caelestia.Services
@ -150,8 +151,11 @@ Item {
source: Players.getArtUrl(Players.active) source: Players.getArtUrl(Players.active)
asynchronous: true asynchronous: true
fillMode: Image.PreserveAspectCrop 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.enabled: true
layer.effect: Mask { layer.effect: Mask {

View file

@ -1,4 +1,5 @@
import QtQuick import QtQuick
import Quickshell
import Caelestia.Config import Caelestia.Config
import Caelestia.Models import Caelestia.Models
import qs.components import qs.components
@ -64,11 +65,13 @@ Item {
} }
CachingImage { CachingImage {
anchors.fill: parent
path: root.modelData.path path: root.modelData.path
smooth: !root.PathView.view.moving smooth: !root.PathView.view.moving
cache: true sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
anchors.fill: parent return Qt.size(image.implicitWidth * dpr, image.implicitHeight * dpr);
}
} }
} }

View file

@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell
import Caelestia.Config import Caelestia.Config
import qs.components import qs.components
import qs.components.effects import qs.components.effects
@ -22,8 +23,10 @@ Item {
asynchronous: true asynchronous: true
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
sourceSize.width: width sourceSize: {
sourceSize.height: height const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
}
layer.enabled: true layer.enabled: true
layer.effect: OpacityMask { layer.effect: OpacityMask {

View file

@ -51,7 +51,7 @@ ColumnLayout {
asynchronous: true asynchronous: true
source: Paths.absolutePath(Config.paths.lockNoNotifsPic) source: Paths.absolutePath(Config.paths.lockNoNotifsPic)
fillMode: Image.PreserveAspectFit 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.enabled: true
layer.effect: Colouriser { layer.effect: Colouriser {

View file

@ -73,8 +73,10 @@ StyledRect {
Image { Image {
source: Qt.resolvedUrl(root.image) source: Qt.resolvedUrl(root.image)
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
sourceSize.width: TokenConfig.sizes.notifs.image sourceSize: {
sourceSize.height: TokenConfig.sizes.notifs.image const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1);
return Qt.size(size, size);
}
cache: false cache: false
asynchronous: true asynchronous: true
width: TokenConfig.sizes.notifs.image width: TokenConfig.sizes.notifs.image

View file

@ -124,8 +124,10 @@ StyledRect {
anchors.fill: parent anchors.fill: parent
source: Qt.resolvedUrl(root.modelData.image) source: Qt.resolvedUrl(root.modelData.image)
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
sourceSize.width: TokenConfig.sizes.notifs.image sourceSize: {
sourceSize.height: TokenConfig.sizes.notifs.image const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1);
return Qt.size(size, size);
}
cache: false cache: false
asynchronous: true asynchronous: true
} }

View file

@ -48,7 +48,7 @@ Column {
AnimatedImage { AnimatedImage {
width: Tokens.sizes.session.button width: Tokens.sizes.session.button
height: Tokens.sizes.session.button height: Tokens.sizes.session.button
sourceSize.width: width sourceSize.width: width * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1)
playing: visible playing: visible
asynchronous: true asynchronous: true

View file

@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import Caelestia.Config import Caelestia.Config
import qs.components import qs.components
@ -96,7 +97,7 @@ Item {
asynchronous: true asynchronous: true
source: Paths.absolutePath(Config.paths.noNotifsPic) source: Paths.absolutePath(Config.paths.noNotifsPic)
fillMode: Image.PreserveAspectFit 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.enabled: true
layer.effect: Colouriser { layer.effect: Colouriser {

View file

@ -85,8 +85,10 @@ StyledRect {
Image { Image {
source: Qt.resolvedUrl(root.image) source: Qt.resolvedUrl(root.image)
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
sourceSize.width: TokenConfig.sizes.notifs.image sourceSize: {
sourceSize.height: TokenConfig.sizes.notifs.image const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1);
return Qt.size(size, size);
}
cache: false cache: false
asynchronous: true asynchronous: true
width: TokenConfig.sizes.notifs.image width: TokenConfig.sizes.notifs.image

View file

@ -89,3 +89,4 @@ add_subdirectory(Internal)
add_subdirectory(Models) add_subdirectory(Models)
add_subdirectory(Services) add_subdirectory(Services)
add_subdirectory(Blobs) add_subdirectory(Blobs)
add_subdirectory(Images)

View file

@ -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
)

View file

@ -0,0 +1,120 @@
#include "cachingimageprovider.hpp"
#include "imagecacher.hpp"
#include <qfileinfo.h>
#include <qimage.h>
#include <qimagereader.h>
#include <qloggingcategory.h>
#include <qrunnable.h>
#include <qthreadpool.h>
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<qreal>(sourceSize.height())));
else
size.setHeight(qRound(size.width() * sourceSize.height() / static_cast<qreal>(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

View file

@ -0,0 +1,21 @@
#pragma once
#include "imagecacher.hpp"
#include <qquickimageprovider.h>
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

View file

@ -0,0 +1,159 @@
#include "imagecacher.hpp"
#include <qcryptographichash.h>
#include <qdir.h>
#include <qfile.h>
#include <qfileinfo.h>
#include <qimage.h>
#include <qloggingcategory.h>
#include <qmutex.h>
#include <qpainter.h>
#include <qsavefile.h>
#include <qthreadpool.h>
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

View file

@ -0,0 +1,38 @@
#pragma once
#include <qmutex.h>
#include <qobject.h>
#include <qset.h>
#include <qsize.h>
#include <qstring.h>
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<QString> m_inflight;
};
} // namespace caelestia::images

View file

@ -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

View 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

View file

@ -2,7 +2,6 @@ qml_module(caelestia-internal
URI Caelestia.Internal URI Caelestia.Internal
SOURCES SOURCES
arcgauge.cpp arcgauge.cpp
cachingimagemanager.cpp
circularbuffer.cpp circularbuffer.cpp
circularindicatormanager.cpp circularindicatormanager.cpp
linearindicatormanager.cpp linearindicatormanager.cpp
@ -14,7 +13,6 @@ qml_module(caelestia-internal
LIBRARIES LIBRARIES
Qt::Gui Qt::Gui
Qt::Quick Qt::Quick
Qt::Concurrent
Qt::Network Qt::Network
Qt::DBus Qt::DBus
) )

View file

@ -1,213 +0,0 @@
#include "cachingimagemanager.hpp"
#include <QtQuick/qquickwindow.h>
#include <qcryptographichash.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qfuturewatcher.h>
#include <qimagereader.h>
#include <qloggingcategory.h>
#include <qpainter.h>
#include <qtconcurrentrun.h>
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

View file

@ -1,65 +0,0 @@
#pragma once
#include <QtQuick/qquickitem.h>
#include <qobject.h>
#include <qpointer.h>
#include <qqmlintegration.h>
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<QQuickItem> 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