git repos / SagoImageBrowser

blame: src/exifreader.cpp

normal view · raw

14cf904f Poul Sander 2026-06-22 18:18 1
/*
14cf904f Poul Sander 2026-06-22 18:18 2
MIT License
14cf904f Poul Sander 2026-06-22 18:18 3
14cf904f Poul Sander 2026-06-22 18:18 4
Copyright (c) 2026 Poul Sander
14cf904f Poul Sander 2026-06-22 18:18 5
14cf904f Poul Sander 2026-06-22 18:18 6
Permission is hereby granted, free of charge, to any person obtaining a copy
14cf904f Poul Sander 2026-06-22 18:18 7
of this software and associated documentation files (the "Software"), to deal
14cf904f Poul Sander 2026-06-22 18:18 8
in the Software without restriction, including without limitation the rights
14cf904f Poul Sander 2026-06-22 18:18 9
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14cf904f Poul Sander 2026-06-22 18:18 10
copies of the Software, and to permit persons to whom the Software is
14cf904f Poul Sander 2026-06-22 18:18 11
furnished to do so, subject to the following conditions:
14cf904f Poul Sander 2026-06-22 18:18 12
14cf904f Poul Sander 2026-06-22 18:18 13
The above copyright notice and this permission notice shall be included in all
14cf904f Poul Sander 2026-06-22 18:18 14
copies or substantial portions of the Software.
14cf904f Poul Sander 2026-06-22 18:18 15
14cf904f Poul Sander 2026-06-22 18:18 16
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14cf904f Poul Sander 2026-06-22 18:18 17
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14cf904f Poul Sander 2026-06-22 18:18 18
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14cf904f Poul Sander 2026-06-22 18:18 19
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
14cf904f Poul Sander 2026-06-22 18:18 20
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
14cf904f Poul Sander 2026-06-22 18:18 21
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
14cf904f Poul Sander 2026-06-22 18:18 22
SOFTWARE.
14cf904f Poul Sander 2026-06-22 18:18 23
14cf904f Poul Sander 2026-06-22 18:18 24
*/
14cf904f Poul Sander 2026-06-22 18:18 25
14cf904f Poul Sander 2026-06-22 18:18 26
53fdab93 Poul Sander 2026-05-18 18:45 27
#include "exifreader.h"
53fdab93 Poul Sander 2026-05-18 18:45 28
80a9926d Poul Sander 2026-05-19 20:28 29
#include <QFile>
53fdab93 Poul Sander 2026-05-18 18:45 30
#include <QImageReader>
4b975496 Poul Sander 2026-06-20 22:41 31
#include <QCoreApplication>
53fdab93 Poul Sander 2026-05-18 18:45 32
53fdab93 Poul Sander 2026-05-18 18:45 33
#include <exiv2/exiv2.hpp>
53fdab93 Poul Sander 2026-05-18 18:45 34
80a9926d Poul Sander 2026-05-19 20:28 35
#include <sys/stat.h>
80a9926d Poul Sander 2026-05-19 20:28 36
#include <fcntl.h>
700d6f2d Poul Sander 2026-06-06 15:44 37
#include <filesystem>
80a9926d Poul Sander 2026-05-19 20:28 38
#include <unistd.h>
80a9926d Poul Sander 2026-05-19 20:28 39
0eae7b4d Poul Sander 2026-05-18 19:22 40
// Raw EXIF date format is "YYYY:MM:DD HH:MM:SS"; reformat date separators to dashes
0eae7b4d Poul Sander 2026-05-18 19:22 41
static QString formatDate(const QString &raw)
0eae7b4d Poul Sander 2026-05-18 19:22 42
{
0eae7b4d Poul Sander 2026-05-18 19:22 43
    if (raw.size() >= 10 && raw[4] == u':' && raw[7] == u':')
0eae7b4d Poul Sander 2026-05-18 19:22 44
        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 45
    return raw;
0eae7b4d Poul Sander 2026-05-18 19:22 46
}
0eae7b4d Poul Sander 2026-05-18 19:22 47
0eae7b4d Poul Sander 2026-05-18 19:22 48
static QString findTag(const Exiv2::ExifData &exif, const std::string &key)
0eae7b4d Poul Sander 2026-05-18 19:22 49
{
0eae7b4d Poul Sander 2026-05-18 19:22 50
    try {
0eae7b4d Poul Sander 2026-05-18 19:22 51
        Exiv2::ExifData::const_iterator it = exif.findKey(Exiv2::ExifKey(key));
0eae7b4d Poul Sander 2026-05-18 19:22 52
        if (it != exif.end())
0eae7b4d Poul Sander 2026-05-18 19:22 53
            return QString::fromStdString(it->print(&exif)).trimmed();
0eae7b4d Poul Sander 2026-05-18 19:22 54
    } catch (...) {}
0eae7b4d Poul Sander 2026-05-18 19:22 55
    return {};
0eae7b4d Poul Sander 2026-05-18 19:22 56
}
0eae7b4d Poul Sander 2026-05-18 19:22 57
0eae7b4d Poul Sander 2026-05-18 19:22 58
static void addField(QList<QPair<QString, QString>> &result, const char *label, const QString &value)
0eae7b4d Poul Sander 2026-05-18 19:22 59
{
0eae7b4d Poul Sander 2026-05-18 19:22 60
    if (!value.isEmpty())
4b975496 Poul Sander 2026-06-20 22:41 61
        result.append({QCoreApplication::translate("ExifReader", label), value});
0eae7b4d Poul Sander 2026-05-18 19:22 62
}
0eae7b4d Poul Sander 2026-05-18 19:22 63
091a4f7d Poul Sander 2026-06-09 19:54 64
static double parseGpsCoordinate(const Exiv2::ExifData &exif, const std::string &key)
091a4f7d Poul Sander 2026-06-09 19:54 65
{
091a4f7d Poul Sander 2026-06-09 19:54 66
    try {
091a4f7d Poul Sander 2026-06-09 19:54 67
        Exiv2::ExifData::const_iterator it = exif.findKey(Exiv2::ExifKey(key));
091a4f7d Poul Sander 2026-06-09 19:54 68
        if (it != exif.end() && it->count() >= 3) {
091a4f7d Poul Sander 2026-06-09 19:54 69
            double d = it->toRational(0).first / static_cast<double>(it->toRational(0).second);
091a4f7d Poul Sander 2026-06-09 19:54 70
            double m = it->toRational(1).first / static_cast<double>(it->toRational(1).second);
091a4f7d Poul Sander 2026-06-09 19:54 71
            double s = it->toRational(2).first / static_cast<double>(it->toRational(2).second);
091a4f7d Poul Sander 2026-06-09 19:54 72
            return d + m / 60.0 + s / 3600.0;
091a4f7d Poul Sander 2026-06-09 19:54 73
        }
091a4f7d Poul Sander 2026-06-09 19:54 74
    } catch (...) {}
091a4f7d Poul Sander 2026-06-09 19:54 75
    return 0.0;
091a4f7d Poul Sander 2026-06-09 19:54 76
}
091a4f7d Poul Sander 2026-06-09 19:54 77
48534775 Poul Sander 2026-06-08 18:57 78
// EXIF orientation values run 1-8 and may encode mirroring. A 90-degree
48534775 Poul Sander 2026-06-08 18:57 79
// rotation maps each value to another; the tables below cover all eight so
48534775 Poul Sander 2026-06-08 18:57 80
// mirrored images stay correct. Unknown values are treated as 1 (normal).
48534775 Poul Sander 2026-06-08 18:57 81
static int orientationRotatedClockwise(int orientation)
48534775 Poul Sander 2026-06-08 18:57 82
{
48534775 Poul Sander 2026-06-08 18:57 83
    switch (orientation) {
48534775 Poul Sander 2026-06-08 18:57 84
    case 1: return 6;
48534775 Poul Sander 2026-06-08 18:57 85
    case 2: return 7;
48534775 Poul Sander 2026-06-08 18:57 86
    case 3: return 8;
48534775 Poul Sander 2026-06-08 18:57 87
    case 4: return 5;
48534775 Poul Sander 2026-06-08 18:57 88
    case 5: return 2;
48534775 Poul Sander 2026-06-08 18:57 89
    case 6: return 3;
48534775 Poul Sander 2026-06-08 18:57 90
    case 7: return 4;
48534775 Poul Sander 2026-06-08 18:57 91
    case 8: return 1;
48534775 Poul Sander 2026-06-08 18:57 92
    default: return 6;
48534775 Poul Sander 2026-06-08 18:57 93
    }
48534775 Poul Sander 2026-06-08 18:57 94
}
48534775 Poul Sander 2026-06-08 18:57 95
48534775 Poul Sander 2026-06-08 18:57 96
static int orientationRotatedCounterClockwise(int orientation)
48534775 Poul Sander 2026-06-08 18:57 97
{
48534775 Poul Sander 2026-06-08 18:57 98
    switch (orientation) {
48534775 Poul Sander 2026-06-08 18:57 99
    case 1: return 8;
48534775 Poul Sander 2026-06-08 18:57 100
    case 2: return 5;
48534775 Poul Sander 2026-06-08 18:57 101
    case 3: return 6;
48534775 Poul Sander 2026-06-08 18:57 102
    case 4: return 7;
48534775 Poul Sander 2026-06-08 18:57 103
    case 5: return 4;
48534775 Poul Sander 2026-06-08 18:57 104
    case 6: return 1;
48534775 Poul Sander 2026-06-08 18:57 105
    case 7: return 2;
48534775 Poul Sander 2026-06-08 18:57 106
    case 8: return 3;
48534775 Poul Sander 2026-06-08 18:57 107
    default: return 8;
48534775 Poul Sander 2026-06-08 18:57 108
    }
48534775 Poul Sander 2026-06-08 18:57 109
}
48534775 Poul Sander 2026-06-08 18:57 110
53fdab93 Poul Sander 2026-05-18 18:45 111
bool ExifData::isEmpty() const
53fdab93 Poul Sander 2026-05-18 18:45 112
{
beb4a823 Poul Sander 2026-05-19 21:53 113
    return captionAbstract.isEmpty() && description.isEmpty()
beb4a823 Poul Sander 2026-05-19 21:53 114
        && dateTime.isEmpty() && make.isEmpty() && model.isEmpty()
53fdab93 Poul Sander 2026-05-18 18:45 115
        && exposureTime.isEmpty() && fNumber.isEmpty() && iso.isEmpty()
53fdab93 Poul Sander 2026-05-18 18:45 116
        && focalLength.isEmpty() && flash.isEmpty()
091a4f7d Poul Sander 2026-06-09 19:54 117
        && filename.isEmpty() && dimensions.isEmpty() && fileSize.isEmpty()
091a4f7d Poul Sander 2026-06-09 19:54 118
        && orientation.isEmpty() && latitude.isEmpty() && longitude.isEmpty()
091a4f7d Poul Sander 2026-06-09 19:54 119
        && osmLink.isEmpty();
53fdab93 Poul Sander 2026-05-18 18:45 120
}
53fdab93 Poul Sander 2026-05-18 18:45 121
53fdab93 Poul Sander 2026-05-18 18:45 122
QList<QPair<QString, QString>> ExifData::toList() const
53fdab93 Poul Sander 2026-05-18 18:45 123
{
53fdab93 Poul Sander 2026-05-18 18:45 124
    QList<QPair<QString, QString>> result;
4b975496 Poul Sander 2026-06-20 22:41 125
    addField(result, QT_TRANSLATE_NOOP("ExifReader", "Caption"),      captionAbstract);
4b975496 Poul Sander 2026-06-20 22:41 126
    addField(result, QT_TRANSLATE_NOOP("ExifReader", "Description"),  description);
4b975496 Poul Sander 2026-06-20 22:41 127
    addField(result, QT_TRANSLATE_NOOP("ExifReader", "Date/Time"),    dateTime);
4b975496 Poul Sander 2026-06-20 22:41 128
    addField(result, QT_TRANSLATE_NOOP("ExifReader", "Camera Make"),  make);
4b975496 Poul Sander 2026-06-20 22:41 129
    addField(result, QT_TRANSLATE_NOOP("ExifReader", "Camera Model"), model);
4b975496 Poul Sander 2026-06-20 22:41 130
    addField(result, QT_TRANSLATE_NOOP("ExifReader", "Exposure"),     exposureTime);
4b975496 Poul Sander 2026-06-20 22:41 131
    addField(result, QT_TRANSLATE_NOOP("ExifReader", "Aperture"),     fNumber);
4b975496 Poul Sander 2026-06-20 22:41 132
    addField(result, QT_TRANSLATE_NOOP("ExifReader", "ISO"),          iso);
4b975496 Poul Sander 2026-06-20 22:41 133
    addField(result, QT_TRANSLATE_NOOP("ExifReader", "Focal Length"), focalLength);
4b975496 Poul Sander 2026-06-20 22:41 134
    addField(result, QT_TRANSLATE_NOOP("ExifReader", "Flash"),        flash);
4b975496 Poul Sander 2026-06-20 22:41 135
    addField(result, QT_TRANSLATE_NOOP("ExifReader", "Filename"),     filename);
4b975496 Poul Sander 2026-06-20 22:41 136
    addField(result, QT_TRANSLATE_NOOP("ExifReader", "Dimensions"),   dimensions);
4b975496 Poul Sander 2026-06-20 22:41 137
    addField(result, QT_TRANSLATE_NOOP("ExifReader", "File Size"),    fileSize);
4b975496 Poul Sander 2026-06-20 22:41 138
    addField(result, QT_TRANSLATE_NOOP("ExifReader", "Orientation"),  orientation);
4b975496 Poul Sander 2026-06-20 22:41 139
    addField(result, QT_TRANSLATE_NOOP("ExifReader", "Latitude"),     latitude);
4b975496 Poul Sander 2026-06-20 22:41 140
    addField(result, QT_TRANSLATE_NOOP("ExifReader", "Longitude"),    longitude);
4b975496 Poul Sander 2026-06-20 22:41 141
    addField(result, QT_TRANSLATE_NOOP("ExifReader", "OpenStreetMap"), osmLink);
53fdab93 Poul Sander 2026-05-18 18:45 142
    return result;
53fdab93 Poul Sander 2026-05-18 18:45 143
}
53fdab93 Poul Sander 2026-05-18 18:45 144
80a9926d Poul Sander 2026-05-19 20:28 145
ExifData ExifReader::read(const QByteArray &path)
53fdab93 Poul Sander 2026-05-18 18:45 146
{
53fdab93 Poul Sander 2026-05-18 18:45 147
    ExifData data;
53fdab93 Poul Sander 2026-05-18 18:45 148
80a9926d Poul Sander 2026-05-19 20:28 149
    struct ::stat st{};
80a9926d Poul Sander 2026-05-19 20:28 150
    if (::stat(path.constData(), &st) != 0 || !S_ISREG(st.st_mode))
53fdab93 Poul Sander 2026-05-18 18:45 151
        return data;
53fdab93 Poul Sander 2026-05-18 18:45 152
700d6f2d Poul Sander 2026-06-06 15:44 153
    // Filename
700d6f2d Poul Sander 2026-06-06 15:44 154
    {
700d6f2d Poul Sander 2026-06-06 15:44 155
        const std::filesystem::path fsPath(path.toStdString());
700d6f2d Poul Sander 2026-06-06 15:44 156
        const std::string nativeName = fsPath.filename().native();
700d6f2d Poul Sander 2026-06-06 15:44 157
        data.filename = QString::fromLocal8Bit(
700d6f2d Poul Sander 2026-06-06 15:44 158
            nativeName.data(), static_cast<qsizetype>(nativeName.size()));
700d6f2d Poul Sander 2026-06-06 15:44 159
    }
700d6f2d Poul Sander 2026-06-06 15:44 160
53fdab93 Poul Sander 2026-05-18 18:45 161
    // File size
53fdab93 Poul Sander 2026-05-18 18:45 162
    {
80a9926d Poul Sander 2026-05-19 20:28 163
        qint64 size = static_cast<qint64>(st.st_size);
53fdab93 Poul Sander 2026-05-18 18:45 164
        if (size < 1024)
53fdab93 Poul Sander 2026-05-18 18:45 165
            data.fileSize = QString::number(size) + QLatin1String(" B");
53fdab93 Poul Sander 2026-05-18 18:45 166
        else if (size < 1024 * 1024)
53fdab93 Poul Sander 2026-05-18 18:45 167
            data.fileSize = QString::number(size / 1024.0, 'f', 1) + QLatin1String(" KiB");
53fdab93 Poul Sander 2026-05-18 18:45 168
        else
53fdab93 Poul Sander 2026-05-18 18:45 169
            data.fileSize = QString::number(size / (1024.0 * 1024.0), 'f', 1) + QLatin1String(" MiB");
53fdab93 Poul Sander 2026-05-18 18:45 170
    }
53fdab93 Poul Sander 2026-05-18 18:45 171
53fdab93 Poul Sander 2026-05-18 18:45 172
    // Image dimensions via Qt (reads only the header, no full decode needed)
53fdab93 Poul Sander 2026-05-18 18:45 173
    {
80a9926d Poul Sander 2026-05-19 20:28 174
        int fd = ::open(path.constData(), O_RDONLY | O_CLOEXEC);
80a9926d Poul Sander 2026-05-19 20:28 175
        if (fd >= 0) {
80a9926d Poul Sander 2026-05-19 20:28 176
            QFile file;
80a9926d Poul Sander 2026-05-19 20:28 177
            if (file.open(fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle)) {
80a9926d Poul Sander 2026-05-19 20:28 178
                QImageReader reader(&file);
80a9926d Poul Sander 2026-05-19 20:28 179
                reader.setDecideFormatFromContent(true);
80a9926d Poul Sander 2026-05-19 20:28 180
                QSize sz = reader.size();
80a9926d Poul Sander 2026-05-19 20:28 181
                        if (sz.isValid())
80a9926d Poul Sander 2026-05-19 20:28 182
                    data.dimensions = QString::fromLatin1("%1 * %2")
80a9926d Poul Sander 2026-05-19 20:28 183
                                          .arg(sz.width()).arg(sz.height());
80a9926d Poul Sander 2026-05-19 20:28 184
            } else {
80a9926d Poul Sander 2026-05-19 20:28 185
                ::close(fd);
80a9926d Poul Sander 2026-05-19 20:28 186
            }
80a9926d Poul Sander 2026-05-19 20:28 187
        }
53fdab93 Poul Sander 2026-05-18 18:45 188
    }
53fdab93 Poul Sander 2026-05-18 18:45 189
53fdab93 Poul Sander 2026-05-18 18:45 190
    // EXIF tags via exiv2
53fdab93 Poul Sander 2026-05-18 18:45 191
    try {
80a9926d Poul Sander 2026-05-19 20:28 192
        std::unique_ptr<Exiv2::Image> image =
80a9926d Poul Sander 2026-05-19 20:28 193
            Exiv2::ImageFactory::open(path.toStdString());
53fdab93 Poul Sander 2026-05-18 18:45 194
        image->readMetadata();
53fdab93 Poul Sander 2026-05-18 18:45 195
        const Exiv2::ExifData &exif = image->exifData();
53fdab93 Poul Sander 2026-05-18 18:45 196
0eae7b4d Poul Sander 2026-05-18 19:22 197
        data.dateTime = formatDate(findTag(exif, "Exif.Photo.DateTimeOriginal"));
53fdab93 Poul Sander 2026-05-18 18:45 198
        if (data.dateTime.isEmpty())
0eae7b4d Poul Sander 2026-05-18 19:22 199
            data.dateTime = formatDate(findTag(exif, "Exif.Image.DateTime"));
0eae7b4d Poul Sander 2026-05-18 19:22 200
        data.make         = findTag(exif, "Exif.Image.Make");
0eae7b4d Poul Sander 2026-05-18 19:22 201
        data.model        = findTag(exif, "Exif.Image.Model");
0eae7b4d Poul Sander 2026-05-18 19:22 202
        data.exposureTime = findTag(exif, "Exif.Photo.ExposureTime");
0eae7b4d Poul Sander 2026-05-18 19:22 203
        data.fNumber      = findTag(exif, "Exif.Photo.FNumber");
0eae7b4d Poul Sander 2026-05-18 19:22 204
        data.iso          = findTag(exif, "Exif.Photo.ISOSpeedRatings");
0eae7b4d Poul Sander 2026-05-18 19:22 205
        data.focalLength  = findTag(exif, "Exif.Photo.FocalLength");
0eae7b4d Poul Sander 2026-05-18 19:22 206
        data.flash        = findTag(exif, "Exif.Photo.Flash");
beb4a823 Poul Sander 2026-05-19 21:53 207
        data.description  = findTag(exif, "Exif.Image.ImageDescription");
091a4f7d Poul Sander 2026-06-09 19:54 208
        data.orientation  = findTag(exif, "Exif.Image.Orientation");
091a4f7d Poul Sander 2026-06-09 19:54 209
091a4f7d Poul Sander 2026-06-09 19:54 210
        // GPS
091a4f7d Poul Sander 2026-06-09 19:54 211
        QString latStr = findTag(exif, "Exif.GPSInfo.GPSLatitude");
091a4f7d Poul Sander 2026-06-09 19:54 212
        QString latRef = findTag(exif, "Exif.GPSInfo.GPSLatitudeRef");
091a4f7d Poul Sander 2026-06-09 19:54 213
        if (!latStr.isEmpty() && !latRef.isEmpty()) {
e2a71efb Poul Sander 2026-06-09 21:42 214
            data.latitude = latStr + " " + latRef;
091a4f7d Poul Sander 2026-06-09 19:54 215
        }
091a4f7d Poul Sander 2026-06-09 19:54 216
091a4f7d Poul Sander 2026-06-09 19:54 217
        QString lonStr = findTag(exif, "Exif.GPSInfo.GPSLongitude");
091a4f7d Poul Sander 2026-06-09 19:54 218
        QString lonRef = findTag(exif, "Exif.GPSInfo.GPSLongitudeRef");
091a4f7d Poul Sander 2026-06-09 19:54 219
        if (!lonStr.isEmpty() && !lonRef.isEmpty()) {
e2a71efb Poul Sander 2026-06-09 21:42 220
            data.longitude = lonStr + " " + lonRef;
091a4f7d Poul Sander 2026-06-09 19:54 221
        }
091a4f7d Poul Sander 2026-06-09 19:54 222
091a4f7d Poul Sander 2026-06-09 19:54 223
        if (!data.latitude.isEmpty() && !data.longitude.isEmpty()) {
091a4f7d Poul Sander 2026-06-09 19:54 224
            double lat = parseGpsCoordinate(exif, "Exif.GPSInfo.GPSLatitude");
e2a71efb Poul Sander 2026-06-09 21:42 225
            if (latRef == "S") lat = -lat;
091a4f7d Poul Sander 2026-06-09 19:54 226
            double lon = parseGpsCoordinate(exif, "Exif.GPSInfo.GPSLongitude");
e2a71efb Poul Sander 2026-06-09 21:42 227
            if (lonRef == "W") lon = -lon;
091a4f7d Poul Sander 2026-06-09 19:54 228
            data.osmLink = QString::fromLatin1("https://www.openstreetmap.org/?mlat=%1&mlon=%2#map=16/%1/%2")
091a4f7d Poul Sander 2026-06-09 19:54 229
                               .arg(lat, 0, 'f', 6).arg(lon, 0, 'f', 6);
091a4f7d Poul Sander 2026-06-09 19:54 230
        }
beb4a823 Poul Sander 2026-05-19 21:53 231
beb4a823 Poul Sander 2026-05-19 21:53 232
        // IPTC Caption-Abstract
beb4a823 Poul Sander 2026-05-19 21:53 233
        const Exiv2::IptcData &iptc = image->iptcData();
beb4a823 Poul Sander 2026-05-19 21:53 234
        auto iptcIt = iptc.findKey(Exiv2::IptcKey("Iptc.Application2.Caption"));
beb4a823 Poul Sander 2026-05-19 21:53 235
        if (iptcIt != iptc.end())
beb4a823 Poul Sander 2026-05-19 21:53 236
            data.captionAbstract = QString::fromStdString(iptcIt->toString()).trimmed();
53fdab93 Poul Sander 2026-05-18 18:45 237
    } catch (const Exiv2::Error &) {
53fdab93 Poul Sander 2026-05-18 18:45 238
        // No EXIF or unsupported format — leave tags empty
53fdab93 Poul Sander 2026-05-18 18:45 239
    }
53fdab93 Poul Sander 2026-05-18 18:45 240
53fdab93 Poul Sander 2026-05-18 18:45 241
    return data;
53fdab93 Poul Sander 2026-05-18 18:45 242
}
beb4a823 Poul Sander 2026-05-19 21:53 243
beb4a823 Poul Sander 2026-05-19 21:53 244
bool ExifReader::saveCaption(const QByteArray &path,
beb4a823 Poul Sander 2026-05-19 21:53 245
                             const QString    &caption,
beb4a823 Poul Sander 2026-05-19 21:53 246
                             const ExifData   &oldData)
