File indexing completed on 2024-04-28 15:25:39

0001 /*
0002     KImageIO Routines to read (and perhaps in the future, write) images
0003     in the high dynamic range EXR format.
0004 
0005     SPDX-FileCopyrightText: 2003 Brad Hards <bradh@frogmouth.net>
0006     SPDX-FileCopyrightText: 2023 Mirco Miranda <mircomir@outlook.com>
0007 
0008     SPDX-License-Identifier: LGPL-2.0-or-later
0009 */
0010 
0011 /* *** EXR_USE_LEGACY_CONVERSIONS ***
0012  * If defined, the result image is an 8-bit RGB(A) converted
0013  * without icc profiles. Otherwise, a 16-bit images is generated.
0014  * NOTE: The use of legacy conversions are discouraged due to
0015  *       imprecise image result.
0016  */
0017 //#define EXR_USE_LEGACY_CONVERSIONS // default commented -> you should define it in your cmake file
0018 
0019 /* *** EXR_ALLOW_LINEAR_COLORSPACE ***
0020  * If defined, the linear data is kept and it is the display program that
0021  * must convert to the monitor profile. Otherwise the data is converted to sRGB
0022  * to accommodate programs that do not support color profiles.
0023  * NOTE: If EXR_USE_LEGACY_CONVERSIONS is active, this is ignored.
0024  */
0025 //#define EXR_ALLOW_LINEAR_COLORSPACE // default: commented -> you should define it in your cmake file
0026 
0027 #include "exr_p.h"
0028 #include "util_p.h"
0029 
0030 #include <IexThrowErrnoExc.h>
0031 #include <ImathBox.h>
0032 #include <ImfArray.h>
0033 #include <ImfBoxAttribute.h>
0034 #include <ImfChannelListAttribute.h>
0035 #include <ImfCompressionAttribute.h>
0036 #include <ImfConvert.h>
0037 #include <ImfFloatAttribute.h>
0038 #include <ImfInputFile.h>
0039 #include <ImfInt64.h>
0040 #include <ImfIntAttribute.h>
0041 #include <ImfLineOrderAttribute.h>
0042 #include <ImfRgbaFile.h>
0043 #include <ImfStandardAttributes.h>
0044 #include <ImfStringAttribute.h>
0045 #include <ImfVecAttribute.h>
0046 #include <ImfVersion.h>
0047 
0048 #include <iostream>
0049 
0050 #include <QColorSpace>
0051 #include <QDataStream>
0052 #include <QDebug>
0053 #include <QFloat16>
0054 #include <QImage>
0055 #include <QImageIOPlugin>
0056 
0057 #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
0058 #include <QTimeZone>
0059 #endif
0060 
0061 // Allow the code to works on all QT versions supported by KDE
0062 // project (Qt 5.15 and Qt 6.x) to easy backports fixes.
0063 #if (QT_VERSION_MAJOR >= 6) && !defined(EXR_USE_LEGACY_CONVERSIONS)
0064 // If uncommented, the image is rendered in a float16 format, the result is very precise
0065 #define EXR_USE_QT6_FLOAT_IMAGE // default uncommented
0066 #endif
0067 
0068 class K_IStream : public Imf::IStream
0069 {
0070 public:
0071     K_IStream(QIODevice *dev, const QByteArray &fileName)
0072         : IStream(fileName.data())
0073         , m_dev(dev)
0074     {
0075     }
0076 
0077     bool read(char c[], int n) override;
0078 #if OPENEXR_VERSION_MAJOR > 2
0079     uint64_t tellg() override;
0080     void seekg(uint64_t pos) override;
0081 #else
0082     Imf::Int64 tellg() override;
0083     void seekg(Imf::Int64 pos) override;
0084 #endif
0085     void clear() override;
0086 
0087 private:
0088     QIODevice *m_dev;
0089 };
0090 
0091 bool K_IStream::read(char c[], int n)
0092 {
0093     qint64 result = m_dev->read(c, n);
0094     if (result > 0) {
0095         return true;
0096     } else if (result == 0) {
0097         throw Iex::InputExc("Unexpected end of file");
0098     } else { // negative value {
0099         Iex::throwErrnoExc("Error in read", result);
0100     }
0101     return false;
0102 }
0103 
0104 #if OPENEXR_VERSION_MAJOR > 2
0105 uint64_t K_IStream::tellg()
0106 #else
0107 Imf::Int64 K_IStream::tellg()
0108 #endif
0109 {
0110     return m_dev->pos();
0111 }
0112 
0113 #if OPENEXR_VERSION_MAJOR > 2
0114 void K_IStream::seekg(uint64_t pos)
0115 #else
0116 void K_IStream::seekg(Imf::Int64 pos)
0117 #endif
0118 {
0119     m_dev->seek(pos);
0120 }
0121 
0122 void K_IStream::clear()
0123 {
0124     // TODO
0125 }
0126 
0127 #ifdef EXR_USE_LEGACY_CONVERSIONS
0128 // source: https://openexr.com/en/latest/ReadingAndWritingImageFiles.html
0129 inline unsigned char gamma(float x)
0130 {
0131     x = std::pow(5.5555f * std::max(0.f, x), 0.4545f) * 84.66f;
0132     return (unsigned char)qBound(0.f, x, 255.f);
0133 }
0134 inline QRgb RgbaToQrgba(struct Imf::Rgba &imagePixel)
0135 {
0136     return qRgba(gamma(float(imagePixel.r)),
0137                  gamma(float(imagePixel.g)),
0138                  gamma(float(imagePixel.b)),
0139                  (unsigned char)(qBound(0.f, imagePixel.a * 255.f, 255.f) + 0.5f));
0140 }
0141 #endif
0142 
0143 EXRHandler::EXRHandler()
0144 {
0145 }
0146 
0147 bool EXRHandler::canRead() const
0148 {
0149     if (canRead(device())) {
0150         setFormat("exr");
0151         return true;
0152     }
0153     return false;
0154 }
0155 
0156 bool EXRHandler::read(QImage *outImage)
0157 {
0158     try {
0159         int width;
0160         int height;
0161 
0162         K_IStream istr(device(), QByteArray());
0163         Imf::RgbaInputFile file(istr);
0164         Imath::Box2i dw = file.dataWindow();
0165         bool isRgba = file.channels() & Imf::RgbaChannels::WRITE_A;
0166 
0167         width = dw.max.x - dw.min.x + 1;
0168         height = dw.max.y - dw.min.y + 1;
0169 
0170 #if defined(EXR_USE_LEGACY_CONVERSIONS)
0171         QImage image = imageAlloc(width, height, isRgba ? QImage::Format_ARGB32 : QImage::Format_RGB32);
0172 #elif defined(EXR_USE_QT6_FLOAT_IMAGE)
0173         QImage image = imageAlloc(width, height, isRgba ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBX16FPx4);
0174 #else
0175         QImage image = imageAlloc(width, height, isRgba ? QImage::Format_RGBA64 : QImage::Format_RGBX64);
0176 #endif
0177         if (image.isNull()) {
0178             qWarning() << "Failed to allocate image, invalid size?" << QSize(width, height);
0179             return false;
0180         }
0181 
0182         // set some useful metadata
0183         auto &&h = file.header();
0184         if (auto comments = h.findTypedAttribute<Imf::StringAttribute>("comments")) {
0185             image.setText(QStringLiteral("Comment"), QString::fromStdString(comments->value()));
0186         }
0187         if (auto owner = h.findTypedAttribute<Imf::StringAttribute>("owner")) {
0188             image.setText(QStringLiteral("Owner"), QString::fromStdString(owner->value()));
0189         }
0190         if (auto capDate = h.findTypedAttribute<Imf::StringAttribute>("capDate")) {
0191             float off = 0;
0192             if (auto utcOffset = h.findTypedAttribute<Imf::FloatAttribute>("utcOffset")) {
0193                 off = utcOffset->value();
0194             }
0195             auto dateTime = QDateTime::fromString(QString::fromStdString(capDate->value()), QStringLiteral("yyyy:MM:dd HH:mm:ss"));
0196             if (dateTime.isValid()) {
0197 #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
0198                 dateTime.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(off));
0199 #else
0200                 dateTime.setOffsetFromUtc(off);
0201 #endif
0202                 image.setText(QStringLiteral("Date"), dateTime.toString(Qt::ISODate));
0203             }
0204         }
0205         if (auto xDensity = h.findTypedAttribute<Imf::FloatAttribute>("xDensity")) {
0206             float par = 1;
0207             if (auto pixelAspectRatio = h.findTypedAttribute<Imf::FloatAttribute>("pixelAspectRatio")) {
0208                 par = pixelAspectRatio->value();
0209             }
0210             image.setDotsPerMeterX(qRound(xDensity->value() * 100.0 / 2.54));
0211             image.setDotsPerMeterY(qRound(xDensity->value() * par * 100.0 / 2.54));
0212         }
0213 
0214         Imf::Array<Imf::Rgba> pixels;
0215         pixels.resizeErase(width);
0216 
0217         // somehow copy pixels into image
0218         for (int y = 0; y < height; ++y) {
0219             auto my = dw.min.y + y;
0220             if (my <= dw.max.y) { // paranoia check
0221                 file.setFrameBuffer(&pixels[0] - dw.min.x - qint64(my) * width, 1, width);
0222                 file.readPixels(my, my);
0223 
0224 #if defined(EXR_USE_LEGACY_CONVERSIONS)
0225                 auto scanLine = reinterpret_cast<QRgb *>(image.scanLine(y));
0226                 for (int x = 0; x < width; ++x) {
0227                     *(scanLine + x) = RgbaToQrgba(pixels[x]);
0228                 }
0229 #elif defined(EXR_USE_QT6_FLOAT_IMAGE)
0230                 auto scanLine = reinterpret_cast<qfloat16 *>(image.scanLine(y));
0231                 for (int x = 0; x < width; ++x) {
0232                     auto xcs = x * 4;
0233                     *(scanLine + xcs) = qfloat16(qBound(0.f, float(pixels[x].r), 1.f));
0234                     *(scanLine + xcs + 1) = qfloat16(qBound(0.f, float(pixels[x].g), 1.f));
0235                     *(scanLine + xcs + 2) = qfloat16(qBound(0.f, float(pixels[x].b), 1.f));
0236                     *(scanLine + xcs + 3) = qfloat16(isRgba ? qBound(0.f, float(pixels[x].a), 1.f) : 1.f);
0237                 }
0238 #else
0239                 auto scanLine = reinterpret_cast<QRgba64 *>(image.scanLine(y));
0240                 for (int x = 0; x < width; ++x) {
0241                     *(scanLine + x) = QRgba64::fromRgba64(quint16(qBound(0.f, float(pixels[x].r) * 65535.f + 0.5f, 65535.f)),
0242                                                           quint16(qBound(0.f, float(pixels[x].g) * 65535.f + 0.5f, 65535.f)),
0243                                                           quint16(qBound(0.f, float(pixels[x].b) * 65535.f + 0.5f, 65535.f)),
0244                                                           isRgba ? quint16(qBound(0.f, float(pixels[x].a) * 65535.f + 0.5f, 65535.f)) : quint16(65535));
0245                 }
0246 #endif
0247             }
0248         }
0249 
0250         // final color operations
0251 #ifndef EXR_USE_LEGACY_CONVERSIONS
0252         image.setColorSpace(QColorSpace(QColorSpace::SRgbLinear));
0253 #ifndef EXR_ALLOW_LINEAR_COLORSPACE
0254         image.convertToColorSpace(QColorSpace(QColorSpace::SRgb));
0255 #endif // !EXR_ALLOW_LINEAR_COLORSPACE
0256 #endif // !EXR_USE_LEGACY_CONVERSIONS
0257 
0258         *outImage = image;
0259 
0260         return true;
0261     } catch (const std::exception &exc) {
0262         //      qDebug() << exc.what();
0263         return false;
0264     }
0265 }
0266 
0267 bool EXRHandler::canRead(QIODevice *device)
0268 {
0269     if (!device) {
0270         qWarning("EXRHandler::canRead() called with no device");
0271         return false;
0272     }
0273 
0274     const QByteArray head = device->peek(4);
0275 
0276     return Imf::isImfMagic(head.data());
0277 }
0278 
0279 QImageIOPlugin::Capabilities EXRPlugin::capabilities(QIODevice *device, const QByteArray &format) const
0280 {
0281     if (format == "exr") {
0282         return Capabilities(CanRead);
0283     }
0284     if (!format.isEmpty()) {
0285         return {};
0286     }
0287     if (!device->isOpen()) {
0288         return {};
0289     }
0290 
0291     Capabilities cap;
0292     if (device->isReadable() && EXRHandler::canRead(device)) {
0293         cap |= CanRead;
0294     }
0295     return cap;
0296 }
0297 
0298 QImageIOHandler *EXRPlugin::create(QIODevice *device, const QByteArray &format) const
0299 {
0300     QImageIOHandler *handler = new EXRHandler;
0301     handler->setDevice(device);
0302     handler->setFormat(format);
0303     return handler;
0304 }
0305 
0306 #include "moc_exr_p.cpp"