EXR: store of "adobe_rsrc" attribute with XMP/EXIF/Resolution data

This commit is contained in:
Mirco Miranda
2026-09-10 16:44:54 +02:00
parent 6a3c7f2c86
commit 5e5072d590
6 changed files with 336 additions and 40 deletions
+6 -3
View File
@@ -384,9 +384,12 @@ The following defines can be defined in cmake to modify the behavior of the
plugin:
- `EXR_CONVERT_TO_SRGB`: the linear data is converted to sRGB on read to
accommodate programs that do not support color profiles.
- `EXR_DISABLE_XMP_ATTRIBUTE`: disables the stores XMP values in a non-standard
attribute named "xmp". Note that Gimp reads the "xmp" attribute and Darktable
writes it as well.
- `EXR_DISABLE_XMP_ATTRIBUTE`: disables the storage of XMP values in a
non-standard attribute named "xmp". Note that Gimp reads the "xmp" attribute
and Darktable writes it as well.
- `EXR_DISABLE_ADOBE_ATTRIBUTE`: disables the storage of non-standard
attribute "adobe_rsrc". Note that Photoshop uses "adobe_rsrc" attribute to
read and write XMP/EXIF/IPTC data.
The plugin can set the following additional metadata:
- `EXRLayerName`: A string containing the name of the EXR layer used to decode
+121 -24
View File
@@ -21,8 +21,8 @@
*/
//#define EXR_CONVERT_TO_SRGB // default: commented -> you should define it in your cmake file
/* *** EXR_STORE_XMP_ATTRIBUTE ***
* If defined, disables the stores XMP values in a non-standard attribute named "xmp".
/* *** EXR_DISABLE_XMP_ATTRIBUTE ***
* If defined, disables the storage of XMP values in the non-standard attribute named "xmp"
* The QImage metadata used is "XML:com.adobe.xmp".
* NOTE: The use of non-standard attributes is possible but discouraged by the specification. However,
* metadata is essential for good image management and programs like darktable also set this
@@ -30,6 +30,15 @@
*/
//#define EXR_DISABLE_XMP_ATTRIBUTE // default: commented -> you should define it in your cmake file
/* *** EXR_DISABLE_ADOBE_ATTRIBUTE ***
* If defined, disables the storage of non-standard attribute "adobe_rsrc".
* NOTE: The use of non-standard attributes is possible but discouraged by the specification. However,
* metadata is essential for good image management and programs like Photoshop also set this
* attribute. Photoshop uses "adobe_rsrc" attribute to read/write XMP/EXIF/IPTC data.
*/
//#define EXR_DISABLE_ADOBE_ATTRIBUTE // default: commented -> you should define it in your cmake file
/* *** EXR_MAX_IMAGE_WIDTH and EXR_MAX_IMAGE_HEIGHT ***
* The maximum size in pixel allowed by the plugin.
*/
@@ -92,6 +101,12 @@ Q_LOGGING_CATEGORY(LOG_EXRPLUGIN, "kf.imageformats.plugins.exr", QtDebugMsg)
Q_LOGGING_CATEGORY(LOG_EXRPLUGIN, "kf.imageformats.plugins.exr", QtWarningMsg)
#endif
#ifndef EXR_DISABLE_ADOBE_ATTRIBUTE
#if QT_VERSION_CHECK(OPENEXR_VERSION_MAJOR, OPENEXR_VERSION_MINOR, OPENEXR_VERSION_PATCH) < QT_VERSION_CHECK(3, 3, 0)
#define EXR_DISABLE_ADOBE_ATTRIBUTE
#endif
#endif
class K_IStream : public Imf::IStream
{
public:
@@ -213,6 +228,7 @@ EXRHandler::EXRHandler()
, m_imageCount(0)
, m_startPos(-1)
, m_subType(EXR_SUBFORMAT_RGB)
, m_transformation(QImageIOHandler::TransformationNone)
{
// Set the number of threads to use (0 is allowed)
Imf::setGlobalThreadCount(QThread::idealThreadCount() / 2);
@@ -292,6 +308,27 @@ static void printAttributes(const Imf::Header &h)
}
#endif
static PSDImageResourceSection readPSDImageResourceSection(const Imf::Header &header)
{
if (auto adobe = header.findTypedAttribute<Imf::OpaqueAttribute>("adobe_rsrc")) {
auto &&data = adobe->data();
auto ba = QByteArray(data, data.size());
ba.prepend((data.size()) & 0xFF);
ba.prepend((data.size() >> 8) & 0xFF);
ba.prepend((data.size() >> 16) & 0xFF);
ba.prepend((data.size() >> 24) & 0xFF);
QDataStream ds(ba);
ds.setByteOrder(QDataStream::BigEndian);
auto ok = false;
auto irs = readImageResourceSection(ds, &ok);
if (ok) {
return irs;
}
}
return{};
}
/*!
* \brief readMetadata
* Reads EXR attributes from the \a header and set its as metadata in the \a image.
@@ -389,24 +426,12 @@ static void readMetadata(const Imf::Header &header, QImage &image)
}
// Photoshop image resource section
if (auto adobe = header.findTypedAttribute<Imf::OpaqueAttribute>("adobe_rsrc")) {
auto &&data = adobe->data();
auto ba = QByteArray(data, data.size());
ba.prepend((data.size()) & 0xFF);
ba.prepend((data.size() >> 8) & 0xFF);
ba.prepend((data.size() >> 16) & 0xFF);
ba.prepend((data.size() >> 24) & 0xFF);
QDataStream ds(ba);
ds.setByteOrder(QDataStream::BigEndian);
auto ok = false;
auto irs = readImageResourceSection(ds, &ok);
if (ok) {
auto irs = readPSDImageResourceSection(header);
if (irs.contains(IRI_EXIFDATA1)) {
auto exif = MicroExif::fromByteArray(irs.value(IRI_EXIFDATA1).data);
exif.updateImageMetadata(image);
exif.updateImageResolution(image);
}
if (irs.contains(IRI_XMPMETADATA)) {
auto irb = irs.value(IRI_XMPMETADATA);
auto xmp = QString::fromUtf8(irb.data);
@@ -414,8 +439,6 @@ static void readMetadata(const Imf::Header &header, QImage &image)
image.setText(QStringLiteral(META_KEY_XMP_ADOBE), xmp);
}
}
}
}
/*!
* \brief readColorSpace
@@ -580,8 +603,11 @@ bool makePreview(const QImage &image, Imf::Array2D<Imf::PreviewRgba> &pixels)
* \brief setMetadata
* Reades the metadata from \a image and set its as attributes in the \a header.
*/
static void setMetadata(const QImage &image, Imf::Header &header)
static void setMetadata(const QImage &image, Imf::Header &header, const QImageIOHandler::Transformation &transformation)
{
PSDImageResourceSection irs;
QString xmpData;
auto dateTime = QDateTime::currentDateTime();
for (auto &&key : image.textKeys()) {
auto text = image.text(key);
@@ -612,11 +638,9 @@ static void setMetadata(const QImage &image, Imf::Header &header)
}
}
#ifndef EXR_DISABLE_XMP_ATTRIBUTE // warning: Non-standard attribute!
if (!key.compare(QStringLiteral(META_KEY_XMP_ADOBE), Qt::CaseInsensitive)) {
header.insert("xmp", Imf::StringAttribute(text.toStdString()));
xmpData = text;
}
#endif
if (!key.compare(QStringLiteral(META_KEY_MANUFACTURER), Qt::CaseInsensitive)) {
header.insert("cameraMake", Imf::StringAttribute(text.toStdString()));
@@ -676,6 +700,41 @@ static void setMetadata(const QImage &image, Imf::Header &header)
header.insert("pixelAspectRatio", Imf::FloatAttribute(float(image.dotsPerMeterY()) / float(image.dotsPerMeterX())));
}
#ifndef EXR_DISABLE_XMP_ATTRIBUTE
if (!xmpData.isEmpty()) {
header.insert("xmp", Imf::StringAttribute(xmpData.toStdString()));
}
#endif
#ifndef EXR_DISABLE_ADOBE_ATTRIBUTE
auto resInfo = PSDResolutionInfoBlock::fromImage(image);
if (resInfo.isValid()) {
PSDImageResourceBlock irb;
irb.data = resInfo.toByteArray();
if (!irb.data.isEmpty())
irs.insert(IRI_RESOLUTIONINFO, irb);
}
auto exif = MicroExif::fromImage(image);
if (!exif.isEmpty()) {
exif.setTransformation(transformation);
PSDImageResourceBlock irb;
irb.data = exif.toByteArray(QDataStream::BigEndian);
irs.insert(IRI_EXIFDATA1, irb);
}
if (!xmpData.isEmpty()) {
PSDImageResourceBlock irb;
irb.data = xmpData.toUtf8();
irs.insert(IRI_XMPMETADATA, irb);
}
if (!irs.isEmpty()) {
bool ok = false;
auto ba = irs.toByteArray(&ok);
if (ok) {
header.insert("adobe_rsrc", Imf::OpaqueAttribute("adobe_rsrc", ba.size(), ba.data()));
}
}
#endif
// set default chroma (default constructor ITU-R BT.709-3 -> sRGB)
// The image is converted to Linear sRGB so, the chroma is the default EXR value.
// If a file doesnt have a chromaticities attribute, display software should assume that the
@@ -718,7 +777,7 @@ bool EXRHandler::write(const QImage &image)
}
// set metadata (EXR attributes)
setMetadata(image, header);
setMetadata(image, header, m_transformation);
// write the EXR
K_OStream ostr(device());
@@ -791,6 +850,15 @@ void EXRHandler::setOption(ImageOption option, const QVariant &value)
m_subType = EXR_SUBFORMAT_RGB;
}
}
#ifndef EXR_DISABLE_ADOBE_ATTRIBUTE
if (option == QImageIOHandler::ImageTransformation) {
auto ok = false;
auto t = value.toInt(&ok);
if (ok) {
m_transformation = QImageIOHandler::Transformation(t);
}
}
#endif
}
bool EXRHandler::supportsOption(ImageOption option) const
@@ -815,6 +883,11 @@ bool EXRHandler::supportsOption(ImageOption option) const
if (option == QImageIOHandler::SupportedSubTypes) {
return true;
}
#ifndef EXR_DISABLE_ADOBE_ATTRIBUTE
if (option == QImageIOHandler::ImageTransformation) {
return true;
}
#endif
return false;
}
@@ -870,13 +943,37 @@ QVariant EXRHandler::option(ImageOption option) const
}
if (option == QImageIOHandler::SupportedSubTypes) {
return QVariant::fromValue(QList<QByteArray>() << EXR_SUBFORMAT_RGB << EXR_SUBFORMAT_YC);
v = QVariant::fromValue(QList<QByteArray>() << EXR_SUBFORMAT_RGB << EXR_SUBFORMAT_YC);
}
if (option == QImageIOHandler::SubType) {
return QVariant::fromValue(m_subType);
v = QVariant::fromValue(m_subType);
}
#ifndef EXR_DISABLE_ADOBE_ATTRIBUTE
if (option == QImageIOHandler::ImageTransformation) {
if (auto d = device()) {
// transactions works on both random and sequential devices
d->startTransaction();
if (m_startPos > -1) {
d->seek(m_startPos);
}
try {
K_IStream istr(d);
Imf::RgbaInputFile file(istr);
auto irs = readPSDImageResourceSection(file.header());
if (irs.contains(IRI_EXIFDATA1)) {
auto exif = MicroExif::fromByteArray(irs.value(IRI_EXIFDATA1).data);
v = int(exif.transformation());
}
} catch (const std::exception &) {
// broken file or unsupported version
}
d->rollbackTransaction();
}
}
#endif
return v;
}
+6
View File
@@ -126,6 +126,12 @@ private:
* - YC: converts RGB data into luminance (Y) and 2x2 subsampled chroma (RY, BY), cutting raw color data by ~50% for significantly smaller file sizes.
*/
QByteArray m_subType;
/*!
* \brief m_transformation
* The Qt image transformation.
*/
QImageIOHandler::Transformation m_transformation;
};
class EXRPlugin : public QImageIOPlugin
+4 -2
View File
@@ -84,6 +84,8 @@
#define EXIF_TAG_SIZEOF(dataType) (quint16(dataType) & 0x3F)
#define EXIF_TAG_DATATYPE(dataType) (quint16(dataType) >> 6)
#define GPS_GPSVERSION_VALUE QList<quint8>{0x02, 0x04, 0x00, 0x00}
enum class ExifTagType : quint16 {
// Base data types
Byte = EXIF_TAG_VALUE(1, 1),
@@ -1325,7 +1327,7 @@ QByteArray MicroExif::gpsIfdByteArray(const QDataStream::ByteOrder &byteOrder, c
QDataStream ds(&ba, QIODevice::WriteOnly);
ds.setByteOrder(byteOrder);
auto gpsTags = m_gpsTags;
gpsTags.insert(GPS_GPSVERSION, QByteArray("2400"));
gpsTags.insert(GPS_GPSVERSION, QVariant::fromValue(GPS_GPSVERSION_VALUE));
TagPos positions;
if (!writeIfd(ds, version, gpsTags, positions, 0, staticGpsTagTypes))
return {};
@@ -1723,7 +1725,7 @@ void MicroExif::updateTags(Tags &tiffTags, Tags &exifTags, Tags &gpsTags, const
tiffTags.remove(EXIF_GPSIFD);
} else {
tiffTags.insert(EXIF_GPSIFD, quint32());
gpsTags.insert(GPS_GPSVERSION, QByteArray("2400"));
gpsTags.insert(GPS_GPSVERSION, QVariant::fromValue(GPS_GPSVERSION_VALUE));
}
}
+125
View File
@@ -44,6 +44,23 @@ QString readPascalString(QDataStream &s, qint32 alignBytes, qint32 *size)
return str;
}
bool writePascalString(const QString &str, QDataStream &s, qint32 alignBytes)
{
auto data = str.toLatin1();
if(data.size() > 250) {
data = data.left(250);
}
auto sz = data.size();
s << quint8(sz);
if (sz && s.writeRawData(data.data(), sz) != sz) {
return false;
}
for(alignBytes = std::max(1, alignBytes), sz += 1; sz % alignBytes; ++sz) {
s << char();
}
return s.status() == QDataStream::Ok;
}
PSDImageResourceSection readImageResourceSection(QDataStream &s, bool *ok)
{
PSDImageResourceSection irs;
@@ -138,3 +155,111 @@ PSDImageResourceSection readImageResourceSection(QDataStream &s, bool *ok)
return irs;
}
bool writeImageResourceSection(const PSDImageResourceSection &irs, QDataStream &s)
{
bool ok = false;
auto ba = irs.toByteArray(&ok);
if (!ok) {
return false;
}
s << quint32(ba.size());
if (s.writeRawData(ba.data(), ba.size()) != ba.size()) {
return false;
}
return (s.status() == QDataStream::Ok);
}
QByteArray PSDImageResourceSection::toByteArray(bool *ok) const
{
QByteArray ba;
bool tmp = true;
if (ok == nullptr)
ok = &tmp;
*ok = true;
if (!isEmpty()) {
QDataStream s(&ba, QDataStream::WriteOnly);
s.setByteOrder(QDataStream::BigEndian);
auto ids = keys();
for(auto &&id : ids) {
auto irb = value(id);
if (irb.data.isEmpty()) {
continue;
}
// signature
s << quint32(S_8BIM);
// resource id
s << quint16(id);
// resource name (2 bytes aligned)
if (!writePascalString(irb.name, s, 2)) {
*ok = false;
return{};
}
// data (2 bytes aligned)
auto sz = irb.data.size();
s << quint32(sz);
if (s.writeRawData(irb.data.data(), sz) != sz) {
*ok = false;
return{};
}
if (sz % 2) {
s << char();
}
if (s.status() != QDataStream::Ok) {
*ok = false;
return{};
}
}
}
Q_ASSERT(ba.size() % 2 == 0);
return ba;
}
PSDResolutionInfoBlock::PSDResolutionInfoBlock(qint32 ppmX, qint32 ppmY)
: m_ppmX(ppmX)
, m_ppmY(ppmY)
{
}
bool PSDResolutionInfoBlock::isValid() const
{
return m_ppmX > 0 && m_ppmY > 0;
}
PSDResolutionInfoBlock PSDResolutionInfoBlock::fromImage(const QImage &image)
{
return PSDResolutionInfoBlock(image.dotsPerMeterX(), image.dotsPerMeterY());
}
QByteArray PSDResolutionInfoBlock::toByteArray() const
{
QByteArray ba;
QDataStream ds(&ba, QIODevice::WriteOnly);
ds.setByteOrder(QDataStream::BigEndian);
auto hres = qRoundOrZero(dppm2dpi(m_ppmX) * 65536);
ds << hres;
ds << quint16(1); // dpi
ds << quint16(2); // cm (display)
auto vres = qRoundOrZero(dppm2dpi(m_ppmY) * 65536);
ds << vres;
ds << quint16(1);
ds << quint16(2);
if (hres == 0 || vres == 0) {
return{};
}
return ba;
}
+64 -1
View File
@@ -9,6 +9,7 @@
#include <QDataStream>
#include <QHash>
#include <QImage>
#include <QString>
enum Signature : quint32 {
@@ -55,7 +56,50 @@ struct PSDImageResourceBlock {
QByteArray data;
};
using PSDImageResourceSection = QHash<ImageResourceId, PSDImageResourceBlock>;
class PSDImageResourceSection : public QHash<ImageResourceId, PSDImageResourceBlock>
{
public:
PSDImageResourceSection() : QHash<ImageResourceId, PSDImageResourceBlock>() {}
PSDImageResourceSection(const PSDImageResourceSection& other) = default;
PSDImageResourceSection& operator =(const PSDImageResourceSection& other) = default;
/*!
* \brief toByteArray
* \param ok Pointer to the operation result variable.
* \return The binary IRB to be written into the PSD file.
*/
QByteArray toByteArray(bool *ok = nullptr) const;
};
class PSDResolutionInfoBlock
{
public:
PSDResolutionInfoBlock(qint32 ppmX, qint32 ppmY);
PSDResolutionInfoBlock(const PSDResolutionInfoBlock& other) = default;
PSDResolutionInfoBlock& operator =(const PSDResolutionInfoBlock& other) = default;
/*!
* \brief isValid
* \return true if both m_ppmX and m_ppmY are grater than 0. Otherwise false.
*/
bool isValid() const;
/*!
* \brief fromImage
* Initialize the class using the image resolution.
*/
static PSDResolutionInfoBlock fromImage(const QImage& image);
/*!
* \brief toByteArray
* \return The binary data ready for the IMage Resource Section.
*/
QByteArray toByteArray() const;
private:
qint32 m_ppmX;
qint32 m_ppmY;
};
/*!
@@ -68,6 +112,16 @@ using PSDImageResourceSection = QHash<ImageResourceId, PSDImageResourceBlock>;
*/
QString readPascalString(QDataStream &s, qint32 alignBytes = 1, qint32 *size = nullptr);
/*!
* \brief writePascalString
* Writes the Pascal string as defined in the PSD specification.
* \param str The string to be written.
* \param s The stream.
* \param alignBytes Alignment of the string.
* \return True on success, otherwise false.
*/
bool writePascalString(const QString& str, QDataStream &s, qint32 alignBytes = 1);
/*!
* \brief readImageResourceSection
* Reads the image resource section.
@@ -77,4 +131,13 @@ QString readPascalString(QDataStream &s, qint32 alignBytes = 1, qint32 *size = n
*/
PSDImageResourceSection readImageResourceSection(QDataStream &s, bool *ok = nullptr);
/*!
* \brief writeImageResourceSection
* Writes the image resource section.
* \param irs The image resource section raw data.
* \param s The stream.
* \return True on success, otherwise false.
*/
bool writeImageResourceSection(const PSDImageResourceSection& irs, QDataStream &s);
#endif // PHOTOSHOP_P_H