File indexing completed on 2024-05-19 03:56:44

0001 /*
0002     SPDX-FileCopyrightText: 2012 Vishesh Handa <me@vhanda.in>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-or-later
0005 */
0006 
0007 
0008 #include "exiv2extractor.h"
0009 #include "kfilemetadata_debug.h"
0010 #include <QTimeZone>
0011 #include <cmath>
0012 #include <exiv2/exiv2.hpp>
0013 
0014 using namespace KFileMetaData;
0015 
0016 namespace {
0017 void add(ExtractionResult* result, const Exiv2::ExifData& data,
0018          Property::Property prop, const Exiv2::ExifKey& key,
0019          QMetaType::Type type);
0020 
0021 double fetchGpsDouble(const Exiv2::ExifData& data, const Exiv2::ExifKey& key);
0022 double fetchGpsAltitude(const Exiv2::ExifData& data);
0023 QByteArray fetchByteArray(const Exiv2::ExifData& data, const Exiv2::ExifKey& key);
0024 }
0025 
0026 Exiv2Extractor::Exiv2Extractor(QObject* parent)
0027     : ExtractorPlugin(parent)
0028 {
0029 #ifdef EXV_ENABLE_BMFF
0030     Exiv2::enableBMFF(true);
0031 #endif
0032 }
0033 
0034 namespace
0035 {
0036 static const QStringList supportedMimeTypes = {
0037     QStringLiteral("image/bmp"),
0038     QStringLiteral("image/gif"),
0039     QStringLiteral("image/jp2"),
0040     QStringLiteral("image/jpeg"),
0041     QStringLiteral("image/pgf"),
0042     QStringLiteral("image/png"),
0043     QStringLiteral("image/tiff"),
0044     QStringLiteral("image/webp"),
0045 #ifdef EXV_ENABLE_BMFF
0046     QStringLiteral("image/avif"),
0047     QStringLiteral("image/heif"),
0048     QStringLiteral("image/jxl"),
0049     QStringLiteral("image/x-canon-cr3"),
0050 #endif
0051     QStringLiteral("image/x-exv"),
0052     QStringLiteral("image/x-canon-cr2"),
0053     QStringLiteral("image/x-canon-crw"),
0054     QStringLiteral("image/x-fuji-raf"),
0055     QStringLiteral("image/x-minolta-mrw"),
0056     QStringLiteral("image/x-nikon-nef"),
0057     QStringLiteral("image/x-olympus-orf"),
0058     QStringLiteral("image/x-panasonic-rw2"),
0059     QStringLiteral("image/x-pentax-pef"),
0060     QStringLiteral("image/x-photoshop"),
0061     QStringLiteral("image/x-samsung-srw"),
0062     QStringLiteral("image/x-tga"),
0063 };
0064 
0065 QString toString(const Exiv2::Value& value)
0066 {
0067     const std::string str = value.toString();
0068     return QString::fromUtf8(str.c_str(), str.length());
0069 }
0070 
0071 QVariant toVariantDateTime(const Exiv2::Value& value)
0072 {
0073     if (value.typeId() == Exiv2::asciiString) {
0074         QDateTime val = ExtractorPlugin::dateTimeFromString(QString::fromLatin1(value.toString().c_str()));
0075         if (val.isValid()) {
0076             // Datetime is stored in exif as local time.
0077             val.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(0));
0078             return QVariant(val);
0079         }
0080     }
0081 
0082     return QVariant();
0083 }
0084 
0085 QVariant toVariantLong(const Exiv2::Value& value)
0086 {
0087     if (value.typeId() == Exiv2::unsignedLong || value.typeId() == Exiv2::signedLong) {
0088 #if EXIV2_TEST_VERSION(0,28,0)
0089         qlonglong val = value.toInt64();
0090 #else
0091         qlonglong val = value.toLong();
0092 #endif
0093         return QVariant(val);
0094     }
0095 
0096     QString str(toString(value));
0097     bool ok = false;
0098     int val = str.toInt(&ok);
0099     if (ok) {
0100         return QVariant(val);
0101     }
0102 
0103     return QVariant();
0104 }
0105 
0106 QVariant toVariantDouble(const Exiv2::Value& value)
0107 {
0108     if (value.typeId() == Exiv2::tiffFloat || value.typeId() == Exiv2::tiffDouble
0109         || value.typeId() == Exiv2::unsignedRational || value.typeId() == Exiv2::signedRational) {
0110         return QVariant(static_cast<double>(value.toFloat()));
0111     }
0112 
0113     QString str(toString(value));
0114     bool ok = false;
0115     double val = str.toDouble(&ok);
0116     if (ok) {
0117         return QVariant(val);
0118     }
0119 
0120     return QVariant();
0121 }
0122 
0123 QVariant toVariantString(const Exiv2::Value& value)
0124 {
0125     QString str = toString(value);
0126     if (!str.isEmpty()) {
0127         return QVariant(str);
0128     }
0129 
0130     return QVariant();
0131 }
0132 
0133 QVariant toVariant(const Exiv2::Value& value, QMetaType::Type type) {
0134     if (value.count() == 0) {
0135         return QVariant();
0136     }
0137     switch (type) {
0138     case QMetaType::Int:
0139         return toVariantLong(value);
0140 
0141     case QMetaType::QDateTime:
0142         return toVariantDateTime(value);
0143 
0144     case QMetaType::Double:
0145         return toVariantDouble(value);
0146 
0147     case QMetaType::QString:
0148     default:
0149         return toVariantString(value);
0150     }
0151 }
0152 }
0153 
0154 QStringList Exiv2Extractor::mimetypes() const
0155 {
0156     return supportedMimeTypes;
0157 }
0158 
0159 void Exiv2Extractor::extract(ExtractionResult* result)
0160 {
0161     using namespace std::string_literals;
0162 
0163     QByteArray arr = result->inputUrl().toUtf8();
0164     std::string fileString(arr.data(), arr.length());
0165 
0166 #if EXIV2_TEST_VERSION(0, 28, 0)
0167     Exiv2::Image::UniquePtr image;
0168 #else
0169     Exiv2::Image::AutoPtr image;
0170 #endif
0171     try {
0172         image = Exiv2::ImageFactory::open(fileString);
0173     } catch (const std::exception&) {
0174         return;
0175     }
0176     if (!image.get()) {
0177         return;
0178     }
0179 
0180     try {
0181         image->readMetadata();
0182     } catch (const std::exception&) {
0183         return;
0184     }
0185     result->addType(Type::Image);
0186 
0187     if (!(result->inputFlags() & ExtractionResult::ExtractMetaData)) {
0188         return;
0189     }
0190 
0191     if (image->pixelHeight()) {
0192         result->add(Property::Height, image->pixelHeight());
0193     }
0194 
0195     if (image->pixelWidth()) {
0196         result->add(Property::Width, image->pixelWidth());
0197     }
0198 
0199     std::string comment = image->comment();
0200     if (!comment.empty()) {
0201         result->add(Property::Comment, QString::fromUtf8(comment.c_str(), comment.length()));
0202     }
0203 
0204     const Exiv2::ExifData& data = image->exifData();
0205 
0206     using EK = Exiv2::ExifKey;
0207     add(result, data, Property::Manufacturer, EK{"Exif.Image.Make"s}, QMetaType::QString);
0208     add(result, data, Property::Model, EK{"Exif.Image.Model"s}, QMetaType::QString);
0209     add(result, data, Property::Description, EK{"Exif.Image.ImageDescription"s}, QMetaType::QString);
0210     add(result, data, Property::Artist, EK{"Exif.Image.Artist"s}, QMetaType::QString);
0211     add(result, data, Property::Copyright, EK{"Exif.Image.Copyright"s}, QMetaType::QString);
0212     add(result, data, Property::Generator, EK{"Exif.Image.Software"s}, QMetaType::QString);
0213     add(result, data, Property::ImageDateTime, EK{"Exif.Image.DateTime"s}, QMetaType::QDateTime);
0214     add(result, data, Property::ImageOrientation, EK{"Exif.Image.Orientation"s}, QMetaType::Int);
0215     add(result, data, Property::PhotoFlash, EK{"Exif.Photo.Flash"s}, QMetaType::Int);
0216     add(result, data, Property::PhotoPixelXDimension, EK{"Exif.Photo.PixelXDimension"s}, QMetaType::Int);
0217     add(result, data, Property::PhotoPixelYDimension, EK{"Exif.Photo.PixelYDimension"s}, QMetaType::Int);
0218     add(result, data, Property::PhotoDateTimeOriginal, EK{"Exif.Photo.DateTimeOriginal"s}, QMetaType::QDateTime);
0219     add(result, data, Property::PhotoFocalLength, EK{"Exif.Photo.FocalLength"s}, QMetaType::Double);
0220     add(result, data, Property::PhotoFocalLengthIn35mmFilm, EK{"Exif.Photo.FocalLengthIn35mmFilm"s}, QMetaType::Double);
0221     add(result, data, Property::PhotoExposureTime, EK{"Exif.Photo.ExposureTime"s}, QMetaType::Double);
0222     add(result, data, Property::PhotoExposureBiasValue, EK{"Exif.Photo.ExposureBiasValue"s}, QMetaType::Double);
0223     add(result, data, Property::PhotoFNumber, EK{"Exif.Photo.FNumber"s}, QMetaType::Double);
0224     add(result, data, Property::PhotoApertureValue, EK{"Exif.Photo.ApertureValue"s}, QMetaType::Double);
0225     add(result, data, Property::PhotoWhiteBalance, EK{"Exif.Photo.WhiteBalance"s}, QMetaType::Int);
0226     add(result, data, Property::PhotoMeteringMode, EK{"Exif.Photo.MeteringMode"s}, QMetaType::Int);
0227     add(result, data, Property::PhotoISOSpeedRatings, EK{"Exif.Photo.ISOSpeedRatings"s}, QMetaType::Int);
0228     add(result, data, Property::PhotoSaturation, EK{"Exif.Photo.Saturation"s}, QMetaType::Int);
0229     add(result, data, Property::PhotoSharpness, EK{"Exif.Photo.Sharpness"s}, QMetaType::Int);
0230     // https://exiv2.org/tags.html "Exif.Photo.ImageTitle" not natively supported, use tag value
0231     add(result, data, Property::Title, EK{0xa436, "Photo"s}, QMetaType::QString);
0232 
0233     double latitude = fetchGpsDouble(data, EK{"Exif.GPSInfo.GPSLatitude"s});
0234     double longitude = fetchGpsDouble(data, EK{"Exif.GPSInfo.GPSLongitude"s});
0235     double altitude = fetchGpsAltitude(data);
0236 
0237     QByteArray latRef = fetchByteArray(data, EK{"Exif.GPSInfo.GPSLatitudeRef"s});
0238     if (!latRef.isEmpty() && latRef[0] == 'S') {
0239         latitude *= -1;
0240     }
0241 
0242     QByteArray longRef = fetchByteArray(data, EK{"Exif.GPSInfo.GPSLongitudeRef"s});
0243     if (!longRef.isEmpty() && longRef[0] == 'W') {
0244         longitude *= -1;
0245     }
0246 
0247     if (!std::isnan(latitude)) {
0248         result->add(Property::PhotoGpsLatitude, latitude);
0249     }
0250 
0251     if (!std::isnan(longitude)) {
0252         result->add(Property::PhotoGpsLongitude, longitude);
0253     }
0254 
0255     if (!std::isnan(altitude)) {
0256         result->add(Property::PhotoGpsAltitude, altitude);
0257     }
0258 
0259     const Exiv2::XmpData& xmpData = image->xmpData();
0260     for (const auto& entry : xmpData) {
0261         if (entry.groupName() != "dc"s) {
0262             continue;
0263         }
0264 
0265         std::map<std::string, Property::Property> propMap = {
0266             { "subject"s, Property::Subject },
0267             { "title"s, Property::Title},
0268             { "description"s, Property::Description},
0269         };
0270         if (auto type = propMap.find(entry.tagName()); type != propMap.end()) {
0271             auto xmpType = Exiv2::XmpValue::xmpArrayType(entry.value().typeId());
0272             size_t limit = ((xmpType == Exiv2::XmpValue::xaBag) || (xmpType == Exiv2::XmpValue::xaSeq)) ? entry.count() : 1;
0273             for (size_t i = 0; i < limit; i++) {
0274                 auto value = QString::fromStdString(entry.toString(i));
0275                 if (!value.isEmpty()) {
0276                     result->add(type->second, value);
0277                 }
0278             }
0279         }
0280     }
0281 }
0282 
0283 namespace {
0284 void add(ExtractionResult* result, const Exiv2::ExifData& data,
0285          Property::Property prop, const Exiv2::ExifKey& key,
0286          QMetaType::Type type)
0287 {
0288     Exiv2::ExifData::const_iterator it = data.findKey(key);
0289     if (it != data.end()) {
0290         QVariant value = toVariant(it->value(), type);
0291         if (!value.isNull()) {
0292             result->add(prop, value);
0293         }
0294     }
0295 }
0296 
0297 double fetchGpsDouble(const Exiv2::ExifData& data, const Exiv2::ExifKey& key)
0298 {
0299     Exiv2::ExifData::const_iterator it = data.findKey(key);
0300     if (it != data.end() && it->count() == 3) {
0301         double n = 0.0;
0302         double d = 0.0;
0303 
0304         n = (*it).toRational(0).first;
0305         d = (*it).toRational(0).second;
0306 
0307         if (d == 0.0) {
0308             return std::numeric_limits<double>::quiet_NaN();
0309         }
0310 
0311         double deg = n / d;
0312 
0313         n = (*it).toRational(1).first;
0314         d = (*it).toRational(1).second;
0315 
0316         if (d == 0.0) {
0317             return deg;
0318         }
0319 
0320         double min = n / d;
0321         if (min != -1.0) {
0322             deg += min / 60.0;
0323         }
0324 
0325         n = (*it).toRational(2).first;
0326         d = (*it).toRational(2).second;
0327 
0328         if (d == 0.0) {
0329             return deg;
0330         }
0331 
0332         double sec = n / d;
0333         if (sec != -1.0) {
0334             deg += sec / 3600.0;
0335         }
0336 
0337         return deg;
0338     }
0339 
0340     return std::numeric_limits<double>::quiet_NaN();
0341 }
0342 
0343 double fetchGpsAltitude(const Exiv2::ExifData& data)
0344 {
0345     using namespace std::string_literals;
0346 
0347     double alt = std::numeric_limits<double>::quiet_NaN();
0348     Exiv2::ExifData::const_iterator it = data.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSAltitude"s));
0349     if (it != data.end() && it->count() > 0 &&
0350         (it->value().typeId() == Exiv2::unsignedRational || it->value().typeId() == Exiv2::signedRational)) {
0351         auto ratio = it->value().toRational();
0352         if (ratio.second == 0) {
0353             return alt;
0354         }
0355         it = data.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSAltitudeRef"s));
0356         if (it != data.end() && it->count() > 0 &&
0357             (it->value().typeId() == Exiv2::unsignedByte || it->value().typeId() == Exiv2::signedByte)) {
0358 #if EXIV2_TEST_VERSION(0,28,0)
0359             auto altRef = it->value().toInt64();
0360 #else
0361             auto altRef = it->value().toLong();
0362 #endif
0363             if (altRef) {
0364                 alt = -1.0 * ratio.first / ratio.second;
0365             } else {
0366                 alt = 1.0 * ratio.first / ratio.second;
0367             }
0368         }
0369     }
0370     return alt;
0371 }
0372 QByteArray fetchByteArray(const Exiv2::ExifData& data, const Exiv2::ExifKey& key)
0373 {
0374     Exiv2::ExifData::const_iterator it = data.findKey(key);
0375     if (it != data.end() && it->count() > 0) {
0376         std::string str = it->value().toString();
0377         return QByteArray(str.c_str(), str.size());
0378     }
0379 
0380     return QByteArray();
0381 }
0382 } // <anonymous> namespace
0383 
0384 #include "moc_exiv2extractor.cpp"