| 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 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 | using namespace tSystem;  |
| 45 | namespace tImage  |
| 46 | {  |
| 47 |   |
| 48 |   |
| 49 | void tImageHDR::SetupGammaTables(float gammaCorr)  |
| 50 | {  |
| 51 | double gamma = double(gammaCorr);  |
| 52 | if (GammaTable)  |
| 53 | return;  |
| 54 |   |
| 55 | double invGamma = 1.0 / gamma;  |
| 56 |   |
| 57 | // This table is used to convert from Radiance format to 24-bit.  |
| 58 | GammaTable = (uint8(*)[256])malloc((MaxGammaShift+1)*256);  |
| 59 | double mult = 1.0/256.0;  |
| 60 | for (int i = 0; i <= MaxGammaShift; i++)  |
| 61 | {  |
| 62 | for (int j = 0; j < 256; j++)  |
| 63 | GammaTable[i][j] = uint8(uint32( 256.0 * pow( (j+0.5)*mult, invGamma ) ));  |
| 64 | mult *= 0.5;  |
| 65 | }  |
| 66 |   |
| 67 | // These tables are used to go from 24-bit to a Radiance-encoded value.  |
| 68 | MantissaTable = (uint8*)malloc(256);  |
| 69 | ExponentTable = (uint8*)malloc(256);  |
| 70 | int i = 0;  |
| 71 | mult = 256.0;  |
| 72 | for (int j = 256; j--; )  |
| 73 | {  |
| 74 | while ((MantissaTable[j] = uint8(uint32(mult * pow((j+0.5)/256.0, gamma)))) < 128)  |
| 75 | {  |
| 76 | i++;  |
| 77 | mult *= 2.0;  |
| 78 | }  |
| 79 | ExponentTable[j] = i;  |
| 80 | }  |
| 81 | }  |
| 82 |   |
| 83 |   |
| 84 | void tImageHDR::CleanupGammaTables()  |
| 85 | {  |
| 86 | if (MantissaTable) free(MantissaTable);  |
| 87 | MantissaTable = nullptr;  |
| 88 | if (ExponentTable) free(ExponentTable);  |
| 89 | ExponentTable = nullptr;  |
| 90 | if (GammaTable) free(GammaTable);  |
| 91 | GammaTable = nullptr;  |
| 92 | }  |
| 93 |   |
| 94 |   |
| 95 | bool tImageHDR::Load(const tString& hdrFile, float gamma, int exposure)  |
| 96 | {  |
| 97 | Clear();  |
| 98 |   |
| 99 | if (tSystem::tGetFileType(hdrFile) != tSystem::tFileType::HDR)  |
| 100 | return false;  |
| 101 |   |
| 102 | if (!tFileExists(hdrFile))  |
| 103 | return false;  |
| 104 |   |
| 105 | int numBytes = 0;  |
| 106 | uint8* hdrFileInMemory = tLoadFile(hdrFile, nullptr, &numBytes, true);  |
| 107 | bool success = Set(hdrFileInMemory, numBytes, gamma, exposure);  |
| 108 | delete[] hdrFileInMemory;  |
| 109 |   |
| 110 | return success;  |
| 111 | }  |
| 112 |   |
| 113 |   |
| 114 | bool tImageHDR::LegacyReadRadianceColours(tPixel* scanline, int len)  |
| 115 | {  |
| 116 | int rshift = 0;  |
| 117 | int i;  |
| 118 |   |
| 119 | while (len > 0)  |
| 120 | {  |
| 121 | scanline[0].R = GetB();  |
| 122 | scanline[0].G = GetB();  |
| 123 | scanline[0].B = GetB();  |
| 124 | scanline[0].A = i = GetB();  |
| 125 | if (i == EOF)  |
| 126 | return false;  |
| 127 | if (scanline[0].R == 1 && scanline[0].G == 1 && scanline[0].B == 1)  |
| 128 | {  |
| 129 | for (i = scanline[0].A << rshift; i > 0; i--)  |
| 130 | {  |
| 131 | scanline[0] = scanline[-1];  |
| 132 | scanline++;  |
| 133 | len--;  |
| 134 | }  |
| 135 | rshift += 8;  |
| 136 | }  |
| 137 | else  |
| 138 | {  |
| 139 | scanline++;  |
| 140 | len--;  |
| 141 | rshift = 0;  |
| 142 | }  |
| 143 | }  |
| 144 | return true;  |
| 145 | }  |
| 146 |   |
| 147 |   |
| 148 | bool tImageHDR::ReadRadianceColours(tPixel* scanline, int len)  |
| 149 | {  |
| 150 | int i, j;  |
| 151 | int code, val;  |
| 152 |   |
| 153 | // Determine if scanline is legacy and needs to be processed the old way.  |
| 154 | if ((len < MinScanLen) | (len > MaxScanLen))  |
| 155 | return LegacyReadRadianceColours(scanline, len);  |
| 156 |   |
| 157 | i = GetB();  |
| 158 | if (i == EOF)  |
| 159 | return false;  |
| 160 | if (i != 2)  |
| 161 | {  |
| 162 | UngetB(i);  |
| 163 | return LegacyReadRadianceColours(scanline, len);  |
| 164 | }  |
| 165 | scanline[0].G = GetB();  |
| 166 | scanline[0].B = GetB();  |
| 167 | i = GetB();  |
| 168 | if (i == EOF)  |
| 169 | return false;  |
| 170 |   |
| 171 | if (scanline[0].G != 2 || scanline[0].B & 128)  |
| 172 | {  |
| 173 | scanline[0].R = 2;  |
| 174 | scanline[0].A = i;  |
| 175 | return LegacyReadRadianceColours(scanline+1, len-1);  |
| 176 | }  |
| 177 | if ((scanline[0].B << 8 | i) != len)  |
| 178 | return false;  |
| 179 |   |
| 180 | // Read each component.  |
| 181 | for (i = 0; i < 4; i++)  |
| 182 | {  |
| 183 | for (j = 0; j < len; )  |
| 184 | {  |
| 185 | code = GetB();  |
| 186 | if (code == EOF)  |
| 187 | return false;  |
| 188 |   |
| 189 | if (code > 128)  |
| 190 | {  |
| 191 | // RLE run.  |
| 192 | code &= 127;  |
| 193 | val = GetB();  |
| 194 | if (val == EOF)  |
| 195 | return false;  |
| 196 | if (j + code > len)  |
| 197 | return false; // Overrun.  |
| 198 | while (code--)  |
| 199 | scanline[j++].E[i] = val;  |
| 200 | }  |
| 201 | else  |
| 202 | {  |
| 203 | // New non-RLE colour.  |
| 204 | if (j + code > len)  |
| 205 | return false; // Overrun.  |
| 206 | while (code--)  |
| 207 | {  |
| 208 | val = GetB();  |
| 209 | if (val == EOF)  |
| 210 | return false;  |
| 211 | scanline[j++].E[i] = val;  |
| 212 | }  |
| 213 | }  |
| 214 | }  |
| 215 | }  |
| 216 | return true;  |
| 217 | }  |
| 218 |   |
| 219 |   |
| 220 | bool tImageHDR::ConvertRadianceToGammaCorrected(tPixel* scan, int len)  |
| 221 | {  |
| 222 | if (!GammaTable)  |
| 223 | return false;  |
| 224 |   |
| 225 | while (len-- > 0)  |
| 226 | {  |
| 227 | int expo = scan[0].A - ExpXS;  |
| 228 | if (expo < -MaxGammaShift)  |
| 229 | {  |
| 230 | if (expo < -MaxGammaShift-8)  |
| 231 | {  |
| 232 | scan[0].MakeBlack();  |
| 233 | }  |
| 234 | else  |
| 235 | {  |
| 236 | int i = (-MaxGammaShift-1) - expo;  |
| 237 | scan[0].R = GammaTable[MaxGammaShift][ ((scan[0].R >> i) + 1) >> 1 ];  |
| 238 | scan[0].G = GammaTable[MaxGammaShift][ ((scan[0].G >> i) + 1) >> 1 ];  |
| 239 | scan[0].B = GammaTable[MaxGammaShift][ ((scan[0].B >> i) + 1) >> 1 ];  |
| 240 | }  |
| 241 | }  |
| 242 | else if (expo > 0)  |
| 243 | {  |
| 244 | if (expo > 8)  |
| 245 | {  |
| 246 | scan[0].MakeWhite();  |
| 247 | }  |
| 248 | else  |
| 249 | {  |
| 250 | int i;  |
| 251 | i = (scan[0].R<<1 | 1) << (expo-1); scan[0].R = i > 255 ? 255 : GammaTable[0][i];  |
| 252 | i = (scan[0].G<<1 | 1) << (expo-1); scan[0].G = i > 255 ? 255 : GammaTable[0][i];  |
| 253 | i = (scan[0].B<<1 | 1) << (expo-1); scan[0].B = i > 255 ? 255 : GammaTable[0][i];  |
| 254 | }  |
| 255 | }  |
| 256 | else  |
| 257 | {  |
| 258 | scan[0].R = GammaTable[-expo][scan[0].R];  |
| 259 | scan[0].G = GammaTable[-expo][scan[0].G];  |
| 260 | scan[0].B = GammaTable[-expo][scan[0].B];  |
| 261 | }  |
| 262 | scan[0].A = ExpXS;  |
| 263 | scan++;  |
| 264 | }  |
| 265 | return true;  |
| 266 | }  |
| 267 |   |
| 268 |   |
| 269 | void tImageHDR::AdjustExposure(tPixel* scan, int len, int adjust)  |
| 270 | {  |
| 271 | // Shift a scanline of colors by 2^adjust.  |
| 272 | if (adjust == 0)  |
| 273 | return;  |
| 274 |   |
| 275 | int minexp = adjust < 0 ? -adjust : 0;  |
| 276 | while (len-- > 0)  |
| 277 | {  |
| 278 | if (scan[0].A <= minexp)  |
| 279 | scan[0].MakeZero();  |
| 280 | else  |
| 281 | scan[0].A += adjust;  |
| 282 | scan++;  |
| 283 | }  |
| 284 | }  |
| 285 |   |
| 286 |   |
| 287 | bool tImageHDR::Set(uint8* hdrFileInMemory, int numBytes, float gammaCorr, int exposureAdj)  |
| 288 | {  |
| 289 | Clear();  |
| 290 | if ((numBytes <= 0) || !hdrFileInMemory)  |
| 291 | return false;  |
| 292 |   |
| 293 | SetupGammaTables(gammaCorr);  |
| 294 |   |
| 295 | // Search for the first double 0x0A (linefeed).  |
| 296 | // Note that hdrFileInMemory has an extra EOF at the end. The (numBytes+1)th character.  |
| 297 | int doubleLFIndex = -1;  |
| 298 | for (int c = 0; c < numBytes; c++)  |
| 299 | {  |
| 300 | if ((hdrFileInMemory[c] == 0x0A) && (hdrFileInMemory[c+1] == 0x0A))  |
| 301 | {  |
| 302 | doubleLFIndex = c;  |
| 303 | break;  |
| 304 | }  |
| 305 | }  |
| 306 | if (doubleLFIndex == -1)  |
| 307 | {  |
| 308 | CleanupGammaTables();  |
| 309 | return false;  |
| 310 | }  |
| 311 |   |
| 312 | // We are not allowed any '\0' characters in the header. Some Mac-generated images have one!  |
| 313 | for (int c = 0; c < doubleLFIndex; c++)  |
| 314 | {  |
| 315 | if (hdrFileInMemory[c] == '\0')  |
| 316 | hdrFileInMemory[c] = '_';  |
| 317 | }  |
| 318 |   |
| 319 | char* foundY = tStd::tStrstr((char*)hdrFileInMemory, "-Y" );  |
| 320 | char* foundX = tStd::tStrstr((char*)hdrFileInMemory, "+X" );  |
| 321 | char* eolY = tStd::tStrchr(foundY, '\n');  |
| 322 | char* eolX = tStd::tStrchr(foundX, '\n');  |
| 323 | if (!eolX || (eolX != eolY))  |
| 324 | {  |
| 325 | CleanupGammaTables();  |
| 326 | return false;  |
| 327 | }  |
| 328 | *eolX = '\0';  |
| 329 | tString ((char*)hdrFileInMemory);  |
| 330 | *eolX = '\n';  |
| 331 | ReadP = (uint8*)(eolX+1);  |
| 332 |   |
| 333 | tList<tStringItem> lines;  |
| 334 | tStd::tExplode(lines, header, '\n');  |
| 335 |   |
| 336 | // Display the header lines.  |
| 337 | // for (tStringItem* headerLine = lines.First(); headerLine; headerLine = headerLine->Next())  |
| 338 | // tPrintf("HDR Info: %s\n", headerLine->Chars());  |
| 339 |   |
| 340 | tStringItem* resLine = lines.Last();  |
| 341 |   |
| 342 | tList<tStringItem> comps;  |
| 343 | tStd::tExplode(comps, *resLine, ' ');  |
| 344 | Height = comps.First()->Next()->AsInt();  |
| 345 | Width = comps.First()->Next()->Next()->Next()->AsInt();  |
| 346 |   |
| 347 | Pixels = new tPixel[Width*Height];  |
| 348 | tPixel* scanin = new tPixel[Width];  |
| 349 |   |
| 350 | bool ok = true;  |
| 351 | for (int y = Height-1; y >= 0; y--)  |
| 352 | {  |
| 353 | ok = ReadRadianceColours(scanin, Width);  |
| 354 | if (!ok)  |
| 355 | break;  |
| 356 |   |
| 357 | AdjustExposure(scanin, Width, exposureAdj);  |
| 358 |   |
| 359 | ok = ConvertRadianceToGammaCorrected(scanin, Width);  |
| 360 | if (!ok)  |
| 361 | break;  |
| 362 |   |
| 363 | WriteP = (uint8*)&Pixels[y * Width];  |
| 364 | for (int x = 0; x < Width; x++)  |
| 365 | {  |
| 366 | PutB(scanin[x].R);  |
| 367 | PutB(scanin[x].G);  |
| 368 | PutB(scanin[x].B);  |
| 369 | PutB(255);  |
| 370 | }  |
| 371 | }  |
| 372 |   |
| 373 | CleanupGammaTables();  |
| 374 | delete[] scanin;  |
| 375 | if (!ok)  |
| 376 | {  |
| 377 | delete[] Pixels;  |
| 378 | Pixels = nullptr;  |
| 379 | return false;  |
| 380 | }  |
| 381 |   |
| 382 | SrcPixelFormat = tPixelFormat::HDR_RAD;  |
| 383 | return true;  |
| 384 | }  |
| 385 |   |
| 386 |   |
| 387 | bool tImageHDR::Set(tPixel* pixels, int width, int height, bool steal)  |
| 388 | {  |
| 389 | Clear();  |
| 390 | if (!pixels || (width <= 0) || (height <= 0))  |
| 391 | return false;  |
| 392 |   |
| 393 | Width = width;  |
| 394 | Height = height;  |
| 395 | if (steal)  |
| 396 | {  |
| 397 | Pixels = pixels;  |
| 398 | }  |
| 399 | else  |
| 400 | {  |
| 401 | Pixels = new tPixel[Width*Height];  |
| 402 | tStd::tMemcpy(Pixels, pixels, Width*Height*sizeof(tPixel));  |
| 403 | }  |
| 404 |   |
| 405 | SrcPixelFormat = tPixelFormat::R8G8B8A8;  |
| 406 | return true;  |
| 407 | }  |
| 408 |   |
| 409 |   |
| 410 | bool tImageHDR::Save(const tString& hdrFile) const  |
| 411 | {  |
| 412 | tAssertMsg(false, "HDR Save not implemented." );  |
| 413 | if (!IsValid())  |
| 414 | return false;  |
| 415 |   |
| 416 | if (tSystem::tGetFileType(hdrFile) != tSystem::tFileType::HDR)  |
| 417 | return false;  |
| 418 |   |
| 419 | tFileHandle file = tOpenFile(hdrFile.ConstText(), "wb" );  |
| 420 | if (!file)  |
| 421 | return false;  |
| 422 |   |
| 423 | // Write the data....  |
| 424 |   |
| 425 | tCloseFile(file);  |
| 426 | return true;  |
| 427 | }  |
| 428 |   |
| 429 |   |
| 430 | tPixel* tImageHDR::StealPixels()  |
| 431 | {  |
| 432 | tPixel* pixels = Pixels;  |
| 433 | Pixels = nullptr;  |
| 434 | Width = 0;  |
| 435 | Height = 0;  |
| 436 | return pixels;  |
| 437 | }  |
| 438 |   |
| 439 |   |
| 440 | }  |
| 441 | |