#include "thumbnailworker.h" #include "thumbnailcache.h" #include #include #include #include namespace { constexpr int kDisplaySize = 128; // THREADING: Pure function called from a worker thread. No shared mutable state. QImage downscaleForDisplay(const QImage &src) { if (src.isNull()) return src; if (src.width() <= kDisplaySize && src.height() <= kDisplaySize) return src; return src.scaled(kDisplaySize, kDisplaySize, Qt::KeepAspectRatio, Qt::SmoothTransformation); } } // namespace ThumbnailWorker::ThumbnailWorker(const QByteArray &path, int row, ThumbnailCache::Size size, std::atomic_bool *cancelFlag) : m_path(path), m_row(row), m_size(size), m_cancelFlag(cancelFlag) { setAutoDelete(true); } // THREADING: This function runs on a QThreadPool worker thread, NOT the main // thread. It must not access any ImageModel state. The only shared data it // touches is the atomic m_cancelFlag (for cooperative cancellation). All other // members (m_path, m_row, m_size) are immutable after construction. Results // are delivered to the main thread via the finished() signal connected with // Qt::QueuedConnection. void ThumbnailWorker::run() { if (*m_cancelFlag) return; // Per the spec, never thumbnail files that live inside the cache itself. if (ThumbnailCache::isInsideCache(m_path)) return; const int target = static_cast(m_size); QImage cached = ThumbnailCache::load(m_path, m_size); if (*m_cancelFlag) return; if (!cached.isNull()) { emit finished(m_row, downscaleForDisplay(cached)); return; } // Open the file via POSIX open() so that paths with non-UTF-8 bytes // (e.g. Latin-1 encoded filenames) are handled correctly. int fd = ::open(m_path.constData(), O_RDONLY | O_CLOEXEC); if (fd < 0) return; QFile file; if (!file.open(fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle)) { ::close(fd); return; } QImageReader reader(&file); reader.setAutoTransform(true); // Scale during decoding to avoid hitting the allocation limit // on very large images (Qt 6 defaults to 128 MB). QSize fullSize = reader.size(); if (fullSize.isValid()) { QSize scaled = fullSize.scaled(target, target, Qt::KeepAspectRatio); reader.setScaledSize(scaled); } QImage image = reader.read(); if (*m_cancelFlag) return; if (image.isNull()) return; // Ensure the longest side is exactly the target (the decoder hint may have // been ignored for some formats). Aspect ratio preserved, no padding canvas. QImage thumb; if (image.width() > target || image.height() > target) thumb = image.scaled(target, target, Qt::KeepAspectRatio, Qt::SmoothTransformation); else thumb = image; ThumbnailCache::save(m_path, m_size, thumb); if (*m_cancelFlag) return; emit finished(m_row, downscaleForDisplay(thumb)); }