mirror of
https://invent.kde.org/frameworks/kimageformats.git
synced 2026-05-25 13:08:28 -04:00
EXR: fix incorrect loading of EXR files saved by Photoshop 2026
This commit is contained in:
@@ -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
|
||||||
|
|||||||
BIN
autotests/read/exr/ps2026_testcard_rgb.exr
Normal file
BIN
autotests/read/exr/ps2026_testcard_rgb.exr
Normal file
Binary file not shown.
15
autotests/read/exr/ps2026_testcard_rgb.exr.json
Normal file
15
autotests/read/exr/ps2026_testcard_rgb.exr.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
BIN
autotests/read/exr/ps2026_testcard_rgb.png
Normal file
BIN
autotests/read/exr/ps2026_testcard_rgb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user