git repos / SagoImageBrowser

blame: src/exifreader.cpp

normal view · raw

53fdab93 Poul Sander 2026-05-18 18:45 1
#include "exifreader.h"
53fdab93 Poul Sander 2026-05-18 18:45 2
80a9926d Poul Sander 2026-05-19 20:28 3
#include <QFile>
53fdab93 Poul Sander 2026-05-18 18:45 4
#include <QImageReader>
53fdab93 Poul Sander 2026-05-18 18:45 5
53fdab93 Poul Sander 2026-05-18 18:45 6
#include <exiv2/exiv2.hpp>
53fdab93 Poul Sander 2026-05-18 18:45 7
80a9926d Poul Sander 2026-05-19 20:28 8
#include <sys/stat.h>
80a9926d Poul Sander 2026-05-19 20:28 9
#include <fcntl.h>
700d6f2d Poul Sander 2026-06-06 15:44 10
#include <filesystem>
80a9926d Poul Sander 2026-05-19 20:28 11
#include <unistd.h>
80a9926d Poul Sander 2026-05-19 20:28 12
0eae7b4d Poul Sander 2026-05-18 19:22 13
// Raw EXIF date format is "YYYY:MM:DD HH:MM:SS"; reformat date separators to dashes
0eae7b4d Poul Sander 2026-05-18 19:22 14
static QString formatDate(const QString &raw)
0eae7b4d Poul Sander 2026-05-18 19:22 15
{
0eae7b4d Poul Sander 2026-05-18 19:22 16
    if (raw.size() >= 10 && raw[4] == u':' && raw[7] == u':')
0eae7b4d Poul Sander 2026-05-18 19:22 17
        return raw.left(4) + u'-' + raw.mid(5, 2) + u'-' + raw.mid(8, 2) + raw.mid(10);
0eae7b4d Poul Sander 2026-05-18 19:22 18
    return raw;
0eae7b4d Poul Sander 2026-05-18 19:22 19
}
0eae7b4d Poul Sander 2026-05-18 19:22 20
0eae7b4d Poul Sander 2026-05-18 19:22 21
static QString findTag(const Exiv2::ExifData &exif, const std::string &key)
0eae7b4d Poul Sander 2026-05-18 19:22 22
{
0eae7b4d Poul Sander 2026-05-18 19:22 23
    try {
0eae7b4d Poul Sander 2026-05-18 19:22 24
        Exiv2::ExifData::const_iterator it = exif.findKey(Exiv2::ExifKey(key));
0eae7b4d Poul Sander 2026-05-18 19:22 25
        if (it != exif.end())
0eae7b4d Poul Sander 2026-05-18 19:22 26
            return QString::fromStdString(it->print(&exif)).trimmed();
0eae7b4d Poul Sander 2026-05-18 19:22 27
    } catch (...) {}
0eae7b4d Poul Sander 2026-05-18 19:22 28
    return {};
0eae7b4d Poul Sander 2026-05-18 19:22 29
}
0eae7b4d Poul Sander 2026-05-18 19:22 30
0eae7b4d Poul Sander 2026-05-18 19:22 31
static void addField(QList<QPair<QString, QString>> &result, const char *label, const QString &value)
0eae7b4d Poul Sander 2026-05-18 19:22 32
{
0eae7b4d Poul Sander 2026-05-18 19:22 33
    if (!value.isEmpty())
0eae7b4d Poul Sander 2026-05-18 19:22 34
        result.append({QString::fromLatin1(label), value});
0eae7b4d Poul Sander 2026-05-18 19:22 35
}
0eae7b4d Poul Sander 2026-05-18 19:22 36
53fdab93 Poul Sander 2026-05-18 18:45 37
bool ExifData::isEmpty() const
53fdab93 Poul Sander 2026-05-18 18:45 38
{
beb4a823 Poul Sander 2026-05-19 21:53 39
    return captionAbstract.isEmpty() && description.isEmpty()
beb4a823 Poul Sander 2026-05-19 21:53 40
        && dateTime.isEmpty() && make.isEmpty() && model.isEmpty()
53fdab93 Poul Sander 2026-05-18 18:45 41
        && exposureTime.isEmpty() && fNumber.isEmpty() && iso.isEmpty()
53fdab93 Poul Sander 2026-05-18 18:45 42
        && focalLength.isEmpty() && flash.isEmpty()
700d6f2d Poul Sander 2026-06-06 15:44 43
        && filename.isEmpty() && dimensions.isEmpty() && fileSize.isEmpty();
53fdab93 Poul Sander 2026-05-18 18:45 44
}
53fdab93 Poul Sander 2026-05-18 18:45 45
53fdab93 Poul Sander 2026-05-18 18:45 46
QList<QPair<QString, QString>> ExifData::toList() const
53fdab93 Poul Sander 2026-05-18 18:45 47
{
53fdab93 Poul Sander 2026-05-18 18:45 48
    QList<QPair<QString, QString>> result;
beb4a823 Poul Sander 2026-05-19 21:53 49
    addField(result, "Caption",      captionAbstract);
beb4a823 Poul Sander 2026-05-19 21:53 50
    addField(result, "Description",  description);
0eae7b4d Poul Sander 2026-05-18 19:22 51
    addField(result, "Date/Time",    dateTime);
0eae7b4d Poul Sander 2026-05-18 19:22 52
    addField(result, "Camera Make",  make);
0eae7b4d Poul Sander 2026-05-18 19:22 53
    addField(result, "Camera Model", model);
0eae7b4d Poul Sander 2026-05-18 19:22 54
    addField(result, "Exposure",     exposureTime);
0eae7b4d Poul Sander 2026-05-18 19:22 55
    addField(result, "Aperture",     fNumber);
0eae7b4d Poul Sander 2026-05-18 19:22 56
    addField(result, "ISO",          iso);
0eae7b4d Poul Sander 2026-05-18 19:22 57
    addField(result, "Focal Length", focalLength);
0eae7b4d Poul Sander 2026-05-18 19:22 58
    addField(result, "Flash",        flash);
700d6f2d Poul Sander 2026-06-06 15:44 59
    addField(result, "Filename",     filename);
0eae7b4d Poul Sander 2026-05-18 19:22 60
    addField(result, "Dimensions",   dimensions);
0eae7b4d Poul Sander 2026-05-18 19:22 61
    addField(result, "File Size",    fileSize);
53fdab93 Poul Sander 2026-05-18 18:45 62
    return result;
53fdab93 Poul Sander 2026-05-18 18:45 63
}
53fdab93 Poul Sander 2026-05-18 18:45 64
80a9926d Poul Sander 2026-05-19 20:28 65
ExifData ExifReader::read(const QByteArray &path)
53fdab93 Poul Sander 2026-05-18 18:45 66
{
53fdab93 Poul Sander 2026-05-18 18:45 67
    ExifData data;
53fdab93 Poul Sander 2026-05-18 18:45 68
80a9926d Poul Sander 2026-05-19 20:28 69
    struct ::stat st{};
80a9926d Poul Sander 2026-05-19 20:28 70
    if (::stat(path.constData(), &st) != 0 || !S_ISREG(st.st_mode))
53fdab93 Poul Sander 2026-05-18 18:45 71
        return data;
53fdab93 Poul Sander 2026-05-18 18:45 72
700d6f2d Poul Sander 2026-06-06 15:44 73
    // Filename
700d6f2d Poul Sander 2026-06-06 15:44 74
    {
700d6f2d Poul Sander 2026-06-06 15:44 75
        const std::filesystem::path fsPath(path.toStdString());
700d6f2d Poul Sander 2026-06-06 15:44 76
        const std::string nativeName = fsPath.filename().native();
700d6f2d Poul Sander 2026-06-06 15:44 77
        data.filename = QString::fromLocal8Bit(
700d6f2d Poul Sander 2026-06-06 15:44 78
            nativeName.data(), static_cast<qsizetype>(nativeName.size()));
700d6f2d Poul Sander 2026-06-06 15:44 79
    }
700d6f2d Poul Sander 2026-06-06 15:44 80
53fdab93 Poul Sander 2026-05-18 18:45 81
    // File size
53fdab93 Poul Sander 2026-05-18 18:45 82
    {
80a9926d Poul Sander 2026-05-19 20:28 83
        qint64 size = static_cast<qint64>(st.st_size);
53fdab93 Poul Sander 2026-05-18 18:45 84
        if (size < 1024)
53fdab93 Poul Sander 2026-05-18 18:45 85
            data.fileSize = QString::number(size) + QLatin1String(" B");
53fdab93 Poul Sander 2026-05-18 18:45 86
        else if (size < 1024 * 1024)
53fdab93 Poul Sander 2026-05-18 18:45 87
            data.fileSize = QString::number(size / 1024.0, 'f', 1) + QLatin1String(" KiB");
53fdab93 Poul Sander 2026-05-18 18:45 88
        else
53fdab93 Poul Sander 2026-05-18 18:45 89
            data.fileSize = QString::number(size / (1024.0 * 1024.0), 'f', 1) + QLatin1String(" MiB");
53fdab93 Poul Sander 2026-05-18 18:45 90
    }
53fdab93 Poul Sander 2026-05-18 18:45 91
53fdab93 Poul Sander 2026-05-18 18:45 92
    // Image dimensions via Qt (reads only the header, no full decode needed)
53fdab93 Poul Sander 2026-05-18 18:45 93
    {
80a9926d Poul Sander 2026-05-19 20:28 94
        int fd = ::open(path.constData(), O_RDONLY | O_CLOEXEC);
80a9926d Poul Sander 2026-05-19 20:28 95
        if (fd >= 0) {
80a9926d Poul Sander 2026-05-19 20:28 96
            QFile file;
80a9926d Poul Sander 2026-05-19 20:28 97
            if (file.open(fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle)) {
80a9926d Poul Sander 2026-05-19 20:28 98
                QImageReader reader(&file);
80a9926d Poul Sander 2026-05-19 20:28 99
                reader.setDecideFormatFromContent(true);
80a9926d Poul Sander 2026-05-19 20:28 100
                QSize sz = reader.size();
80a9926d Poul Sander 2026-05-19 20:28 101
                        if (sz.isValid())
80a9926d Poul Sander 2026-05-19 20:28 102
                    data.dimensions = QString::fromLatin1("%1 * %2")
80a9926d Poul Sander 2026-05-19 20:28 103
                                          .arg(sz.width()).arg(sz.height());
80a9926d Poul Sander 2026-05-19 20:28 104
            } else {
80a9926d Poul Sander 2026-05-19 20:28 105
                ::close(fd);
80a9926d Poul Sander 2026-05-19 20:28 106
            }
80a9926d Poul Sander 2026-05-19 20:28 107
        }
53fdab93 Poul Sander 2026-05-18 18:45 108
    }
53fdab93 Poul Sander 2026-05-18 18:45 109
53fdab93 Poul Sander 2026-05-18 18:45 110
    // EXIF tags via exiv2
53fdab93 Poul Sander 2026-05-18 18:45 111
    try {
80a9926d Poul Sander 2026-05-19 20:28 112
        std::unique_ptr<Exiv2::Image> image =
80a9926d Poul Sander 2026-05-19 20:28 113
            Exiv2::ImageFactory::open(path.toStdString());
53fdab93 Poul Sander 2026-05-18 18:45 114
        image->readMetadata();
53fdab93 Poul Sander 2026-05-18 18:45 115
        const Exiv2::ExifData &exif = image->exifData();
53fdab93 Poul Sander 2026-05-18 18:45 116
0eae7b4d Poul Sander 2026-05-18 19:22 117
        data.dateTime = formatDate(findTag(exif, "Exif.Photo.DateTimeOriginal"));
53fdab93 Poul Sander 2026-05-18 18:45 118
        if (data.dateTime.isEmpty())
0eae7b4d Poul Sander 2026-05-18 19:22 119
            data.dateTime = formatDate(findTag(exif, "Exif.Image.DateTime"));
0eae7b4d Poul Sander 2026-05-18 19:22 120
        data.make         = findTag(exif, "Exif.Image.Make");
0eae7b4d Poul Sander 2026-05-18 19:22 121
        data.model        = findTag(exif, "Exif.Image.Model");
0eae7b4d Poul Sander 2026-05-18 19:22 122
        data.exposureTime = findTag(exif, "Exif.Photo.ExposureTime");
0eae7b4d Poul Sander 2026-05-18 19:22 123
        data.fNumber      = findTag(exif, "Exif.Photo.FNumber");
0eae7b4d Poul Sander 2026-05-18 19:22 124
        data.iso          = findTag(exif, "Exif.Photo.ISOSpeedRatings");
0eae7b4d Poul Sander 2026-05-18 19:22 125
        data.focalLength  = findTag(exif, "Exif.Photo.FocalLength");
0eae7b4d Poul Sander 2026-05-18 19:22 126
        data.flash        = findTag(exif, "Exif.Photo.Flash");
beb4a823 Poul Sander 2026-05-19 21:53 127
        data.description  = findTag(exif, "Exif.Image.ImageDescription");
beb4a823 Poul Sander 2026-05-19 21:53 128
beb4a823 Poul Sander 2026-05-19 21:53 129
        // IPTC Caption-Abstract
beb4a823 Poul Sander 2026-05-19 21:53 130
        const Exiv2::IptcData &iptc = image->iptcData();
beb4a823 Poul Sander 2026-05-19 21:53 131
        auto iptcIt = iptc.findKey(Exiv2::IptcKey("Iptc.Application2.Caption"));
beb4a823 Poul Sander 2026-05-19 21:53 132
        if (iptcIt != iptc.end())
beb4a823 Poul Sander 2026-05-19 21:53 133
            data.captionAbstract = QString::fromStdString(iptcIt->toString()).trimmed();
53fdab93 Poul Sander 2026-05-18 18:45 134
    } catch (const Exiv2::Error &) {
53fdab93 Poul Sander 2026-05-18 18:45 135
        // No EXIF or unsupported format — leave tags empty
53fdab93 Poul Sander 2026-05-18 18:45 136
    }
53fdab93 Poul Sander 2026-05-18 18:45 137
53fdab93 Poul Sander 2026-05-18 18:45 138
    return data;
53fdab93 Poul Sander 2026-05-18 18:45 139
}
beb4a823 Poul Sander 2026-05-19 21:53 140
beb4a823 Poul Sander 2026-05-19 21:53 141
bool ExifReader::saveCaption(const QByteArray &path,
beb4a823 Poul Sander 2026-05-19 21:53 142
                             const QString    &caption,
beb4a823 Poul Sander 2026-05-19 21:53 143
                             const ExifData   &oldData)
