EXR: fix incorrect loading of EXR files saved by Photoshop 2026

This commit is contained in:
Mirco Miranda
2026-04-14 17:46:40 +02:00
parent 742b5097f6
commit 276338199a
7 changed files with 83 additions and 25 deletions

View File

@@ -324,6 +324,10 @@ plugin:
attribute named "xmp". Note that Gimp reads the "xmp" attribute and Darktable attribute named "xmp". Note that Gimp reads the "xmp" attribute and Darktable
writes it as well. writes it as well.
The plugin can set the following additional metadata:
- `EXRLayerName`: A string containing the name of the EXR layer used to decode
the image.
### The EPS plugin ### The EPS plugin
The plugin uses `Ghostscript` to convert the raster image. When reading it The plugin uses `Ghostscript` to convert the raster image. When reading it

Binary file not shown.

View File

@@ -0,0 +1,15 @@
[
{
"fileName" : "ps2026_testcard_rgb.png",
"colorSpace" : {
"description" : "sRGB build-in (Profilo RGB lineare)",
"primaries" : "SRgb",
"transferFunction" : "Linear",
"gamma" : 1
},
"resolution" : {
"dotsPerMeterX" : 3937,
"dotsPerMeterY" : 3937
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -2,7 +2,7 @@
{ {
"fileName" : "rgb-gimp.png", "fileName" : "rgb-gimp.png",
"colorSpace" : { "colorSpace" : {
"description" : "", "description" : "Embedded RGB (linear)",
"primaries" : "Custom", "primaries" : "Custom",
"transferFunction" : "Linear", "transferFunction" : "Linear",
"gamma" : 1 "gamma" : 1

View File

@@ -58,6 +58,7 @@
#include <ImathBox.h> #include <ImathBox.h>
#include <ImfArray.h> #include <ImfArray.h>
#include <ImfBoxAttribute.h> #include <ImfBoxAttribute.h>
#include <ImfOpaqueAttribute.h>
#include <ImfChannelListAttribute.h> #include <ImfChannelListAttribute.h>
#include <ImfCompressionAttribute.h> #include <ImfCompressionAttribute.h>
#include <ImfConvert.h> #include <ImfConvert.h>
@@ -227,8 +228,9 @@ static QImage::Format imageFormat(const Imf::RgbaInputFile &file)
/*! /*!
* \brief viewList * \brief viewList
* \param header * \param header The image header.
* \return The list of available views. * \return The list of available views.
* \note This plugin does not support compositing layers which are returned as single images.
*/ */
static QStringList viewList(const Imf::Header &h) static QStringList viewList(const Imf::Header &h)
{ {
@@ -238,14 +240,44 @@ static QStringList viewList(const Imf::Header &h)
l << QString::fromStdString(v); l << QString::fromStdString(v);
} }
} }
if (l.isEmpty()) {
// Recent versions of Photoshop save images by setting the layer.
// Channels are named Layer 1.A, Layer 1.B, etc., so I have to set
// the layer or the images will appear black.
auto channels = h.channels();
for (auto i = channels.begin(); i != channels.end(); ++i) {
auto name = QString::fromLatin1(i.name(), -1);
auto idx = name.indexOf(QChar(u'.'));
if (idx > -1)
l << name.left(idx);
}
l.removeDuplicates();
}
return l; return l;
} }
static QString setLayerName(Imf::RgbaInputFile &file, qint32 imageNumber = -1)
{
// set the image to load
QString layerName;
auto &&header = file.header();
if (imageNumber > -1) {
auto views = viewList(header);
if (imageNumber < views.count())
layerName = views.at(imageNumber);
}
// set the layer name
if (!layerName.isEmpty()) {
file.setLayerName(layerName.toStdString());
}
return layerName;
}
#ifdef QT_DEBUG #ifdef QT_DEBUG
static void printAttributes(const Imf::Header &h) static void printAttributes(const Imf::Header &h)
{ {
for (auto i = h.begin(); i != h.end(); ++i) { for (auto i = h.begin(); i != h.end(); ++i) {
qCDebug(LOG_EXRPLUGIN) << i.name(); qCDebug(LOG_EXRPLUGIN) << i.name() << i.attribute().typeName();
} }
} }
#endif #endif
@@ -340,6 +372,15 @@ static void readColorSpace(const Imf::Header &header, QImage &image)
{ {
// final color operations // final color operations
QColorSpace cs; QColorSpace cs;
// Photoshop 2026 allow to save the ICC profile as "iccProfile" attribute
if (auto iccProfile = header.findTypedAttribute<Imf::OpaqueAttribute>("iccProfile")) {
auto &&v = iccProfile->data();
cs = QColorSpace::fromIccProfile(QByteArray::fromRawData(v, v.size()));
}
if (!cs.isValid()) {
// Creating the ICC profile from Chromaticities
if (auto chroma = header.findTypedAttribute<Imf::ChromaticitiesAttribute>("chromaticities")) { if (auto chroma = header.findTypedAttribute<Imf::ChromaticitiesAttribute>("chromaticities")) {
auto &&v = chroma->value(); auto &&v = chroma->value();
cs = QColorSpace(QPointF(v.white.x, v.white.y), cs = QColorSpace(QPointF(v.white.x, v.white.y),
@@ -347,8 +388,13 @@ static void readColorSpace(const Imf::Header &header, QImage &image)
QPointF(v.green.x, v.green.y), QPointF(v.green.x, v.green.y),
QPointF(v.blue.x, v.blue.y), QPointF(v.blue.x, v.blue.y),
QColorSpace::TransferFunction::Linear); QColorSpace::TransferFunction::Linear);
if (cs.isValid())
cs.setDescription(QStringLiteral("Embedded RGB (linear)"));
} }
}
if (!cs.isValid()) { if (!cs.isValid()) {
// Use a linear profile
cs = QColorSpace(QColorSpace::SRgbLinear); cs = QColorSpace(QColorSpace::SRgbLinear);
} }
image.setColorSpace(cs); image.setColorSpace(cs);
@@ -377,12 +423,7 @@ bool EXRHandler::read(QImage *outImage)
auto &&header = file.header(); auto &&header = file.header();
// set the image to load // set the image to load
if (m_imageNumber > -1) { auto layerName = setLayerName(file, m_imageNumber);
auto views = viewList(header);
if (m_imageNumber < views.count()) {
file.setLayerName(views.at(m_imageNumber).toStdString());
}
}
// get image info // get image info
Imath::Box2i dw = file.dataWindow(); Imath::Box2i dw = file.dataWindow();
@@ -401,6 +442,9 @@ bool EXRHandler::read(QImage *outImage)
qCWarning(LOG_EXRPLUGIN) << "Failed to allocate image, invalid size?" << QSize(width, height); qCWarning(LOG_EXRPLUGIN) << "Failed to allocate image, invalid size?" << QSize(width, height);
return false; return false;
} }
if (!layerName.isEmpty()) {
image.setText(QStringLiteral("EXRLayerName"), layerName);
}
Imf::Array2D<Imf::Rgba> pixels; Imf::Array2D<Imf::Rgba> pixels;
pixels.resizeErase(EXR_LINES_PER_BLOCK, width); pixels.resizeErase(EXR_LINES_PER_BLOCK, width);
@@ -688,12 +732,7 @@ QVariant EXRHandler::option(ImageOption option) const
try { try {
K_IStream istr(d); K_IStream istr(d);
Imf::RgbaInputFile file(istr); Imf::RgbaInputFile file(istr);
if (m_imageNumber > -1) { // set the image to read setLayerName(file, m_imageNumber);
auto views = viewList(file.header());
if (m_imageNumber < views.count()) {
file.setLayerName(views.at(m_imageNumber).toStdString());
}
}
Imath::Box2i dw = file.dataWindow(); Imath::Box2i dw = file.dataWindow();
v = QVariant(QSize(dw.max.x - dw.min.x + 1, dw.max.y - dw.min.y + 1)); v = QVariant(QSize(dw.max.x - dw.min.x + 1, dw.max.y - dw.min.y + 1));
} catch (const std::exception &) { } catch (const std::exception &) {
@@ -713,6 +752,7 @@ QVariant EXRHandler::option(ImageOption option) const
try { try {
K_IStream istr(d); K_IStream istr(d);
Imf::RgbaInputFile file(istr); Imf::RgbaInputFile file(istr);
setLayerName(file, m_imageNumber);
v = QVariant::fromValue(imageFormat(file)); v = QVariant::fromValue(imageFormat(file));
} catch (const std::exception &) { } catch (const std::exception &) {
// broken file or unsupported version // broken file or unsupported version
@@ -787,12 +827,9 @@ bool EXRHandler::canRead(QIODevice *device)
return false; return false;
} }
#if OPENEXR_VERSION_MAJOR == 3 && OPENEXR_VERSION_MINOR > 2
// openexpr >= 3.3 uses seek and tell extensively
if (device->isSequential()) { if (device->isSequential()) {
return false; return false;
} }
#endif
const QByteArray head = device->peek(4); const QByteArray head = device->peek(4);

View File

@@ -86,6 +86,8 @@ private:
* - 7: lossy 4-by-4 pixel block compression, fields are compressed more * - 7: lossy 4-by-4 pixel block compression, fields are compressed more
* - 8: lossy DCT based compression, in blocks of 32 scanlines. More efficient for partial buffer access. * - 8: lossy DCT based compression, in blocks of 32 scanlines. More efficient for partial buffer access.
* - 9: lossy DCT based compression, in blocks of 256 scanlines. More efficient space wise and faster to decode full frames than DWAA_COMPRESSION. * - 9: lossy DCT based compression, in blocks of 256 scanlines. More efficient space wise and faster to decode full frames than DWAA_COMPRESSION.
* - 10: High-Throughput JPEG2000 (HTJ2K), 256 scanlines (requires OpenEXR 3.4+).
* - 11: High-Throughput JPEG2000 (HTJ2K), 32 scanlines (requires OpenEXR 3.4+).
*/ */
qint32 m_compressionRatio; qint32 m_compressionRatio;