1// tImageEXR.cpp 
2// 
3// This knows how to load and save OpenEXR images (.exr). It knows the details of the exr high dynamic range 
4// file format and loads the data into a tPixel array. These tPixels may be 'stolen' by the tPicture's constructor if 
5// an EXR file is specified. After the array is stolen the tImageEXR is invalid. This is purely for performance. 
6// 
7// Copyright (c) 2020-2022, 2024 Tristan Grimmer. 
8// Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby 
9// granted, provided that the above copyright notice and this permission notice appear in all copies. 
10// 
11// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL 
12// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 
13// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 
14// AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 
15// PERFORMANCE OF THIS SOFTWARE. 
16// 
17// The EXR namespace and functions are a modification of ImageView.cpp from OpenEXR under the following licence: 
18// 
19// Copyright (c) 2012, Industrial Light & Magic, a division of Lucas Digital Ltd. LLC. All rights reserved. 
20// 
21// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 
22// * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 
23// * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 
24// * Neither the name of Industrial Light & Magic nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.  
25// 
26// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 
27// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 
28// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
29 
30#include <Foundation/tStandard.h> 
31#include <Foundation/tString.h> 
32#include <System/tMachine.h> 
33#include <System/tFile.h> 
34#include "Image/tImageEXR.h" 
35#include "Image/tPicture.h" 
36#include <OpenEXR/loadImage.h> 
37#include <OpenEXR/ImfMultiPartInputFile.h> 
38#include <OpenEXR/halfFunction.h> 
39using namespace tSystem
40using namespace IMF
41using namespace IMATH
42using namespace tImage
43 
44 
45namespace EXR 
46
47 inline float KneeFun(double x, double f); 
48 float FindKneeFun(float x, float y); 
49 uint8 Dither(float v, int x, int y); 
50 
51 // This fog colour is used when de-fogging. 
52 void ComputeFogColour(float& fogR, float& fogG, float& fogB, const IMF::Array<IMF::Rgba>& pixels); 
53 
54 struct Gamma 
55
56 Gamma(float gamma, float exposure, float defog, float kneeLow, float kneeHigh); 
57 float operator()(half h); 
58 float invg, m, d, kl, f, s
59 }; 
60
61 
62 
63float EXR::KneeFun(double x, double f
64
65 return float (IMATH::Math<double>::log(x: x * f + 1.0) / f); 
66
67 
68  
69float EXR::FindKneeFun(float x, float y
70
71 float f0 = 0; float f1 = 1
72 while (KneeFun(x, f: f1) > y
73
74 f0 = f1
75 f1 = f1 * 2
76
77 
78 for (int i = 0; i < 30; ++i
79
80 float f2 = (f0 + f1) / 2.0f
81 float y2 = KneeFun(x, f: f2); 
82 if (y2 < y
83 f1 = f2
84 else 
85 f0 = f2
86
87 
88 return (f0 + f1) / 2.0f
89
90 
91 
92void EXR::ComputeFogColour(float& fogR, float& fogG, float& fogB, const IMF::Array<IMF::Rgba>& pixels
93
94 double fogRd = 0.0
95 double fogGd = 0.0
96 double fogBd = 0.0
97 double WeightSum = 0.0
98 int numPixels = pixels.size(); 
99 
100 for (int j = 0; j < numPixels; ++j
101
102 const IMF::Rgba& rawPixel = pixels[j]; 
103 float weight = tMath::tSaturate(val: float(rawPixel.a)); // Makes sure transparent pixels don't contribute to the colour. 
104 
105 fogRd += rawPixel.r.isFinite() ? double(rawPixel.r) : 0.0
106 fogGd += rawPixel.g.isFinite() ? double(rawPixel.g) : 0.0
107 fogBd += rawPixel.b.isFinite() ? double(rawPixel.b) : 0.0
108 WeightSum += weight
109
110 
111 fogRd /= WeightSum; fogGd /= WeightSum; fogBd /= WeightSum
112 fogR = float(fogRd); fogG = float(fogGd); fogB = float(fogBd); 
113
114 
115 
116uint8 EXR::Dither(float v, int x, int y
117
118 static const float d[4][4] = 
119
120 { 00.0f/16.0f, 08.0f/16.0f, 02.0f/16.0f, 10.0f/16.0f }, 
121 { 12.0f/16.0f, 04.0f/16.0f, 14.0f/16.0f, 06.0f/16.0f }, 
122 { 03.0f/16.0f, 11.0f/16.0f, 01.0f/16.0f, 09.0f/16.0f }, 
123 { 15.0f/16.0f, 07.0f/16.0f, 13.0f/16.0f, 05.0f/16.0f
124 }; 
125 
126 return uint8(v + d[y&3][x&3]); 
127
128 
129 
130EXR::Gamma::Gamma(float gamma, float exposure, float defog, float kneeLow, float kneeHigh) : 
131 invg(1.0f/gamma), 
132 m(tMath::tPow(a: 2.0f, b: exposure + 2.47393f)), 
133 d(defog), 
134 kl(tMath::tPow(a: 2.0f, b: kneeLow)), 
135 f(FindKneeFun(x: tMath::tPow(a: 2.0f, b: kneeHigh) - kl, y: tMath::tPow(a: 2.0f, b: 3.5f) - kl)), 
136 s(255.0f * tMath::tPow(a: 2.0f, b: -3.5f * invg)) 
137
138
139 
140 
141float EXR::Gamma::operator()(half h
142
143 float x = tMath::tMax(a: 0.0f, b: (h - d)); // Defog 
144 x *= m; // Exposure 
145 if (x > kl) x = kl + KneeFun(x: x - kl, f); // Knee 
146 x = tMath::tPow(a: x, b: invg); // Gamma 
147 return tMath::tClamp(val: x*s, min: 0.0f, max: 255.0f); // Clamp 
148
149 
150 
151bool tImageEXR::Load(const tString& exrFile, const LoadParams& loadParams
152
153 Clear(); 
154 if (tSystem::tGetFileType(file: exrFile) != tSystem::tFileType::EXR
155 return false
156 
157 if (!tFileExists(file: exrFile)) 
158 return false
159 
160 float defog = loadParams.Defog
161 float exposure = loadParams.Exposure
162 float kneeLow = loadParams.KneeLow
163 float kneeHigh = loadParams.KneeHigh
164 float gamma = loadParams.Gamma
165 
166 // Leave two cores free unless we are on a three core or lower machine, in which case we always use a min of 2 threads. 
167 int numThreads = tMath::tClampMin(val: (tSystem::tGetNumCores()) - 2, min: 2); 
168 setGlobalThreadCount(numThreads); 
169 
170 int outZsize = 0
171 Header outHeader
172 IMF::Array<IMF::Rgba> pixels
173 IMF::Array<float*> zbuffer
174 IMF::Array<uint> sampleCount
175 
176 MultiPartInputFile mpfile(exrFile.Chr()); 
177 int numParts = mpfile.parts(); 
178 if (numParts <= 0
179 return false
180 
181 for (int partNum = 0; partNum < numParts; partNum++) 
182
183 try 
184
185 bool preview = false
186 int lx = -1; int ly = -1; // For tiled image shows level (lx,ly) 
187 bool compositeDeep = true
188 
189 EXR::loadImage 
190
191 fileName: exrFile.Chr(), 
192 channel: nullptr, // Channels. Null means all. 
193 layer: nullptr, // Layers. O means first one. 
194 preview, lx, ly
195 partnum: partNum
196 zsize&: outZsize, header&: outHeader
197 pixels, zbuffer, sampleCount
198 deepComp: compositeDeep 
199 ); 
200
201 catch (IEX_NAMESPACE::BaseExc& err
202
203 tPrintf(f: "Error: Can't read exr file. %s\n", err.what()); 
204 return false
205
206 
207 const Box2i& displayWindow = outHeader.displayWindow(); 
208 const Box2i& dataWindow = outHeader.dataWindow(); 
209 float pixelAspectRatio = outHeader.pixelAspectRatio(); 
210 int w = displayWindow.max.x - displayWindow.min.x + 1
211 int h = displayWindow.max.y - displayWindow.min.y + 1
212 int dx = dataWindow.min.x - displayWindow.min.x
213 int dy = dataWindow.min.y - displayWindow.min.y
214 int width = dataWindow.max.x - dataWindow.min.x + 1
215 int height = dataWindow.max.y - dataWindow.min.y + 1
216 
217 // Set width, height, and allocate and set Pixels. 
218 tFrame* newFrame = new tFrame
219 newFrame->PixelFormatSrc = tPixelFormat::OPENEXR
220 newFrame->Width = width
221 newFrame->Height = height
222 newFrame->Pixels = new tPixel4b[width*height]; 
223 
224 // Map floating-point pixel values 0.0 and 1.0 to the display's white and black respectively. 
225 // if bool zerooneexposure true. 
226 // exposure = 1.02607f; 
227 // kneeHigh = 3.5f; 
228  
229 float fogR = 0.0f
230 float fogG = 0.0f
231 float fogB = 0.0f
232 
233 // Save some time if we can. 
234 if (defog > 0.0f
235 EXR::ComputeFogColour(fogR, fogG, fogB, pixels); 
236 
237 halfFunction<float> redGamma(EXR::Gamma(gamma, exposure, defog * fogR, kneeLow, kneeHigh), -HALF_MAX, HALF_MAX, 0.0f, 255.0f, 0.0f, 0.0f); 
238 halfFunction<float> grnGamma(EXR::Gamma(gamma, exposure, defog * fogG, kneeLow, kneeHigh), -HALF_MAX, HALF_MAX, 0.0f, 255.0f, 0.0f, 0.0f); 
239 halfFunction<float> bluGamma(EXR::Gamma(gamma, exposure, defog * fogB, kneeLow, kneeHigh), -HALF_MAX, HALF_MAX, 0.0f, 255.0f, 0.0f, 0.0f); 
240 
241 // Conversion from raw pixel data to data for the OpenGL frame buffer: 
242 // 1) Compensate for fogging by subtracting defog from the raw pixel values. 
243 // 2) Multiply the defogged pixel values by 2^(exposure + 2.47393). 
244 // 3) Values that are now 1.0 are called "middle gray". If defog and exposure are both set to 0.0, then middle gray 
245 // corresponds to a raw pixel value of 0.18. In step 6, middle gray values will be mapped to an intensity 3.5 
246 // f-stops below the display's maximum intensity. 
247 // 4) Apply a knee function. The knee function has two parameters, kneeLow and kneeHigh. Pixel values below 
248 // 2^kneeLow are not changed by the knee function. Pixel values above kneeLow are lowered according to a 
249 // logarithmic curve, such that the value 2^kneeHigh is mapped to 2^3.5. (In step 6 this value will be mapped to 
250 // the the display's maximum intensity.) 
251 // 5) Gamma-correct the pixel values, according to the screen's gamma. (We assume that the gamma curve is a simple 
252 // power function.) 
253 // 6) Scale the values such that middle gray pixels are mapped to a frame buffer value that is 3.5 f-stops below the 
254 // display's maximum intensity. (84.65 if the screen's gamma is 2.2) 
255 // 7) Clamp the values to [0, 255]. 
256 // 
257 // Texview has 0,0 at bottom-left. Rows start from bottom. 
258 int p = 0
259 for (int yi = height-1; yi >= 0; yi--) 
260
261 for (int xi = 0; xi < width; xi++) 
262
263 int idx = yi*width + xi
264 const IMF::Rgba& rawPixel = pixels[idx]; 
265 newFrame->Pixels[p++] = tPixel4b 
266 ( 
267 EXR::Dither( v: redGamma(rawPixel.r), x: xi, y: yi ), 
268 EXR::Dither( v: grnGamma(rawPixel.g), x: xi, y: yi ), 
269 EXR::Dither( v: bluGamma(rawPixel.b), x: xi, y: yi ), 
270 uint8( tMath::tClamp( val: tMath::tFloatToInt(val: float(rawPixel.a)*255.0f), min: 0, max: 0xFF ) ) 
271 ); 
272
273
274 
275 Frames.Append(item: newFrame); 
276
277 
278 PixelFormat = tPixelFormat::R8G8B8A8
279 PixelFormatSrc = tPixelFormat::OPENEXR
280 ColourProfile = tColourProfile::sRGB
281 ColourProfileSrc = tColourProfile::HDRa
282 return true
283
284 
285 
286bool tImage::tImageEXR::Set(tList<tFrame>& srcFrames, bool stealFrames
287
288 Clear(); 
289 if (srcFrames.GetNumItems() <= 0
290 return false
291 
292 PixelFormatSrc = srcFrames.Head()->PixelFormatSrc
293 PixelFormat = tPixelFormat::R8G8B8A8
294 ColourProfileSrc = tColourProfile::sRGB; // We assume srcFrames must be sRGB. 
295 ColourProfile = tColourProfile::sRGB
296 
297 if (stealFrames
298
299 while (tFrame* frame = srcFrames.Remove()) 
300 Frames.Append(item: frame); 
301
302 else 
303
304 for (tFrame* frame = srcFrames.Head(); frame; frame = frame->Next()) 
305 Frames.Append(item: new tFrame(*frame)); 
306
307 
308 return true
309
310 
311 
312bool tImageEXR::Set(tPixel4b* pixels, int width, int height, bool steal
313
314 Clear(); 
315 if (!pixels || (width <= 0) || (height <= 0)) 
316 return false
317 
318 tFrame* frame = new tFrame(); 
319 if (steal
320 frame->StealFrom(src: pixels, width, height); 
321 else 
322 frame->Set(srcPixels: pixels, width, height); 
323 Frames.Append(item: frame); 
324 
325 PixelFormatSrc = tPixelFormat::R8G8B8A8
326 PixelFormat = tPixelFormat::R8G8B8A8
327 ColourProfileSrc = tColourProfile::sRGB; // We assume pixels must be sRGB. 
328 ColourProfile = tColourProfile::sRGB
329 
330 return true
331
332 
333 
334bool tImageEXR::Set(tFrame* frame, bool steal
335
336 Clear(); 
337 if (!frame || !frame->IsValid()) 
338 return false
339 
340 PixelFormatSrc = frame->PixelFormatSrc
341 PixelFormat = tPixelFormat::R8G8B8A8
342 ColourProfileSrc = tColourProfile::sRGB; // We assume frame must be sRGB. 
343 ColourProfile = tColourProfile::sRGB
344 
345 if (steal
346 Frames.Append(item: frame); 
347 else 
348 Frames.Append(item: new tFrame(*frame)); 
349 
350 return true
351
352 
353 
354bool tImageEXR::Set(tPicture& picture, bool steal
355
356 Clear(); 
357 if (!picture.IsValid()) 
358 return false
359 
360 PixelFormatSrc = picture.PixelFormatSrc
361 PixelFormat = tPixelFormat::R8G8B8A8
362 // We don't know colour profile of tPicture. 
363 
364 // This is worth some explanation. If steal is true the picture becomes invalid and the 
365 // 'set' call will steal the stolen pixels. If steal is false GetPixels is called and the 
366 // 'set' call will memcpy them out... which makes sure the picture is still valid after and 
367 // no-one is sharing the pixel buffer. We don't check the success of 'set' because it must 
368 // succeed if picture was valid. 
369 tPixel4b* pixels = steal ? picture.StealPixels() : picture.GetPixels(); 
370 bool success = Set(pixels, width: picture.GetWidth(), height: picture.GetHeight(), steal); 
371 tAssert(success); 
372 return true
373
374 
375 
376tFrame* tImageEXR::GetFrame(bool steal
377
378 if (!IsValid()) 
379 return nullptr
380 
381 return steal ? Frames.Remove() : new tFrame( *Frames.First() ); 
382
383