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 +#include + +#include + +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> ExifData::toList() const +{ + QList> 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 +#include +#include + +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> 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 #include #include +#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 #include @@ -25,6 +26,7 @@ #include #include #include +#include #include 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);