1 /*
2     KImageIO Routines to read (and perhaps in the future, write) images
3     in the high dynamic range EXR format.
4 
5     SPDX-FileCopyrightText: 2003 Brad Hards <bradh@frogmouth.net>
6 
7     SPDX-License-Identifier: LGPL-2.0-or-later
8 */
9 
10 #include "exr_p.h"
11 
12 #include <IexThrowErrnoExc.h>
13 #include <ImathBox.h>
14 #include <ImfArray.h>
15 #include <ImfBoxAttribute.h>
16 #include <ImfChannelListAttribute.h>
17 #include <ImfCompressionAttribute.h>
18 #include <ImfConvert.h>
19 #include <ImfFloatAttribute.h>
20 #include <ImfInputFile.h>
21 #include <ImfInt64.h>
22 #include <ImfIntAttribute.h>
23 #include <ImfLineOrderAttribute.h>
24 #include <ImfRgbaFile.h>
25 #include <ImfStandardAttributes.h>
26 #include <ImfStringAttribute.h>
27 #include <ImfVecAttribute.h>
28 #include <ImfVersion.h>
29 
30 #include <iostream>
31 
32 #include <QDataStream>
33 #include <QDebug>
34 #include <QImage>
35 #include <QImageIOPlugin>
36 
37 class K_IStream : public Imf::IStream
38 {
39 public:
K_IStream(QIODevice * dev,const QByteArray & fileName)40     K_IStream(QIODevice *dev, const QByteArray &fileName)
41         : IStream(fileName.data())
42         , m_dev(dev)
43     {
44     }
45 
46     bool read(char c[], int n) override;
47 #if OPENEXR_VERSION_MAJOR > 2
48     uint64_t tellg() override;
49     void seekg(uint64_t pos) override;
50 #else
51     uint64_t tellg() override;
52     void seekg(uint64_t pos) override;
53 #endif
54     void clear() override;
55 
56 private:
57     QIODevice *m_dev;
58 };
59 
read(char c[],int n)60 bool K_IStream::read(char c[], int n)
61 {
62     qint64 result = m_dev->read(c, n);
63     if (result > 0) {
64         return true;
65     } else if (result == 0) {
66         throw Iex::InputExc("Unexpected end of file");
67     } else { // negative value {
68         Iex::throwErrnoExc("Error in read", result);
69     }
70     return false;
71 }
72 
73 #if OPENEXR_VERSION_MAJOR > 2
tellg()74 uint64_t K_IStream::tellg()
75 #else
76 uint64_t K_IStream::tellg()
77 #endif
78 {
79     return m_dev->pos();
80 }
81 
82 #if OPENEXR_VERSION_MAJOR > 2
seekg(uint64_t pos)83 void K_IStream::seekg(uint64_t pos)
84 #else
85 void K_IStream::seekg(uint64_t pos)
86 #endif
87 {
88     m_dev->seek(pos);
89 }
90 
clear()91 void K_IStream::clear()
92 {
93     // TODO
94 }
95 
96 /* this does a conversion from the ILM Half (equal to Nvidia Half)
97  * format into the normal 32 bit pixel format. Process is from the
98  * ILM code.
99  */
RgbaToQrgba(struct Imf::Rgba & imagePixel)100 QRgb RgbaToQrgba(struct Imf::Rgba &imagePixel)
101 {
102     float r;
103     float g;
104     float b;
105     float a;
106 
107     //  1) Compensate for fogging by subtracting defog
108     //     from the raw pixel values.
109     // Response: We work with defog of 0.0, so this is a no-op
110 
111     //  2) Multiply the defogged pixel values by
112     //     2^(exposure + 2.47393).
113     // Response: We work with exposure of 0.0.
114     // (2^2.47393) is 5.55555
115     r = imagePixel.r * 5.55555;
116     g = imagePixel.g * 5.55555;
117     b = imagePixel.b * 5.55555;
118     a = imagePixel.a * 5.55555;
119 
120     //  3) Values, which are now 1.0, are called "middle gray".
121     //     If defog and exposure are both set to 0.0, then
122     //     middle gray corresponds to a raw pixel value of 0.18.
123     //     In step 6, middle gray values will be mapped to an
124     //     intensity 3.5 f-stops below the display's maximum
125     //     intensity.
126     // Response: no apparent content.
127 
128     //  4) Apply a knee function.  The knee function has two
129     //     parameters, kneeLow and kneeHigh.  Pixel values
130     //     below 2^kneeLow are not changed by the knee
131     //     function.  Pixel values above kneeLow are lowered
132     //     according to a logarithmic curve, such that the
133     //     value 2^kneeHigh is mapped to 2^3.5 (in step 6,
134     //     this value will be mapped to the display's
135     //     maximum intensity).
136     // Response: kneeLow = 0.0 (2^0.0 => 1); kneeHigh = 5.0 (2^5 =>32)
137     if (r > 1.0) {
138         r = 1.0 + std::log((r - 1.0) * 0.184874 + 1) / 0.184874;
139     }
140     if (g > 1.0) {
141         g = 1.0 + std::log((g - 1.0) * 0.184874 + 1) / 0.184874;
142     }
143     if (b > 1.0) {
144         b = 1.0 + std::log((b - 1.0) * 0.184874 + 1) / 0.184874;
145     }
146     if (a > 1.0) {
147         a = 1.0 + std::log((a - 1.0) * 0.184874 + 1) / 0.184874;
148     }
149     //
150     //  5) Gamma-correct the pixel values, assuming that the
151     //     screen's gamma is 0.4545 (or 1/2.2).
152     r = std::pow(r, 0.4545);
153     g = std::pow(g, 0.4545);
154     b = std::pow(b, 0.4545);
155     a = std::pow(a, 0.4545);
156 
157     //  6) Scale the values such that pixels middle gray
158     //     pixels are mapped to 84.66 (or 3.5 f-stops below
159     //     the display's maximum intensity).
160     //
161     //  7) Clamp the values to [0, 255].
162     return qRgba((unsigned char)(Imath::clamp(r * 84.66f, 0.f, 255.f)),
163                  (unsigned char)(Imath::clamp(g * 84.66f, 0.f, 255.f)),
164                  (unsigned char)(Imath::clamp(b * 84.66f, 0.f, 255.f)),
165                  (unsigned char)(Imath::clamp(a * 84.66f, 0.f, 255.f)));
166 }
167 
EXRHandler()168 EXRHandler::EXRHandler()
169 {
170 }
171 
canRead() const172 bool EXRHandler::canRead() const
173 {
174     if (canRead(device())) {
175         setFormat("exr");
176         return true;
177     }
178     return false;
179 }
180 
read(QImage * outImage)181 bool EXRHandler::read(QImage *outImage)
182 {
183     try {
184         int width;
185         int height;
186 
187         K_IStream istr(device(), QByteArray());
188         Imf::RgbaInputFile file(istr);
189         Imath::Box2i dw = file.dataWindow();
190 
191         width = dw.max.x - dw.min.x + 1;
192         height = dw.max.y - dw.min.y + 1;
193 
194         QImage image(width, height, QImage::Format_RGB32);
195         if (image.isNull()) {
196             qWarning() << "Failed to allocate image, invalid size?" << QSize(width, height);
197             return false;
198         }
199 
200         Imf::Array2D<Imf::Rgba> pixels;
201         pixels.resizeErase(height, width);
202 
203         file.setFrameBuffer(&pixels[0][0] - dw.min.x - dw.min.y * width, 1, width);
204         file.readPixels(dw.min.y, dw.max.y);
205 
206         // somehow copy pixels into image
207         for (int y = 0; y < height; y++) {
208             for (int x = 0; x < width; x++) {
209                 // copy pixels(x,y) into image(x,y)
210                 image.setPixel(x, y, RgbaToQrgba(pixels[y][x]));
211             }
212         }
213 
214         *outImage = image;
215 
216         return true;
217     } catch (const std::exception &exc) {
218         //      qDebug() << exc.what();
219         return false;
220     }
221 }
222 
canRead(QIODevice * device)223 bool EXRHandler::canRead(QIODevice *device)
224 {
225     if (!device) {
226         qWarning("EXRHandler::canRead() called with no device");
227         return false;
228     }
229 
230     const QByteArray head = device->peek(4);
231 
232     return Imf::isImfMagic(head.data());
233 }
234 
capabilities(QIODevice * device,const QByteArray & format) const235 QImageIOPlugin::Capabilities EXRPlugin::capabilities(QIODevice *device, const QByteArray &format) const
236 {
237     if (format == "exr") {
238         return Capabilities(CanRead);
239     }
240     if (!format.isEmpty()) {
241         return {};
242     }
243     if (!device->isOpen()) {
244         return {};
245     }
246 
247     Capabilities cap;
248     if (device->isReadable() && EXRHandler::canRead(device)) {
249         cap |= CanRead;
250     }
251     return cap;
252 }
253 
create(QIODevice * device,const QByteArray & format) const254 QImageIOHandler *EXRPlugin::create(QIODevice *device, const QByteArray &format) const
255 {
256     QImageIOHandler *handler = new EXRHandler;
257     handler->setDevice(device);
258     handler->setFormat(format);
259     return handler;
260 }
261