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
48534775 Poul Sander 2026-06-08 18:57 37
// EXIF orientation values run 1-8 and may encode mirroring. A 90-degree
48534775 Poul Sander 2026-06-08 18:57 38
// rotation maps each value to another; the tables below cover all eight so
48534775 Poul Sander 2026-06-08 18:57 39
// mirrored images stay correct. Unknown values are treated as 1 (normal).
48534775 Poul Sander 2026-06-08 18:57 40
static int orientationRotatedClockwise(int orientation)
48534775 Poul Sander 2026-06-08 18:57 41
{
48534775 Poul Sander 2026-06-08 18:57 42
    switch (orientation) {
48534775 Poul Sander 2026-06-08 18:57 43
    case 1: return 6;
48534775 Poul Sander 2026-06-08 18:57 44
    case 2: return 7;
48534775 Poul Sander 2026-06-08 18:57 45
    case 3: return 8;
48534775 Poul Sander 2026-06-08 18:57 46
    case 4: return 5;
48534775 Poul Sander 2026-06-08 18:57 47
    case 5: return 2;
48534775 Poul Sander 2026-06-08 18:57 48
    case 6: return 3;
48534775 Poul Sander 2026-06-08 18:57 49
    case 7: return 4;
48534775 Poul Sander 2026-06-08 18:57 50
    case 8: return 1;
48534775 Poul Sander 2026-06-08 18:57 51
    default: return 6;
48534775 Poul Sander 2026-06-08 18:57 52
    }
48534775 Poul Sander 2026-06-08 18:57 53
}
48534775 Poul Sander 2026-06-08 18:57 54
48534775 Poul Sander 2026-06-08 18:57 55
static int orientationRotatedCounterClockwise(int orientation)
48534775 Poul Sander 2026-06-08 18:57 56
{
48534775 Poul Sander 2026-06-08 18:57 57
    switch (orientation) {
48534775 Poul Sander 2026-06-08 18:57 58
    case 1: return 8;
48534775 Poul Sander 2026-06-08 18:57 59
    case 2: return 5;
48534775 Poul Sander 2026-06-08 18:57 60
    case 3: return 6;
48534775 Poul Sander 2026-06-08 18:57 61
    case 4: return 7;
48534775 Poul Sander 2026-06-08 18:57 62
    case 5: return 4;
48534775 Poul Sander 2026-06-08 18:57 63
    case 6: return 1;
48534775 Poul Sander 2026-06-08 18:57 64
    case 7: return 2;
48534775 Poul Sander 2026-06-08 18:57 65
    case 8: return 3;
48534775 Poul Sander 2026-06-08 18:57 66
    default: return 8;
48534775 Poul Sander 2026-06-08 18:57 67
    }
48534775 Poul Sander 2026-06-08 18:57 68
}
48534775 Poul Sander 2026-06-08 18:57 69
53fdab93 Poul Sander 2026-05-18 18:45 70
bool ExifData::isEmpty() const
53fdab93 Poul Sander 2026-05-18 18:45 71
{
beb4a823 Poul Sander 2026-05-19 21:53 72
    return captionAbstract.isEmpty() && description.isEmpty()
beb4a823 Poul Sander 2026-05-19 21:53 73
        && dateTime.isEmpty() && make.isEmpty() && model.isEmpty()
53fdab93 Poul Sander 2026-05-18 18:45 74
        && exposureTime.isEmpty() && fNumber.isEmpty() && iso.isEmpty()
53fdab93 Poul Sander 2026-05-18 18:45 75
        && focalLength.isEmpty() && flash.isEmpty()
700d6f2d Poul Sander 2026-06-06 15:44 76
        && filename.isEmpty() && dimensions.isEmpty() && fileSize.isEmpty();
53fdab93 Poul Sander 2026-05-18 18:45 77
}
53fdab93 Poul Sander 2026-05-18 18:45 78
53fdab93 Poul Sander 2026-05-18 18:45 79
QList<QPair<QString, QString>> ExifData::toList() const
53fdab93 Poul Sander 2026-05-18 18:45 80
{
53fdab93 Poul Sander 2026-05-18 18:45 81
    QList<QPair<QString, QString>> result;
beb4a823 Poul Sander 2026-05-19 21:53 82
    addField(result, "Caption",      captionAbstract);
beb4a823 Poul Sander 2026-05-19 21:53 83
    addField(result, "Description",  description);
0eae7b4d Poul Sander 2026-05-18 19:22 84
    addField(result, "Date/Time",    dateTime);
0eae7b4d Poul Sander 2026-05-18 19:22 85
    addField(result, "Camera Make",  make);
0eae7b4d Poul Sander 2026-05-18 19:22 86
    addField(result, "Camera Model", model);
0eae7b4d Poul Sander 2026-05-18 19:22 87
    addField(result, "Exposure",     exposureTime);
0eae7b4d Poul Sander 2026-05-18 19:22 88
    addField(result, "Aperture",     fNumber);
0eae7b4d Poul Sander 2026-05-18 19:22 89
    addField(result, "ISO",          iso);
0eae7b4d Poul Sander 2026-05-18 19:22 90
    addField(result, "Focal Length", focalLength);
0eae7b4d Poul Sander 2026-05-18 19:22 91
    addField(result, "Flash",        flash);
700d6f2d Poul Sander 2026-06-06 15:44 92
    addField(result, "Filename",     filename);
0eae7b4d Poul Sander 2026-05-18 19:22 93
    addField(result, "Dimensions",   dimensions);
0eae7b4d Poul Sander 2026-05-18 19:22 94
    addField(result, "File Size",    fileSize);
53fdab93 Poul Sander 2026-05-18 18:45 95
    return result;
53fdab93 Poul Sander 2026-05-18 18:45 96
}
53fdab93 Poul Sander 2026-05-18 18:45 97
80a9926d Poul Sander 2026-05-19 20:28 98
ExifData ExifReader::read(const QByteArray &path)
53fdab93 Poul Sander 2026-05-18 18:45 99
{
53fdab93 Poul Sander 2026-05-18 18:45 100
    ExifData data;
53fdab93 Poul Sander 2026-05-18 18:45 101
80a9926d Poul Sander 2026-05-19 20:28 102
    struct ::stat st{};
80a9926d Poul Sander 2026-05-19 20:28 103
    if (::stat(path.constData(), &st) != 0 || !S_ISREG(st.st_mode))
53fdab93 Poul Sander 2026-05-18 18:45 104
        return data;
53fdab93 Poul Sander 2026-05-18 18:45 105
700d6f2d Poul Sander 2026-06-06 15:44 106
    // Filename
700d6f2d Poul Sander 2026-06-06 15:44 107
    {
700d6f2d Poul Sander 2026-06-06 15:44 108
        const std::filesystem::path fsPath(path.toStdString());
700d6f2d Poul Sander 2026-06-06 15:44 109
        const std::string nativeName = fsPath.filename().native();
700d6f2d Poul Sander 2026-06-06 15:44 110
        data.filename = QString::fromLocal8Bit(
700d6f2d Poul Sander 2026-06-06 15:44 111
            nativeName.data(), static_cast<qsizetype>(nativeName.size()));
700d6f2d Poul Sander 2026-06-06 15:44 112
    }
700d6f2d Poul Sander 2026-06-06 15:44 113
53fdab93 Poul Sander 2026-05-18 18:45 114
    // File size
53fdab93 Poul Sander 2026-05-18 18:45 115
    {
80a9926d Poul Sander 2026-05-19 20:28 116
        qint64 size = static_cast<qint64>(st.st_size);
53fdab93 Poul Sander 2026-05-18 18:45 117
        if (size < 1024)
53fdab93 Poul Sander 2026-05-18 18:45 118
            data.fileSize = QString::number(size) + QLatin1String(" B");
53fdab93 Poul Sander 2026-05-18 18:45 119
        else if (size < 1024 * 1024)
53fdab93 Poul Sander 2026-05-18 18:45 120
            data.fileSize = QString::number(size / 1024.0, 'f', 1) + QLatin1String(" KiB");
53fdab93 Poul Sander 2026-05-18 18:45 121
        else
53fdab93 Poul Sander 2026-05-18 18:45 122
            data.fileSize = QString::number(size / (1024.0 * 1024.0), 'f', 1) + QLatin1String(" MiB");
53fdab93 Poul Sander 2026-05-18 18:45 123
    }
53fdab93 Poul Sander 2026-05-18 18:45 124
53fdab93 Poul Sander 2026-05-18 18:45 125
    // Image dimensions via Qt (reads only the header, no full decode needed)
53fdab93 Poul Sander 2026-05-18 18:45 126
    {
80a9926d Poul Sander 2026-05-19 20:28 127
        int fd = ::open(path.constData(), O_RDONLY | O_CLOEXEC);
80a9926d Poul Sander 2026-05-19 20:28 128
        if (fd >= 0) {
80a9926d Poul Sander 2026-05-19 20:28 129
            QFile file;
80a9926d Poul Sander 2026-05-19 20:28 130
            if (file.open(fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle)) {
80a9926d Poul Sander 2026-05-19 20:28 131
                QImageReader reader(&file);
80a9926d Poul Sander 2026-05-19 20:28 132
                reader.setDecideFormatFromContent(true);
80a9926d Poul Sander 2026-05-19 20:28 133
                QSize sz = reader.size();
80a9926d Poul Sander 2026-05-19 20:28 134
                        if (sz.isValid())
80a9926d Poul Sander 2026-05-19 20:28 135
                    data.dimensions = QString::fromLatin1("%1 * %2")
80a9926d Poul Sander 2026-05-19 20:28 136
                                          .arg(sz.width()).arg(sz.height());
80a9926d Poul Sander 2026-05-19 20:28 137
            } else {
80a9926d Poul Sander 2026-05-19 20:28 138
                ::close(fd);
80a9926d Poul Sander 2026-05-19 20:28 139
            }
80a9926d Poul Sander 2026-05-19 20:28 140
        }
53fdab93 Poul Sander 2026-05-18 18:45 141
    }
53fdab93 Poul Sander 2026-05-18 18:45 142
53fdab93 Poul Sander 2026-05-18 18:45 143
    // EXIF tags via exiv2
53fdab93 Poul Sander 2026-05-18 18:45 144
    try {
80a9926d Poul Sander 2026-05-19 20:28 145
        std::unique_ptr<Exiv2::Image> image =
80a9926d Poul Sander 2026-05-19 20:28 146
            Exiv2::ImageFactory::open(path.toStdString());
53fdab93 Poul Sander 2026-05-18 18:45 147
        image->readMetadata();
53fdab93 Poul Sander 2026-05-18 18:45 148
        const Exiv2::ExifData &exif = image->exifData();
53fdab93 Poul Sander 2026-05-18 18:45 149
0eae7b4d Poul Sander 2026-05-18 19:22 150
        data.dateTime = formatDate(findTag(exif, "Exif.Photo.DateTimeOriginal"));
53fdab93 Poul Sander 2026-05-18 18:45 151
        if (data.dateTime.isEmpty())
0eae7b4d Poul Sander 2026-05-18 19:22 152
            data.dateTime = formatDate(findTag(exif, "Exif.Image.DateTime"));
0eae7b4d Poul Sander 2026-05-18 19:22 153
        data.make         = findTag(exif, "Exif.Image.Make");
0eae7b4d Poul Sander 2026-05-18 19:22 154
        data.model        = findTag(exif, "Exif.Image.Model");
0eae7b4d Poul Sander 2026-05-18 19:22 155
        data.exposureTime = findTag(exif, "Exif.Photo.ExposureTime");
0eae7b4d Poul Sander 2026-05-18 19:22 156
        data.fNumber      = findTag(exif, "Exif.Photo.FNumber");
0eae7b4d Poul Sander 2026-05-18 19:22 157
        data.iso          = findTag(exif, "Exif.Photo.ISOSpeedRatings");
0eae7b4d Poul Sander 2026-05-18 19:22 158
        data.focalLength  = findTag(exif, "Exif.Photo.FocalLength");
0eae7b4d Poul Sander 2026-05-18 19:22 159
        data.flash        = findTag(exif, "Exif.Photo.Flash");
beb4a823 Poul Sander 2026-05-19 21:53 160
        data.description  = findTag(exif, "Exif.Image.ImageDescription");
beb4a823 Poul Sander 2026-05-19 21:53 161
beb4a823 Poul Sander 2026-05-19 21:53 162
        // IPTC Caption-Abstract
beb4a823 Poul Sander 2026-05-19 21:53 163
        const Exiv2::IptcData &iptc = image->iptcData();
beb4a823 Poul Sander 2026-05-19 21:53 164
        auto iptcIt = iptc.findKey(Exiv2::IptcKey("Iptc.Application2.Caption"));
beb4a823 Poul Sander 2026-05-19 21:53 165
        if (iptcIt != iptc.end())
beb4a823 Poul Sander 2026-05-19 21:53 166
            data.captionAbstract = QString::fromStdString(iptcIt->toString()).trimmed();
53fdab93 Poul Sander 2026-05-18 18:45 167
    } catch (const Exiv2::Error &) {
53fdab93 Poul Sander 2026-05-18 18:45 168
        // No EXIF or unsupported format — leave tags empty
53fdab93 Poul Sander 2026-05-18 18:45 169
    }
53fdab93 Poul Sander 2026-05-18 18:45 170
53fdab93 Poul Sander 2026-05-18 18:45 171
    return data;
53fdab93 Poul Sander 2026-05-18 18:45 172
}
beb4a823 Poul Sander 2026-05-19 21:53 173
beb4a823 Poul Sander 2026-05-19 21:53 174
bool ExifReader::saveCaption(const QByteArray &path,
beb4a823 Poul Sander 2026-05-19 21:53 175
                             const QString    &caption,
beb4a823 Poul Sander 2026-05-19 21:53 176
                             const ExifData   &oldData)
