1 /*
2 Copyright (c) 2016-2021 Timur Gafarov
3 
4 Boost Software License - Version 1.0 - August 17th, 2003
5 
6 Permission is hereby granted, free of charge, to any person or organization
7 obtaining a copy of the software and accompanying documentation covered by
8 this license (the "Software") to use, reproduce, display, distribute,
9 execute, and transmit the Software, and to prepare derivative works of the
10 Software, and to permit third-parties to whom the Software is furnished to
11 do so, all subject to the following:
12 
13 The copyright notices in the Software and this entire statement, including
14 the above license grant, this restriction and the following disclaimer,
15 must be included in all copies of the Software, in whole or in part, and
16 all derivative works of the Software, unless such copies or derivative
17 works are solely in the form of machine-executable object code generated by
18 a source language processor.
19 
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
23 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
24 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
25 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26 DEALINGS IN THE SOFTWARE.
27 */
28 
29 /**
30  * Decode and encode Radiance HDR/RGBE images
31  *
32  * Copyright: Timur Gafarov 2016-2021.
33  * License: $(LINK2 boost.org/LICENSE_1_0.txt, Boost License 1.0).
34  * Authors: Timur Gafarov
35  */
36 module dlib.image.io.hdr;
37 
38 import std.stdio;
39 import std.math;
40 import dlib.core.memory;
41 import dlib.core.stream;
42 import dlib.core.compound;
43 import dlib.container.array;
44 import dlib.filesystem.local;
45 import dlib.image.color;
46 import dlib.image.image;
47 import dlib.image.hdri;
48 import dlib.image.io;
49 import dlib.math.utils;
50 
51 struct ColorRGBE
52 {
53     ubyte r;
54     ubyte g;
55     ubyte b;
56     ubyte e;
57 }
58 
59 ColorRGBE floatToRGBE(Color4f c)
60 {
61     ColorRGBE rgbe;
62 
63     float v = c.r;
64     if (c.g > v)
65         v = c.g;
66     if (c.b > v)
67         v = c.b;
68     if (v < EPSILON)
69     {
70         rgbe.r = 0;
71         rgbe.g = 0;
72         rgbe.b = 0;
73         rgbe.e = 0;
74     }
75     else
76     {
77         int e;
78         v = frexp(v, e) * 256.0f / v;
79         rgbe.r = cast(ubyte)(c.r * v);
80         rgbe.g = cast(ubyte)(c.g * v);
81         rgbe.b = cast(ubyte)(c.b * v);
82         rgbe.e = cast(ubyte)(e + 128);
83     }
84 
85     return rgbe;
86 }
87 
88 void readLineFromStream(InputStream istrm, ref Array!char line)
89 {
90     char c;
91     do
92     {
93         if (istrm.readable)
94             istrm.readBytes(&c, 1);
95         else
96             break;
97 
98         if (c != '\n')
99             line.append(c);
100     }
101     while(c != '\n');
102 }
103 
104 class HDRLoadException: ImageLoadException
105 {
106     this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null)
107     {
108         super(msg, file, line, next);
109     }
110 }
111 
112 /**
113  * Load HDR from file using local FileSystem.
114  * Causes GC allocation
115  */
116 SuperHDRImage loadHDR(string filename)
117 {
118     InputStream input = openForInput(filename);
119     auto img = loadHDR(input);
120     input.close();
121     return img;
122 }
123 
124 /**
125  * Load HDR from stream using default image factory.
126  * Causes GC allocation
127  */
128 SuperHDRImage loadHDR(InputStream istrm)
129 {
130     Compound!(SuperHDRImage, string) res =
131         loadHDR(istrm, defaultHDRImageFactory);
132     if (res[0] is null)
133         throw new HDRLoadException(res[1]);
134     else
135         return res[0];
136 }
137 
138 /**
139  * Load HDR from stream using specified image factory.
140  * GC-free
141  */
142 Compound!(SuperHDRImage, string) loadHDR(
143     InputStream istrm,
144     SuperHDRImageFactory imgFac)
145 {
146     SuperHDRImage img = null;
147 
148     Compound!(SuperHDRImage, string) error(string errorMsg)
149     {
150         if (img)
151         {
152             img.free();
153             img = null;
154         }
155         return compound(img, errorMsg);
156     }
157 
158     char[11] magic;
159     istrm.fillArray(magic);
160     if (magic != "#?RADIANCE\n")
161     {
162         if (magic[0..7] == "#?RGBE\n")
163         {
164             istrm.position = 7;
165         }
166         else
167             return error("loadHDR error: signature check failed");
168     }
169 
170     // Read header
171     Array!char line;
172     do
173     {
174         line.free();
175         readLineFromStream(istrm, line);
176         // TODO: parse assignments
177     }
178     while (line.length);
179 
180     // Read resolution line
181     line.free();
182     readLineFromStream(istrm, line);
183 
184     char xsign, ysign;
185     uint width, height;
186     int count = sscanf(line.data.ptr, "%cY %u %cX %u", &ysign, &height, &xsign, &width);
187     if (count != 4)
188         return error("loadHDR error: invalid resolution line");
189 
190     // Read pixel data
191     ubyte[] dataRGBE = New!(ubyte[])(width * height * 4);
192     ubyte[4] col;
193     for (uint y = 0; y < height; y++)
194     {
195         istrm.readBytes(col.ptr, 4);
196         //Header of 0x2, 0x2 is new Radiance RLE scheme
197         if (col[0] == 2 && col[1] == 2 && col[2] >= 0)
198         {
199             // Each channel is run length encoded seperately
200             for (uint chi = 0; chi < 4; chi++)
201             {
202                 uint x = 0;
203                 while (x < width)
204                 {
205                     uint start = (y * width + x) * 4;
206                     ubyte num = 0;
207                     istrm.readBytes(&num, 1);
208                     if (num <= 128) // No run, just read the values
209                     {
210                         for (uint i = 0; i < num; i++)
211                         {
212                             ubyte value;
213                             istrm.readBytes(&value, 1);
214                             dataRGBE[start + chi + i*4] = value;
215                         }
216                     }
217                     else // We have a run, so get the value and set all the values for this run
218                     {
219                         ubyte value;
220                         istrm.readBytes(&value, 1);
221                         num -= 128;
222                         for (uint i = 0; i < num; i++)
223                         {
224                             dataRGBE[start + chi + i*4] = value;
225                         }
226                     }
227 
228                     x += num;
229                 }
230             }
231         }
232         else // Old Radiance RLE scheme
233         {
234             for (uint x = 0; x < width; x++)
235             {
236                 if (x > 0)
237                     istrm.readBytes(col.ptr, 4);
238 
239                 uint prev = (y * width + x - 1) * 4;
240                 uint start = (y * width + x) * 4;
241 
242                 // Check for the RLE header for this scanline
243                 if (col[0] == 1 && col[1] == 1 && col[2] == 1)
244                 {
245                     // Do the run
246                     int num = (cast(int)col[3]) & 0xFF;
247 
248                     ubyte r = dataRGBE[prev];
249                     ubyte g = dataRGBE[prev + 1];
250                     ubyte b = dataRGBE[prev + 2];
251                     ubyte e = dataRGBE[prev + 3];
252 
253                     for (uint i = 0; i < num; i++)
254                     {
255                         dataRGBE[start + i*4 + 0] = r;
256                         dataRGBE[start + i*4 + 1] = g;
257                         dataRGBE[start + i*4 + 2] = b;
258                         dataRGBE[start + i*4 + 3] = e;
259                     }
260 
261                     x += num-1;
262                 }
263                 else // No runs here, just read the data
264                 {
265                     dataRGBE[start] = col[0];
266                     dataRGBE[start + 1] = col[1];
267                     dataRGBE[start + 2] = col[2];
268                     dataRGBE[start + 3] = col[3];
269                 }
270             }
271         }
272     }
273 
274     // Convert RGBE to IEEE floats
275     img = imgFac.createImage(width, height);
276     foreach(y; 0..height)
277     foreach(x; 0..width)
278     {
279         size_t start = (width * y + x) * 4;
280         ubyte exponent = dataRGBE[start + 3];
281         if (exponent == 0)
282         {
283             img[x, y] = Color4f(0, 0, 0, 1);
284         }
285         else
286         {
287             float v = ldexp(1.0, cast(int)exponent - (128 + 8));
288             float r = cast(float)(dataRGBE[start]) * v;
289             float g = cast(float)(dataRGBE[start + 1]) * v;
290             float b = cast(float)(dataRGBE[start + 2]) * v;
291             img[x, y] = Color4f(r, g, b, 1);
292         }
293     }
294 
295     Delete(dataRGBE);
296 
297     return compound(img, "");
298 }
299 
300 /**
301  * Save HDR to file using local FileSystem.
302  * Causes GC allocation
303  */
304 void saveHDR(SuperHDRImage img, string filename)
305 {
306     OutputStream output = openForOutput(filename);
307     Compound!(bool, string) res = saveHDR(img, output);
308     output.close();
309 }
310 
311 /**
312  * Save HDR to stream.
313  * GC-free
314  */
315 Compound!(bool, string) saveHDR(SuperHDRImage img, OutputStream output)
316 {
317     Compound!(bool, string) error(string errorMsg)
318     {
319         return compound(false, errorMsg);
320     }
321 
322     // Signature and header
323     string hdrStart = "#?RADIANCE\n\n"; // double LF needed to mark end of header
324     output.writeArray(hdrStart);
325 
326     // Resolution line
327     char[256] resolution;
328     int len = sprintf(resolution.ptr, "-Y %d +X %d\n", img.height, img.width);
329     output.writeArray(resolution[0..len]);
330 
331     ubyte[] scanline = New!(ubyte[])(img.width * 4);
332 
333     for (uint y = 0; y < img.height; y++)
334     {
335         ubyte[4] scanlineHeader;
336         scanlineHeader[0] = 2;
337         scanlineHeader[1] = 2;
338         scanlineHeader[2] = cast(ubyte)(img.width >> 8);
339         scanlineHeader[3] = cast(ubyte)(img.width & 0xFF);
340         output.writeArray(scanlineHeader);
341 
342         // Convert a scanline to RGBE decomposing channels
343         for (uint x = 0; x < img.width; x++)
344         {
345             ColorRGBE rgbe = img[x, y].floatToRGBE;
346             scanline[x] = rgbe.r;
347             scanline[x + img.width] = rgbe.g;
348             scanline[x + img.width * 2] = rgbe.b;
349             scanline[x + img.width * 3] = rgbe.e;
350         }
351 
352         // Write channels
353         foreach(ch; 0..4)
354         {
355             uint offset = ch * img.width;
356             writeBufferRLE(output, scanline[offset..offset+img.width]);
357         }
358     }
359 
360     Delete(scanline);
361 
362     return compound(true, "");
363 }
364 
365 /*
366  * Based on code by Bruce Walter:
367  * http://www.graphics.cornell.edu/~bjw/rgbe/rgbe.c
368  */
369 void writeBufferRLE(OutputStream output, ubyte[] data)
370 {
371     enum MINRUNLENGTH = 4;
372     int cur, beg_run, run_count, old_run_count, nonrun_count;
373     ubyte[2] buf;
374 
375     cur = 0;
376     while(cur < data.length)
377     {
378         beg_run = cur;
379 
380         // find next run of length at least 4 if one exists
381         run_count = old_run_count = 0;
382         while((run_count < MINRUNLENGTH) && (beg_run < data.length))
383         {
384             beg_run += run_count;
385             old_run_count = run_count;
386             run_count = 1;
387             while( (beg_run + run_count < data.length) && (run_count < 127)
388                 && (data[beg_run] == data[beg_run + run_count]))
389 	            run_count++;
390         }
391 
392         // if data before next big run is a short run then write it as such
393         if ((old_run_count > 1) && (old_run_count == beg_run - cur))
394         {
395             buf[0] = cast(ubyte)(128 + old_run_count); // write short run
396             buf[1] = data[cur];
397             output.writeArray(buf);
398             cur = beg_run;
399         }
400 
401         // write out bytes until we reach the start of the next run
402         while(cur < beg_run)
403         {
404             nonrun_count = beg_run - cur;
405             if (nonrun_count > 128)
406 	            nonrun_count = 128;
407             buf[0] = cast(ubyte)nonrun_count;
408             output.writeBytes(buf.ptr, 1);
409             output.writeBytes(&data[cur], nonrun_count);
410             cur += nonrun_count;
411         }
412 
413         // write out next run if one was found
414         if (run_count >= MINRUNLENGTH)
415         {
416             buf[0] = cast(ubyte)(128 + run_count);
417             buf[1] = data[beg_run];
418             output.writeArray(buf);
419             cur += run_count;
420         }
421     }
422 }