Files
rawTherapee/rtengine/metadata.cc
Lawrence Lee 0ac49e4d9a Support Exiv2 >= v0.28.0
The various Datum classes no longer have the toLong method and must be
replaced with toInt64.
ErrorCode is an enum class instead of an enum.
Error classes are reduced to Exiv2::Error.
2023-05-13 16:43:47 -07:00

625 lines
17 KiB
C++

/* -*- C++ -*-
*
* This file is part of RawTherapee.
*
* Copyright (c) 2019 Alberto Griggio <alberto.griggio@gmail.com>
*
* RawTherapee is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* RawTherapee is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with RawTherapee. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <glib/gstdio.h>
#include <iostream>
#include <giomm.h>
#include <set>
#include "metadata.h"
#include "settings.h"
#include "imagedata.h"
#include "../rtgui/version.h"
#include "../rtgui/pathutils.h"
#if EXIV2_TEST_VERSION(0,28,0)
using Exiv2Error = Exiv2::Error;
#else
using Exiv2Error = Exiv2::AnyError;
#endif
namespace rtengine {
extern const Settings *settings;
std::unique_ptr<Exiv2Metadata::ImageCache> Exiv2Metadata::cache_(nullptr);
namespace {
class Error: public Exiv2Error {
public:
Error(const std::string &msg):
#if EXIV2_TEST_VERSION(0,28,0)
Exiv2Error(Exiv2::ErrorCode::kerGeneralError),
#endif
msg_(msg) {}
const char *what() const throw() { return msg_.c_str(); }
int code() const throw() { return 0; }
private:
std::string msg_;
};
constexpr size_t IMAGE_CACHE_SIZE = 200;
std::unique_ptr<Exiv2::Image> open_exiv2(const Glib::ustring& fname,
bool check_exif)
{
#ifdef EXV_UNICODE_PATH
glong ws_size = 0;
gunichar2* const ws = g_utf8_to_utf16(fname.c_str(), -1, nullptr, &ws_size, nullptr);
std::wstring wfname;
wfname.reserve(ws_size);
for (glong i = 0; i < ws_size; ++i) {
wfname.push_back(ws[i]);
}
g_free(ws);
auto image = Exiv2::ImageFactory::open(wfname);
#else
auto image = Exiv2::ImageFactory::open(Glib::filename_from_utf8(fname));
#endif
image->readMetadata();
if (!image->good() || (check_exif && image->exifData().empty())) {
#if EXIV2_TEST_VERSION(0,27,0)
auto error_code = Exiv2::ErrorCode::kerErrorMessage;
#else
auto error_code = 1;
#endif
throw Exiv2::Error(error_code, "exiv2: invalid image");
}
std::unique_ptr<Exiv2::Image> ret(image.release());
return ret;
}
template <class Data, class Key>
void clear_metadata_key(Data &data, const Key &key)
{
while (true) {
auto it = data.findKey(key);
if (it == data.end()) {
break;
} else {
data.erase(it);
}
}
}
template <typename Iterator, typename Integer = std::size_t>
auto to_long(const Iterator &iter, Integer n = Integer{0}) -> decltype(
#if EXIV2_TEST_VERSION(0,28,0)
iter->toInt64()
) {
return iter->toInt64(n);
#else
iter->toLong()
) {
return iter->toLong(n);
#endif
}
} // namespace
Exiv2Metadata::Exiv2Metadata():
src_(""),
merge_xmp_(false),
image_(nullptr),
exif_(new rtengine::procparams::ExifPairs),
iptc_(new rtengine::procparams::IPTCPairs)
{
}
Exiv2Metadata::Exiv2Metadata(const Glib::ustring &path):
src_(path),
merge_xmp_(settings->metadata_xmp_sync != Settings::MetadataXmpSync::NONE),
image_(nullptr),
exif_(new rtengine::procparams::ExifPairs),
iptc_(new rtengine::procparams::IPTCPairs)
{
}
Exiv2Metadata::Exiv2Metadata(const Glib::ustring &path, bool merge_xmp_sidecar):
src_(path),
merge_xmp_(merge_xmp_sidecar),
image_(nullptr),
exif_(new rtengine::procparams::ExifPairs),
iptc_(new rtengine::procparams::IPTCPairs)
{
}
void Exiv2Metadata::load() const
{
if (!src_.empty() && !image_.get() && Glib::file_test(src_.c_str(), Glib::FILE_TEST_EXISTS)) {
CacheVal val;
auto finfo = Gio::File::create_for_path(src_)->query_info(G_FILE_ATTRIBUTE_TIME_MODIFIED);
Glib::TimeVal xmp_mtime(0, 0);
if (merge_xmp_) {
auto xmpname = xmpSidecarPath(src_);
if (Glib::file_test(xmpname.c_str(), Glib::FILE_TEST_EXISTS)) {
xmp_mtime = Gio::File::create_for_path(xmpname)->query_info(G_FILE_ATTRIBUTE_TIME_MODIFIED)->modification_time();
}
}
if (cache_ && cache_->get(src_, val) && val.image_mtime >= finfo->modification_time() && val.use_xmp == merge_xmp_ && val.xmp_mtime >= xmp_mtime) {
image_ = val.image;
} else {
auto img = open_exiv2(src_, true);
image_.reset(img.release());
if (merge_xmp_) {
do_merge_xmp(image_.get(), false);
}
if (cache_) {
val.image = image_;
val.image_mtime = finfo->modification_time();
val.xmp_mtime = xmp_mtime;
val.use_xmp = merge_xmp_;
cache_->set(src_, val);
}
}
}
}
Exiv2::ExifData& Exiv2Metadata::exifData()
{
return image_.get() ? image_->exifData() : exif_data_;
}
const Exiv2::ExifData& Exiv2Metadata::exifData() const
{
return const_cast<Exiv2Metadata *>(this)->exifData();
}
Exiv2::IptcData& Exiv2Metadata::iptcData()
{
return image_.get() ? image_->iptcData() : iptc_data_;
}
const Exiv2::IptcData& Exiv2Metadata::iptcData() const
{
return const_cast<Exiv2Metadata *>(this)->iptcData();
}
Exiv2::XmpData& Exiv2Metadata::xmpData()
{
return image_.get() ? image_->xmpData() : xmp_data_;
}
const Exiv2::XmpData& Exiv2Metadata::xmpData() const
{
return const_cast<Exiv2Metadata *>(this)->xmpData();
}
const Glib::ustring& Exiv2Metadata::filename() const
{
return src_;
}
const rtengine::procparams::ExifPairs& Exiv2Metadata::exif() const
{
return *exif_;
}
const rtengine::procparams::IPTCPairs& Exiv2Metadata::iptc() const
{
return *iptc_;
}
void Exiv2Metadata::setExif(const rtengine::procparams::ExifPairs &exif)
{
*exif_ = exif;
}
void Exiv2Metadata::setIptc(const rtengine::procparams::IPTCPairs &iptc)
{
*iptc_ = iptc;
}
void Exiv2Metadata::do_merge_xmp(Exiv2::Image *dst, bool keep_all) const
{
try {
auto xmp = getXmpSidecar(src_);
Exiv2::ExifData exif;
Exiv2::IptcData iptc;
Exiv2::copyXmpToIptc(xmp, iptc);
Exiv2::moveXmpToExif(xmp, exif);
std::unordered_map<std::string, std::unordered_set<std::string>> seen;
if (!keep_all) {
remove_unwanted(exif);
}
for (auto &datum : exif) {
dst->exifData()[datum.key()] = datum;
}
for (auto &datum : iptc) {
auto &s = seen[datum.key()];
if (s.empty()) {
clear_metadata_key(dst->iptcData(), Exiv2::IptcKey(datum.key()));
dst->iptcData()[datum.key()] = datum;
s.insert(datum.toString());
} else if (s.insert(datum.toString()).second) {
dst->iptcData().add(datum);
}
}
seen.clear();
for (auto &datum : xmp) {
auto &s = seen[datum.key()];
if (s.empty()) {
clear_metadata_key(dst->xmpData(), Exiv2::XmpKey(datum.key()));
dst->xmpData()[datum.key()] = datum;
s.insert(datum.toString());
} else if (s.insert(datum.toString()).second) {
dst->xmpData().add(datum);
}
}
} catch (std::exception &exc) {
if (settings->verbose) {
std::cerr << "Error loading metadata from XMP sidecar: "
<< exc.what() << std::endl;
}
}
}
void Exiv2Metadata::saveToImage(const Glib::ustring &path, bool preserve_all_tags) const
{
auto dst = open_exiv2(path, false);
if (image_.get()) {
dst->setIptcData(image_->iptcData());
dst->setXmpData(image_->xmpData());
if (merge_xmp_) {
do_merge_xmp(dst.get(), preserve_all_tags);
}
auto srcexif = image_->exifData();
if (!preserve_all_tags) {
remove_unwanted(srcexif);
}
//dst->setExifData(srcexif);
for (auto &tag : srcexif) {
if (tag.count() > 0) {
dst->exifData()[tag.key()] = tag;
}
}
} else {
dst->setExifData(exif_data_);
dst->setIptcData(iptc_data_);
dst->setXmpData(xmp_data_);
}
dst->exifData()["Exif.Image.Software"] = "RawTherapee " RTVERSION;
import_exif_pairs(dst->exifData());
import_iptc_pairs(dst->iptcData());
bool xmp_tried = false;
bool iptc_tried = false;
for (int i = 0; i < 3; ++i) {
try {
dst->writeMetadata();
return;
} catch (Exiv2::Error &exc) {
if (exc.code() == Exiv2::ErrorCode::kerTooLargeJpegSegment) {
std::string msg = exc.what();
if (msg.find("XMP") != std::string::npos &&
!dst->xmpData().empty()) {
dst->xmpData().clear();
if (!xmp_tried && merge_xmp_) {
do_merge_xmp(dst.get(), preserve_all_tags);
xmp_tried = true;
}
} else if (msg.find("IPTC") != std::string::npos &&
!dst->iptcData().empty()) {
dst->iptcData().clear();
if (!iptc_tried) {
import_iptc_pairs(dst->iptcData());
iptc_tried = true;
}
}
} else {
throw exc;
}
}
}
}
void Exiv2Metadata::remove_unwanted(Exiv2::ExifData &dst) const
{
Exiv2::ExifThumb thumb(dst);
thumb.erase();
static const std::set<std::string> badtags = {
"Exif.Image.Orientation",
"Exif.Image2.JPEGInterchangeFormat",
"Exif.Image2.JPEGInterchangeFormatLength",
"Exif.Image.NewSubfileType",
"Exif.Image.SubfileType",
"Exif.Image.ImageWidth",
"Exif.Image.ImageLength",
"Exif.Image.BitsPerSample",
"Exif.Image.Compression",
"Exif.Image.PhotometricInterpretation",
"Exif.Image.Thresholding",
"Exif.Image.CellWidth",
"Exif.Image.CellLength",
"Exif.Image.FillOrder",
"Exif.Image.StripOffsets",
"Exif.Image.Orientation",
"Exif.Image.SamplesPerPixel",
"Exif.Image.RowsPerStrip",
"Exif.Image.StripByteCounts",
"Exif.Image.XResolution",
"Exif.Image.YResolution",
"Exif.Image.PlanarConfiguration",
"Exif.Image.GrayResponseUnit",
"Exif.Image.GrayResponseCurve",
"Exif.Image.T4Options",
"Exif.Image.T6Options",
"Exif.Image.ResolutionUnit",
"Exif.Image.PageNumber",
"Exif.Image.Predictor",
"Exif.Image.TileWidth",
"Exif.Image.TileLength",
"Exif.Image.TileOffsets",
"Exif.Image.TileByteCounts",
"Exif.Image.SubIFDs",
"Exif.Image.ExtraSamples",
"Exif.Image.SampleFormat",
"Exif.Image.SMinSampleValue",
"Exif.Image.SMaxSampleValue",
"Exif.Image.Indexed",
"Exif.Image.JPEGTables",
"Exif.Image.OPIProxy",
"Exif.Image.JPEGProc",
"Exif.Image.JPEGInterchangeFormat",
"Exif.Image.JPEGInterchangeFormatLength",
"Exif.Image.JPEGRestartInterval",
"Exif.Image.JPEGLosslessPredictors",
"Exif.Image.JPEGPointTransforms",
"Exif.Image.JPEGQTables",
"Exif.Image.JPEGDCTables",
"Exif.Image.JPEGACTables",
"Exif.Image.TIFFEPStandardID",
"Exif.Image.DNGVersion",
"Exif.Image.DNGBackwardVersion",
"Exif.Image.DNGPrivateData",
"Exif.Image.OriginalRawFileData",
"Exif.Image.SubTileBlockSize",
"Exif.Image.RowInterleaveFactor",
"Exif.Photo.ComponentsConfiguration",
"Exif.Photo.CompressedBitsPerPixel"
};
static const std::vector<std::string> badpatterns = {
"Exif.SubImage"
};
if (exif_keys_ && !src_.empty()) {
try {
FramesData fd(src_);
fd.fillBasicTags(dst);
} catch (std::exception &exc) {
std::cout << "Error reading metadata from " << src_
<< std::endl;
}
}
for (auto it = dst.begin(); it != dst.end(); ) {
int relevant = exif_keys_ ? (exif_keys_->find(it->key()) != exif_keys_->end() ? 1 : 0) : -1;
if (badtags.find(it->key()) != badtags.end() && relevant != 1) {
it = dst.erase(it);
} else if (relevant == 0) {
it = dst.erase(it);
} else {
bool found = false;
for (auto &p : badpatterns) {
if (it->key().find(p) == 0) {
it = dst.erase(it);
found = true;
break;
}
}
if (!found) {
++it;
}
}
}
}
void Exiv2Metadata::import_exif_pairs(Exiv2::ExifData &out) const
{
for (auto &p : *exif_) {
try {
out[p.first] = p.second;
} catch (std::exception &exc) {
if (settings->verbose) {
std::cout << "Error setting " << p.first << " to " << p.second
<< ": " << exc.what() << std::endl;
}
}
}
}
void Exiv2Metadata::import_iptc_pairs(Exiv2::IptcData &out) const
{
for (auto &p : *iptc_) {
try {
auto &v = p.second;
if (v.size() >= 1) {
clear_metadata_key(out, Exiv2::IptcKey(p.first));
Exiv2::Iptcdatum d(Exiv2::IptcKey(p.first));
d.setValue(v[0]);
out[p.first] = d;
for (size_t j = 1; j < v.size(); ++j) {
d.setValue(v[j]);
out.add(d);
}
}
} catch (std::exception &exc) {
if (settings->verbose) {
std::cout << "Error setting " << p.first
<< ": " << exc.what() << std::endl;
}
}
}
}
void Exiv2Metadata::saveToXmp(const Glib::ustring &path) const
{
Exiv2::XmpData xmp;
Exiv2::copyExifToXmp(exifData(), xmp);
Exiv2::copyIptcToXmp(iptcData(), xmp);
for (auto &datum : xmpData()) {
xmp[datum.key()] = datum;
}
Exiv2::ExifData exif;
Exiv2::IptcData iptc;
import_exif_pairs(exif);
import_iptc_pairs(iptc);
Exiv2::copyExifToXmp(exif, xmp);
Exiv2::copyIptcToXmp(iptc, xmp);
std::string data;
bool err = false;
if (Exiv2::XmpParser::encode(data, xmp, Exiv2::XmpParser::omitPacketWrapper|Exiv2::XmpParser::useCompactFormat) != 0) {
err = true;
} else {
FILE *out = g_fopen(path.c_str(), "wb");
if (!out || fputs(data.c_str(), out) == EOF) {
err = true;
}
if (out) {
fclose(out);
}
}
if (err) {
throw Error("error saving XMP sidecar " + path);
}
}
void Exiv2Metadata::setExifKeys(const std::vector<std::string> *keys)
{
exif_keys_.reset();
if (keys) {
exif_keys_ = std::make_shared<std::unordered_set<std::string>>();
exif_keys_->insert(keys->begin(), keys->end());
}
}
void Exiv2Metadata::getDimensions(int &w, int &h) const
{
if (image_) {
if (dynamic_cast<const Exiv2::XmpSidecar *>(image_.get())) {
auto &exif = image_->exifData();
auto itw = exif.findKey(Exiv2::ExifKey("Exif.Image.ImageWidth"));
auto ith = exif.findKey(Exiv2::ExifKey("Exif.Image.ImageLength"));
if (itw != exif.end() && ith != exif.end()) {
w = to_long(itw);
h = to_long(ith);
} else {
w = h = -1;
}
} else {
w = image_->pixelWidth();
h = image_->pixelHeight();
}
} else {
w = h = -1;
}
}
Glib::ustring Exiv2Metadata::xmpSidecarPath(const Glib::ustring &path)
{
Glib::ustring fn = path;
if (settings->xmp_sidecar_style == Settings::XmpSidecarStyle::STD) {
fn = removeExtension(fn);
}
return fn + ".xmp";
}
Exiv2::XmpData Exiv2Metadata::getXmpSidecar(const Glib::ustring &path)
{
Exiv2::XmpData ret;
auto fname = xmpSidecarPath(path);
if (Glib::file_test(fname, Glib::FILE_TEST_EXISTS)) {
auto image = open_exiv2(fname, false);
ret = image->xmpData();
}
return ret;
}
void Exiv2Metadata::init()
{
cache_.reset(new ImageCache(IMAGE_CACHE_SIZE));
Exiv2::XmpParser::initialize();
#ifdef EXV_ENABLE_BMFF
Exiv2::enableBMFF(true);
#endif
}
void Exiv2Metadata::cleanup()
{
Exiv2::XmpParser::terminate();
}
Exiv2::ExifData Exiv2Metadata::getOutputExifData() const
{
Exiv2::ExifData exif = exifData();
try {
auto xmp = getXmpSidecar(src_);
Exiv2::moveXmpToExif(xmp, exif);
} catch (std::exception &exc) {
if (settings->verbose) {
std::cerr << "Error loading metadata from XMP sidecar: "
<< exc.what() << std::endl;
}
}
remove_unwanted(exif);
import_exif_pairs(exif);
for (auto it = exif.begin(); it != exif.end(); ) {
if (it->count() > 0) {
++it;
} else {
it = exif.erase(it);
}
}
return exif;
}
} // namespace rtengine