✨ feat: cache loaded images and computed dominant colors using db
This commit is contained in:
+1
-1
@@ -25,7 +25,7 @@ endif()
|
|||||||
|
|
||||||
configure_file(WallReel/version.h.in ${CMAKE_BINARY_DIR}/generated/version.h)
|
configure_file(WallReel/version.h.in ${CMAKE_BINARY_DIR}/generated/version.h)
|
||||||
|
|
||||||
find_package(Qt6 REQUIRED COMPONENTS Quick Widgets QuickControls2 Concurrent)
|
find_package(Qt6 REQUIRED COMPONENTS Quick Widgets QuickControls2 Concurrent Sql)
|
||||||
|
|
||||||
qt_standard_project_setup(REQUIRES 6.5)
|
qt_standard_project_setup(REQUIRES 6.5)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ qt_add_qml_module(${CORELIB_NAME}
|
|||||||
URI ${COREMODULE_URI}
|
URI ${COREMODULE_URI}
|
||||||
VERSION ${MODULE_VERSION_MAJOR}.${MODULE_VERSION_MINOR}
|
VERSION ${MODULE_VERSION_MAJOR}.${MODULE_VERSION_MINOR}
|
||||||
SOURCES
|
SOURCES
|
||||||
|
Cache/manager.hpp Cache/manager.cpp
|
||||||
Image/data.hpp Image/data.cpp
|
Image/data.hpp Image/data.cpp
|
||||||
Image/model.hpp Image/model.cpp
|
Image/model.hpp Image/model.cpp
|
||||||
Palette/data.hpp
|
Palette/data.hpp
|
||||||
@@ -21,6 +22,7 @@ target_link_libraries(${CORELIB_NAME} PUBLIC
|
|||||||
Qt6::Widgets
|
Qt6::Widgets
|
||||||
Qt6::QuickControls2
|
Qt6::QuickControls2
|
||||||
Qt6::Concurrent
|
Qt6::Concurrent
|
||||||
|
Qt6::Sql
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(${CORELIB_NAME} PUBLIC
|
target_include_directories(${CORELIB_NAME} PUBLIC
|
||||||
|
|||||||
@@ -0,0 +1,247 @@
|
|||||||
|
#include "manager.hpp"
|
||||||
|
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QMutexLocker>
|
||||||
|
#include <QSqlError>
|
||||||
|
#include <QSqlQuery>
|
||||||
|
#include <QThread>
|
||||||
|
|
||||||
|
#include "logger.hpp"
|
||||||
|
|
||||||
|
using namespace Qt::StringLiterals;
|
||||||
|
|
||||||
|
namespace WallReel::Core::Cache {
|
||||||
|
|
||||||
|
QString Manager::cacheKey(const QFileInfo& fileInfo, const QSize& imageSize) {
|
||||||
|
const QString raw = fileInfo.absoluteFilePath() + QString::number(fileInfo.lastModified().toMSecsSinceEpoch()) + u'x' + QString::number(imageSize.width()) + u'x' + QString::number(imageSize.height());
|
||||||
|
return QString::fromLatin1(
|
||||||
|
QCryptographicHash::hash(raw.toUtf8(), QCryptographicHash::Sha256).toHex());
|
||||||
|
}
|
||||||
|
|
||||||
|
Manager::Manager(const QDir& cacheDir)
|
||||||
|
: m_cacheDir(cacheDir), m_dbPath(cacheDir.filePath(u"cache.db"_s)), m_connectionPrefix(u"WallReelCache:"_s + QString::fromLatin1(QCryptographicHash::hash(m_dbPath.toUtf8(), QCryptographicHash::Md5).toHex())) {
|
||||||
|
Logger::debug(u"Initializing cache db: %1"_s.arg(m_dbPath));
|
||||||
|
// Open a connection on the constructing thread so the schema is
|
||||||
|
// guaranteed to exist before any worker thread first calls _db().
|
||||||
|
_db();
|
||||||
|
}
|
||||||
|
|
||||||
|
Manager::~Manager() {
|
||||||
|
QSet<QString> names;
|
||||||
|
{
|
||||||
|
QMutexLocker lock(&m_connectionsMutex);
|
||||||
|
names = std::move(m_connectionNames);
|
||||||
|
}
|
||||||
|
Logger::debug(u"Closing %1 cache db connection(s)"_s.arg(names.size()));
|
||||||
|
for (const QString& connName : std::as_const(names)) {
|
||||||
|
{
|
||||||
|
// Scope: release the QSqlDatabase copy before removeDatabase()
|
||||||
|
QSqlDatabase db = QSqlDatabase::database(connName, false);
|
||||||
|
if (db.isOpen())
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
QSqlDatabase::removeDatabase(connName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Manager::clearCache(Type type) {
|
||||||
|
QSqlDatabase db = _db();
|
||||||
|
if (!db.isOpen())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if ((type & Type::Image) != Type::None) {
|
||||||
|
int removed = 0;
|
||||||
|
QSqlQuery selectQuery(db);
|
||||||
|
if (selectQuery.exec(QStringLiteral("SELECT file_name FROM image_cache"))) {
|
||||||
|
while (selectQuery.next()) {
|
||||||
|
QFile::remove(m_cacheDir.filePath(selectQuery.value(0).toString()));
|
||||||
|
++removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QSqlQuery(db).exec(QStringLiteral("DELETE FROM image_cache"));
|
||||||
|
Logger::info(u"Cleared %1 image cache file(s)"_s.arg(removed));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((type & Type::Color) != Type::None) {
|
||||||
|
QSqlQuery(db).exec(QStringLiteral("DELETE FROM color_cache"));
|
||||||
|
Logger::info(u"Cleared color cache"_s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor Manager::getColor(const QString& key, const std::function<QColor()>& computeFunc) {
|
||||||
|
QSqlDatabase db = _db();
|
||||||
|
if (db.isOpen()) {
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.prepare(QStringLiteral(
|
||||||
|
"SELECT r, g, b, a FROM color_cache WHERE key = :key"));
|
||||||
|
query.bindValue(u":key"_s, key);
|
||||||
|
|
||||||
|
if (query.exec() && query.next()) {
|
||||||
|
Logger::debug(u"Color cache hit [%1]"_s.arg(key));
|
||||||
|
return QColor(
|
||||||
|
query.value(0).toInt(),
|
||||||
|
query.value(1).toInt(),
|
||||||
|
query.value(2).toInt(),
|
||||||
|
query.value(3).toInt());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::debug(u"Color cache miss [%1], computing"_s.arg(key));
|
||||||
|
const QColor color = computeFunc();
|
||||||
|
|
||||||
|
if (!color.isValid()) {
|
||||||
|
Logger::warn(u"ComputeFunc returned invalid color for key [%1]"_s.arg(key));
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db.isOpen()) {
|
||||||
|
QSqlQuery insertQuery(db);
|
||||||
|
insertQuery.prepare(QStringLiteral(
|
||||||
|
"INSERT OR REPLACE INTO color_cache (key, r, g, b, a) "
|
||||||
|
"VALUES (:key, :r, :g, :b, :a)"));
|
||||||
|
insertQuery.bindValue(u":key"_s, key);
|
||||||
|
insertQuery.bindValue(u":r"_s, color.red());
|
||||||
|
insertQuery.bindValue(u":g"_s, color.green());
|
||||||
|
insertQuery.bindValue(u":b"_s, color.blue());
|
||||||
|
insertQuery.bindValue(u":a"_s, color.alpha());
|
||||||
|
if (!insertQuery.exec())
|
||||||
|
Logger::warn(u"Failed to cache color [%1]: %2"_s
|
||||||
|
.arg(key, insertQuery.lastError().text()));
|
||||||
|
else
|
||||||
|
Logger::debug(u"Color cached [%1]"_s.arg(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
QFileInfo Manager::getImage(const QString& key, const std::function<QImage()>& computeFunc) {
|
||||||
|
QSqlDatabase db = _db();
|
||||||
|
if (db.isOpen()) {
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.prepare(QStringLiteral(
|
||||||
|
"SELECT file_name FROM image_cache WHERE key = :key"));
|
||||||
|
query.bindValue(u":key"_s, key);
|
||||||
|
|
||||||
|
if (query.exec() && query.next()) {
|
||||||
|
const QFileInfo cached(m_cacheDir.filePath(query.value(0).toString()));
|
||||||
|
if (cached.exists()) {
|
||||||
|
Logger::debug(u"Image cache hit [%1] -> %2"_s
|
||||||
|
.arg(key, cached.absoluteFilePath()));
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// File was deleted externally — evict the stale DB record.
|
||||||
|
Logger::warn(u"Image cache stale, file missing [%1], evicting"_s.arg(key));
|
||||||
|
QSqlQuery evict(db);
|
||||||
|
evict.prepare(QStringLiteral("DELETE FROM image_cache WHERE key = :key"));
|
||||||
|
evict.bindValue(u":key"_s, key);
|
||||||
|
evict.exec();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::debug(u"Image cache miss [%1], computing"_s.arg(key));
|
||||||
|
const QImage image = computeFunc();
|
||||||
|
if (image.isNull()) {
|
||||||
|
Logger::warn(u"ComputeFunc returned null image for key [%1]"_s.arg(key));
|
||||||
|
return QFileInfo{};
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString fileName = key + u".png"_s;
|
||||||
|
const QString filePath = m_cacheDir.filePath(fileName);
|
||||||
|
|
||||||
|
if (!image.save(filePath, "PNG")) {
|
||||||
|
Logger::warn(u"Failed to save image to %1"_s.arg(filePath));
|
||||||
|
return QFileInfo{};
|
||||||
|
}
|
||||||
|
Logger::debug(u"Image saved to %1"_s.arg(filePath));
|
||||||
|
|
||||||
|
if (db.isOpen()) {
|
||||||
|
QSqlQuery insertQuery(db);
|
||||||
|
insertQuery.prepare(QStringLiteral(
|
||||||
|
"INSERT OR REPLACE INTO image_cache (key, file_name) "
|
||||||
|
"VALUES (:key, :file_name)"));
|
||||||
|
insertQuery.bindValue(u":key"_s, key);
|
||||||
|
insertQuery.bindValue(u":file_name"_s, fileName);
|
||||||
|
if (!insertQuery.exec())
|
||||||
|
Logger::warn(u"Failed to record image in db [%1]: %2"_s
|
||||||
|
.arg(key, insertQuery.lastError().text()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return QFileInfo(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an open QSqlDatabase for the calling thread, creating it on first use.
|
||||||
|
QSqlDatabase Manager::_db() const {
|
||||||
|
// thread_local: one slot per OS thread, initialized on first call in that thread.
|
||||||
|
// For QThreadPool workers the thread is reused across tasks, so the connection
|
||||||
|
// is opened once per worker thread for the lifetime of the Manager.
|
||||||
|
thread_local QHash<QString /*connectionPrefix*/, QString /*connName*/> tlsConns;
|
||||||
|
|
||||||
|
auto it = tlsConns.find(m_connectionPrefix);
|
||||||
|
if (it != tlsConns.end()) {
|
||||||
|
QSqlDatabase db = QSqlDatabase::database(*it, false);
|
||||||
|
if (db.isOpen())
|
||||||
|
return db;
|
||||||
|
// Reopen if closed externally.
|
||||||
|
Logger::debug(u"Reopening cache db connection [%1]"_s.arg(*it));
|
||||||
|
if (!db.open()) {
|
||||||
|
Logger::warn(u"Cannot reopen cache database: %1"_s.arg(db.lastError().text()));
|
||||||
|
return QSqlDatabase{};
|
||||||
|
}
|
||||||
|
QSqlQuery q(db);
|
||||||
|
q.exec(u"PRAGMA journal_mode=WAL"_s);
|
||||||
|
q.exec(u"PRAGMA synchronous=NORMAL"_s);
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First use of this Manager in this thread.
|
||||||
|
const QString connName = m_connectionPrefix + u':' +
|
||||||
|
QString::number(reinterpret_cast<quintptr>(QThread::currentThreadId()));
|
||||||
|
|
||||||
|
QSqlDatabase db = QSqlDatabase::addDatabase(u"QSQLITE"_s, connName);
|
||||||
|
db.setDatabaseName(m_dbPath);
|
||||||
|
|
||||||
|
if (!db.open()) {
|
||||||
|
Logger::warn(u"Cannot open cache database %1: %2"_s
|
||||||
|
.arg(m_dbPath, db.lastError().text()));
|
||||||
|
db = QSqlDatabase{};
|
||||||
|
QSqlDatabase::removeDatabase(connName);
|
||||||
|
return QSqlDatabase{};
|
||||||
|
}
|
||||||
|
Logger::debug(u"Opened cache db connection [%1]"_s.arg(connName));
|
||||||
|
|
||||||
|
tlsConns.insert(m_connectionPrefix, connName);
|
||||||
|
{
|
||||||
|
QMutexLocker lock(&m_connectionsMutex);
|
||||||
|
m_connectionNames.insert(connName);
|
||||||
|
}
|
||||||
|
|
||||||
|
QSqlQuery q(db);
|
||||||
|
q.exec(u"PRAGMA journal_mode=WAL"_s);
|
||||||
|
q.exec(u"PRAGMA synchronous=NORMAL"_s);
|
||||||
|
q.exec(u"PRAGMA foreign_keys=ON"_s);
|
||||||
|
_setupTables(db);
|
||||||
|
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Manager::_setupTables(QSqlDatabase& db) const {
|
||||||
|
QSqlQuery q(db);
|
||||||
|
q.exec(QStringLiteral(
|
||||||
|
"CREATE TABLE IF NOT EXISTS color_cache ("
|
||||||
|
" key TEXT PRIMARY KEY NOT NULL,"
|
||||||
|
" r INTEGER NOT NULL,"
|
||||||
|
" g INTEGER NOT NULL,"
|
||||||
|
" b INTEGER NOT NULL,"
|
||||||
|
" a INTEGER NOT NULL"
|
||||||
|
")"));
|
||||||
|
q.exec(QStringLiteral(
|
||||||
|
"CREATE TABLE IF NOT EXISTS image_cache ("
|
||||||
|
" key TEXT PRIMARY KEY NOT NULL,"
|
||||||
|
" file_name TEXT NOT NULL"
|
||||||
|
")"));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace WallReel::Core::Cache
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#ifndef WALLREEL_CACHE_MANAGER_HPP
|
||||||
|
#define WALLREEL_CACHE_MANAGER_HPP
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QMutex>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QtSql>
|
||||||
|
|
||||||
|
#include "types.hpp"
|
||||||
|
|
||||||
|
namespace WallReel::Core::Cache {
|
||||||
|
|
||||||
|
class Manager {
|
||||||
|
public:
|
||||||
|
static QString cacheKey(const QFileInfo& fileInfo, const QSize& imageSize);
|
||||||
|
|
||||||
|
Manager(const QDir& cacheDir);
|
||||||
|
|
||||||
|
~Manager();
|
||||||
|
|
||||||
|
void clearCache(Type type = Type::Image | Type::Color);
|
||||||
|
|
||||||
|
QColor getColor(const QString& key, const std::function<QColor()>& computeFunc);
|
||||||
|
|
||||||
|
QFileInfo getImage(const QString& key, const std::function<QImage()>& computeFunc);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QDir m_cacheDir;
|
||||||
|
QString m_dbPath;
|
||||||
|
QString m_connectionPrefix;
|
||||||
|
|
||||||
|
mutable QMutex m_connectionsMutex;
|
||||||
|
mutable QSet<QString> m_connectionNames;
|
||||||
|
|
||||||
|
QSqlDatabase _db() const;
|
||||||
|
void _setupTables(QSqlDatabase& db) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace WallReel::Core::Cache
|
||||||
|
|
||||||
|
#endif // WALLREEL_CACHE_MANAGER_HPP
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#ifndef WALLREEL_CACHE_TYPES_HPP
|
||||||
|
#define WALLREEL_CACHE_TYPES_HPP
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
namespace WallReel::Core::Cache {
|
||||||
|
|
||||||
|
enum class Type : uint32_t {
|
||||||
|
None = 0,
|
||||||
|
Image = 1, ///< Cache for processed images
|
||||||
|
Color = 1 << 1, ///< Cache for palette color matching results
|
||||||
|
All = ~0u
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr Type operator|(Type a, Type b) {
|
||||||
|
using T = std::underlying_type_t<Type>;
|
||||||
|
return static_cast<Type>(static_cast<T>(a) | static_cast<T>(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline constexpr Type operator&(Type a, Type b) {
|
||||||
|
using T = std::underlying_type_t<Type>;
|
||||||
|
return static_cast<Type>(static_cast<T>(a) & static_cast<T>(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
using Data = std::variant<std::monostate, QFileInfo, QColor>;
|
||||||
|
|
||||||
|
} // namespace WallReel::Core::Cache
|
||||||
|
|
||||||
|
#endif // WALLREEL_CACHE_TYPES_HPP
|
||||||
@@ -4,13 +4,12 @@
|
|||||||
#include <QImageReader>
|
#include <QImageReader>
|
||||||
|
|
||||||
#include "Palette/domcolor.hpp"
|
#include "Palette/domcolor.hpp"
|
||||||
#include "logger.hpp"
|
|
||||||
|
|
||||||
WallReel::Core::Image::Data* WallReel::Core::Image::Data::create(
|
WallReel::Core::Image::Data* WallReel::Core::Image::Data::create(
|
||||||
const QString& path,
|
const QString& path,
|
||||||
const QSize& size,
|
const QSize& size,
|
||||||
const QDir& cacheDir) {
|
Cache::Manager& cacheMgr) {
|
||||||
Data* ret = new Data(path, size, cacheDir);
|
Data* ret = new Data(path, size, cacheMgr);
|
||||||
if (!ret->isValid()) {
|
if (!ret->isValid()) {
|
||||||
delete ret;
|
delete ret;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -18,32 +17,13 @@ WallReel::Core::Image::Data* WallReel::Core::Image::Data::create(
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
WallReel::Core::Image::Data::Data(const QString& path, const QSize& targetSize, const QDir& cacheDir)
|
WallReel::Core::Image::Data::Data(const QString& path, const QSize& targetSize, Cache::Manager& cacheMgr)
|
||||||
: m_file(path), m_targetSize(targetSize) {
|
: m_cacheMgr(cacheMgr), m_file(path), m_targetSize(targetSize) {
|
||||||
m_id = _generateId(path, targetSize);
|
m_id = cacheMgr.cacheKey(m_file, m_targetSize);
|
||||||
const auto cachePath = cacheDir.absoluteFilePath(_generateCacheFileName(m_id));
|
m_cachedFile = cacheMgr.getImage(m_id, [this]() {
|
||||||
m_cachedFile = QFileInfo(cachePath);
|
|
||||||
|
|
||||||
// If cached file exists, use it directly
|
|
||||||
if (m_cachedFile.exists()) {
|
|
||||||
Logger::debug(QString("Cache hit for image: %1").arg(m_file.absoluteFilePath()));
|
|
||||||
if (!_loadFromCache()) {
|
|
||||||
Logger::warn(QString("Failed to load cached image from path: %1").arg(m_cachedFile.absoluteFilePath()));
|
|
||||||
if (!_loadFresh()) {
|
|
||||||
Logger::warn(QString("Failed to load fresh image from path: %1").arg(m_file.absoluteFilePath()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!_loadFresh()) {
|
|
||||||
Logger::warn(QString("Failed to load fresh image from path: %1").arg(m_file.absoluteFilePath()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WallReel::Core::Image::Data::_loadFresh() {
|
|
||||||
QImageReader reader(m_file.absoluteFilePath());
|
QImageReader reader(m_file.absoluteFilePath());
|
||||||
if (!reader.canRead()) {
|
if (!reader.canRead()) {
|
||||||
return false;
|
return QImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
const QSize originalSize = reader.size();
|
const QSize originalSize = reader.size();
|
||||||
@@ -64,7 +44,7 @@ bool WallReel::Core::Image::Data::_loadFresh() {
|
|||||||
|
|
||||||
QImage image;
|
QImage image;
|
||||||
if (!reader.read(&image)) {
|
if (!reader.read(&image)) {
|
||||||
return false;
|
return QImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If reader doesn't support built-in scaling or the image still do not match the target size, do manual scaling
|
// If reader doesn't support built-in scaling or the image still do not match the target size, do manual scaling
|
||||||
@@ -83,34 +63,20 @@ bool WallReel::Core::Image::Data::_loadFresh() {
|
|||||||
if (image.format() != QImage::Format_ARGB32_Premultiplied) {
|
if (image.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||||
image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||||
}
|
}
|
||||||
|
return image;
|
||||||
// Get dominant color
|
});
|
||||||
m_dominantColor = Palette::getDominantColor(image);
|
m_dominantColor = cacheMgr.getColor(m_id, [this]() {
|
||||||
|
|
||||||
// Save to cache
|
|
||||||
if (!image.save(m_cachedFile.absoluteFilePath())) {
|
|
||||||
Logger::warn(QString("Failed to save cached image to path: %1").arg(m_cachedFile.absoluteFilePath()));
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
Logger::debug(QString("Cached image saved to path: %1").arg(m_cachedFile.absoluteFilePath()));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WallReel::Core::Image::Data::_loadFromCache() {
|
|
||||||
QImageReader reader(m_cachedFile.absoluteFilePath());
|
QImageReader reader(m_cachedFile.absoluteFilePath());
|
||||||
if (!reader.canRead()) {
|
if (!reader.canRead()) {
|
||||||
return false;
|
return QColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
QImage image;
|
QImage image;
|
||||||
if (!reader.read(&image)) {
|
if (!reader.read(&image)) {
|
||||||
return false;
|
return QColor();
|
||||||
}
|
}
|
||||||
|
return Palette::getDominantColor(image);
|
||||||
// Get dominant color
|
});
|
||||||
m_dominantColor = Palette::getDominantColor(image);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QImage WallReel::Core::Image::Data::loadImage() const {
|
QImage WallReel::Core::Image::Data::loadImage() const {
|
||||||
@@ -125,15 +91,3 @@ QImage WallReel::Core::Image::Data::loadImage() const {
|
|||||||
}
|
}
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString WallReel::Core::Image::Data::_generateId(const QString& path, const QSize& size) {
|
|
||||||
auto info = QFileInfo(path);
|
|
||||||
auto key = QString("%1|%2|%3|%4x%5").arg(info.absoluteFilePath()).arg(info.lastModified().toSecsSinceEpoch()).arg(info.size()).arg(size.width()).arg(size.height());
|
|
||||||
|
|
||||||
QByteArray hash = QCryptographicHash::hash(key.toUtf8(), QCryptographicHash::Md5);
|
|
||||||
return QString::fromLatin1(hash.toHex());
|
|
||||||
}
|
|
||||||
|
|
||||||
QString WallReel::Core::Image::Data::_generateCacheFileName(const QString& id) {
|
|
||||||
return id + ".png";
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include "Cache/manager.hpp"
|
||||||
|
|
||||||
// Development note
|
// Development note
|
||||||
/*
|
/*
|
||||||
Current implementation of image loading and caching:
|
Current implementation of image loading and caching:
|
||||||
@@ -34,10 +36,6 @@ Why this approach - Main purposes
|
|||||||
- Resizing during loading fundamentally eliminates the possibility of the frontend storing large
|
- Resizing during loading fundamentally eliminates the possibility of the frontend storing large
|
||||||
images in memory. (and not all image formats support `sourceSize` property in the right way)
|
images in memory. (and not all image formats support `sourceSize` property in the right way)
|
||||||
|
|
||||||
Possible improvements:
|
|
||||||
- Cache other properties of the image (dominant color for example) to entirely avoid processing the
|
|
||||||
image in loading stage. A simple key-value store should be sufficient.
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace WallReel::Core::Image {
|
namespace WallReel::Core::Image {
|
||||||
@@ -47,6 +45,8 @@ namespace WallReel::Core::Image {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
class Data {
|
class Data {
|
||||||
|
Cache::Manager& m_cacheMgr;
|
||||||
|
|
||||||
QString m_id; ///< Unique identifier for the image
|
QString m_id; ///< Unique identifier for the image
|
||||||
QFileInfo m_file; ///< File information of the image
|
QFileInfo m_file; ///< File information of the image
|
||||||
QFileInfo m_cachedFile; ///< Cached file information for the loaded image
|
QFileInfo m_cachedFile; ///< Cached file information for the loaded image
|
||||||
@@ -54,15 +54,7 @@ class Data {
|
|||||||
QColor m_dominantColor; ///< Dominant color of the image, used for palette matching
|
QColor m_dominantColor; ///< Dominant color of the image, used for palette matching
|
||||||
QHash<QString, QString> m_colorCache; ///< Cache for palette color matching results, key is palette name, value is matched color name
|
QHash<QString, QString> m_colorCache; ///< Cache for palette color matching results, key is palette name, value is matched color name
|
||||||
|
|
||||||
Data(const QString& path, const QSize& size, const QDir& cacheDir);
|
Data(const QString& path, const QSize& size, Cache::Manager& cacheMgr);
|
||||||
|
|
||||||
bool _loadFromCache();
|
|
||||||
|
|
||||||
bool _loadFresh();
|
|
||||||
|
|
||||||
static QString _generateId(const QString& path, const QSize& size);
|
|
||||||
|
|
||||||
static QString _generateCacheFileName(const QString& id);
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
@@ -72,7 +64,7 @@ class Data {
|
|||||||
* @param size Target size for loaded image, the image will be scaled and cropped to this size and stored in memory
|
* @param size Target size for loaded image, the image will be scaled and cropped to this size and stored in memory
|
||||||
* @return Data*
|
* @return Data*
|
||||||
*/
|
*/
|
||||||
static Data* create(const QString& path, const QSize& size, const QDir& cacheDir);
|
static Data* create(const QString& path, const QSize& size, Cache::Manager& cacheMgr);
|
||||||
|
|
||||||
QSize getTargetSize() const { return m_targetSize; }
|
QSize getTargetSize() const { return m_targetSize; }
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,15 @@
|
|||||||
|
|
||||||
WallReel::Core::Image::Model::Model(
|
WallReel::Core::Image::Model::Model(
|
||||||
const Config::SortConfigItems& sortConfig,
|
const Config::SortConfigItems& sortConfig,
|
||||||
const QDir& cacheDir,
|
Cache::Manager& cacheMgr,
|
||||||
const QSize& thumbnailSize,
|
const QSize& thumbnailSize,
|
||||||
QObject* parent)
|
QObject* parent)
|
||||||
: QAbstractListModel(parent),
|
: QAbstractListModel(parent),
|
||||||
m_sortConfig(sortConfig),
|
m_sortConfig(sortConfig),
|
||||||
m_cacheDir(cacheDir),
|
m_cacheMgr(cacheMgr),
|
||||||
m_thumbnailSize(thumbnailSize),
|
m_thumbnailSize(thumbnailSize),
|
||||||
m_currentSortType(sortConfig.type) {
|
m_currentSortType(sortConfig.type),
|
||||||
|
m_currentSortReverse(sortConfig.reverse) {
|
||||||
connect(
|
connect(
|
||||||
&m_watcher,
|
&m_watcher,
|
||||||
&QFutureWatcher<Data*>::finished,
|
&QFutureWatcher<Data*>::finished,
|
||||||
@@ -184,10 +185,10 @@ void WallReel::Core::Image::Model::loadAndProcess(const QStringList& paths) {
|
|||||||
// These are all small objects so capturing by value should be fine
|
// These are all small objects so capturing by value should be fine
|
||||||
const auto thumbnailSize = m_thumbnailSize;
|
const auto thumbnailSize = m_thumbnailSize;
|
||||||
const auto counterPtr = &m_processedCount;
|
const auto counterPtr = &m_processedCount;
|
||||||
const auto cacheDir = m_cacheDir;
|
const auto cacheMgr = &m_cacheMgr;
|
||||||
QFuture<Data*> future =
|
QFuture<Data*> future =
|
||||||
QtConcurrent::mapped(paths, [thumbnailSize, counterPtr, cacheDir](const QString& path) {
|
QtConcurrent::mapped(paths, [thumbnailSize, counterPtr, cacheMgr](const QString& path) {
|
||||||
auto data = Data::create(path, thumbnailSize, cacheDir);
|
auto data = Data::create(path, thumbnailSize, *cacheMgr);
|
||||||
counterPtr->fetch_add(1, std::memory_order_relaxed);
|
counterPtr->fetch_add(1, std::memory_order_relaxed);
|
||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
|
#include "Cache/manager.hpp"
|
||||||
#include "Config/data.hpp"
|
#include "Config/data.hpp"
|
||||||
#include "data.hpp"
|
#include "data.hpp"
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ class Model : public QAbstractListModel {
|
|||||||
|
|
||||||
Model(
|
Model(
|
||||||
const Config::SortConfigItems& sortConfig,
|
const Config::SortConfigItems& sortConfig,
|
||||||
const QDir& cacheDir,
|
Cache::Manager& cacheMgr,
|
||||||
const QSize& thumbnailSize,
|
const QSize& thumbnailSize,
|
||||||
QObject* parent = nullptr);
|
QObject* parent = nullptr);
|
||||||
|
|
||||||
@@ -161,7 +162,7 @@ class Model : public QAbstractListModel {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
const Config::SortConfigItems& m_sortConfig;
|
const Config::SortConfigItems& m_sortConfig;
|
||||||
QDir m_cacheDir;
|
Cache::Manager& m_cacheMgr;
|
||||||
QSize m_thumbnailSize;
|
QSize m_thumbnailSize;
|
||||||
|
|
||||||
QList<Data*> m_data;
|
QList<Data*> m_data;
|
||||||
|
|||||||
+4
-1
@@ -4,6 +4,7 @@
|
|||||||
#include <QQmlApplicationEngine>
|
#include <QQmlApplicationEngine>
|
||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
|
|
||||||
|
#include "Cache/manager.hpp"
|
||||||
#include "Core/Config/manager.hpp"
|
#include "Core/Config/manager.hpp"
|
||||||
#include "Core/Image/model.hpp"
|
#include "Core/Image/model.hpp"
|
||||||
#include "Core/Palette/data.hpp"
|
#include "Core/Palette/data.hpp"
|
||||||
@@ -44,9 +45,11 @@ int main(int argc, char* argv[]) {
|
|||||||
"Config",
|
"Config",
|
||||||
config);
|
config);
|
||||||
|
|
||||||
|
auto cacheMgr = new Cache::Manager(Utils::getCacheDir());
|
||||||
|
|
||||||
auto imageModel = new Image::Model(
|
auto imageModel = new Image::Model(
|
||||||
config->getSortConfig(),
|
config->getSortConfig(),
|
||||||
Utils::getCacheDir(),
|
*cacheMgr,
|
||||||
config->getFocusImageSize(),
|
config->getFocusImageSize(),
|
||||||
config);
|
config);
|
||||||
qmlRegisterSingletonInstance(
|
qmlRegisterSingletonInstance(
|
||||||
|
|||||||
Reference in New Issue
Block a user