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 }