beb4a823 Poul Sander 2026-05-19 21:53 144
{
beb4a823 Poul Sander 2026-05-19 21:53 145
    try {
beb4a823 Poul Sander 2026-05-19 21:53 146
        std::unique_ptr<Exiv2::Image> image =
beb4a823 Poul Sander 2026-05-19 21:53 147
            Exiv2::ImageFactory::open(path.toStdString());
beb4a823 Poul Sander 2026-05-19 21:53 148
        image->readMetadata();
beb4a823 Poul Sander 2026-05-19 21:53 149
beb4a823 Poul Sander 2026-05-19 21:53 150
        // Erase all existing Caption-Abstract entries then add the new one
beb4a823 Poul Sander 2026-05-19 21:53 151
        Exiv2::IptcData &iptc = image->iptcData();
beb4a823 Poul Sander 2026-05-19 21:53 152
        {
beb4a823 Poul Sander 2026-05-19 21:53 153
            Exiv2::IptcKey key("Iptc.Application2.Caption");
beb4a823 Poul Sander 2026-05-19 21:53 154
            auto it = iptc.findKey(key);
beb4a823 Poul Sander 2026-05-19 21:53 155
            while (it != iptc.end()) {
beb4a823 Poul Sander 2026-05-19 21:53 156
                it = iptc.erase(it);
beb4a823 Poul Sander 2026-05-19 21:53 157
                it = iptc.findKey(key);
beb4a823 Poul Sander 2026-05-19 21:53 158
            }
beb4a823 Poul Sander 2026-05-19 21:53 159
        }
beb4a823 Poul Sander 2026-05-19 21:53 160
        Exiv2::Iptcdatum datum(Exiv2::IptcKey("Iptc.Application2.Caption"));
beb4a823 Poul Sander 2026-05-19 21:53 161
        datum.setValue(caption.toStdString());
beb4a823 Poul Sander 2026-05-19 21:53 162
        iptc.add(datum);
beb4a823 Poul Sander 2026-05-19 21:53 163
beb4a823 Poul Sander 2026-05-19 21:53 164
        // If Caption-Abstract and Description were previously in sync, keep them in sync
beb4a823 Poul Sander 2026-05-19 21:53 165
        if (oldData.captionAbstract == oldData.description) {
beb4a823 Poul Sander 2026-05-19 21:53 166
            image->exifData()["Exif.Image.ImageDescription"] = caption.toStdString();
beb4a823 Poul Sander 2026-05-19 21:53 167
        }
beb4a823 Poul Sander 2026-05-19 21:53 168
beb4a823 Poul Sander 2026-05-19 21:53 169
        image->writeMetadata();
beb4a823 Poul Sander 2026-05-19 21:53 170
        return true;
beb4a823 Poul Sander 2026-05-19 21:53 171
    } catch (const Exiv2::Error &) {
beb4a823 Poul Sander 2026-05-19 21:53 172
        return false;
beb4a823 Poul Sander 2026-05-19 21:53 173
    }
beb4a823 Poul Sander 2026-05-19 21:53 174
}
1970-01-01 00:00 175