git repos / SagoImageBrowser

commit 80a9926d

Poul Sander · 2026-05-19 20:28
80a9926d20e6e662214fd2964baf7377b4d9710a patch · browse files
parent 486078489917f7b8a73188307102508f290d90e1

The browser does no longer fail if a file has an invalid filename

Changed files

M src/exifreader.cpp before
M src/exifreader.h before
M src/imagemodel.cpp before
M src/imagemodel.h before
M src/imageviewwidget.cpp before
M src/imageviewwidget.h before
M src/mainwindow.cpp before
M src/mainwindow.h before
M src/thumbnailcache.cpp before
M src/thumbnailcache.h before
M src/thumbnailworker.cpp before
M src/thumbnailworker.h before
diff --git a/src/exifreader.cpp b/src/exifreader.cpp index 06db53d..0a9ddb7 100644 --- a/src/exifreader.cpp +++ b/src/exifreader.cpp
@@ -1,10 +1,14 @@
#include "exifreader.h"
-#include <QFileInfo>
+#include <QFile>
#include <QImageReader>
#include <exiv2/exiv2.hpp>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
// Raw EXIF date format is "YYYY:MM:DD HH:MM:SS"; reformat date separators to dashes
static QString formatDate(const QString &raw)
{
@@ -53,17 +57,17 @@ QList<QPair<QString, QString>> ExifData::toList() const
return result;
}
-ExifData ExifReader::read(const QString &path)
+ExifData ExifReader::read(const QByteArray &path)
{
ExifData data;
- QFileInfo fi(path);
- if (!fi.exists() || fi.isDir())
+ struct ::stat st{};
+ if (::stat(path.constData(), &st) != 0 || !S_ISREG(st.st_mode))
return data;
// File size
{
- qint64 size = fi.size();
+ qint64 size = static_cast<qint64>(st.st_size);
if (size < 1024)
data.fileSize = QString::number(size) + QLatin1String(" B");
else if (size < 1024 * 1024)
@@ -74,17 +78,26 @@ ExifData ExifReader::read(const QString &path)
// Image dimensions via Qt (reads only the header, no full decode needed)
{
- QImageReader reader(path);
- reader.setDecideFormatFromContent(true);
- QSize sz = reader.size();
- if (sz.isValid())
- data.dimensions = QString::fromLatin1("%1 * %2")
- .arg(sz.width()).arg(sz.height());
+ int fd = ::open(path.constData(), O_RDONLY | O_CLOEXEC);
+ if (fd >= 0) {
+ QFile file;
+ if (file.open(fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle)) {
+ QImageReader reader(&file);
+ reader.setDecideFormatFromContent(true);
+ QSize sz = reader.size();
+ if (sz.isValid())
+ data.dimensions = QString::fromLatin1("%1 * %2")
+ .arg(sz.width()).arg(sz.height());
+ } else {
+ ::close(fd);
+ }
+ }
}
// EXIF tags via exiv2
try {
- std::unique_ptr<Exiv2::Image> image = Exiv2::ImageFactory::open(path.toStdString());
+ std::unique_ptr<Exiv2::Image> image =
+ Exiv2::ImageFactory::open(path.toStdString());
image->readMetadata();
const Exiv2::ExifData &exif = image->exifData();
diff --git a/src/exifreader.h b/src/exifreader.h index 7b354c4..6d2957a 100644 --- a/src/exifreader.h +++ b/src/exifreader.h
@@ -1,5 +1,6 @@
#pragma once
+#include <QByteArray>
#include <QString>
#include <QList>
#include <QPair>
@@ -21,5 +22,5 @@ struct ExifData {
};
namespace ExifReader {
-ExifData read(const QString &path);
+ExifData read(const QByteArray &path);
} // namespace ExifReader
diff --git a/src/imagemodel.cpp b/src/imagemodel.cpp index 6fb9368..93cc8d4 100644 --- a/src/imagemodel.cpp +++ b/src/imagemodel.cpp
@@ -1,11 +1,14 @@
#include "imagemodel.h"
#include "thumbnailworker.h"
-#include <QDir>
+#include <QFile>
#include <QImageReader>
-#include <QFileInfo>
#include <QFileIconProvider>
+#include <algorithm>
+#include <filesystem>
+#include <vector>
+
ImageModel::ImageModel(QObject *parent)
: QAbstractListModel(parent)
{
@@ -69,6 +72,11 @@ void ImageModel::setThumbnailSize(ThumbnailCache::Size size)
void ImageModel::setDirectory(const QString &path)
{
+ setDirectory(QFile::encodeName(path));
+}
+
+void ImageModel::setDirectory(const QByteArray &path)
+{
m_cancelFlag = true;
m_threadPool.waitForDone();
m_cancelFlag = false;
@@ -94,49 +102,101 @@ void ImageModel::setDirectory(const QString &path)
m_pendingRows.clear();
m_currentDir = path;
- QDir dir(path);
+ // Build set of supported image extensions (lower-case, without dot)
+ QSet<QByteArray> imageExts;
+ for (const QByteArray &fmt : QImageReader::supportedImageFormats())
+ imageExts.insert(fmt.toLower());
+
+ // Use std::filesystem::directory_iterator so that directory entries with
+ // non-UTF-8 filenames (e.g. Latin-1 encoded names) are not silently
+ // skipped or mangled as they would be with QDir::entryInfoList().
+ namespace fs = std::filesystem;
+
+ std::vector<std::string> dirs;
+ std::vector<std::string> imageFiles;
+
+ try {
+ fs::path dirPath(path.toStdString());
+
+ // Add ".." parent entry directly with the literal display name ".."
+ // (mirrors QDir::NoDot behaviour)
+ fs::path parentPath = dirPath.parent_path();
+ if (!parentPath.empty() && parentPath != dirPath) {
+ QPixmap folderPixmap = m_folderIcon.pixmap(128, 128);
+ m_items.append({QByteArray::fromStdString(parentPath.native()),
+ QStringLiteral(".."), folderPixmap, true, true});
+ }
- // Add subdirectories first
- dir.setFilter(QDir::AllDirs | QDir::NoDot);
- dir.setSorting(QDir::Name);
- const auto dirs = dir.entryInfoList();
- QPixmap folderPixmap = m_folderIcon.pixmap(128, 128);
- for (const QFileInfo &d : dirs)
- {
- QString name = d.fileName(); // preserves ".." for the parent entry
- m_items.append({d.absoluteFilePath(), name, folderPixmap, true, true});
+ for (const fs::directory_entry &entry :
+ fs::directory_iterator(dirPath,
+ fs::directory_options::skip_permission_denied))
+ {
+ std::error_code ec;
+ const std::string fname = entry.path().filename().native();
+ if (fname.empty() || fname[0] == '.')
+ continue; // skip hidden entries
+
+ if (entry.is_directory(ec)) {
+ dirs.push_back(entry.path().native());
+ } else if (entry.is_regular_file(ec)) {
+ // Filter by image extension
+ const std::string &nativeExt = entry.path().extension().native();
+ if (nativeExt.empty())
+ continue;
+ // extension() includes the dot; strip it and lower-case
+ QByteArray ext = QByteArray(nativeExt.c_str() + 1,
+ static_cast<qsizetype>(nativeExt.size()) - 1).toLower();
+ if (imageExts.contains(ext))
+ imageFiles.push_back(entry.path().native());
+ }
+ }
+ } catch (const fs::filesystem_error &) {
+ // Directory unreadable — show empty listing
}
- // Then add image files
- QStringList filters;
+ // Sort subdirectories by filename
+ std::sort(dirs.begin(), dirs.end(),
+ [](const auto &a, const auto &b) {
+ return fs::path(a).filename() < fs::path(b).filename();
+ });
- const auto formats = QImageReader::supportedImageFormats();
- for (const QByteArray &fmt : formats)
- filters << "*." + fmt;
+ std::sort(imageFiles.begin(), imageFiles.end(),
+ [](const auto &a, const auto &b) {
+ return fs::path(a).filename() < fs::path(b).filename();
+ });
- dir.setNameFilters(filters);
- dir.setFilter(QDir::Files);
- dir.setSorting(QDir::Name);
-
- const auto files = dir.entryInfoList();
+ QPixmap folderPixmap = m_folderIcon.pixmap(128, 128);
+ for (const std::string &nativePath : dirs)
+ {
+ QByteArray entryPath = QByteArray::fromStdString(nativePath);
+ std::string fname = fs::path(nativePath).filename().native();
+ QString displayName = QString::fromLocal8Bit(fname.c_str(),
+ static_cast<qsizetype>(fname.size()));
+ m_items.append({entryPath, displayName, folderPixmap, true, true});
+ }
// Restore cached thumbnails for this folder if available
const ThumbnailMap *cached = m_folderThumbnailCache.contains(path)
? &m_folderThumbnailCache[path]
: nullptr;
- for (const QFileInfo &file : files)
+ for (const std::string &nativePath : imageFiles)
{
+ QByteArray entryPath = QByteArray::fromStdString(nativePath);
+ std::string fname = fs::path(nativePath).filename().native();
+ QString displayName = QString::fromLocal8Bit(fname.c_str(),
+ static_cast<qsizetype>(fname.size()));
+
QPixmap thumb = m_placeholder;
bool loaded = false;
if (cached) {
- auto it = cached->find(file.absoluteFilePath());
+ auto it = cached->find(entryPath);
if (it != cached->end()) {
thumb = it.value();
loaded = true;
}
}
- m_items.append({file.absoluteFilePath(), file.fileName(), thumb, loaded, false});
+ m_items.append({entryPath, displayName, thumb, loaded, false});
}
endResetModel();
@@ -187,7 +247,7 @@ void ImageModel::queueRow(int row)
m_threadPool.start(worker);
}
-QString ImageModel::filePath(const QModelIndex &index) const
+QByteArray ImageModel::filePath(const QModelIndex &index) const
{
if (!index.isValid())
return {};
@@ -202,7 +262,7 @@ bool ImageModel::isFolder(const QModelIndex &index) const
return m_items[index.row()].isFolder;
}
-QString ImageModel::currentDirectory() const
+QByteArray ImageModel::currentDirectory() const
{
return m_currentDir;
}
diff --git a/src/imagemodel.h b/src/imagemodel.h index 515de46..e64708d 100644 --- a/src/imagemodel.h +++ b/src/imagemodel.h
@@ -3,6 +3,7 @@
#include "thumbnailcache.h"
#include <QAbstractListModel>
+#include <QByteArray>
#include <QPixmap>
#include <QVector>
#include <QThreadPool>
@@ -22,10 +23,12 @@ public:
explicit ImageModel(QObject *parent = nullptr);
~ImageModel();
- void setDirectory(const QString &path);
- QString filePath(const QModelIndex &index) const;
+ // path is the raw native OS byte string (handles non-UTF-8 filenames)
+ void setDirectory(const QByteArray &path);
+ void setDirectory(const QString &path); // convenience: converts via QFile::encodeName
+ QByteArray filePath(const QModelIndex &index) const;
bool isFolder(const QModelIndex &index) const;
- QString currentDirectory() const;
+ QByteArray currentDirectory() const;
void requestThumbnails(int firstRow, int lastRow);
void setCacheMaxSize(int n);
@@ -39,7 +42,7 @@ public:
private:
struct Item
{
- QString path;
+ QByteArray path; // raw native OS bytes — safe for non-UTF-8 filenames
QString displayName;
QPixmap thumbnail;
bool loaded = false;
@@ -48,7 +51,7 @@ private:
void queueRow(int row);
- using ThumbnailMap = QHash<QString, QPixmap>;
+ using ThumbnailMap = QHash<QByteArray, QPixmap>;
QVector<Item> m_items;
QThreadPool m_threadPool;
@@ -57,11 +60,11 @@ private:
QSet<int> m_pendingRows;
std::atomic_bool m_cancelFlag{false};
QIcon m_folderIcon;
- QString m_currentDir;
+ QByteArray m_currentDir;
// Folder thumbnail cache (last N visited directories)
- QHash<QString, ThumbnailMap> m_folderThumbnailCache;
- QList<QString> m_cacheOrder;
+ QHash<QByteArray, ThumbnailMap> m_folderThumbnailCache;
+ QList<QByteArray> m_cacheOrder;
int m_cacheMaxSize = 4;
ThumbnailCache::Size m_thumbnailSize = ThumbnailCache::Size::Normal;
diff --git a/src/imageviewwidget.cpp b/src/imageviewwidget.cpp index b94233d..44802b2 100644 --- a/src/imageviewwidget.cpp +++ b/src/imageviewwidget.cpp
@@ -3,21 +3,38 @@
#include <QPainter>
#include <QKeyEvent>
#include <QMouseEvent>
+#include <QFile>
#include <QImageReader>
#include <QPalette>
#include <algorithm>
#include <iostream>
+#include <fcntl.h>
+#include <unistd.h>
+
// --- ImageLoadWorker ---
-ImageLoadWorker::ImageLoadWorker(const QString &path, QObject *parent)
+ImageLoadWorker::ImageLoadWorker(const QByteArray &path, QObject *parent)
: QThread(parent), m_path(path)
{
}
void ImageLoadWorker::run()
{
- QImageReader reader(m_path);
+ int fd = ::open(m_path.constData(), O_RDONLY | O_CLOEXEC);
+ if (fd < 0) {
+ emit imageLoaded(m_path, QPixmap{});
+ return;
+ }
+
+ QFile file;
+ if (!file.open(fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle)) {
+ ::close(fd);
+ emit imageLoaded(m_path, QPixmap{});
+ return;
+ }
+
+ QImageReader reader(&file);
reader.setAutoTransform(true);
reader.setDecideFormatFromContent(true);
reader.setAllocationLimit(0);
@@ -44,9 +61,19 @@ ImageViewWidget::ImageViewWidget(QWidget *parent)
setBackgroundColor("black");
}
-QPixmap ImageViewWidget::loadImageFromDisk(const QString &path)
+QPixmap ImageViewWidget::loadImageFromDisk(const QByteArray &path)
{
- QImageReader reader(path);
+ int fd = ::open(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);
reader.setDecideFormatFromContent(true);
reader.setAllocationLimit(0);
@@ -59,10 +86,10 @@ QPixmap ImageViewWidget::loadImageFromDisk(const QString &path)
image = image.convertToFormat(QImage::Format_ARGB32);
return QPixmap::fromImage(image);
}
- return QPixmap();
+ return {};
}
-void ImageViewWidget::setImage(const QString &path)
+void ImageViewWidget::setImage(const QByteArray &path)
{
m_currentPath = path;
m_exifData = ExifData{}; // clear stale EXIF until new data is set
@@ -83,7 +110,7 @@ void ImageViewWidget::setImage(const QString &path)
// Trigger prefetch of neighbors if they have been set
if (!m_neighborPaths.isEmpty()) {
// Evict entries that are no longer in the neighbor window
- QSet<QString> keep(m_neighborPaths.begin(), m_neighborPaths.end());
+ QSet<QByteArray> keep(m_neighborPaths.begin(), m_neighborPaths.end());
keep.insert(m_currentPath);
for (auto it = m_cache.begin(); it != m_cache.end(); ) {
if (!keep.contains(it.key()))
@@ -93,13 +120,13 @@ void ImageViewWidget::setImage(const QString &path)
}
// Prefetch neighbors that are not yet cached
- for (const QString &neighbor : m_neighborPaths) {
+ for (const QByteArray &neighbor : m_neighborPaths) {
prefetchImage(neighbor);
}
}
}
-void ImageViewWidget::prefetchImage(const QString &path)
+void ImageViewWidget::prefetchImage(const QByteArray &path)
{
if (m_cache.contains(path) || m_pendingLoads.contains(path))
return;
@@ -111,19 +138,19 @@ void ImageViewWidget::prefetchImage(const QString &path)
worker->start();
}
-void ImageViewWidget::onImageLoaded(const QString &path, const QPixmap &pixmap)
+void ImageViewWidget::onImageLoaded(const QByteArray &path, const QPixmap &pixmap)
{
m_pendingLoads.remove(path);
// Only cache if path is still relevant (current or a neighbor)
- QSet<QString> keep(m_neighborPaths.begin(), m_neighborPaths.end());
+ QSet<QByteArray> keep(m_neighborPaths.begin(), m_neighborPaths.end());
keep.insert(m_currentPath);
if (keep.contains(path)) {
m_cache[path] = pixmap;
}
}
-void ImageViewWidget::setNeighborPaths(const QStringList &paths)
+void ImageViewWidget::setNeighborPaths(const QList<QByteArray> &paths)
{
m_neighborPaths = paths;
}
diff --git a/src/imageviewwidget.h b/src/imageviewwidget.h index 4d87b1b..3895eca 100644 --- a/src/imageviewwidget.h +++ b/src/imageviewwidget.h
@@ -3,10 +3,10 @@
#include <QWidget>
#include <QImage>
#include <QPixmap>
-#include <QString>
-#include <QPoint>
+#include <QByteArray>
+#include <QList>
#include <QMap>
-#include <QStringList>
+#include <QSet>
#include <QThread>
#include "exifreader.h"
@@ -14,12 +14,12 @@ class ImageLoadWorker : public QThread
{
Q_OBJECT
public:
- explicit ImageLoadWorker(const QString &path, QObject *parent = nullptr);
+ explicit ImageLoadWorker(const QByteArray &path, QObject *parent = nullptr);
void run() override;
signals:
- void imageLoaded(const QString &path, const QPixmap &pixmap);
+ void imageLoaded(const QByteArray &path, const QPixmap &pixmap);
private:
- QString m_path;
+ QByteArray m_path;
};
class ImageViewWidget : public QWidget
@@ -35,15 +35,15 @@ public:
explicit ImageViewWidget(QWidget *parent = nullptr);
- void setImage(const QString &path);
+ void setImage(const QByteArray &path);
void setBackgroundColor(const QString &color);
void zoomIn();
void zoomOut();
void zoomOriginal();
void zoomFitToScreen();
- void prefetchImage(const QString &path);
- void setNeighborPaths(const QStringList &paths);
+ void prefetchImage(const QByteArray &path);
+ void setNeighborPaths(const QList<QByteArray> &paths);
void clearCache();
void setExifData(const ExifData &data);
@@ -64,19 +64,19 @@ protected:
private:
void clampOffset(int imgW, int imgH);
- QPixmap loadImageFromDisk(const QString &path);
- void onImageLoaded(const QString &path, const QPixmap &pixmap);
+ QPixmap loadImageFromDisk(const QByteArray &path);
+ void onImageLoaded(const QByteArray &path, const QPixmap &pixmap);
QPixmap m_pixmap;
ZoomMode m_zoomMode = FitToScreen;
double m_zoomFactor = 1.0;
QString m_backgroundColor = "black";
- QString m_currentPath;
+ QByteArray m_currentPath;
// Image cache: path -> pixmap
- QMap<QString, QPixmap> m_cache;
- QStringList m_neighborPaths; // ordered list of paths around current image
- QSet<QString> m_pendingLoads; // paths currently being loaded in background
+ QMap<QByteArray, QPixmap> m_cache;
+ QList<QByteArray> m_neighborPaths; // ordered list of paths around current image
+ QSet<QByteArray> m_pendingLoads; // paths currently being loaded in background
// Panning state
QPoint m_offset; // current pan offset (pixels)
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 12ff796..110812f 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp
@@ -5,6 +5,7 @@
#include "thumbnailcache.h"
#include "exifreader.h"
+#include <QFile>
#include <QFileSystemModel>
#include <QTreeView>
#include <QListView>
@@ -29,6 +30,10 @@
#include <QTableWidget>
#include <iostream>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
namespace
{
ThumbnailCache::Size thumbnailSizeFromString(const QString &s)
@@ -160,9 +165,17 @@ void MainWindow::setupMenuBar()
void MainWindow::navigateToFolder(const QString &path)
{
+ navigateToFolder(QFile::encodeName(path));
+}
+
+void MainWindow::navigateToFolder(const QByteArray &path)
+{
m_previousFolderPath = m_imageModel->currentDirectory();
m_imageModel->setDirectory(path);
- QModelIndex dirIdx = m_dirModel->index(path);
+ // The tree view uses QFileSystemModel which works with QString; convert for it.
+ // If the path contains non-UTF-8 bytes the index lookup may fail — that is
+ // acceptable (the tree view simply won't highlight the node).
+ QModelIndex dirIdx = m_dirModel->index(QFile::decodeName(path));
if (dirIdx.isValid())
m_treeView->setCurrentIndex(dirIdx);
QTimer::singleShot(0, this, [this]() {
@@ -178,19 +191,19 @@ void MainWindow::openPath(const QString &path)
return;
if (info.isDir()) {
- navigateToFolder(info.absoluteFilePath());
+ navigateToFolder(QFile::encodeName(info.absoluteFilePath()));
} else if (info.isFile()) {
- navigateToFolder(info.absolutePath());
+ navigateToFolder(QFile::encodeName(info.absolutePath()));
// ImageModel::setDirectory is synchronous, so items are ready now.
// Find the file in the model and open it.
+ QByteArray target = QFile::encodeName(info.absoluteFilePath());
for (int i = 0; i < m_imageModel->rowCount(); ++i) {
QModelIndex idx = m_imageModel->index(i);
- if (m_imageModel->filePath(idx) == info.absoluteFilePath()) {
+ if (m_imageModel->filePath(idx) == target) {
m_listView->setCurrentIndex(idx);
m_listView->scrollTo(idx);
showPreview(idx);
- // ImageModel only lists image files and folders; non-folder = image.
if (!m_imageModel->isFolder(idx))
enterSingleImageMode(idx);
break;
@@ -208,7 +221,7 @@ void MainWindow::selectPreviousFolderIfExists()
for (int i = 0; i < m_imageModel->rowCount(); ++i) {
QModelIndex idx = m_imageModel->index(i);
if (m_imageModel->isFolder(idx)) {
- QString folderPath = m_imageModel->filePath(idx);
+ QByteArray folderPath = m_imageModel->filePath(idx);
if (folderPath == m_previousFolderPath) {
m_listView->setCurrentIndex(idx);
m_listView->scrollTo(idx);
@@ -246,7 +259,7 @@ void MainWindow::setupConnections()
connect(m_listView, &QListView::doubleClicked, this,
[this](const QModelIndex &index) {
if (m_imageModel->isFolder(index)) {
- QString path = m_imageModel->filePath(index);
+ QByteArray path = m_imageModel->filePath(index);
navigateToFolder(path);
} else {
enterSingleImageMode(index);
@@ -321,31 +334,36 @@ void MainWindow::showPreview(const QModelIndex &index)
if (!index.isValid())
return;
- QString path = m_imageModel->filePath(index);
-
- QImageReader reader(path);
- reader.setAutoTransform(true);
- // Scale during decoding for the preview to avoid the 128 MB allocation limit
- QSize fullSize = reader.size();
- if (fullSize.isValid()) {
- QSize target = fullSize.scaled(m_previewLabel->size(), Qt::KeepAspectRatio);
- reader.setScaledSize(target);
+ QByteArray path = m_imageModel->filePath(index);
+
+ int fd = ::open(path.constData(), O_RDONLY | O_CLOEXEC);
+ if (fd >= 0) {
+ QFile file;
+ if (file.open(fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle)) {
+ QImageReader reader(&file);
+ reader.setAutoTransform(true);
+ QSize fullSize = reader.size();
+ if (fullSize.isValid()) {
+ QSize target = fullSize.scaled(m_previewLabel->size(), Qt::KeepAspectRatio);
+ reader.setScaledSize(target);
+ }
+ QImage image = reader.read();
+ m_previewLabel->setPixmap(
+ QPixmap::fromImage(image).scaled(
+ m_previewLabel->size(),
+ Qt::KeepAspectRatio,
+ Qt::SmoothTransformation));
+ }
}
- QImage image = reader.read();
-
- m_previewLabel->setPixmap(
- QPixmap::fromImage(image).scaled(
- m_previewLabel->size(),
- Qt::KeepAspectRatio,
- Qt::SmoothTransformation));
updateExifInfo(path);
}
-void MainWindow::updateExifInfo(const QString &path)
+void MainWindow::updateExifInfo(const QByteArray &path)
{
- QFileInfo fi(path);
- if (fi.isDir()) {
+ // Check if this is a folder by attempting stat
+ struct ::stat st{};
+ if (::stat(path.constData(), &st) != 0 || S_ISDIR(st.st_mode)) {
m_exifTable->setRowCount(0);
return;
}
@@ -378,7 +396,7 @@ void MainWindow::enterSingleImageMode(const QModelIndex &index)
}
m_imageView->setBackgroundColor(m_backgroundColorPreference);
- QString path = m_imageModel->filePath(index);
+ QByteArray path = m_imageModel->filePath(index);
m_imageView->setNeighborPaths(computeNeighborPaths(index));
m_imageView->setImage(path);
m_imageView->setExifData(ExifReader::read(path));
@@ -412,9 +430,9 @@ void MainWindow::leaveSingleImageMode()
m_listView->setFocus();
}
-QStringList MainWindow::computeNeighborPaths(const QModelIndex &index) const
+QList<QByteArray> MainWindow::computeNeighborPaths(const QModelIndex &index) const
{
- QStringList paths;
+ QList<QByteArray> paths;
int row = index.row();
// Collect up to CacheKeepBehind non-folder images before current
@@ -475,7 +493,7 @@ void MainWindow::keyPressEvent(QKeyEvent *event)
if (current.isValid())
{
if (m_imageModel->isFolder(current)) {
- QString path = m_imageModel->filePath(current);
+ QByteArray path = m_imageModel->filePath(current);
navigateToFolder(path);
} else {
enterSingleImageMode(current);
diff --git a/src/mainwindow.h b/src/mainwindow.h index fef271a..444b448 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h
@@ -1,5 +1,6 @@
#pragma once
+#include <QByteArray>
#include <QMainWindow>
class QTreeView;
@@ -31,15 +32,16 @@ private:
void savePreferences();
void onPreferencesTriggered();
void resetLayoutToDefault();
- void navigateToFolder(const QString &path);
+ void navigateToFolder(const QByteArray &path);
+ void navigateToFolder(const QString &path); // convenience: converts via QFile::encodeName
void selectPreviousFolderIfExists();
void setFullScreenMode(bool fullScreen);
void loadVisibleThumbnails();
void showPreview(const QModelIndex &index);
- void updateExifInfo(const QString &path);
+ void updateExifInfo(const QByteArray &path);
void enterSingleImageMode(const QModelIndex &index);
void leaveSingleImageMode();
- QStringList computeNeighborPaths(const QModelIndex &index) const;
+ QList<QByteArray> computeNeighborPaths(const QModelIndex &index) const;
void keyPressEvent(QKeyEvent *event) override;
bool eventFilter(QObject *obj, QEvent *event) override;
@@ -61,7 +63,7 @@ private:
ImageViewWidget *m_imageView;
bool m_wasMaximized = false;
bool m_imageViewWasFullScreen = false;
- QString m_previousFolderPath;
+ QByteArray m_previousFolderPath;
QMenuBar *m_menuBar = nullptr;
QDockWidget *m_rootDock = nullptr;
QDockWidget *m_folderDock = nullptr;
diff --git a/src/thumbnailcache.cpp b/src/thumbnailcache.cpp index fd25ee2..8f6ad00 100644 --- a/src/thumbnailcache.cpp +++ b/src/thumbnailcache.cpp
@@ -3,12 +3,12 @@
#include <QCryptographicHash>
#include <QDir>
#include <QFile>
-#include <QFileInfo>
-#include <QMimeDatabase>
#include <QStandardPaths>
#include <QTemporaryFile>
#include <QUrl>
+#include <sys/stat.h>
+
namespace
{
constexpr const char *kSoftware = "SagoImageBrowser";
@@ -38,43 +38,41 @@ QString ThumbnailCache::subdirFor(Size size)
return cacheRoot() + "/" + name;
}
-QString ThumbnailCache::canonicalUri(const QString &absPath)
+QByteArray ThumbnailCache::canonicalUri(const QByteArray &absPath)
{
- // Per the spec, use the absolute file URI (percent-encoded).
- return QUrl::fromLocalFile(absPath).toString(QUrl::FullyEncoded);
+ // Build a correct file:// URI by percent-encoding the raw byte path.
+ // Forward slashes are kept unencoded; all other bytes (including non-UTF-8
+ // ones such as Latin-1 \xF8) are percent-encoded.
+ return "file://" + QUrl::toPercentEncoding(absPath, "/");
}
-QString ThumbnailCache::hashedName(const QString &canonicalUri)
+QString ThumbnailCache::hashedName(const QByteArray &canonicalUri)
{
- QByteArray hash = QCryptographicHash::hash(canonicalUri.toUtf8(),
- QCryptographicHash::Md5)
- .toHex();
+ QByteArray hash =
+ QCryptographicHash::hash(canonicalUri, QCryptographicHash::Md5).toHex();
return QString::fromLatin1(hash) + ".png";
}
-bool ThumbnailCache::isInsideCache(const QString &absPath)
+bool ThumbnailCache::isInsideCache(const QByteArray &absPath)
{
- QString abs = QFileInfo(absPath).absoluteFilePath();
- QString root = QFileInfo(cacheRoot()).absoluteFilePath();
- if (root.isEmpty())
- return false;
- return abs.startsWith(root + "/") || abs == root;
+ QByteArray root = cacheRoot().toUtf8(); // cache root is always valid UTF-8
+ return absPath.startsWith(root + '/') || absPath == root;
}
-QImage ThumbnailCache::load(const QString &sourcePath, Size size)
+QImage ThumbnailCache::load(const QByteArray &sourcePath, Size size)
{
- QFileInfo info(sourcePath);
- if (!info.exists() || !info.isFile())
+ struct ::stat st{};
+ if (::stat(sourcePath.constData(), &st) != 0 || !S_ISREG(st.st_mode))
return {};
- const QString uri = canonicalUri(info.absoluteFilePath());
+ const QByteArray uri = canonicalUri(sourcePath);
const QString cachePath = subdirFor(size) + "/" + hashedName(uri);
QImage img;
if (!img.load(cachePath, "PNG"))
return {};
- const qint64 mtime = info.lastModified().toSecsSinceEpoch();
+ const qint64 mtime = static_cast<qint64>(st.st_mtime);
bool ok = false;
const qint64 cachedMTime = img.text("Thumb::MTime").toLongLong(&ok);
if (!ok || cachedMTime != mtime)
@@ -83,16 +81,16 @@ QImage ThumbnailCache::load(const QString &sourcePath, Size size)
return img;
}
-void ThumbnailCache::save(const QString &sourcePath, Size size, const QImage &thumbImage)
+void ThumbnailCache::save(const QByteArray &sourcePath, Size size, const QImage &thumbImage)
{
if (thumbImage.isNull())
return;
- QFileInfo info(sourcePath);
- if (!info.exists() || !info.isFile())
+ struct ::stat st{};
+ if (::stat(sourcePath.constData(), &st) != 0 || !S_ISREG(st.st_mode))
return;
- const QString uri = canonicalUri(info.absoluteFilePath());
+ const QByteArray uri = canonicalUri(sourcePath);
const QString subdir = subdirFor(size);
const QString finalPath = subdir + "/" + hashedName(uri);
@@ -103,16 +101,10 @@ void ThumbnailCache::save(const QString &sourcePath, Size size, const QImage &th
QFile::setPermissions(cacheRoot(), dirPerms());
QImage tagged = thumbImage;
- tagged.setText("Thumb::URI", uri);
+ tagged.setText("Thumb::URI", QString::fromLatin1(uri));
tagged.setText("Thumb::MTime",
- QString::number(info.lastModified().toSecsSinceEpoch()));
- tagged.setText("Thumb::Size", QString::number(info.size()));
- QMimeDatabase mimeDb;
- QString mime = mimeDb.mimeTypeForFile(info).name();
- if (!mime.isEmpty())
- tagged.setText("Thumb::Mimetype", mime);
- tagged.setText("Thumb::Image::Width", QString::number(thumbImage.width()));
- tagged.setText("Thumb::Image::Height", QString::number(thumbImage.height()));
+ QString::number(static_cast<qint64>(st.st_mtime)));
+ tagged.setText("Thumb::Size", QString::number(static_cast<qint64>(st.st_size)));
tagged.setText("Software", QString::fromLatin1(kSoftware));
QTemporaryFile tmp(subdir + "/tmp_XXXXXX.png");
diff --git a/src/thumbnailcache.h b/src/thumbnailcache.h index 67b9155..d9c0f66 100644 --- a/src/thumbnailcache.h +++ b/src/thumbnailcache.h
@@ -1,5 +1,6 @@
#pragma once
+#include <QByteArray>
#include <QString>
#include <QImage>
@@ -13,17 +14,17 @@ public:
};
// Returns a non-null QImage if a valid cached thumbnail exists for sourcePath
- // at the requested size. Validity is determined by comparing the original
- // file's mtime to the Thumb::MTime tEXt chunk in the cached PNG.
- static QImage load(const QString &sourcePath, Size size);
+ // at the requested size. sourcePath must be the raw native OS byte string.
+ static QImage load(const QByteArray &sourcePath, Size size);
// Atomically writes thumbImage to the cache with the required Freedesktop
- // tEXt chunks. No-op if sourcePath cannot be stat'ed.
- static void save(const QString &sourcePath, Size size, const QImage &thumbImage);
+ // tEXt chunks. sourcePath must be the raw native OS byte string.
+ static void save(const QByteArray &sourcePath, Size size, const QImage &thumbImage);
static QString cacheRoot();
static QString subdirFor(Size size);
- static QString canonicalUri(const QString &absPath);
- static QString hashedName(const QString &canonicalUri);
- static bool isInsideCache(const QString &absPath);
+ // Canonical file:// URI built from raw byte path (correct percent-encoding)
+ static QByteArray canonicalUri(const QByteArray &absPath);
+ static QString hashedName(const QByteArray &canonicalUri);
+ static bool isInsideCache(const QByteArray &absPath);
};
diff --git a/src/thumbnailworker.cpp b/src/thumbnailworker.cpp index 04868a5..3e1024f 100644 --- a/src/thumbnailworker.cpp +++ b/src/thumbnailworker.cpp
@@ -2,8 +2,12 @@
#include "thumbnailcache.h"
+#include <QFile>
#include <QImageReader>
+#include <fcntl.h>
+#include <unistd.h>
+
namespace
{
constexpr int kDisplaySize = 128;
@@ -20,7 +24,7 @@ QImage downscaleForDisplay(const QImage &src)
}
} // namespace
-ThumbnailWorker::ThumbnailWorker(const QString &path,
+ThumbnailWorker::ThumbnailWorker(const QByteArray &path,
int row,
ThumbnailCache::Size size,
std::atomic_bool *cancelFlag)
@@ -53,7 +57,20 @@ void ThumbnailWorker::run()
return;
}
- QImageReader reader(m_path);
+ // 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
diff --git a/src/thumbnailworker.h b/src/thumbnailworker.h index c481763..65d6485 100644 --- a/src/thumbnailworker.h +++ b/src/thumbnailworker.h
@@ -2,6 +2,7 @@
#include "thumbnailcache.h"
+#include <QByteArray>
#include <QRunnable>
#include <QObject>
#include <QImage>
@@ -12,7 +13,7 @@ class ThumbnailWorker : public QObject, public QRunnable
Q_OBJECT
public:
- ThumbnailWorker(const QString &path,
+ ThumbnailWorker(const QByteArray &path,
int row,
ThumbnailCache::Size size,
std::atomic_bool *cancelFlag);
@@ -23,7 +24,7 @@ signals:
void finished(int row, const QImage &image);
private:
- QString m_path;
+ QByteArray m_path;
int m_row;
ThumbnailCache::Size m_size;
std::atomic_bool *m_cancelFlag;