Cockatrice/cockatrice/src/interface/card_picture_loader/card_picture_loader.cpp
BruebachL a85203e457
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
Migrate theme asset loads to scheme-variant resolution (#7209-2) (#7276)
Replace remaining QPixmap("theme:...") call sites with themePixmap() so icons, replay controls, card backs, and other images resolve to -dark/-light variants under theme schemes. Stem-exact 1:1 migration; behavior unchanged for non-variant themes.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-11 17:59:57 +02:00

357 lines
12 KiB
C++

#include "card_picture_loader.h"
#include "../../client/settings/cache_settings.h"
#include "../pixel_map_generator.h"
#include "card_picture_loader_cache_method.h"
#include "card_picture_loader_local_schemes.h"
#include <QApplication>
#include <QBuffer>
#include <QDebug>
#include <QDirIterator>
#include <QFileInfo>
#include <QMainWindow>
#include <QMovie>
#include <QNetworkRequest>
#include <QPainter>
#include <QPixmapCache>
#include <QScreen>
#include <QStatusBar>
#include <QThread>
#include <algorithm>
#include <libcockatrice/settings/cache_storage_settings.h>
#include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/paths_settings.h>
#include <utility>
// never cache more than 300 cards at once for a single deck
#define CACHED_CARD_PER_DECK_MAX 300
// wait at least this long before retrying a card whose picture failed to load
static constexpr int RETRY_FAILED_CARDS_SECS = 300;
CardPictureLoader::CardPictureLoader() : QObject(nullptr)
{
worker = new CardPictureLoaderWorker;
connect(&SettingsCache::instance().paths(), &PathsSettings::picsPathChanged, this,
&CardPictureLoader::picsPathChanged);
connect(&SettingsCache::instance().downloads(), &DownloadSettings::picDownloadChanged, this,
&CardPictureLoader::picDownloadChanged);
qRegisterMetaType<ExactCard>();
connect(worker, &CardPictureLoaderWorker::imageLoaded, this, &CardPictureLoader::imageLoaded);
statusBar = new CardPictureLoaderStatusBar(nullptr);
QMainWindow *mainWindow = qobject_cast<QMainWindow *>(QApplication::activeWindow());
if (mainWindow) {
mainWindow->statusBar()->addPermanentWidget(statusBar);
}
connect(worker, &CardPictureLoaderWorker::imageRequestQueued, statusBar,
&CardPictureLoaderStatusBar::addQueuedImageLoad);
connect(worker, &CardPictureLoaderWorker::imageRequestSucceeded, statusBar,
&CardPictureLoaderStatusBar::addSuccessfulImageLoad);
}
CardPictureLoader::~CardPictureLoader()
{
worker->deleteLater();
}
void CardPictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size)
{
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(CardPictureLoaderLog) << "PictureLoader: cache miss for" << backCacheKey;
QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
if (tmpPixmap.isNull()) {
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback'! Using fallback pixmap.";
tmpPixmap = QPixmap(size);
tmpPixmap.fill(Qt::gray); // Fallback to a gray pixmap
} else {
qCDebug(CardPictureLoaderLog) << "Successfully loaded 'theme:cardback'.";
}
pixmap = tmpPixmap.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmapCache::insert(backCacheKey, pixmap);
}
}
void CardPictureLoader::getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSize size)
{
QString backCacheKey =
"_trice_card_back_inprogress_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
if (tmpPixmap.isNull()) {
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for in-progress state! Using fallback.";
tmpPixmap = QPixmap(size);
tmpPixmap.fill(Qt::blue); // Fallback with blue color
} else {
qCDebug(CardPictureLoaderCardBackCacheFailLog)
<< "Successfully loaded 'theme:cardback' for in-progress state.";
}
pixmap = tmpPixmap.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmapCache::insert(backCacheKey, pixmap);
}
}
void CardPictureLoader::getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize size)
{
QString backCacheKey =
"_trice_card_back_failed_" + QString::number(size.width()) + "x" + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "PictureLoader: cache miss for" << backCacheKey;
QPixmap tmpPixmap = themePixmap(QStringLiteral("cardback"));
if (tmpPixmap.isNull()) {
qCWarning(CardPictureLoaderLog) << "Failed to load 'theme:cardback' for failed state! Using fallback.";
tmpPixmap = QPixmap(size);
tmpPixmap.fill(Qt::red); // Fallback with red color
} else {
qCDebug(CardPictureLoaderCardBackCacheFailLog) << "Successfully loaded 'theme:cardback' for failed state.";
}
pixmap = tmpPixmap.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmapCache::insert(backCacheKey, pixmap);
}
}
void CardPictureLoader::getPixmap(QPixmap &pixmap, const ExactCard &card, QSize size)
{
if (!card) {
qCWarning(CardPictureLoaderLog) << "getPixmap called with null card!";
return;
}
QString key = card.getPixmapCacheKey();
QString sizeKey = key + QLatin1Char('_') + QString::number(size.width()) + "x" + QString::number(size.height());
if (QPixmapCache::find(sizeKey, &pixmap)) {
return; // Use cached version
}
// load the image and create a copy of the correct size
QPixmap bigPixmap;
if (QPixmapCache::find(key, &bigPixmap)) {
if (bigPixmap.isNull()) {
// Leave the pixmap null so callers fall back to a solid color
// instead of showing the card back.
QDateTime failedAtTime = getInstance().failedAt.value(key);
if (!failedAtTime.isValid() ||
failedAtTime.addSecs(RETRY_FAILED_CARDS_SECS) < QDateTime::currentDateTime()) {
getInstance().failedAt.remove(key);
QPixmapCache::remove(key);
getInstance().worker->enqueueImageLoad(card);
}
return;
}
QScreen *screen = qApp->primaryScreen();
qreal dpr = screen ? screen->devicePixelRatio() : 1.0;
qCDebug(CardPictureLoaderLog) << "Scaling cached image for" << card.getName();
pixmap = bigPixmap.scaled(size * dpr, Qt::KeepAspectRatio, Qt::SmoothTransformation);
pixmap.setDevicePixelRatio(dpr);
QPixmapCache::insert(sizeKey, pixmap);
return;
}
// add the card to the load queue
qCDebug(CardPictureLoaderLog) << "Enqueuing " << card.getName() << " for " << card.getPixmapCacheKey();
getInstance().worker->enqueueImageLoad(card);
}
void CardPictureLoader::imageLoaded(const ExactCard &card, const QImage &image)
{
QPixmap finalPixmap;
if (image.isNull()) {
getInstance().failedAt.insert(card.getPixmapCacheKey(), QDateTime::currentDateTime());
qCDebug(CardPictureLoaderLog) << "Caching NULL pixmap for" << card.getName();
} else {
getInstance().failedAt.remove(card.getPixmapCacheKey());
if (card.getInfo().getUiAttributes().upsideDownArt) {
#if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0))
QImage mirrorImage = image.flipped(Qt::Horizontal | Qt::Vertical);
#else
QImage mirrorImage = image.mirrored(true, true);
#endif
finalPixmap = QPixmap::fromImage(mirrorImage);
} else {
finalPixmap = QPixmap::fromImage(image);
}
}
QPixmapCache::insert(card.getPixmapCacheKey(), finalPixmap);
if (static_cast<CardPictureLoaderCacheMethod::CacheMethod>(
SettingsCache::instance().cacheStorage().getCardPictureLoaderCacheMethod()) ==
CardPictureLoaderCacheMethod::CacheMethod::FILESYSTEM_CACHE) {
saveCardImageToLocalStorage(card, finalPixmap);
}
// imageLoaded should only be reached if the exactCard isn't already in cache.
// (plus there's a deduplication mechanism in CardPictureLoaderWorker)
// It should be safe to connect the CardInfo here without worrying about redundant connections.
connect(card.getCardPtr().data(), &QObject::destroyed, this, [cacheKey = card.getPixmapCacheKey()] {
QPixmapCache::remove(cacheKey);
getInstance().failedAt.remove(cacheKey);
});
card.emitPixmapUpdated();
}
void CardPictureLoader::saveCardImageToLocalStorage(const ExactCard &card, const QPixmap &pixmap)
{
if (pixmap.isNull() || !card) {
return;
}
const QString picsRoot = SettingsCache::instance().paths().getPicsPath();
CardPictureLoaderLocalSchemes::NamingScheme scheme = static_cast<CardPictureLoaderLocalSchemes::NamingScheme>(
SettingsCache::instance().cacheStorage().getLocalCardImageStorageNamingScheme());
QString pattern;
for (const auto &s : CardPictureLoaderLocalSchemes::exportSchemes()) {
if (s.id == scheme) {
pattern = s.pattern;
break;
}
}
if (picsRoot.isEmpty() || pattern.isEmpty()) {
return;
}
// Base directory: <picsPath>/downloadedPics
QDir baseDir(picsRoot);
if (!baseDir.exists("downloadedPics")) {
baseDir.mkpath("downloadedPics");
}
baseDir.cd("downloadedPics");
// Collect card metadata
const QString cardName = card.getInfo().getCorrectedName();
QString setName;
QString collectorNumber;
QString uuid;
PrintingInfo printing = card.getPrinting();
if (printing.getSet()) {
setName = printing.getSet()->getCorrectedShortName();
collectorNumber = printing.getProperty("num");
uuid = printing.getUuid();
}
// Build path from scheme
QString relativePath =
CardPictureLoaderLocalSchemes::expandPattern(pattern, cardName, setName, collectorNumber, uuid);
if (relativePath.isEmpty()) {
return;
}
// append extension
relativePath += ".png";
// Normalize slashes
relativePath = QDir::cleanPath(relativePath);
QFileInfo outInfo(baseDir.filePath(relativePath));
// Do not overwrite existing files
if (outInfo.exists()) {
return;
}
QDir outDir = outInfo.dir();
// Ensure directory exists
if (!outDir.exists()) {
if (!baseDir.mkpath(outDir.path())) {
qCWarning(CardPictureLoaderLog) << "Failed to create directory for downloaded card image:" << outDir.path();
return;
}
}
// Save image
QImage image = pixmap.toImage();
if (!image.save(outInfo.absoluteFilePath(), "PNG")) {
qCWarning(CardPictureLoaderLog) << "Failed to save card image to" << outInfo.absoluteFilePath();
}
}
void CardPictureLoader::clearPixmapCache()
{
QPixmapCache::clear();
}
void CardPictureLoader::clearNetworkCache()
{
getInstance().worker->clearNetworkCache();
}
void CardPictureLoader::cacheCardPixmaps(const QList<ExactCard> &cards)
{
QPixmap tmp;
int max = qMin(cards.size(), CACHED_CARD_PER_DECK_MAX);
for (int i = 0; i < max; ++i) {
const ExactCard &card = cards.at(i);
if (!card) {
continue;
}
QString key = card.getPixmapCacheKey();
if (QPixmapCache::find(key, &tmp)) {
continue;
}
getInstance().worker->enqueueImageLoad(card);
}
}
void CardPictureLoader::picDownloadChanged()
{
QPixmapCache::clear();
}
void CardPictureLoader::picsPathChanged()
{
QPixmapCache::clear();
}
bool CardPictureLoader::hasCustomArt()
{
auto picsPath = SettingsCache::instance().paths().getPicsPath();
QDirIterator it(picsPath, QDir::Dirs | QDir::NoDotAndDotDot);
// Check if there is at least one non-directory file in the pics path, other
// than in the "downloadedPics" subdirectory.
while (it.hasNext()) {
#if (QT_VERSION >= QT_VERSION_CHECK(6, 3, 0))
QFileInfo dir(it.nextFileInfo());
#else
// nextFileInfo() is only available in Qt 6.3+, for previous versions, we build
// the QFileInfo from a QString which requires more system calls.
QFileInfo dir(it.next());
#endif
if (it.fileName() == "downloadedPics") {
continue;
}
QDirIterator subIt(it.filePath(), QDir::Files, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
if (subIt.hasNext()) {
return true;
}
}
return false;
}