beb4a823 Poul Sander 2026-05-19 21:53 177
{
beb4a823 Poul Sander 2026-05-19 21:53 178
    try {
beb4a823 Poul Sander 2026-05-19 21:53 179
        std::unique_ptr<Exiv2::Image> image =
beb4a823 Poul Sander 2026-05-19 21:53 180
            Exiv2::ImageFactory::open(path.toStdString());
beb4a823 Poul Sander 2026-05-19 21:53 181
        image->readMetadata();
beb4a823 Poul Sander 2026-05-19 21:53 182
beb4a823 Poul Sander 2026-05-19 21:53 183
        // Erase all existing Caption-Abstract entries then add the new one
beb4a823 Poul Sander 2026-05-19 21:53 184
        Exiv2::IptcData &iptc = image->iptcData();
beb4a823 Poul Sander 2026-05-19 21:53 185
        {
beb4a823 Poul Sander 2026-05-19 21:53 186
            Exiv2::IptcKey key("Iptc.Application2.Caption");
beb4a823 Poul Sander 2026-05-19 21:53 187
            auto it = iptc.findKey(key);
beb4a823 Poul Sander 2026-05-19 21:53 188
            while (it != iptc.end()) {
beb4a823 Poul Sander 2026-05-19 21:53 189
                it = iptc.erase(it);
beb4a823 Poul Sander 2026-05-19 21:53 190
                it = iptc.findKey(key);
beb4a823 Poul Sander 2026-05-19 21:53 191
            }
beb4a823 Poul Sander 2026-05-19 21:53 192
        }
beb4a823 Poul Sander 2026-05-19 21:53 193
        Exiv2::Iptcdatum datum(Exiv2::IptcKey("Iptc.Application2.Caption"));
beb4a823 Poul Sander 2026-05-19 21:53 194
        datum.setValue(caption.toStdString());
beb4a823 Poul Sander 2026-05-19 21:53 195
        iptc.add(datum);
beb4a823 Poul Sander 2026-05-19 21:53 196
beb4a823 Poul Sander 2026-05-19 21:53 197
        // If Caption-Abstract and Description were previously in sync, keep them in sync
beb4a823 Poul Sander 2026-05-19 21:53 198
        if (oldData.captionAbstract == oldData.description) {
beb4a823 Poul Sander 2026-05-19 21:53 199
            image->exifData()["Exif.Image.ImageDescription"] = caption.toStdString();
beb4a823 Poul Sander 2026-05-19 21:53 200
        }
beb4a823 Poul Sander 2026-05-19 21:53 201
beb4a823 Poul Sander 2026-05-19 21:53 202
        image->writeMetadata();
beb4a823 Poul Sander 2026-05-19 21:53 203
        return true;
beb4a823 Poul Sander 2026-05-19 21:53 204
    } catch (const Exiv2::Error &) {
beb4a823 Poul Sander 2026-05-19 21:53 205
        return false;
beb4a823 Poul Sander 2026-05-19 21:53 206
    }
beb4a823 Poul Sander 2026-05-19 21:53 207
}
48534775 Poul Sander 2026-06-08 18:57 208
48534775 Poul Sander 2026-06-08 18:57 209
bool ExifReader::rotate(const QByteArray &path, bool clockwise)
48534775 Poul Sander 2026-06-08 18:57 210
{
48534775 Poul Sander 2026-06-08 18:57 211
    try {
48534775 Poul Sander 2026-06-08 18:57 212
        std::unique_ptr<Exiv2::Image> image =
48534775 Poul Sander 2026-06-08 18:57 213
            Exiv2::ImageFactory::open(path.toStdString());
48534775 Poul Sander 2026-06-08 18:57 214
        image->readMetadata();
48534775 Poul Sander 2026-06-08 18:57 215
        Exiv2::ExifData &exif = image->exifData();
48534775 Poul Sander 2026-06-08 18:57 216
48534775 Poul Sander 2026-06-08 18:57 217
        int current = 1; // EXIF default when the tag is absent
48534775 Poul Sander 2026-06-08 18:57 218
        auto it = exif.findKey(Exiv2::ExifKey("Exif.Image.Orientation"));
48534775 Poul Sander 2026-06-08 18:57 219
        if (it != exif.end())
48534775 Poul Sander 2026-06-08 18:57 220
            current = static_cast<int>(it->toInt64());
48534775 Poul Sander 2026-06-08 18:57 221
48534775 Poul Sander 2026-06-08 18:57 222
        const int updated = clockwise ? orientationRotatedClockwise(current)
48534775 Poul Sander 2026-06-08 18:57 223
                                      : orientationRotatedCounterClockwise(current);
48534775 Poul Sander 2026-06-08 18:57 224
        exif["Exif.Image.Orientation"] = static_cast<uint16_t>(updated);
48534775 Poul Sander 2026-06-08 18:57 225
48534775 Poul Sander 2026-06-08 18:57 226
        image->writeMetadata();
48534775 Poul Sander 2026-06-08 18:57 227
        return true;
48534775 Poul Sander 2026-06-08 18:57 228
    } catch (const Exiv2::Error &) {
48534775 Poul Sander 2026-06-08 18:57 229
        return false;
48534775 Poul Sander 2026-06-08 18:57 230
    }
48534775 Poul Sander 2026-06-08 18:57 231
}
1970-01-01 00:00 232