File indexing completed on 2024-04-28 03:54:41

0001 /*
0002     The high dynamic range EXR format support for QImage.
0003 
0004     SPDX-FileCopyrightText: 2003 Brad Hards <bradh@frogmouth.net>
0005     SPDX-FileCopyrightText: 2023 Mirco Miranda <mircomir@outlook.com>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 /* *** EXR_USE_LEGACY_CONVERSIONS ***
0011  * If defined, the result image is an 8-bit RGB(A) converted
0012  * without icc profiles. Otherwise, a 16-bit images is generated.
0013  * NOTE: The use of legacy conversions are discouraged due to
0014  *       imprecise image result.
0015  */
0016 //#define EXR_USE_LEGACY_CONVERSIONS // default commented -> you should define it in your cmake file
0017 
0018 /* *** EXR_CONVERT_TO_SRGB ***
0019  * If defined, the linear data is converted to sRGB on read to accommodate
0020  * programs that do not support color profiles.
0021  * Otherwise the data are kept as is and it is the display program that
0022  * must convert to the monitor profile.
0023  * NOTE: If EXR_USE_LEGACY_CONVERSIONS is active, this is ignored.
0024  */
0025 //#define EXR_CONVERT_TO_SRGB // default: commented -> you should define it in your cmake file
0026 
0027 /* *** EXR_STORE_XMP_ATTRIBUTE ***
0028  * If defined, disables the stores XMP values in a non-standard attribute named "xmp".
0029  * The QImage metadata used is "XML:com.adobe.xmp".
0030  * NOTE: The use of non-standard attributes is possible but discouraged by the specification. However,
0031  *       metadata is essential for good image management and programs like darktable also set this
0032  *       attribute. Gimp reads the "xmp" attribute and Darktable writes it as well.
0033  */
0034 //#define EXR_DISABLE_XMP_ATTRIBUTE // default: commented -> you should define it in your cmake file
0035 
0036 /* *** EXR_MAX_IMAGE_WIDTH and EXR_MAX_IMAGE_HEIGHT ***
0037  * The maximum size in pixel allowed by the plugin.
0038  */
0039 #ifndef EXR_MAX_IMAGE_WIDTH
0040 #define EXR_MAX_IMAGE_WIDTH 300000
0041 #endif
0042 #ifndef EXR_MAX_IMAGE_HEIGHT
0043 #define EXR_MAX_IMAGE_HEIGHT EXR_MAX_IMAGE_WIDTH
0044 #endif
0045 
0046 /* *** EXR_LINES_PER_BLOCK ***
0047  * Allows certain compression schemes to work in multithreading
0048  * Requires up to "LINES_PER_BLOCK * MAX_IMAGE_WIDTH * 8"
0049  * additional RAM (e.g. if 128, up to 307MiB of RAM).
0050  * There is a performance gain with the following parameters (based on empirical tests):
0051  * - PIZ compression needs 64+ lines
0052  * - ZIPS compression needs 8+ lines
0053  * - ZIP compression needs 32+ lines
0054  * - Others not tested
0055  *
0056  * NOTE: The OpenEXR documentation states that the higher the better :)
0057  */
0058 #ifndef EXR_LINES_PER_BLOCK
0059 #define EXR_LINES_PER_BLOCK 128
0060 #endif
0061 
0062 #include "exr_p.h"
0063 #include "scanlineconverter_p.h"
0064 #include "util_p.h"
0065 
0066 #include <IexThrowErrnoExc.h>
0067 #include <ImathBox.h>
0068 #include <ImfArray.h>
0069 #include <ImfBoxAttribute.h>
0070 #include <ImfChannelListAttribute.h>
0071 #include <ImfCompressionAttribute.h>
0072 #include <ImfConvert.h>
0073 #include <ImfFloatAttribute.h>
0074 #include <ImfInputFile.h>
0075 #include <ImfInt64.h>
0076 #include <ImfIntAttribute.h>
0077 #include <ImfLineOrderAttribute.h>
0078 #include <ImfPreviewImage.h>
0079 #include <ImfRgbaFile.h>
0080 #include <ImfStandardAttributes.h>
0081 #include <ImfVersion.h>
0082 
0083 #include <iostream>
0084 
0085 #include <QColorSpace>
0086 #include <QDataStream>
0087 #include <QDebug>
0088 #include <QFloat16>
0089 #include <QImage>
0090 #include <QImageIOPlugin>
0091 #include <QLocale>
0092 #include <QThread>
0093 #include <QTimeZone>
0094 
0095 // Allow the code to works on all QT versions supported by KDE
0096 // project (Qt 5.15 and Qt 6.x) to easy backports fixes.
0097 #if !defined(EXR_USE_LEGACY_CONVERSIONS)
0098 // If uncommented, the image is rendered in a float16 format, the result is very precise
0099 #define EXR_USE_QT6_FLOAT_IMAGE // default uncommented
0100 #endif
0101 
0102 class K_IStream : public Imf::IStream
0103 {
0104 public:
0105     K_IStream(QIODevice *dev, const QByteArray &fileName)
0106         : IStream(fileName.data())
0107         , m_dev(dev)
0108     {
0109     }
0110 
0111     bool read(char c[], int n) override;
0112 #if OPENEXR_VERSION_MAJOR > 2
0113     uint64_t tellg() override;
0114     void seekg(uint64_t pos) override;
0115 #else
0116     Imf::Int64 tellg() override;
0117     void seekg(Imf::Int64 pos) override;
0118 #endif
0119     void clear() override;
0120 
0121 private:
0122     QIODevice *m_dev;
0123 };
0124 
0125 bool K_IStream::read(char c[], int n)
0126 {
0127     qint64 result = m_dev->read(c, n);
0128     if (result > 0) {
0129         return true;
0130     } else if (result == 0) {
0131         throw Iex::InputExc("Unexpected end of file");
0132     } else { // negative value {
0133         Iex::throwErrnoExc("Error in read", result);
0134     }
0135     return false;
0136 }
0137 
0138 #if OPENEXR_VERSION_MAJOR > 2
0139 uint64_t K_IStream::tellg()
0140 #else
0141 Imf::Int64 K_IStream::tellg()
0142 #endif
0143 {
0144     return m_dev->pos();
0145 }
0146 
0147 #if OPENEXR_VERSION_MAJOR > 2
0148 void K_IStream::seekg(uint64_t pos)
0149 #else
0150 void K_IStream::seekg(Imf::Int64 pos)
0151 #endif
0152 {
0153     m_dev->seek(pos);
0154 }
0155 
0156 void K_IStream::clear()
0157 {
0158     // TODO
0159 }
0160 
0161 class K_OStream : public Imf::OStream
0162 {
0163 public:
0164     K_OStream(QIODevice *dev, const QByteArray &fileName)
0165         : OStream(fileName.data())
0166         , m_dev(dev)
0167     {
0168     }
0169 
0170     void write(const char c[/*n*/], int n) override;
0171 #if OPENEXR_VERSION_MAJOR > 2
0172     uint64_t tellp() override;
0173     void seekp(uint64_t pos) override;
0174 #else
0175     Imf::Int64 tellp() override;
0176     void seekp(Imf::Int64 pos) override;
0177 #endif
0178 
0179 private:
0180     QIODevice *m_dev;
0181 };
0182 
0183 void K_OStream::write(const char c[], int n)
0184 {
0185     qint64 result = m_dev->write(c, n);
0186     if (result > 0) {
0187         return;
0188     } else { // negative value {
0189         Iex::throwErrnoExc("Error in write", result);
0190     }
0191     return;
0192 }
0193 
0194 #if OPENEXR_VERSION_MAJOR > 2
0195 uint64_t K_OStream::tellp()
0196 #else
0197 Imf::Int64 K_OStream::tellg()
0198 #endif
0199 {
0200     return m_dev->pos();
0201 }
0202 
0203 #if OPENEXR_VERSION_MAJOR > 2
0204 void K_OStream::seekp(uint64_t pos)
0205 #else
0206 void K_OStream::seekg(Imf::Int64 pos)
0207 #endif
0208 {
0209     m_dev->seek(pos);
0210 }
0211 
0212 #ifdef EXR_USE_LEGACY_CONVERSIONS
0213 // source: https://openexr.com/en/latest/ReadingAndWritingImageFiles.html
0214 inline unsigned char gamma(float x)
0215 {
0216     x = std::pow(5.5555f * std::max(0.f, x), 0.4545f) * 84.66f;
0217     return (unsigned char)qBound(0.f, x, 255.f);
0218 }
0219 inline QRgb RgbaToQrgba(struct Imf::Rgba &imagePixel)
0220 {
0221     return qRgba(gamma(float(imagePixel.r)),
0222                  gamma(float(imagePixel.g)),
0223                  gamma(float(imagePixel.b)),
0224                  (unsigned char)(qBound(0.f, imagePixel.a * 255.f, 255.f) + 0.5f));
0225 }
0226 #endif
0227 
0228 EXRHandler::EXRHandler()
0229     : m_compressionRatio(-1)
0230     , m_quality(-1)
0231     , m_imageNumber(0)
0232     , m_imageCount(0)
0233     , m_startPos(-1)
0234 {
0235     // Set the number of threads to use (0 is allowed)
0236     Imf::setGlobalThreadCount(QThread::idealThreadCount() / 2);
0237 }
0238 
0239 bool EXRHandler::canRead() const
0240 {
0241     if (canRead(device())) {
0242         setFormat("exr");
0243         return true;
0244     }
0245     return false;
0246 }
0247 
0248 static QImage::Format imageFormat(const Imf::RgbaInputFile &file)
0249 {
0250     auto isRgba = file.channels() & Imf::RgbaChannels::WRITE_A;
0251 #if defined(EXR_USE_LEGACY_CONVERSIONS)
0252     return (isRgba ? QImage::Format_ARGB32 : QImage::Format_RGB32);
0253 #elif defined(EXR_USE_QT6_FLOAT_IMAGE)
0254     return (isRgba ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBX16FPx4);
0255 #else
0256     return (isRgba ? QImage::Format_RGBA64 : QImage::Format_RGBX64);
0257 #endif
0258 }
0259 
0260 /*!
0261  * \brief viewList
0262  * \param header
0263  * \return The list of available views.
0264  */
0265 static QStringList viewList(const Imf::Header &h)
0266 {
0267     QStringList l;
0268     if (auto views = h.findTypedAttribute<Imf::StringVectorAttribute>("multiView")) {
0269         for (auto &&v : views->value()) {
0270             l << QString::fromStdString(v);
0271         }
0272     }
0273     return l;
0274 }
0275 
0276 #ifdef QT_DEBUG
0277 void printAttributes(const Imf::Header &h)
0278 {
0279     for (auto i = h.begin(); i != h.end(); ++i) {
0280         qDebug() << i.name();
0281     }
0282 }
0283 #endif
0284 
0285 /*!
0286  * \brief readMetadata
0287  * Reads EXR attributes from the \a header and set its as metadata in the \a image.
0288  */
0289 static void readMetadata(const Imf::Header &header, QImage &image)
0290 {
0291     // set some useful metadata
0292     if (auto comments = header.findTypedAttribute<Imf::StringAttribute>("comments")) {
0293         image.setText(QStringLiteral("Comment"), QString::fromStdString(comments->value()));
0294     }
0295 
0296     if (auto owner = header.findTypedAttribute<Imf::StringAttribute>("owner")) {
0297         image.setText(QStringLiteral("Owner"), QString::fromStdString(owner->value()));
0298     }
0299 
0300     if (auto lat = header.findTypedAttribute<Imf::FloatAttribute>("latitude")) {
0301         image.setText(QStringLiteral("Latitude"), QLocale::c().toString(lat->value()));
0302     }
0303 
0304     if (auto lon = header.findTypedAttribute<Imf::FloatAttribute>("longitude")) {
0305         image.setText(QStringLiteral("Longitude"), QLocale::c().toString(lon->value()));
0306     }
0307 
0308     if (auto alt = header.findTypedAttribute<Imf::FloatAttribute>("altitude")) {
0309         image.setText(QStringLiteral("Altitude"), QLocale::c().toString(alt->value()));
0310     }
0311 
0312     if (auto capDate = header.findTypedAttribute<Imf::StringAttribute>("capDate")) {
0313         float off = 0;
0314         if (auto utcOffset = header.findTypedAttribute<Imf::FloatAttribute>("utcOffset")) {
0315             off = utcOffset->value();
0316         }
0317         auto dateTime = QDateTime::fromString(QString::fromStdString(capDate->value()), QStringLiteral("yyyy:MM:dd HH:mm:ss"));
0318         if (dateTime.isValid()) {
0319             dateTime.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(off));
0320             image.setText(QStringLiteral("CreationDate"), dateTime.toString(Qt::ISODate));
0321         }
0322     }
0323 
0324     if (auto xDensity = header.findTypedAttribute<Imf::FloatAttribute>("xDensity")) {
0325         float par = 1;
0326         if (auto pixelAspectRatio = header.findTypedAttribute<Imf::FloatAttribute>("pixelAspectRatio")) {
0327             par = pixelAspectRatio->value();
0328         }
0329         image.setDotsPerMeterX(qRound(xDensity->value() * 100.0 / 2.54));
0330         image.setDotsPerMeterY(qRound(xDensity->value() * par * 100.0 / 2.54));
0331     }
0332 
0333     // Non-standard attribute
0334     if (auto xmp = header.findTypedAttribute<Imf::StringAttribute>("xmp")) {
0335         image.setText(QStringLiteral("XML:com.adobe.xmp"), QString::fromStdString(xmp->value()));
0336     }
0337 
0338     /* TODO: OpenEXR 3.2 metadata
0339      *
0340      * New Optional Standard Attributes:
0341      * - Support automated editorial workflow:
0342      *   reelName, imageCounter, ascFramingDecisionList
0343      *
0344      * - Support forensics (“which other shots used that camera and lens before the camera firmware was updated?”):
0345      *   cameraMake, cameraModel, cameraSerialNumber, cameraFirmware, cameraUuid, cameraLabel, lensMake, lensModel,
0346      *   lensSerialNumber, lensFirmware, cameraColorBalance
0347      *
0348      * -Support pickup shots (reproduce critical camera settings):
0349      *   shutterAngle, cameraCCTSetting, cameraTintSetting
0350      *
0351      * - Support metadata-driven match move:
0352      *   sensorCenterOffset, sensorOverallDimensions, sensorPhotositePitch, sensorAcquisitionRectanglenominalFocalLength,
0353      *   effectiveFocalLength, pinholeFocalLength, entrancePupilOffset, tStop(complementing existing 'aperture')
0354      */
0355 }
0356 
0357 /*!
0358  * \brief readColorSpace
0359  * Reads EXR chromaticities from the \a header and set its as color profile in the \a image.
0360  */
0361 static void readColorSpace(const Imf::Header &header, QImage &image)
0362 {
0363     // final color operations
0364 #ifndef EXR_USE_LEGACY_CONVERSIONS
0365 
0366     QColorSpace cs;
0367     if (auto chroma = header.findTypedAttribute<Imf::ChromaticitiesAttribute>("chromaticities")) {
0368         auto &&v = chroma->value();
0369         cs = QColorSpace(QPointF(v.white.x, v.white.y),
0370                          QPointF(v.red.x, v.red.y),
0371                          QPointF(v.green.x, v.green.y),
0372                          QPointF(v.blue.x, v.blue.y),
0373                          QColorSpace::TransferFunction::Linear);
0374     }
0375     if (!cs.isValid()) {
0376         cs = QColorSpace(QColorSpace::SRgbLinear);
0377     }
0378     image.setColorSpace(cs);
0379 
0380 #ifdef EXR_CONVERT_TO_SRGB
0381     image.convertToColorSpace(QColorSpace(QColorSpace::SRgb));
0382 #endif
0383 
0384 #endif // !EXR_USE_LEGACY_CONVERSIONS
0385 }
0386 
0387 bool EXRHandler::read(QImage *outImage)
0388 {
0389     try {
0390         auto d = device();
0391 
0392         // set the image position after the first run.
0393         if (!d->isSequential()) {
0394             if (m_startPos < 0) {
0395                 m_startPos = d->pos();
0396             } else {
0397                 d->seek(m_startPos);
0398             }
0399         }
0400 
0401         K_IStream istr(d, QByteArray());
0402         Imf::RgbaInputFile file(istr);
0403         auto &&header = file.header();
0404 
0405         // set the image to load
0406         if (m_imageNumber > -1) {
0407             auto views = viewList(header);
0408             if (m_imageNumber < views.count()) {
0409                 file.setLayerName(views.at(m_imageNumber).toStdString());
0410             }
0411         }
0412 
0413         // get image info
0414         Imath::Box2i dw = file.dataWindow();
0415         qint32 width = dw.max.x - dw.min.x + 1;
0416         qint32 height = dw.max.y - dw.min.y + 1;
0417 
0418         // limiting the maximum image size on a reasonable size (as done in other plugins)
0419         if (width > EXR_MAX_IMAGE_WIDTH || height > EXR_MAX_IMAGE_HEIGHT) {
0420             qWarning() << "The maximum image size is limited to" << EXR_MAX_IMAGE_WIDTH << "x" << EXR_MAX_IMAGE_HEIGHT << "px";
0421             return false;
0422         }
0423 
0424         // creating the image
0425         QImage image = imageAlloc(width, height, imageFormat(file));
0426         if (image.isNull()) {
0427             qWarning() << "Failed to allocate image, invalid size?" << QSize(width, height);
0428             return false;
0429         }
0430 
0431         Imf::Array2D<Imf::Rgba> pixels;
0432         pixels.resizeErase(EXR_LINES_PER_BLOCK, width);
0433         bool isRgba = image.hasAlphaChannel();
0434 
0435         // somehow copy pixels into image
0436         for (int y = 0, n = 0; y < height; y += n) {
0437             auto my = dw.min.y + y;
0438             if (my > dw.max.y) { // paranoia check
0439                 break;
0440             }
0441 
0442             file.setFrameBuffer(&pixels[0][0] - dw.min.x - qint64(my) * width, 1, width);
0443             file.readPixels(my, std::min(my + EXR_LINES_PER_BLOCK - 1, dw.max.y));
0444 
0445             for (n = 0; n < std::min(EXR_LINES_PER_BLOCK, height - y); ++n) {
0446 #if defined(EXR_USE_LEGACY_CONVERSIONS)
0447                 Q_UNUSED(isRgba)
0448                 auto scanLine = reinterpret_cast<QRgb *>(image.scanLine(y + n));
0449                 for (int x = 0; x < width; ++x) {
0450                     *(scanLine + x) = RgbaToQrgba(pixels[n][x]);
0451                 }
0452 #elif defined(EXR_USE_QT6_FLOAT_IMAGE)
0453                 auto scanLine = reinterpret_cast<qfloat16 *>(image.scanLine(y + n));
0454                 for (int x = 0; x < width; ++x) {
0455                     auto xcs = x * 4;
0456                     *(scanLine + xcs) = qfloat16(qBound(0.f, float(pixels[n][x].r), 1.f));
0457                     *(scanLine + xcs + 1) = qfloat16(qBound(0.f, float(pixels[n][x].g), 1.f));
0458                     *(scanLine + xcs + 2) = qfloat16(qBound(0.f, float(pixels[n][x].b), 1.f));
0459                     *(scanLine + xcs + 3) = qfloat16(isRgba ? qBound(0.f, float(pixels[n][x].a), 1.f) : 1.f);
0460                 }
0461 #else
0462                 auto scanLine = reinterpret_cast<QRgba64 *>(image.scanLine(y + n));
0463                 for (int x = 0; x < width; ++x) {
0464                     *(scanLine + x) = QRgba64::fromRgba64(quint16(qBound(0.f, float(pixels[n][x].r) * 65535.f + 0.5f, 65535.f)),
0465                                                           quint16(qBound(0.f, float(pixels[n][x].g) * 65535.f + 0.5f, 65535.f)),
0466                                                           quint16(qBound(0.f, float(pixels[n][x].b) * 65535.f + 0.5f, 65535.f)),
0467                                                           isRgba ? quint16(qBound(0.f, float(pixels[n][x].a) * 65535.f + 0.5f, 65535.f)) : quint16(65535));
0468                 }
0469 #endif
0470             }
0471         }
0472 
0473         // set some useful metadata
0474         readMetadata(header, image);
0475         // final color operations
0476         readColorSpace(header, image);
0477 
0478         *outImage = image;
0479 
0480         return true;
0481     } catch (const std::exception &) {
0482         return false;
0483     }
0484 }
0485 
0486 /*!
0487  * \brief makePreview
0488  * Creates a preview of maximum 256 x 256 pixels from the \a image.
0489  */
0490 bool makePreview(const QImage &image, Imf::Array2D<Imf::PreviewRgba> &pixels)
0491 {
0492     auto w = image.width();
0493     auto h = image.height();
0494 
0495     QImage preview;
0496     if (w > h) {
0497         preview = image.scaledToWidth(256).convertToFormat(QImage::Format_ARGB32);
0498     } else {
0499         preview = image.scaledToHeight(256).convertToFormat(QImage::Format_ARGB32);
0500     }
0501     if (preview.isNull()) {
0502         return false;
0503     }
0504 
0505     w = preview.width();
0506     h = preview.height();
0507     pixels.resizeErase(h, w);
0508     preview.convertToColorSpace(QColorSpace(QColorSpace::SRgb));
0509 
0510     for (int y = 0; y < h; ++y) {
0511         auto scanLine = reinterpret_cast<const QRgb *>(preview.constScanLine(y));
0512         for (int x = 0; x < w; ++x) {
0513             auto &&out = pixels[y][x];
0514             out.r = qRed(*(scanLine + x));
0515             out.g = qGreen(*(scanLine + x));
0516             out.b = qBlue(*(scanLine + x));
0517             out.a = qAlpha(*(scanLine + x));
0518         }
0519     }
0520 
0521     return true;
0522 }
0523 
0524 /*!
0525  * \brief setMetadata
0526  * Reades the metadata from \a image and set its as attributes in the \a header.
0527  */
0528 static void setMetadata(const QImage &image, Imf::Header &header)
0529 {
0530     auto dateTime = QDateTime::currentDateTime();
0531     for (auto &&key : image.textKeys()) {
0532         auto text = image.text(key);
0533         if (!key.compare(QStringLiteral("Comment"), Qt::CaseInsensitive)) {
0534             header.insert("comments", Imf::StringAttribute(text.toStdString()));
0535         }
0536 
0537         if (!key.compare(QStringLiteral("Owner"), Qt::CaseInsensitive)) {
0538             header.insert("owner", Imf::StringAttribute(text.toStdString()));
0539         }
0540 
0541         // clang-format off
0542         if (!key.compare(QStringLiteral("Latitude"), Qt::CaseInsensitive) ||
0543             !key.compare(QStringLiteral("Longitude"), Qt::CaseInsensitive) ||
0544             !key.compare(QStringLiteral("Altitude"), Qt::CaseInsensitive)) {
0545             // clang-format on
0546             auto ok = false;
0547             auto value = QLocale::c().toFloat(text, &ok);
0548             if (ok) {
0549                 header.insert(qPrintable(key.toLower()), Imf::FloatAttribute(value));
0550             }
0551         }
0552 
0553         if (!key.compare(QStringLiteral("CreationDate"), Qt::CaseInsensitive)) {
0554             auto dt = QDateTime::fromString(text, Qt::ISODate);
0555             if (dt.isValid()) {
0556                 dateTime = dt;
0557             }
0558         }
0559 
0560 #ifndef EXR_DISABLE_XMP_ATTRIBUTE // warning: Non-standard attribute!
0561         if (!key.compare(QStringLiteral("XML:com.adobe.xmp"), Qt::CaseInsensitive)) {
0562             header.insert("xmp", Imf::StringAttribute(text.toStdString()));
0563         }
0564 #endif
0565     }
0566     if (dateTime.isValid()) {
0567         header.insert("capDate", Imf::StringAttribute(dateTime.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")).toStdString()));
0568         header.insert("utcOffset", Imf::FloatAttribute(dateTime.offsetFromUtc()));
0569     }
0570 
0571     if (image.dotsPerMeterX() && image.dotsPerMeterY()) {
0572         header.insert("xDensity", Imf::FloatAttribute(image.dotsPerMeterX() * 2.54f / 100.f));
0573         header.insert("pixelAspectRatio", Imf::FloatAttribute(float(image.dotsPerMeterX()) / float(image.dotsPerMeterY())));
0574     }
0575 
0576     // set default chroma (default constructor ITU-R BT.709-3 -> sRGB)
0577     // The image is converted to Linear sRGB so, the chroma is the default EXR value.
0578     // If a file doesn’t have a chromaticities attribute, display software should assume that the
0579     // file’s primaries and the white point match Rec. ITU-R BT.709-3.
0580     // header.insert("chromaticities", Imf::ChromaticitiesAttribute(Imf::Chromaticities()));
0581 
0582     // TODO: EXR 3.2 attributes (see readMetadata())
0583 }
0584 
0585 bool EXRHandler::write(const QImage &image)
0586 {
0587     try {
0588         // create EXR header
0589         qint32 width = image.width();
0590         qint32 height = image.height();
0591 
0592         // limiting the maximum image size on a reasonable size (as done in other plugins)
0593         if (width > EXR_MAX_IMAGE_WIDTH || height > EXR_MAX_IMAGE_HEIGHT) {
0594             qWarning() << "The maximum image size is limited to" << EXR_MAX_IMAGE_WIDTH << "x" << EXR_MAX_IMAGE_HEIGHT << "px";
0595             return false;
0596         }
0597 
0598         Imf::Header header(width, height);
0599         // set compression scheme (forcing PIZ as default)
0600         header.compression() = Imf::Compression::PIZ_COMPRESSION;
0601         if (m_compressionRatio >= qint32(Imf::Compression::NO_COMPRESSION) && m_compressionRatio < qint32(Imf::Compression::NUM_COMPRESSION_METHODS)) {
0602             header.compression() = Imf::Compression(m_compressionRatio);
0603         }
0604         // set the DCT quality (used by DCT compressions only)
0605         if (m_quality > -1 && m_quality <= 100) {
0606             header.dwaCompressionLevel() = float(m_quality);
0607         }
0608         // make ZIP compression fast (used by ZIP compressions)
0609         header.zipCompressionLevel() = 1;
0610 
0611         // set preview (don't set it for small images)
0612         if (width > 1024 || height > 1024) {
0613             Imf::Array2D<Imf::PreviewRgba> previewPixels;
0614             if (makePreview(image, previewPixels)) {
0615                 header.setPreviewImage(Imf::PreviewImage(previewPixels.width(), previewPixels.height(), &previewPixels[0][0]));
0616             }
0617         }
0618 
0619         // set metadata (EXR attributes)
0620         setMetadata(image, header);
0621 
0622         // write the EXR
0623         K_OStream ostr(device(), QByteArray());
0624         auto channelsType = image.hasAlphaChannel() ? Imf::RgbaChannels::WRITE_RGBA : Imf::RgbaChannels::WRITE_RGB;
0625         if (image.format() == QImage::Format_Mono ||
0626             image.format() == QImage::Format_MonoLSB ||
0627             image.format() == QImage::Format_Grayscale16 ||
0628             image.format() == QImage::Format_Grayscale8) {
0629             channelsType = Imf::RgbaChannels::WRITE_Y;
0630         }
0631         Imf::RgbaOutputFile file(ostr, header, channelsType);
0632         Imf::Array2D<Imf::Rgba> pixels;
0633         pixels.resizeErase(EXR_LINES_PER_BLOCK, width);
0634 
0635         // convert the image and write into the stream
0636 #if defined(EXR_USE_QT6_FLOAT_IMAGE)
0637         auto convFormat = image.hasAlphaChannel() ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBX16FPx4;
0638 #else
0639         auto convFormat = image.hasAlphaChannel() ? QImage::Format_RGBA64 : QImage::Format_RGBX64;
0640 #endif
0641         ScanLineConverter slc(convFormat);
0642         slc.setDefaultSourceColorSpace(QColorSpace(QColorSpace::SRgb));
0643         slc.setTargetColorSpace(QColorSpace(QColorSpace::SRgbLinear));
0644         for (int y = 0, n = 0; y < height; y += n) {
0645             for (n = 0; n < std::min(EXR_LINES_PER_BLOCK, height - y); ++n) {
0646 #if defined(EXR_USE_QT6_FLOAT_IMAGE)
0647                 auto scanLine = reinterpret_cast<const qfloat16 *>(slc.convertedScanLine(image, y + n));
0648                 if (scanLine == nullptr) {
0649                     return false;
0650                 }
0651                 for (int x = 0; x < width; ++x) {
0652                     auto xcs = x * 4;
0653                     pixels[n][x].r = float(*(scanLine + xcs));
0654                     pixels[n][x].g = float(*(scanLine + xcs + 1));
0655                     pixels[n][x].b = float(*(scanLine + xcs + 2));
0656                     pixels[n][x].a = float(*(scanLine + xcs + 3));
0657                 }
0658 #else
0659                 auto scanLine = reinterpret_cast<const QRgba64 *>(slc.convertedScanLine(image, y + n));
0660                 if (scanLine == nullptr) {
0661                     return false;
0662                 }
0663                 for (int x = 0; x < width; ++x) {
0664                     pixels[n][x].r = float((scanLine + x)->red() / 65535.f);
0665                     pixels[n][x].g = float((scanLine + x)->green() / 65535.f);
0666                     pixels[n][x].b = float((scanLine + x)->blue() / 65535.f);
0667                     pixels[n][x].a = float((scanLine + x)->alpha() / 65535.f);
0668                 }
0669 #endif
0670             }
0671             file.setFrameBuffer(&pixels[0][0] - qint64(y) * width, 1, width);
0672             file.writePixels(n);
0673         }
0674     } catch (const std::exception &) {
0675         return false;
0676     }
0677 
0678     return true;
0679 }
0680 
0681 void EXRHandler::setOption(ImageOption option, const QVariant &value)
0682 {
0683     if (option == QImageIOHandler::CompressionRatio) {
0684         auto ok = false;
0685         auto cr = value.toInt(&ok);
0686         if (ok) {
0687             m_compressionRatio = cr;
0688         }
0689     }
0690     if (option == QImageIOHandler::Quality) {
0691         auto ok = false;
0692         auto q = value.toInt(&ok);
0693         if (ok) {
0694             m_quality = q;
0695         }
0696     }
0697 }
0698 
0699 bool EXRHandler::supportsOption(ImageOption option) const
0700 {
0701     if (option == QImageIOHandler::Size) {
0702         return true;
0703     }
0704     if (option == QImageIOHandler::ImageFormat) {
0705         return true;
0706     }
0707     if (option == QImageIOHandler::CompressionRatio) {
0708         return true;
0709     }
0710     if (option == QImageIOHandler::Quality) {
0711         return true;
0712     }
0713     return false;
0714 }
0715 
0716 QVariant EXRHandler::option(ImageOption option) const
0717 {
0718     QVariant v;
0719 
0720     if (option == QImageIOHandler::Size) {
0721         if (auto d = device()) {
0722             // transactions works on both random and sequential devices
0723             d->startTransaction();
0724             try {
0725                 K_IStream istr(d, QByteArray());
0726                 Imf::RgbaInputFile file(istr);
0727                 if (m_imageNumber > -1) { // set the image to read
0728                     auto views = viewList(file.header());
0729                     if (m_imageNumber < views.count()) {
0730                         file.setLayerName(views.at(m_imageNumber).toStdString());
0731                     }
0732                 }
0733                 Imath::Box2i dw = file.dataWindow();
0734                 v = QVariant(QSize(dw.max.x - dw.min.x + 1, dw.max.y - dw.min.y + 1));
0735             } catch (const std::exception &) {
0736                 // broken file or unsupported version
0737             }
0738             d->rollbackTransaction();
0739         }
0740     }
0741 
0742     if (option == QImageIOHandler::ImageFormat) {
0743         if (auto d = device()) {
0744             // transactions works on both random and sequential devices
0745             d->startTransaction();
0746             try {
0747                 K_IStream istr(d, QByteArray());
0748                 Imf::RgbaInputFile file(istr);
0749                 v = QVariant::fromValue(imageFormat(file));
0750             } catch (const std::exception &) {
0751                 // broken file or unsupported version
0752             }
0753             d->rollbackTransaction();
0754         }
0755     }
0756 
0757     if (option == QImageIOHandler::CompressionRatio) {
0758         v = QVariant(m_compressionRatio);
0759     }
0760 
0761     if (option == QImageIOHandler::Quality) {
0762         v = QVariant(m_quality);
0763     }
0764 
0765     return v;
0766 }
0767 
0768 bool EXRHandler::jumpToNextImage()
0769 {
0770     return jumpToImage(m_imageNumber + 1);
0771 }
0772 
0773 bool EXRHandler::jumpToImage(int imageNumber)
0774 {
0775     if (imageNumber < 0 || imageNumber >= imageCount()) {
0776         return false;
0777     }
0778     m_imageNumber = imageNumber;
0779     return true;
0780 }
0781 
0782 int EXRHandler::imageCount() const
0783 {
0784     // NOTE: image count is cached for performance reason
0785     auto &&count = m_imageCount;
0786     if (count > 0) {
0787         return count;
0788     }
0789 
0790     count = QImageIOHandler::imageCount();
0791 
0792     auto d = device();
0793     d->startTransaction();
0794 
0795     try {
0796         K_IStream istr(d, QByteArray());
0797         Imf::RgbaInputFile file(istr);
0798         auto views = viewList(file.header());
0799         if (!views.isEmpty()) {
0800             count = views.size();
0801         }
0802     } catch (const std::exception &) {
0803         // do nothing
0804     }
0805 
0806     d->rollbackTransaction();
0807 
0808     return count;
0809 }
0810 
0811 int EXRHandler::currentImageNumber() const
0812 {
0813     return m_imageNumber;
0814 }
0815 
0816 bool EXRHandler::canRead(QIODevice *device)
0817 {
0818     if (!device) {
0819         qWarning("EXRHandler::canRead() called with no device");
0820         return false;
0821     }
0822 
0823     const QByteArray head = device->peek(4);
0824 
0825     return Imf::isImfMagic(head.data());
0826 }
0827 
0828 QImageIOPlugin::Capabilities EXRPlugin::capabilities(QIODevice *device, const QByteArray &format) const
0829 {
0830     if (format == "exr") {
0831         return Capabilities(CanRead | CanWrite);
0832     }
0833     if (!format.isEmpty()) {
0834         return {};
0835     }
0836     if (!device->isOpen()) {
0837         return {};
0838     }
0839 
0840     Capabilities cap;
0841     if (device->isReadable() && EXRHandler::canRead(device)) {
0842         cap |= CanRead;
0843     }
0844     if (device->isWritable()) {
0845         cap |= CanWrite;
0846     }
0847     return cap;
0848 }
0849 
0850 QImageIOHandler *EXRPlugin::create(QIODevice *device, const QByteArray &format) const
0851 {
0852     QImageIOHandler *handler = new EXRHandler;
0853     handler->setDevice(device);
0854     handler->setFormat(format);
0855     return handler;
0856 }
0857 
0858 #include "moc_exr_p.cpp"