beb4a823 Poul Sander 2026-05-19 21:53 247
{
beb4a823 Poul Sander 2026-05-19 21:53 248
    try {
beb4a823 Poul Sander 2026-05-19 21:53 249
        std::unique_ptr<Exiv2::Image> image =
beb4a823 Poul Sander 2026-05-19 21:53 250
            Exiv2::ImageFactory::open(path.toStdString());
beb4a823 Poul Sander 2026-05-19 21:53 251
        image->readMetadata();
beb4a823 Poul Sander 2026-05-19 21:53 252
beb4a823 Poul Sander 2026-05-19 21:53 253
        // Erase all existing Caption-Abstract entries then add the new one
beb4a823 Poul Sander 2026-05-19 21:53 254
        Exiv2::IptcData &iptc = image->iptcData();
beb4a823 Poul Sander 2026-05-19 21:53 255
        {
beb4a823 Poul Sander 2026-05-19 21:53 256
            Exiv2::IptcKey key("Iptc.Application2.Caption");
beb4a823 Poul Sander 2026-05-19 21:53 257
            auto it = iptc.findKey(key);
beb4a823 Poul Sander 2026-05-19 21:53 258
            while (it != iptc.end()) {
beb4a823 Poul Sander 2026-05-19 21:53 259
                it = iptc.erase(it);
beb4a823 Poul Sander 2026-05-19 21:53 260
                it = iptc.findKey(key);
beb4a823 Poul Sander 2026-05-19 21:53 261
            }
beb4a823 Poul Sander 2026-05-19 21:53 262
        }
beb4a823 Poul Sander 2026-05-19 21:53 263
        Exiv2::Iptcdatum datum(Exiv2::IptcKey("Iptc.Application2.Caption"));
beb4a823 Poul Sander 2026-05-19 21:53 264
        datum.setValue(caption.toStdString());
beb4a823 Poul Sander 2026-05-19 21:53 265
        iptc.add(datum);
beb4a823 Poul Sander 2026-05-19 21:53 266
beb4a823 Poul Sander 2026-05-19 21:53 267
        // If Caption-Abstract and Description were previously in sync, keep them in sync
beb4a823 Poul Sander 2026-05-19 21:53 268
        if (oldData.captionAbstract == oldData.description) {
beb4a823 Poul Sander 2026-05-19 21:53 269
            image->exifData()["Exif.Image.ImageDescription"] = caption.toStdString();
beb4a823 Poul Sander 2026-05-19 21:53 270
        }
beb4a823 Poul Sander 2026-05-19 21:53 271
beb4a823 Poul Sander 2026-05-19 21:53 272
        image->writeMetadata();
beb4a823 Poul Sander 2026-05-19 21:53 273
        return true;
beb4a823 Poul Sander 2026-05-19 21:53 274
    } catch (const Exiv2::Error &) {
beb4a823 Poul Sander 2026-05-19 21:53 275
        return false;
beb4a823 Poul Sander 2026-05-19 21:53 276
    }
beb4a823 Poul Sander 2026-05-19 21:53 277
}
48534775 Poul Sander 2026-06-08 18:57 278
48534775 Poul Sander 2026-06-08 18:57 279
bool ExifReader::rotate(const QByteArray &path, bool clockwise)
48534775 Poul Sander 2026-06-08 18:57 280
{
48534775 Poul Sander 2026-06-08 18:57 281
    try {
48534775 Poul Sander 2026-06-08 18:57 282
        std::unique_ptr<Exiv2::Image> image =
48534775 Poul Sander 2026-06-08 18:57 283
            Exiv2::ImageFactory::open(path.toStdString());
48534775 Poul Sander 2026-06-08 18:57 284
        image->readMetadata();
48534775 Poul Sander 2026-06-08 18:57 285
        Exiv2::ExifData &exif = image->exifData();
48534775 Poul Sander 2026-06-08 18:57 286
48534775 Poul Sander 2026-06-08 18:57 287
        int current = 1; // EXIF default when the tag is absent
48534775 Poul Sander 2026-06-08 18:57 288
        auto it = exif.findKey(Exiv2::ExifKey("Exif.Image.Orientation"));
48534775 Poul Sander 2026-06-08 18:57 289
        if (it != exif.end())
48534775 Poul Sander 2026-06-08 18:57 290
            current = static_cast<int>(it->toInt64());
48534775 Poul Sander 2026-06-08 18:57 291
48534775 Poul Sander 2026-06-08 18:57 292
        const int updated = clockwise ? orientationRotatedClockwise(current)
48534775 Poul Sander 2026-06-08 18:57 293
                                      : orientationRotatedCounterClockwise(current);
48534775 Poul Sander 2026-06-08 18:57 294
        exif["Exif.Image.Orientation"] = static_cast<uint16_t>(updated);
48534775 Poul Sander 2026-06-08 18:57 295
48534775 Poul Sander 2026-06-08 18:57 296
        image->writeMetadata();
48534775 Poul Sander 2026-06-08 18:57 297
        return true;
48534775 Poul Sander 2026-06-08 18:57 298
    } catch (const Exiv2::Error &) {
48534775 Poul Sander 2026-06-08 18:57 299
        return false;
48534775 Poul Sander 2026-06-08 18:57 300
    }
48534775 Poul Sander 2026-06-08 18:57 301
}
1970-01-01 00:00 302