| 1 | // tImageHDR.cpp  |
| 2 | //  |
| 3 | // This knows how to load and save a Radiance High Dynamic Range image (.hdr or .rgbe). It knows the details of the hdr  |
| 4 | // file format and loads the data into a tPixel array. These tPixels may be 'stolen' by the tPicture's constructor if  |
| 5 | // an HDR file is specified. After the array is stolen the tImageHDR 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 conversion code for hdr data is a modified version of code from Radiance. Here is the licence.  |
| 18 | //  |
| 19 | // The Radiance Software License, Version 1.0  |
| 20 | // Copyright (c) 1990 - 2015 The Regents of the University of California, through Lawrence Berkeley National Laboratory. All rights reserved.  |
| 21 | //  |
| 22 | // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:  |
| 23 | // 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.  |
| 24 | // 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the  |
| 25 | // documentation and/or other materials provided with the distribution.  |
| 26 | // 3. The end-user documentation included with the redistribution, if any, must include the following acknowledgment:  |
| 27 | // "This product includes Radiance software (http://radsite.lbl.gov/) developed by the Lawrence Berkeley National Laboratory (http://www.lbl.gov/)."  |
| 28 | // Alternately, this acknowledgment may appear in the software itself, if and wherever such third-party acknowledgments normally appear.  |
| 29 | // 4. The names "Radiance," "Lawrence Berkeley National Laboratory" and "The Regents of the University of California" must not be used to endorse  |
| 30 | // or promote products derived from this software without prior written permission. For written permission, please contact radiance@radsite.lbl.gov.  |
| 31 | // 5. Products derived from this software may not be called "Radiance", nor may "Radiance" appear in their name, without prior written permission of  |
| 32 | // Lawrence Berkeley National Laboratory.  |
| 33 | //  |
| 34 | // THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY  |
| 35 | // AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Lawrence Berkeley National Laboratory OR ITS CONTRIBUTORS BE LIABLE FOR ANY  |
| 36 | // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;  |
| 37 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT  |
| 38 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  |
| 39 |   |
| 40 | #include <Foundation/tStandard.h>  |
| 41 | #include <Foundation/tString.h>  |
| 42 | #include <System/tFile.h>  |
| 43 | #include "Image/tImageHDR.h"  |
| 44 | #include "Image/tPicture.h"  |
| 45 | using namespace tSystem;  |
| 46 | namespace tImage  |
| 47 | {  |
| 48 |   |
| 49 |   |
| 50 | void tImageHDR::SetupGammaTables(float gammaCorr)  |
| 51 | {  |
| 52 | double gamma = double(gammaCorr);  |
| 53 | if (GammaTable)  |
| 54 | return;  |
| 55 |   |
| 56 | double invGamma = 1.0 / gamma;  |
| 57 |   |
| 58 | // This table is used to convert from Radiance format to 24-bit.  |
| 59 | GammaTable = (uint8(*)[256])malloc(size: (MaxGammaShift+1)*256);  |
| 60 | double mult = 1.0/256.0;  |
| 61 | for (int i = 0; i <= MaxGammaShift; i++)  |
| 62 | {  |
| 63 | for (int j = 0; j < 256; j++)  |
| 64 | GammaTable[i][j] = uint8(uint32( 256.0 * pow( x: (j+0.5)*mult, y: invGamma ) ));  |
| 65 | mult *= 0.5;  |
| 66 | }  |
| 67 |   |
| 68 | // These tables are used to go from 24-bit to a Radiance-encoded value.  |
| 69 | MantissaTable = (uint8*)malloc(size: 256);  |
| 70 | ExponentTable = (uint8*)malloc(size: 256);  |
| 71 | int i = 0;  |
| 72 | mult = 256.0;  |
| 73 | for (int j = 256; j--; )  |
| 74 | {  |
| 75 | while ((MantissaTable[j] = uint8(uint32(mult * pow(x: (j+0.5)/256.0, y: gamma)))) < 128)  |
| 76 | {  |
| 77 | i++;  |
| 78 | mult *= 2.0;  |
| 79 | }  |
| 80 | ExponentTable[j] = i;  |
| 81 | }  |
| 82 | }  |
| 83 |   |
| 84 |   |
| 85 | void tImageHDR::CleanupGammaTables()  |
| 86 | {  |
| 87 | if (MantissaTable) free(ptr: MantissaTable);  |
| 88 | MantissaTable = nullptr;  |
| 89 | if (ExponentTable) free(ptr: ExponentTable);  |
| 90 | ExponentTable = nullptr;  |
| 91 | if (GammaTable) free(ptr: GammaTable);  |
| 92 | GammaTable = nullptr;  |
| 93 | }  |
| 94 |   |
| 95 |   |
| 96 | bool tImageHDR::LegacyReadRadianceColours(tPixel4b* scanline, int len)  |
| 97 | {  |
| 98 | int rshift = 0;  |
| 99 | int i;  |
| 100 |   |
| 101 | while (len > 0)  |
| 102 | {  |
| 103 | scanline[0].R = GetB();  |
| 104 | scanline[0].G = GetB();  |
| 105 | scanline[0].B = GetB();  |
| 106 | scanline[0].A = i = GetB();  |
| 107 | if (i == EOF)  |
| 108 | return false;  |
| 109 | if (scanline[0].R == 1 && scanline[0].G == 1 && scanline[0].B == 1)  |
| 110 | {  |
| 111 | for (i = scanline[0].A << rshift; i > 0; i--)  |
| 112 | {  |
| 113 | scanline[0] = scanline[-1];  |
| 114 | scanline++;  |
| 115 | len--;  |
| 116 | }  |
| 117 | rshift += 8;  |
| 118 | }  |
| 119 | else  |
| 120 | {  |
| 121 | scanline++;  |
| 122 | len--;  |
| 123 | rshift = 0;  |
| 124 | }  |
| 125 | }  |
| 126 | return true;  |
| 127 | }  |
| 128 |   |
| 129 |   |
| 130 | bool tImageHDR::ReadRadianceColours(tPixel4b* scanline, int len)  |
| 131 | {  |
| 132 | int i, j;  |
| 133 | int code, val;  |
| 134 |   |
| 135 | // Determine if scanline is legacy and needs to be processed the old way.  |
| 136 | if ((len < MinScanLen) | (len > MaxScanLen))  |
| 137 | return LegacyReadRadianceColours(scanline, len);  |
| 138 |   |
| 139 | i = GetB();  |
| 140 | if (i == EOF)  |
| 141 | return false;  |
| 142 | if (i != 2)  |
| 143 | {  |
| 144 | UngetB(v: i);  |
| 145 | return LegacyReadRadianceColours(scanline, len);  |
| 146 | }  |
| 147 | scanline[0].G = GetB();  |
| 148 | scanline[0].B = GetB();  |
| 149 | i = GetB();  |
| 150 | if (i == EOF)  |
| 151 | return false;  |
| 152 |   |
| 153 | if (scanline[0].G != 2 || scanline[0].B & 128)  |
| 154 | {  |
| 155 | scanline[0].R = 2;  |
| 156 | scanline[0].A = i;  |
| 157 | return LegacyReadRadianceColours(scanline: scanline+1, len: len-1);  |
| 158 | }  |
| 159 | if ((scanline[0].B << 8 | i) != len)  |
| 160 | return false;  |
| 161 |   |
| 162 | // Read each component.  |
| 163 | for (i = 0; i < 4; i++)  |
| 164 | {  |
| 165 | for (j = 0; j < len; )  |
| 166 | {  |
| 167 | code = GetB();  |
| 168 | if (code == EOF)  |
| 169 | return false;  |
| 170 |   |
| 171 | if (code > 128)  |
| 172 | {  |
| 173 | // RLE run.  |
| 174 | code &= 127;  |
| 175 | val = GetB();  |
| 176 | if (val == EOF)  |
| 177 | return false;  |
| 178 | if (j + code > len)  |
| 179 | return false; // Overrun.  |
| 180 | while (code--)  |
| 181 | scanline[j++].E[i] = val;  |
| 182 | }  |
| 183 | else  |
| 184 | {  |
| 185 | // New non-RLE colour.  |
| 186 | if (j + code > len)  |
| 187 | return false; // Overrun.  |
| 188 | while (code--)  |
| 189 | {  |
| 190 | val = GetB();  |
| 191 | if (val == EOF)  |
| 192 | return false;  |
| 193 | scanline[j++].E[i] = val;  |
| 194 | }  |
| 195 | }  |
| 196 | }  |
| 197 | }  |
| 198 | return true;  |
| 199 | }  |
| 200 |   |
| 201 |   |
| 202 | bool tImageHDR::ConvertRadianceToGammaCorrected(tPixel4b* scan, int len)  |
| 203 | {  |
| 204 | if (!GammaTable)  |
| 205 | return false;  |
| 206 |   |
| 207 | while (len-- > 0)  |
| 208 | {  |
| 209 | int expo = scan[0].A - ExpXS;  |
| 210 | if (expo < -MaxGammaShift)  |
| 211 | {  |
| 212 | if (expo < -MaxGammaShift-8)  |
| 213 | {  |
| 214 | scan[0].MakeBlack();  |
| 215 | }  |
| 216 | else  |
| 217 | {  |
| 218 | int i = (-MaxGammaShift-1) - expo;  |
| 219 | scan[0].R = GammaTable[MaxGammaShift][ ((scan[0].R >> i) + 1) >> 1 ];  |
| 220 | scan[0].G = GammaTable[MaxGammaShift][ ((scan[0].G >> i) + 1) >> 1 ];  |
| 221 | scan[0].B = GammaTable[MaxGammaShift][ ((scan[0].B >> i) + 1) >> 1 ];  |
| 222 | }  |
| 223 | }  |
| 224 | else if (expo > 0)  |
| 225 | {  |
| 226 | if (expo > 8)  |
| 227 | {  |
| 228 | scan[0].MakeWhite();  |
| 229 | }  |
| 230 | else  |
| 231 | {  |
| 232 | int i;  |
| 233 | i = (scan[0].R<<1 | 1) << (expo-1); scan[0].R = i > 255 ? 255 : GammaTable[0][i];  |
| 234 | i = (scan[0].G<<1 | 1) << (expo-1); scan[0].G = i > 255 ? 255 : GammaTable[0][i];  |
| 235 | i = (scan[0].B<<1 | 1) << (expo-1); scan[0].B = i > 255 ? 255 : GammaTable[0][i];  |
| 236 | }  |
| 237 | }  |
| 238 | else  |
| 239 | {  |
| 240 | scan[0].R = GammaTable[-expo][scan[0].R];  |
| 241 | scan[0].G = GammaTable[-expo][scan[0].G];  |
| 242 | scan[0].B = GammaTable[-expo][scan[0].B];  |
| 243 | }  |
| 244 | scan[0].A = ExpXS;  |
| 245 | scan++;  |
| 246 | }  |
| 247 | return true;  |
| 248 | }  |
| 249 |   |
| 250 |   |
| 251 | void tImageHDR::AdjustExposure(tPixel4b* scan, int len, int adjust)  |
| 252 | {  |
| 253 | // Shift a scanline of colors by 2^adjust.  |
| 254 | if (adjust == 0)  |
| 255 | return;  |
| 256 |   |
| 257 | int minexp = adjust < 0 ? -adjust : 0;  |
| 258 | while (len-- > 0)  |
| 259 | {  |
| 260 | if (scan[0].A <= minexp)  |
| 261 | scan[0].MakeZero();  |
| 262 | else  |
| 263 | scan[0].A += adjust;  |
| 264 | scan++;  |
| 265 | }  |
| 266 | }  |
| 267 |   |
| 268 |   |
| 269 | bool tImageHDR::Load(const tString& hdrFile, const LoadParams& loadParams)  |
| 270 | {  |
| 271 | Clear();  |
| 272 |   |
| 273 | if (tSystem::tGetFileType(file: hdrFile) != tSystem::tFileType::HDR)  |
| 274 | return false;  |
| 275 |   |
| 276 | if (!tFileExists(file: hdrFile))  |
| 277 | return false;  |
| 278 |   |
| 279 | int numBytes = 0;  |
| 280 | uint8* hdrFileInMemory = tLoadFile(file: hdrFile, buffer: nullptr, fileSize: &numBytes, appendEOF: true);  |
| 281 | bool success = Load(hdrFileInMemory, numBytes, loadParams);  |
| 282 | delete[] hdrFileInMemory;  |
| 283 |   |
| 284 | return success;  |
| 285 | }  |
| 286 |   |
| 287 |   |
| 288 | bool tImageHDR::Load(uint8* hdrFileInMemory, int numBytes, const LoadParams& loadParams)  |
| 289 | {  |
| 290 | Clear();  |
| 291 | if ((numBytes <= 0) || !hdrFileInMemory)  |
| 292 | return false;  |
| 293 |   |
| 294 | float gammaCorr = loadParams.Gamma;  |
| 295 | int exposureAdj = loadParams.Exposure;  |
| 296 | SetupGammaTables(gammaCorr);  |
| 297 |   |
| 298 | // Search for the first double 0x0A (linefeed).  |
| 299 | // Note that hdrFileInMemory has an extra EOF at the end. The (numBytes+1)th character.  |
| 300 | int doubleLFIndex = -1;  |
| 301 | for (int c = 0; c < numBytes; c++)  |
| 302 | {  |
| 303 | if ((hdrFileInMemory[c] == 0x0A) && (hdrFileInMemory[c+1] == 0x0A))  |
| 304 | {  |
| 305 | doubleLFIndex = c;  |
| 306 | break;  |
| 307 | }  |
| 308 | }  |
| 309 | if (doubleLFIndex == -1)  |
| 310 | {  |
| 311 | CleanupGammaTables();  |
| 312 | return false;  |
| 313 | }  |
| 314 |   |
| 315 | // We are not allowed any '\0' characters in the header. Some Mac-generated images have one!  |
| 316 | for (int c = 0; c < doubleLFIndex; c++)  |
| 317 | {  |
| 318 | if (hdrFileInMemory[c] == '\0')  |
| 319 | hdrFileInMemory[c] = '_';  |
| 320 | }  |
| 321 |   |
| 322 | char* foundY = tStd::tStrstr(s: (char*)hdrFileInMemory, r: "-Y" );  |
| 323 | char* foundX = tStd::tStrstr(s: (char*)hdrFileInMemory, r: "+X" );  |
| 324 | char* eolY = tStd::tStrchr(s: foundY, c: '\n');  |
| 325 | char* eolX = tStd::tStrchr(s: foundX, c: '\n');  |
| 326 | if (!eolX || (eolX != eolY))  |
| 327 | {  |
| 328 | CleanupGammaTables();  |
| 329 | return false;  |
| 330 | }  |
| 331 | *eolX = '\0';  |
| 332 | tString ((char*)hdrFileInMemory);  |
| 333 | *eolX = '\n';  |
| 334 | ReadP = (uint8*)(eolX+1);  |
| 335 |   |
| 336 | tList<tStringItem> lines;  |
| 337 | tStd::tExplode(components&: lines, src: header, divider: '\n');  |
| 338 | // Display the header lines.  |
| 339 | // for (tStringItem* headerLine = lines.First(); headerLine; headerLine = headerLine->Next())  |
| 340 | // tPrintf("HDR Info: %s\n", headerLine->Chr());  |
| 341 |   |
| 342 | tStringItem* resLine = lines.Last();  |
| 343 |   |
| 344 | tList<tStringItem> comps;  |
| 345 | tStd::tExplode(components&: comps, src: *resLine, divider: ' ');  |
| 346 | Height = comps.First()->Next()->AsInt();  |
| 347 | Width = comps.First()->Next()->Next()->Next()->AsInt();  |
| 348 |   |
| 349 | Pixels = new tPixel4b[Width*Height];  |
| 350 | tPixel4b* scanin = new tPixel4b[Width];  |
| 351 |   |
| 352 | bool ok = true;  |
| 353 | for (int y = Height-1; y >= 0; y--)  |
| 354 | {  |
| 355 | ok = ReadRadianceColours(scanline: scanin, len: Width);  |
| 356 | if (!ok)  |
| 357 | break;  |
| 358 |   |
| 359 | AdjustExposure(scan: scanin, len: Width, adjust: exposureAdj);  |
| 360 |   |
| 361 | ok = ConvertRadianceToGammaCorrected(scan: scanin, len: Width);  |
| 362 | if (!ok)  |
| 363 | break;  |
| 364 |   |
| 365 | WriteP = (uint8*)&Pixels[y * Width];  |
| 366 | for (int x = 0; x < Width; x++)  |
| 367 | {  |
| 368 | PutB(v: scanin[x].R);  |
| 369 | PutB(v: scanin[x].G);  |
| 370 | PutB(v: scanin[x].B);  |
| 371 | PutB(v: 255);  |
| 372 | }  |
| 373 | }  |
| 374 |   |
| 375 | CleanupGammaTables();  |
| 376 | delete[] scanin;  |
| 377 | if (!ok)  |
| 378 | {  |
| 379 | delete[] Pixels;  |
| 380 | Pixels = nullptr;  |
| 381 | return false;  |
| 382 | }  |
| 383 |   |
| 384 | PixelFormatSrc = tPixelFormat::RADIANCE;  |
| 385 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 386 | ColourProfileSrc = tColourProfile::HDRa; // The source pixels are HDR.  |
| 387 | ColourProfile = tColourProfile::sRGB; // The decoded pixels are in sRGB space.  |
| 388 | return true;  |
| 389 | }  |
| 390 |   |
| 391 |   |
| 392 | bool tImageHDR::Set(tPixel4b* pixels, int width, int height, bool steal)  |
| 393 | {  |
| 394 | Clear();  |
| 395 | if (!pixels || (width <= 0) || (height <= 0))  |
| 396 | return false;  |
| 397 |   |
| 398 | Width = width;  |
| 399 | Height = height;  |
| 400 | if (steal)  |
| 401 | {  |
| 402 | Pixels = pixels;  |
| 403 | }  |
| 404 | else  |
| 405 | {  |
| 406 | Pixels = new tPixel4b[Width*Height];  |
| 407 | tStd::tMemcpy(dest: Pixels, src: pixels, numBytes: Width*Height*sizeof(tPixel4b));  |
| 408 | }  |
| 409 |   |
| 410 | PixelFormatSrc = tPixelFormat::R8G8B8A8;  |
| 411 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 412 | ColourProfileSrc = tColourProfile::sRGB; // We assume pixels must be sRGB.  |
| 413 | ColourProfile = tColourProfile::sRGB;  |
| 414 |   |
| 415 | return true;  |
| 416 | }  |
| 417 |   |
| 418 |   |
| 419 | bool tImageHDR::Set(tFrame* frame, bool steal)  |
| 420 | {  |
| 421 | Clear();  |
| 422 | if (!frame || !frame->IsValid())  |
| 423 | return false;  |
| 424 |   |
| 425 | PixelFormatSrc = frame->PixelFormatSrc;  |
| 426 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 427 | ColourProfileSrc = tColourProfile::sRGB; // We assume frame must be sRGB.  |
| 428 | ColourProfile = tColourProfile::sRGB;  |
| 429 |   |
| 430 | Set(pixels: frame->GetPixels(steal), width: frame->Width, height: frame->Height, steal);  |
| 431 | if (steal)  |
| 432 | delete frame;  |
| 433 |   |
| 434 | return true;  |
| 435 | }  |
| 436 |   |
| 437 |   |
| 438 | bool tImageHDR::Set(tPicture& picture, bool steal)  |
| 439 | {  |
| 440 | Clear();  |
| 441 | if (!picture.IsValid())  |
| 442 | return false;  |
| 443 |   |
| 444 | PixelFormatSrc = picture.PixelFormatSrc;  |
| 445 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 446 | // We don't know colour profile of tPicture.  |
| 447 |   |
| 448 | // This is worth some explanation. If steal is true the picture becomes invalid and the  |
| 449 | // 'set' call will steal the stolen pixels. If steal is false GetPixels is called and the  |
| 450 | // 'set' call will memcpy them out... which makes sure the picture is still valid after and  |
| 451 | // no-one is sharing the pixel buffer. We don't check the success of 'set' because it must  |
| 452 | // succeed if picture was valid.  |
| 453 | tPixel4b* pixels = steal ? picture.StealPixels() : picture.GetPixels();  |
| 454 | bool success = Set(pixels, width: picture.GetWidth(), height: picture.GetHeight(), steal);  |
| 455 | tAssert(success);  |
| 456 | return true;  |
| 457 | }  |
| 458 |   |
| 459 |   |
| 460 | tFrame* tImageHDR::GetFrame(bool steal)  |
| 461 | {  |
| 462 | if (!IsValid())  |
| 463 | return nullptr;  |
| 464 |   |
| 465 | tFrame* frame = new tFrame();  |
| 466 | frame->PixelFormatSrc = PixelFormatSrc;  |
| 467 |   |
| 468 | if (steal)  |
| 469 | {  |
| 470 | frame->StealFrom(src: Pixels, width: Width, height: Height);  |
| 471 | Pixels = nullptr;  |
| 472 | }  |
| 473 | else  |
| 474 | {  |
| 475 | frame->Set(srcPixels: Pixels, width: Width, height: Height);  |
| 476 | }  |
| 477 |   |
| 478 | return frame;  |
| 479 | }  |
| 480 |   |
| 481 |   |
| 482 | tPixel4b* tImageHDR::StealPixels()  |
| 483 | {  |
| 484 | tPixel4b* pixels = Pixels;  |
| 485 | Pixels = nullptr;  |
| 486 | Clear();  |
| 487 | return pixels;  |
| 488 | }  |
| 489 |   |
| 490 |   |
| 491 | }  |
| 492 | |