diff --git a/README.md b/README.md index 383c700..774ec18 100644 --- a/README.md +++ b/README.md @@ -324,6 +324,10 @@ plugin: attribute named "xmp". Note that Gimp reads the "xmp" attribute and Darktable 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 plugin uses `Ghostscript` to convert the raster image. When reading it diff --git a/autotests/read/exr/ps2026_testcard_rgb.exr b/autotests/read/exr/ps2026_testcard_rgb.exr new file mode 100644 index 0000000..5eb0a57 Binary files /dev/null and b/autotests/read/exr/ps2026_testcard_rgb.exr differ diff --git a/autotests/read/exr/ps2026_testcard_rgb.exr.json b/autotests/read/exr/ps2026_testcard_rgb.exr.json new file mode 100644 index 0000000..73df355 --- /dev/null +++ b/autotests/read/exr/ps2026_testcard_rgb.exr.json @@ -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 + } + } +] diff --git a/autotests/read/exr/ps2026_testcard_rgb.png b/autotests/read/exr/ps2026_testcard_rgb.png new file mode 100644 index 0000000..850dd9a Binary files /dev/null and b/autotests/read/exr/ps2026_testcard_rgb.png differ diff --git a/autotests/read/exr/rgb-gimp.exr.json b/autotests/read/exr/rgb-gimp.exr.json index f424125..be3dc90 100644 --- a/autotests/read/exr/rgb-gimp.exr.json +++ b/autotests/read/exr/rgb-gimp.exr.json @@ -2,7 +2,7 @@ { "fileName" : "rgb-gimp.png", "colorSpace" : { - "description" : "", + "description" : "Embedded RGB (linear)", "primaries" : "Custom", "transferFunction" : "Linear", "gamma" : 1 diff --git a/src/imageformats/exr.cpp b/src/imageformats/exr.cpp index c6b6f16..3cedc32 100644 --- a/src/imageformats/exr.cpp +++ b/src/imageformats/exr.cpp @@ -58,6 +58,7 @@ #include #include #include +#include #include #include #include @@ -227,8 +228,9 @@ static QImage::Format imageFormat(const Imf::RgbaInputFile &file) /*! * \brief viewList - * \param header + * \param header The image header. * \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) { @@ -238,14 +240,44 @@ static QStringList viewList(const Imf::Header &h) 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; } +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 static void printAttributes(const Imf::Header &h) { for (auto i = h.begin(); i != h.end(); ++i) { - qCDebug(LOG_EXRPLUGIN) << i.name(); + qCDebug(LOG_EXRPLUGIN) << i.name() << i.attribute().typeName(); } } #endif @@ -340,15 +372,29 @@ static void readColorSpace(const Imf::Header &header, QImage &image) { // final color operations QColorSpace cs; - if (auto chroma = header.findTypedAttribute("chromaticities")) { - auto &&v = chroma->value(); - cs = QColorSpace(QPointF(v.white.x, v.white.y), - QPointF(v.red.x, v.red.y), - QPointF(v.green.x, v.green.y), - QPointF(v.blue.x, v.blue.y), - QColorSpace::TransferFunction::Linear); + + // Photoshop 2026 allow to save the ICC profile as "iccProfile" attribute + if (auto iccProfile = header.findTypedAttribute("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("chromaticities")) { + auto &&v = chroma->value(); + cs = QColorSpace(QPointF(v.white.x, v.white.y), + QPointF(v.red.x, v.red.y), + QPointF(v.green.x, v.green.y), + QPointF(v.blue.x, v.blue.y), + QColorSpace::TransferFunction::Linear); + if (cs.isValid()) + cs.setDescription(QStringLiteral("Embedded RGB (linear)")); + } + } + + if (!cs.isValid()) { + // Use a linear profile cs = QColorSpace(QColorSpace::SRgbLinear); } image.setColorSpace(cs); @@ -377,12 +423,7 @@ bool EXRHandler::read(QImage *outImage) auto &&header = file.header(); // set the image to load - if (m_imageNumber > -1) { - auto views = viewList(header); - if (m_imageNumber < views.count()) { - file.setLayerName(views.at(m_imageNumber).toStdString()); - } - } + auto layerName = setLayerName(file, m_imageNumber); // get image info 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); return false; } + if (!layerName.isEmpty()) { + image.setText(QStringLiteral("EXRLayerName"), layerName); + } Imf::Array2D pixels; pixels.resizeErase(EXR_LINES_PER_BLOCK, width); @@ -688,12 +732,7 @@ QVariant EXRHandler::option(ImageOption option) const try { K_IStream istr(d); Imf::RgbaInputFile file(istr); - if (m_imageNumber > -1) { // set the image to read - auto views = viewList(file.header()); - if (m_imageNumber < views.count()) { - file.setLayerName(views.at(m_imageNumber).toStdString()); - } - } + setLayerName(file, m_imageNumber); Imath::Box2i dw = file.dataWindow(); v = QVariant(QSize(dw.max.x - dw.min.x + 1, dw.max.y - dw.min.y + 1)); } catch (const std::exception &) { @@ -713,6 +752,7 @@ QVariant EXRHandler::option(ImageOption option) const try { K_IStream istr(d); Imf::RgbaInputFile file(istr); + setLayerName(file, m_imageNumber); v = QVariant::fromValue(imageFormat(file)); } catch (const std::exception &) { // broken file or unsupported version @@ -787,12 +827,9 @@ bool EXRHandler::canRead(QIODevice *device) return false; } -#if OPENEXR_VERSION_MAJOR == 3 && OPENEXR_VERSION_MINOR > 2 - // openexpr >= 3.3 uses seek and tell extensively if (device->isSequential()) { return false; } -#endif const QByteArray head = device->peek(4); diff --git a/src/imageformats/exr_p.h b/src/imageformats/exr_p.h index 031cb02..fb202c8 100644 --- a/src/imageformats/exr_p.h +++ b/src/imageformats/exr_p.h @@ -86,6 +86,8 @@ private: * - 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. * - 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;