git repos / SagoImageBrowser

commit 53fdab93

Poul Sander · 2026-05-18 18:45
53fdab93ad12e47925c76806e6c2b8ebca43e8b4 patch · browse files
parent ac476ca8eaff9af7ee53a93d48d8dd760f7d3a27

Add exif information

Changed files

M CMakeLists.txt before
A src/exifreader.cpp
A src/exifreader.h
M src/imageviewwidget.cpp before
M src/imageviewwidget.h before
M src/mainwindow.cpp before
M src/mainwindow.h before
diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ac8fb2..a7fdec4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt
@@ -5,6 +5,8 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Widgets)
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(EXIV2 REQUIRED exiv2)
qt_add_executable(SagoImageBrowser
src/main.cpp
@@ -19,6 +21,9 @@ qt_add_executable(SagoImageBrowser
src/imageviewwidget.cpp
src/imageviewwidget.h
src/preferencesdialog.cpp
- src/preferencesdialog.h)
+ src/preferencesdialog.h
+ src/exifreader.cpp
+ src/exifreader.h)
-target_link_libraries(SagoImageBrowser PRIVATE Qt6::Widgets)
\ No newline at end of file
+target_include_directories(SagoImageBrowser PRIVATE ${EXIV2_INCLUDE_DIRS})
+target_link_libraries(SagoImageBrowser PRIVATE Qt6::Widgets ${EXIV2_LIBRARIES})
\ No newline at end of file
diff --git a/src/exifreader.cpp b/src/exifreader.cpp new file mode 100644 index 0000000..c2efeec --- /dev/null +++ b/src/exifreader.cpp
@@ -0,0 +1,95 @@
+#include "exifreader.h"
+
+#include <QFileInfo>
+#include <QImageReader>
+
+#include <exiv2/exiv2.hpp>
+
+bool ExifData::isEmpty() const
+{
+ return dateTime.isEmpty() && make.isEmpty() && model.isEmpty()
+ && exposureTime.isEmpty() && fNumber.isEmpty() && iso.isEmpty()
+ && focalLength.isEmpty() && flash.isEmpty()
+ && dimensions.isEmpty() && fileSize.isEmpty();
+}
+
+QList<QPair<QString, QString>> ExifData::toList() const
+{
+ QList<QPair<QString, QString>> result;
+ auto add = [&](const char *label, const QString &value) {
+ if (!value.isEmpty())
+ result.append({QString::fromLatin1(label), value});
+ };
+ add("Date/Time", dateTime);
+ add("Camera Make", make);
+ add("Camera Model", model);
+ add("Exposure", exposureTime);
+ add("Aperture", fNumber);
+ add("ISO", iso);
+ add("Focal Length", focalLength);
+ add("Flash", flash);
+ add("Dimensions", dimensions);
+ add("File Size", fileSize);
+ return result;
+}
+
+ExifData ExifReader::read(const QString &path)
+{
+ ExifData data;
+
+ QFileInfo fi(path);
+ if (!fi.exists() || fi.isDir())
+ return data;
+
+ // File size
+ {
+ qint64 size = fi.size();
+ if (size < 1024)
+ data.fileSize = QString::number(size) + QLatin1String(" B");
+ else if (size < 1024 * 1024)
+ data.fileSize = QString::number(size / 1024.0, 'f', 1) + QLatin1String(" KiB");
+ else
+ data.fileSize = QString::number(size / (1024.0 * 1024.0), 'f', 1) + QLatin1String(" MiB");
+ }
+
+ // 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());
+ }
+
+ // EXIF tags via exiv2
+ try {
+ auto image = Exiv2::ImageFactory::open(path.toStdString());
+ image->readMetadata();
+ const Exiv2::ExifData &exif = image->exifData();
+
+ auto findTag = [&](const std::string &key) -> QString {
+ try {
+ auto it = exif.findKey(Exiv2::ExifKey(key));
+ if (it != exif.end())
+ return QString::fromStdString(it->print(&exif)).trimmed();
+ } catch (...) {}
+ return {};
+ };
+
+ data.dateTime = findTag("Exif.Photo.DateTimeOriginal");
+ if (data.dateTime.isEmpty())
+ data.dateTime = findTag("Exif.Image.DateTime");
+ data.make = findTag("Exif.Image.Make");
+ data.model = findTag("Exif.Image.Model");
+ data.exposureTime = findTag("Exif.Photo.ExposureTime");
+ data.fNumber = findTag("Exif.Photo.FNumber");
+ data.iso = findTag("Exif.Photo.ISOSpeedRatings");
+ data.focalLength = findTag("Exif.Photo.FocalLength");
+ data.flash = findTag("Exif.Photo.Flash");
+ } catch (const Exiv2::Error &) {
+ // No EXIF or unsupported format — leave tags empty
+ }
+
+ return data;
+}
diff --git a/src/exifreader.h b/src/exifreader.h new file mode 100644 index 0000000..7b354c4 --- /dev/null +++ b/src/exifreader.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include <QString>
+#include <QList>
+#include <QPair>
+
+struct ExifData {
+ QString dateTime;
+ QString make;
+ QString model;
+ QString exposureTime;
+ QString fNumber;
+ QString iso;
+ QString focalLength;
+ QString flash;
+ QString dimensions;
+ QString fileSize;
+
+ bool isEmpty() const;
+ QList<QPair<QString, QString>> toList() const;
+};
+
+namespace ExifReader {
+ExifData read(const QString &path);
+} // namespace ExifReader
diff --git a/src/imageviewwidget.cpp b/src/imageviewwidget.cpp index aa06e7e..b94233d 100644 --- a/src/imageviewwidget.cpp +++ b/src/imageviewwidget.cpp
@@ -65,6 +65,7 @@ QPixmap ImageViewWidget::loadImageFromDisk(const QString &path)
void ImageViewWidget::setImage(const QString &path)
{
m_currentPath = path;
+ m_exifData = ExifData{}; // clear stale EXIF until new data is set
// Check cache first
if (m_cache.contains(path)) {
@@ -173,6 +174,43 @@ void ImageViewWidget::paintEvent(QPaintEvent *)
int x = (width() - drawn.width()) / 2 + m_offset.x();
int y = (height() - drawn.height()) / 2 + m_offset.y();
painter.drawPixmap(x, y, drawn);
+
+ // EXIF overlay — top-right corner, toggled with 'i'
+ if (m_showExifOverlay) {
+ const auto fields = m_exifData.toList();
+ if (!fields.isEmpty()) {
+ const int padding = 10;
+ const int lineH = 18;
+
+ QFont font = painter.font();
+ font.setPointSize(9);
+ painter.setFont(font);
+ QFontMetrics fm(font);
+
+ int maxW = 0;
+ for (const auto &f : fields)
+ maxW = qMax(maxW, fm.horizontalAdvance(f.first + QLatin1String(": ") + f.second));
+
+ const int boxW = maxW + padding * 2;
+ const int boxH = fields.size() * lineH + padding * 2;
+ const int boxX = width() - boxW - padding;
+ const int boxY = padding;
+
+ painter.save();
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(QColor(0, 0, 0, 160));
+ painter.drawRoundedRect(boxX, boxY, boxW, boxH, 8, 8);
+
+ painter.setPen(Qt::white);
+ for (int i = 0; i < fields.size(); ++i) {
+ const QString line = fields[i].first + QLatin1String(": ") + fields[i].second;
+ painter.drawText(boxX + padding,
+ boxY + padding + fm.ascent() + i * lineH,
+ line);
+ }
+ painter.restore();
+ }
+ }
}
void ImageViewWidget::keyPressEvent(QKeyEvent *event)
@@ -257,9 +295,29 @@ void ImageViewWidget::keyPressEvent(QKeyEvent *event)
return;
}
+ if (event->key() == Qt::Key_I)
+ {
+ toggleExifOverlay();
+ event->accept();
+ return;
+ }
+
QWidget::keyPressEvent(event);
}
+void ImageViewWidget::setExifData(const ExifData &data)
+{
+ m_exifData = data;
+ if (m_showExifOverlay)
+ update();
+}
+
+void ImageViewWidget::toggleExifOverlay()
+{
+ m_showExifOverlay = !m_showExifOverlay;
+ update();
+}
+
void ImageViewWidget::zoomIn()
{
if (m_zoomMode == FitToScreen) {
diff --git a/src/imageviewwidget.h b/src/imageviewwidget.h index b7ebca4..4d87b1b 100644 --- a/src/imageviewwidget.h +++ b/src/imageviewwidget.h
@@ -8,6 +8,7 @@
#include <QMap>
#include <QStringList>
#include <QThread>
+#include "exifreader.h"
class ImageLoadWorker : public QThread
{
@@ -45,6 +46,9 @@ public:
void setNeighborPaths(const QStringList &paths);
void clearCache();
+ void setExifData(const ExifData &data);
+ void toggleExifOverlay();
+
signals:
void closeRequested();
void nextRequested();
@@ -79,4 +83,8 @@ private:
bool m_dragging = false;
QPoint m_dragStart;
QPoint m_offsetAtDragStart;
+
+ // EXIF overlay
+ ExifData m_exifData;
+ bool m_showExifOverlay = false;
};
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index ba2e828..3191178 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp
@@ -3,6 +3,7 @@
#include "imageviewwidget.h"
#include "preferencesdialog.h"
#include "thumbnailcache.h"
+#include "exifreader.h"
#include <QFileSystemModel>
#include <QTreeView>
@@ -25,6 +26,7 @@
#include <QMenu>
#include <QAction>
#include <QSettings>
+#include <QTableWidget>
#include <iostream>
namespace
@@ -119,6 +121,23 @@ void MainWindow::setupUi()
m_previewDock->setObjectName("PreviewDock");
m_previewDock->setWidget(m_previewLabel);
addDockWidget(Qt::RightDockWidgetArea, m_previewDock);
+
+ // EXIF info dock — placed below the preview dock by default
+ m_exifTable = new QTableWidget(0, 2);
+ m_exifTable->setHorizontalHeaderLabels({tr("Field"), tr("Value")});
+ m_exifTable->horizontalHeader()->setStretchLastSection(true);
+ m_exifTable->verticalHeader()->hide();
+ m_exifTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ m_exifTable->setSelectionMode(QAbstractItemView::NoSelection);
+ m_exifTable->setShowGrid(false);
+ m_exifTable->setAlternatingRowColors(true);
+ m_exifTable->setMinimumWidth(200);
+
+ m_exifDock = new QDockWidget(tr("EXIF Info"), this);
+ m_exifDock->setObjectName("ExifDock");
+ m_exifDock->setWidget(m_exifTable);
+ addDockWidget(Qt::RightDockWidgetArea, m_exifDock);
+ splitDockWidget(m_previewDock, m_exifDock, Qt::Vertical);
}
void MainWindow::setupMenuBar()
@@ -132,6 +151,7 @@ void MainWindow::setupMenuBar()
viewMenu->addAction(m_rootDock->toggleViewAction());
viewMenu->addAction(m_folderDock->toggleViewAction());
viewMenu->addAction(m_previewDock->toggleViewAction());
+ viewMenu->addAction(m_exifDock->toggleViewAction());
QMenu *editMenu = m_menuBar->addMenu("&Edit");
QAction *preferencesAction = editMenu->addAction("&Preferences");
@@ -290,6 +310,25 @@ void MainWindow::showPreview(const QModelIndex &index)
m_previewLabel->size(),
Qt::KeepAspectRatio,
Qt::SmoothTransformation));
+
+ updateExifInfo(path);
+}
+
+void MainWindow::updateExifInfo(const QString &path)
+{
+ QFileInfo fi(path);
+ if (fi.isDir()) {
+ m_exifTable->setRowCount(0);
+ return;
+ }
+ const ExifData data = ExifReader::read(path);
+ const auto fields = data.toList();
+ m_exifTable->setRowCount(fields.size());
+ for (int i = 0; i < fields.size(); ++i) {
+ m_exifTable->setItem(i, 0, new QTableWidgetItem(fields[i].first));
+ m_exifTable->setItem(i, 1, new QTableWidgetItem(fields[i].second));
+ }
+ m_exifTable->resizeColumnToContents(0);
}
void MainWindow::enterSingleImageMode(const QModelIndex &index)
@@ -302,9 +341,11 @@ void MainWindow::enterSingleImageMode(const QModelIndex &index)
m_rootDockWasVisible = m_rootDock->isVisible();
m_folderDockWasVisible = m_folderDock->isVisible();
m_previewDockWasVisible = m_previewDock->isVisible();
+ m_exifDockWasVisible = m_exifDock->isVisible();
m_rootDock->hide();
m_folderDock->hide();
m_previewDock->hide();
+ m_exifDock->hide();
m_menuBar->hide();
}
@@ -312,6 +353,8 @@ void MainWindow::enterSingleImageMode(const QModelIndex &index)
QString path = m_imageModel->filePath(index);
m_imageView->setNeighborPaths(computeNeighborPaths(index));
m_imageView->setImage(path);
+ m_imageView->setExifData(ExifReader::read(path));
+ updateExifInfo(path);
m_stack->setCurrentWidget(m_imageView);
m_imageView->setFocus();
@@ -336,6 +379,8 @@ void MainWindow::leaveSingleImageMode()
m_folderDock->show();
if (m_previewDockWasVisible)
m_previewDock->show();
+ if (m_exifDockWasVisible)
+ m_exifDock->show();
m_listView->setFocus();
}
@@ -440,6 +485,7 @@ void MainWindow::applyDockLocking(bool lock)
m_rootDock->setFeatures(features);
m_folderDock->setFeatures(features);
m_previewDock->setFeatures(features);
+ m_exifDock->setFeatures(features);
}
void MainWindow::loadPreferences()
diff --git a/src/mainwindow.h b/src/mainwindow.h index 49ddee6..ba593b4 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h
@@ -10,6 +10,7 @@ class QMenuBar;
class QDockWidget;
class QFileSystemModel;
class QListWidget;
+class QTableWidget;
class ImageModel;
class ImageViewWidget;
class PreferencesDialog;
@@ -34,6 +35,7 @@ private:
void setFullScreenMode(bool fullScreen);
void loadVisibleThumbnails();
void showPreview(const QModelIndex &index);
+ void updateExifInfo(const QString &path);
void enterSingleImageMode(const QModelIndex &index);
void leaveSingleImageMode();
QStringList computeNeighborPaths(const QModelIndex &index) const;
@@ -63,9 +65,12 @@ private:
QDockWidget *m_rootDock = nullptr;
QDockWidget *m_folderDock = nullptr;
QDockWidget *m_previewDock = nullptr;
+ QDockWidget *m_exifDock = nullptr;
+ QTableWidget *m_exifTable = nullptr;
bool m_rootDockWasVisible = true;
bool m_folderDockWasVisible = true;
bool m_previewDockWasVisible = true;
+ bool m_exifDockWasVisible = true;
bool m_lockDockingPreference = false;
void applyDockLocking(bool lock);