diff --git a/src/exifreader.cpp b/src/exifreader.cpp index 0a9ddb7..3f94eb8 100644 --- a/src/exifreader.cpp +++ b/src/exifreader.cpp @@ -35,7 +35,8 @@ static void addField(QList> &result, const char *label, bool ExifData::isEmpty() const { - return dateTime.isEmpty() && make.isEmpty() && model.isEmpty() + return captionAbstract.isEmpty() && description.isEmpty() + && dateTime.isEmpty() && make.isEmpty() && model.isEmpty() && exposureTime.isEmpty() && fNumber.isEmpty() && iso.isEmpty() && focalLength.isEmpty() && flash.isEmpty() && dimensions.isEmpty() && fileSize.isEmpty(); @@ -44,6 +45,8 @@ bool ExifData::isEmpty() const QList> ExifData::toList() const { QList> result; + addField(result, "Caption", captionAbstract); + addField(result, "Description", description); addField(result, "Date/Time", dateTime); addField(result, "Camera Make", make); addField(result, "Camera Model", model); @@ -111,9 +114,51 @@ ExifData ExifReader::read(const QByteArray &path) data.iso = findTag(exif, "Exif.Photo.ISOSpeedRatings"); data.focalLength = findTag(exif, "Exif.Photo.FocalLength"); data.flash = findTag(exif, "Exif.Photo.Flash"); + data.description = findTag(exif, "Exif.Image.ImageDescription"); + + // IPTC Caption-Abstract + const Exiv2::IptcData &iptc = image->iptcData(); + auto iptcIt = iptc.findKey(Exiv2::IptcKey("Iptc.Application2.Caption")); + if (iptcIt != iptc.end()) + data.captionAbstract = QString::fromStdString(iptcIt->toString()).trimmed(); } catch (const Exiv2::Error &) { // No EXIF or unsupported format — leave tags empty } return data; } + +bool ExifReader::saveCaption(const QByteArray &path, + const QString &caption, + const ExifData &oldData) +{ + try { + std::unique_ptr image = + Exiv2::ImageFactory::open(path.toStdString()); + image->readMetadata(); + + // Erase all existing Caption-Abstract entries then add the new one + Exiv2::IptcData &iptc = image->iptcData(); + { + Exiv2::IptcKey key("Iptc.Application2.Caption"); + auto it = iptc.findKey(key); + while (it != iptc.end()) { + it = iptc.erase(it); + it = iptc.findKey(key); + } + } + Exiv2::Iptcdatum datum(Exiv2::IptcKey("Iptc.Application2.Caption")); + datum.setValue(caption.toStdString()); + iptc.add(datum); + + // If Caption-Abstract and Description were previously in sync, keep them in sync + if (oldData.captionAbstract == oldData.description) { + image->exifData()["Exif.Image.ImageDescription"] = caption.toStdString(); + } + + image->writeMetadata(); + return true; + } catch (const Exiv2::Error &) { + return false; + } +} diff --git a/src/exifreader.h b/src/exifreader.h index 6d2957a..450d5cd 100644 --- a/src/exifreader.h +++ b/src/exifreader.h @@ -6,6 +6,8 @@ #include struct ExifData { + QString captionAbstract; // IPTC Caption-Abstract (Iptc.Application2.Caption) + QString description; // Exif.Image.ImageDescription QString dateTime; QString make; QString model; @@ -23,4 +25,12 @@ struct ExifData { namespace ExifReader { ExifData read(const QByteArray &path); + +// Write caption to IPTC Caption-Abstract. +// If oldData.captionAbstract == oldData.description they were in sync; +// in that case also update Exif.Image.ImageDescription to match. +// Returns true on success. +bool saveCaption(const QByteArray &path, + const QString &caption, + const ExifData &oldData); } // namespace ExifReader diff --git a/src/imageviewwidget.cpp b/src/imageviewwidget.cpp index 44802b2..0556a77 100644 --- a/src/imageviewwidget.cpp +++ b/src/imageviewwidget.cpp @@ -238,6 +238,39 @@ void ImageViewWidget::paintEvent(QPaintEvent *) painter.restore(); } } + + // Caption-Abstract overlay — bottom-centre, always shown when non-empty + if (!m_exifData.captionAbstract.isEmpty()) { + const int padding = 10; + const int margin = 20; // distance from bottom edge + const int maxWidth = width() * 3 / 4; + + QFont captionFont = painter.font(); + captionFont.setPointSize(11); + painter.setFont(captionFont); + QFontMetrics cfm(captionFont); + + QRect textBounds = cfm.boundingRect( + QRect(0, 0, maxWidth, 0), + Qt::AlignHCenter | Qt::TextWordWrap, + m_exifData.captionAbstract); + + const int boxW = textBounds.width() + padding * 2; + const int boxH = textBounds.height() + padding * 2; + const int boxX = (width() - boxW) / 2; + const int boxY = height() - boxH - margin; + + painter.save(); + painter.setPen(Qt::NoPen); + painter.setBrush(QColor(0, 0, 0, 180)); + painter.drawRoundedRect(boxX, boxY, boxW, boxH, 8, 8); + + painter.setPen(Qt::white); + painter.drawText(QRect(boxX + padding, boxY + padding, textBounds.width(), textBounds.height()), + Qt::AlignHCenter | Qt::TextWordWrap, + m_exifData.captionAbstract); + painter.restore(); + } } void ImageViewWidget::keyPressEvent(QKeyEvent *event) @@ -329,6 +362,13 @@ void ImageViewWidget::keyPressEvent(QKeyEvent *event) return; } + if (event->key() == Qt::Key_E) + { + emit editCaptionRequested(); + event->accept(); + return; + } + QWidget::keyPressEvent(event); } @@ -345,6 +385,11 @@ void ImageViewWidget::toggleExifOverlay() update(); } +QByteArray ImageViewWidget::currentPath() const +{ + return m_currentPath; +} + void ImageViewWidget::zoomIn() { if (m_zoomMode == FitToScreen) { diff --git a/src/imageviewwidget.h b/src/imageviewwidget.h index 3895eca..7ee4223 100644 --- a/src/imageviewwidget.h +++ b/src/imageviewwidget.h @@ -48,11 +48,13 @@ public: void setExifData(const ExifData &data); void toggleExifOverlay(); + QByteArray currentPath() const; signals: void closeRequested(); void nextRequested(); void previousRequested(); + void editCaptionRequested(); protected: void paintEvent(QPaintEvent *event) override; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 6559d9d..7b2cce9 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -29,6 +29,10 @@ #include #include #include +#include +#include +#include +#include #include #include @@ -266,7 +270,8 @@ void MainWindow::setupConnections() connect(m_imageView, &ImageViewWidget::closeRequested, this, &MainWindow::leaveSingleImageMode); - + connect(m_imageView, &ImageViewWidget::editCaptionRequested, + this, &MainWindow::onEditCaption); connect(m_imageView, &ImageViewWidget::nextRequested, this, [this]() { QModelIndex cur = m_listView->currentIndex(); int next = cur.isValid() ? cur.row() + 1 : 0; @@ -408,6 +413,39 @@ void MainWindow::enterSingleImageMode(const QModelIndex &index) } } +void MainWindow::onEditCaption() +{ + const QByteArray path = m_imageView->currentPath(); + if (path.isEmpty()) + return; + + ExifData data = ExifReader::read(path); + + QDialog dlg(this); + dlg.setWindowTitle(tr("Edit Caption")); + auto *layout = new QVBoxLayout(&dlg); + layout->addWidget(new QLabel(tr("Caption-Abstract:"), &dlg)); + auto *edit = new QPlainTextEdit(&dlg); + edit->setPlainText(data.captionAbstract); + edit->setMinimumWidth(400); + layout->addWidget(edit); + auto *buttons = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg); + layout->addWidget(buttons); + connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); + + if (dlg.exec() != QDialog::Accepted) + return; + + const QString newCaption = edit->toPlainText(); + if (ExifReader::saveCaption(path, newCaption, data)) { + ExifData updated = ExifReader::read(path); + m_imageView->setExifData(updated); + updateExifInfo(path); + } +} + void MainWindow::leaveSingleImageMode() { if (isFullScreen()) { diff --git a/src/mainwindow.h b/src/mainwindow.h index 52b4511..944db13 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -41,6 +41,7 @@ private: void updateExifInfo(const QByteArray &path); void enterSingleImageMode(const QModelIndex &index); void leaveSingleImageMode(); + void onEditCaption(); QList computeNeighborPaths(const QModelIndex &index) const; void keyPressEvent(QKeyEvent *event) override;