// ========================================================== // HDR Loader and writer // // Design and implementation by // - Hervé Drolon (drolon@infonie.fr) // // This file is part of FreeImage 3 // // COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY // OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES // THAT THE COVERED CODE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE // OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED // CODE IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT // THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY // SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL // PART OF THIS LICENSE. NO USE OF ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER // THIS DISCLAIMER. // // Use at your own risk! // ========================================================== #include "FreeImage.h" #include "Utilities.h" // ========================================================== // Plugin Interface // ========================================================== static int s_format_id; // ========================================================== // RGBE library // ========================================================== // ---------------------------------------------------------- // maximum size of a line in the header #define HDR_MAXLINE 256 // flags indicating which fields in an rgbeHeaderInfo are valid #define RGBE_VALID_PROGRAMTYPE 0x01 #define RGBE_VALID_COMMENT 0x02 #define RGBE_VALID_GAMMA 0x04 #define RGBE_VALID_EXPOSURE 0x08 // offsets to red, green, and blue components in a data (float) pixel #define RGBE_DATA_RED 0 #define RGBE_DATA_GREEN 1 #define RGBE_DATA_BLUE 2 // ---------------------------------------------------------- #ifdef _WIN32 #pragma pack(push, 1) #else #pragma pack(1) #endif typedef struct tagHeaderInfo { int valid; // indicate which fields are valid char programtype[16]; // listed at beginning of file to identify it after "#?". defaults to "RGBE" char comment[HDR_MAXLINE]; // comment beginning with "# " float gamma; // image has already been gamma corrected with given gamma. defaults to 1.0 (no correction) float exposure; // a value of 1.0 in an image corresponds to watts/steradian/m^2. defaults to 1.0 } rgbeHeaderInfo; #ifdef _WIN32 #pragma pack(pop) #else #pragma pack() #endif typedef enum { rgbe_read_error, rgbe_write_error, rgbe_format_error, rgbe_memory_error, } rgbe_error_code; // ---------------------------------------------------------- // Prototypes // ---------------------------------------------------------- static BOOL rgbe_Error(rgbe_error_code error_code, const char *msg); static BOOL rgbe_GetLine(FreeImageIO *io, fi_handle handle, char *buffer, int length); static inline void rgbe_FloatToRGBE(BYTE rgbe[4], FIRGBF *rgbf); static inline void rgbe_RGBEToFloat(FIRGBF *rgbf, BYTE rgbe[4]); static BOOL rgbe_ReadHeader(FreeImageIO *io, fi_handle handle, unsigned *width, unsigned *height, rgbeHeaderInfo *header_info); static BOOL rgbe_WriteHeader(FreeImageIO *io, fi_handle handle, unsigned width, unsigned height, rgbeHeaderInfo *info); static BOOL rgbe_ReadPixels(FreeImageIO *io, fi_handle handle, FIRGBF *data, unsigned numpixels); static BOOL rgbe_WritePixels(FreeImageIO *io, fi_handle handle, FIRGBF *data, unsigned numpixels); static BOOL rgbe_ReadPixels_RLE(FreeImageIO *io, fi_handle handle, FIRGBF *data, int scanline_width, unsigned num_scanlines); static BOOL rgbe_WriteBytes_RLE(FreeImageIO *io, fi_handle handle, BYTE *data, int numbytes); static BOOL rgbe_WritePixels_RLE(FreeImageIO *io, fi_handle handle, FIRGBF *data, unsigned scanline_width, unsigned num_scanlines); static BOOL rgbe_ReadMetadata(FIBITMAP *dib, rgbeHeaderInfo *header_info); static BOOL rgbe_WriteMetadata(FIBITMAP *dib, rgbeHeaderInfo *header_info); // ---------------------------------------------------------- /** Default error routine. change this to change error handling */ static BOOL rgbe_Error(rgbe_error_code error_code, const char *msg) { switch (error_code) { case rgbe_read_error: FreeImage_OutputMessageProc(s_format_id, "RGBE read error"); break; case rgbe_write_error: FreeImage_OutputMessageProc(s_format_id, "RGBE write error"); break; case rgbe_format_error: FreeImage_OutputMessageProc(s_format_id, "RGBE bad file format: %s\n", msg); break; default: case rgbe_memory_error: FreeImage_OutputMessageProc(s_format_id, "RGBE error: %s\n",msg); } return FALSE; } /** Get a line from a ASCII io stream */ static BOOL rgbe_GetLine(FreeImageIO *io, fi_handle handle, char *buffer, int length) { int i; memset(buffer, 0, length); for(i = 0; i < length; i++) { if (!io->read_proc(&buffer[i], 1, 1, handle)) return FALSE; if(buffer[i] == 0x0A) break; } return (i < length) ? TRUE : FALSE; } /** Standard conversion from float pixels to rgbe pixels. Note: you can remove the "inline"s if your compiler complains about it */ static inline void rgbe_FloatToRGBE(BYTE rgbe[4], FIRGBF *rgbf) { float v; int e; v = rgbf->red; if (rgbf->green > v) v = rgbf->green; if (rgbf->blue > v) v = rgbf->blue; if (v < 1e-32) { rgbe[0] = rgbe[1] = rgbe[2] = rgbe[3] = 0; } else { v = (float)(frexp(v, &e) * 256.0 / v); rgbe[0] = (BYTE) (rgbf->red * v); rgbe[1] = (BYTE) (rgbf->green * v); rgbe[2] = (BYTE) (rgbf->blue * v); rgbe[3] = (BYTE) (e + 128); } } /** Standard conversion from rgbe to float pixels. Note: Ward uses ldexp(col+0.5,exp-(128+8)). However we wanted pixels in the range [0,1] to map back into the range [0,1]. */ static inline void rgbe_RGBEToFloat(FIRGBF *rgbf, BYTE rgbe[4]) { if (rgbe[3]) { // nonzero pixel float f = (float)(ldexp(1.0, rgbe[3] - (int)(128+8))); rgbf->red = rgbe[0] * f; rgbf->green = rgbe[1] * f; rgbf->blue = rgbe[2] * f; } else { rgbf->red = rgbf->green = rgbf->blue = 0; } } /** Minimal header reading. Modify if you want to parse more information */ static BOOL rgbe_ReadHeader(FreeImageIO *io, fi_handle handle, unsigned *width, unsigned *height, rgbeHeaderInfo *header_info) { char buf[HDR_MAXLINE]; float tempf; int i; BOOL bFormatFound = FALSE; BOOL bHeaderFound = FALSE; header_info->valid = 0; header_info->programtype[0] = 0; header_info->gamma = 1.0; header_info->exposure = 1.0; // get the first line if (!rgbe_GetLine(io, handle, buf, HDR_MAXLINE)) return rgbe_Error(rgbe_read_error, NULL); // check the signature if ((buf[0] != '#')||(buf[1] != '?')) { // if you don't want to require the magic token then comment the next line return rgbe_Error(rgbe_format_error,"bad initial token"); } else { header_info->valid |= RGBE_VALID_PROGRAMTYPE; for(i = 0; i < sizeof(header_info->programtype) - 1; i++) { if ((buf[i+2] == 0) || isspace(buf[i+2])) break; header_info->programtype[i] = buf[i+2]; } header_info->programtype[i] = 0; } for (;;) { // get next line if (!rgbe_GetLine(io, handle, buf, HDR_MAXLINE)) return rgbe_Error(rgbe_read_error, NULL); if ((buf[0] == 0) || (buf[0] == '\n')) { // end of header so break out of loop bHeaderFound = TRUE; break; } else if(strcmp(buf,"FORMAT=32-bit_rle_rgbe\n") == 0) { bFormatFound = TRUE; } else if(sscanf(buf, "GAMMA=%g", &tempf) == 1) { header_info->gamma = tempf; header_info->valid |= RGBE_VALID_GAMMA; } else if(sscanf(buf,"EXPOSURE=%g",&tempf) == 1) { header_info->exposure = tempf; header_info->valid |= RGBE_VALID_EXPOSURE; } else if ((buf[0] == '#') && (buf[1] == 0x20)) { header_info->valid |= RGBE_VALID_COMMENT; strcpy(header_info->comment, buf); } } if (!bHeaderFound || !bFormatFound) { return rgbe_Error(rgbe_format_error, "invalid header"); } // get next line if (!rgbe_GetLine(io, handle, buf, HDR_MAXLINE)) return rgbe_Error(rgbe_read_error, NULL); // get the image width & height if(sscanf(buf,"-Y %d +X %d", height, width) < 2) { if(sscanf(buf,"+X %d +Y %d", height, width) < 2) { return rgbe_Error(rgbe_format_error, "missing image size specifier"); } } return TRUE; } /** default minimal header. modify if you want more information in header */ static BOOL rgbe_WriteHeader(FreeImageIO *io, fi_handle handle, unsigned width, unsigned height, rgbeHeaderInfo *info) { char buffer[HDR_MAXLINE]; const char *programtype = "RADIANCE"; if(info && (info->valid & RGBE_VALID_PROGRAMTYPE)) { programtype = info->programtype; } // The #? is to identify file type, the programtype is optional sprintf(buffer, "#?%s\n", programtype); if(io->write_proc(buffer, 1, (unsigned int)strlen(buffer), handle) < 1) return rgbe_Error(rgbe_write_error, NULL); sprintf(buffer, "%s\n", info->comment); if(io->write_proc(buffer, 1, (unsigned int)strlen(buffer), handle) < 1) return rgbe_Error(rgbe_write_error, NULL); sprintf(buffer, "FORMAT=32-bit_rle_rgbe\n"); if(io->write_proc(buffer, 1, (unsigned int)strlen(buffer), handle) < 1) return rgbe_Error(rgbe_write_error, NULL); if(info && (info->valid & RGBE_VALID_GAMMA)) { sprintf(buffer, "GAMMA=%g\n", info->gamma); if(io->write_proc(buffer, 1, (unsigned int)strlen(buffer), handle) < 1) return rgbe_Error(rgbe_write_error, NULL); } if(info && (info->valid & RGBE_VALID_EXPOSURE)) { sprintf(buffer,"EXPOSURE=%g\n", info->exposure); if(io->write_proc(buffer, 1, (unsigned int)strlen(buffer), handle) < 1) return rgbe_Error(rgbe_write_error, NULL); } sprintf(buffer, "\n-Y %d +X %d\n", height, width); if(io->write_proc(buffer, 1, (unsigned int)strlen(buffer), handle) < 1) return rgbe_Error(rgbe_write_error, NULL); return TRUE; } static BOOL rgbe_ReadMetadata(FIBITMAP *dib, rgbeHeaderInfo *header_info) { return TRUE; } static BOOL rgbe_WriteMetadata(FIBITMAP *dib, rgbeHeaderInfo *header_info) { header_info->gamma = 1; header_info->valid |= RGBE_VALID_GAMMA; header_info->exposure = 0; header_info->valid |= RGBE_VALID_EXPOSURE; return TRUE; } /** Simple read routine. Will not correctly handle run length encoding */ static BOOL rgbe_ReadPixels(FreeImageIO *io, fi_handle handle, FIRGBF *data, unsigned numpixels) { BYTE rgbe[4]; for(unsigned x = 0; x < numpixels; x++) { if(io->read_proc(rgbe, 1, sizeof(rgbe), handle) < 1) { return rgbe_Error(rgbe_read_error, NULL); } rgbe_RGBEToFloat(&data[x], rgbe); } return TRUE; } /** Simple write routine that does not use run length encoding. These routines can be made faster by allocating a larger buffer and fread-ing and fwrite-ing the data in larger chunks. */ static BOOL rgbe_WritePixels(FreeImageIO *io, fi_handle handle, FIRGBF *data, unsigned numpixels) { BYTE rgbe[4]; for(unsigned x = 0; x < numpixels; x++) { rgbe_FloatToRGBE(rgbe, &data[x]); if(io->write_proc(rgbe, sizeof(rgbe), 1, handle) < 1) return rgbe_Error(rgbe_write_error, NULL); } return TRUE; } static BOOL rgbe_ReadPixels_RLE(FreeImageIO *io, fi_handle handle, FIRGBF *data, int scanline_width, unsigned num_scanlines) { BYTE rgbe[4], *scanline_buffer, *ptr, *ptr_end; int i, count; BYTE buf[2]; if ((scanline_width < 8)||(scanline_width > 0x7fff)) { // run length encoding is not allowed so read flat return rgbe_ReadPixels(io, handle, data, scanline_width * num_scanlines); } scanline_buffer = NULL; // read in each successive scanline while(num_scanlines > 0) { if(io->read_proc(rgbe, 1, sizeof(rgbe), handle) < 1) { free(scanline_buffer); return rgbe_Error(rgbe_read_error,NULL); } if ((rgbe[0] != 2) || (rgbe[1] != 2) || (rgbe[2] & 0x80)) { // this file is not run length encoded rgbe_RGBEToFloat(data, rgbe); data ++; free(scanline_buffer); return rgbe_ReadPixels(io, handle, data, scanline_width * num_scanlines - 1); } if ((((int)rgbe[2]) << 8 | rgbe[3]) != scanline_width) { free(scanline_buffer); return rgbe_Error(rgbe_format_error,"wrong scanline width"); } if(scanline_buffer == NULL) { scanline_buffer = (BYTE*)malloc(sizeof(BYTE) * 4 * scanline_width); if(scanline_buffer == NULL) { return rgbe_Error(rgbe_memory_error, "unable to allocate buffer space"); } } ptr = &scanline_buffer[0]; // read each of the four channels for the scanline into the buffer for(i = 0; i < 4; i++) { ptr_end = &scanline_buffer[(i+1)*scanline_width]; while(ptr < ptr_end) { if(io->read_proc(buf, 1, 2 * sizeof(BYTE), handle) < 1) { free(scanline_buffer); return rgbe_Error(rgbe_read_error, NULL); } if(buf[0] > 128) { // a run of the same value count = buf[0] - 128; if ((count == 0) || (count > ptr_end - ptr)) { free(scanline_buffer); return rgbe_Error(rgbe_format_error, "bad scanline data"); } while(count-- > 0) *ptr++ = buf[1]; } else { // a non-run count = buf[0]; if ((count == 0) || (count > ptr_end - ptr)) { free(scanline_buffer); return rgbe_Error(rgbe_format_error, "bad scanline data"); } *ptr++ = buf[1]; if (--count > 0) { if(io->read_proc(ptr, 1, sizeof(BYTE) * count, handle) < 1) { free(scanline_buffer); return rgbe_Error(rgbe_read_error, NULL); } ptr += count; } } } } // now convert data from buffer into floats for(i = 0; i < scanline_width; i++) { rgbe[0] = scanline_buffer[i]; rgbe[1] = scanline_buffer[i+scanline_width]; rgbe[2] = scanline_buffer[i+2*scanline_width]; rgbe[3] = scanline_buffer[i+3*scanline_width]; rgbe_RGBEToFloat(data, rgbe); data ++; } num_scanlines--; } free(scanline_buffer); return TRUE; } /** The code below is only needed for the run-length encoded files. Run length encoding adds considerable complexity but does save some space. For each scanline, each channel (r,g,b,e) is encoded separately for better compression. @return Returns TRUE if successful, returns FALSE otherwise */ static BOOL rgbe_WriteBytes_RLE(FreeImageIO *io, fi_handle handle, BYTE *data, int numbytes) { static const int MINRUNLENGTH = 4; int cur, beg_run, run_count, old_run_count, nonrun_count; BYTE buf[2]; cur = 0; while(cur < numbytes) { beg_run = cur; // find next run of length at least 4 if one exists run_count = old_run_count = 0; while((run_count < MINRUNLENGTH) && (beg_run < numbytes)) { beg_run += run_count; old_run_count = run_count; run_count = 1; while((data[beg_run] == data[beg_run + run_count]) && (beg_run + run_count < numbytes) && (run_count < 127)) run_count++; } // if data before next big run is a short run then write it as such if ((old_run_count > 1)&&(old_run_count == beg_run - cur)) { buf[0] = (BYTE)(128 + old_run_count); // write short run buf[1] = data[cur]; if(io->write_proc(buf, 2 * sizeof(BYTE), 1, handle) < 1) return rgbe_Error(rgbe_write_error, NULL); cur = beg_run; } // write out bytes until we reach the start of the next run while(cur < beg_run) { nonrun_count = beg_run - cur; if (nonrun_count > 128) nonrun_count = 128; buf[0] = (BYTE)nonrun_count; if(io->write_proc(buf, sizeof(buf[0]), 1, handle) < 1) return rgbe_Error(rgbe_write_error,NULL); if(io->write_proc(&data[cur], sizeof(data[0]) * nonrun_count, 1, handle) < 1) return rgbe_Error(rgbe_write_error,NULL); cur += nonrun_count; } // write out next run if one was found if (run_count >= MINRUNLENGTH) { buf[0] = (BYTE)(128 + run_count); buf[1] = data[beg_run]; if(io->write_proc(buf, sizeof(buf[0]) * 2, 1, handle) < 1) return rgbe_Error(rgbe_write_error,NULL); cur += run_count; } } return TRUE; } static BOOL rgbe_WritePixels_RLE(FreeImageIO *io, fi_handle handle, FIRGBF *data, unsigned scanline_width, unsigned num_scanlines) { BYTE rgbe[4]; BYTE *buffer; if ((scanline_width < 8)||(scanline_width > 0x7fff)) { // run length encoding is not allowed so write flat return rgbe_WritePixels(io, handle, data, scanline_width * num_scanlines); } buffer = (BYTE*)malloc(sizeof(BYTE) * 4 * scanline_width); if (buffer == NULL) { // no buffer space so write flat return rgbe_WritePixels(io, handle, data, scanline_width * num_scanlines); } while(num_scanlines-- > 0) { rgbe[0] = (BYTE)2; rgbe[1] = (BYTE)2; rgbe[2] = (BYTE)(scanline_width >> 8); rgbe[3] = (BYTE)(scanline_width & 0xFF); if(io->write_proc(rgbe, sizeof(rgbe), 1, handle) < 1) { free(buffer); return rgbe_Error(rgbe_write_error, NULL); } for(unsigned x = 0; x < scanline_width; x++) { rgbe_FloatToRGBE(rgbe, data); buffer[x] = rgbe[0]; buffer[x+scanline_width] = rgbe[1]; buffer[x+2*scanline_width] = rgbe[2]; buffer[x+3*scanline_width] = rgbe[3]; data ++; } // write out each of the four channels separately run length encoded // first red, then green, then blue, then exponent for(int i = 0; i < 4; i++) { BOOL bOK = rgbe_WriteBytes_RLE(io, handle, &buffer[i*scanline_width], scanline_width); if (!bOK) { free(buffer); return bOK; } } } free(buffer); return TRUE; } // ---------------------------------------------------------- // ========================================================== // Plugin Implementation // ========================================================== static const char * DLL_CALLCONV Format() { return "HDR"; } static const char * DLL_CALLCONV Description() { return "High Dynamic Range Image"; } static const char * DLL_CALLCONV Extension() { return "hdr"; } static const char * DLL_CALLCONV RegExpr() { return NULL; } static const char * DLL_CALLCONV MimeType() { return "image/vnd.radiance"; } static BOOL DLL_CALLCONV Validate(FreeImageIO *io, fi_handle handle) { BYTE hdr_signature[] = { '#', '?' }; BYTE signature[] = { 0, 0 }; io->read_proc(signature, 1, 2, handle); return (memcmp(hdr_signature, signature, 2) == 0); } static BOOL DLL_CALLCONV SupportsExportDepth(int depth) { return FALSE; } static BOOL DLL_CALLCONV SupportsExportType(FREE_IMAGE_TYPE type) { return (type == FIT_RGBF) ? TRUE : FALSE; } static BOOL DLL_CALLCONV SupportsNoPixels() { return TRUE; } // -------------------------------------------------------------------------- static FIBITMAP * DLL_CALLCONV Load(FreeImageIO *io, fi_handle handle, int page, int flags, void *data) { FIBITMAP *dib = NULL; if (!handle) { return NULL; } BOOL header_only = (flags & FIF_LOAD_NOPIXELS) == FIF_LOAD_NOPIXELS; try { rgbeHeaderInfo header_info; unsigned width, height; // Read the header if(rgbe_ReadHeader(io, handle, &width, &height, &header_info) == FALSE) { return NULL; } // allocate a RGBF image dib = FreeImage_AllocateHeaderT(header_only, FIT_RGBF, width, height); if (!dib) { throw FI_MSG_ERROR_MEMORY; } // set the metadata as comments rgbe_ReadMetadata(dib, &header_info); if(header_only) { // header only mode return dib; } // read the image pixels and fill the dib for(unsigned y = 0; y < height; y++) { FIRGBF *scanline = (FIRGBF*)FreeImage_GetScanLine(dib, height - 1 - y); if (!rgbe_ReadPixels_RLE(io, handle, scanline, width, 1)) { FreeImage_Unload(dib); return NULL; } } } catch(const char *text) { if(dib != NULL) { FreeImage_Unload(dib); } FreeImage_OutputMessageProc(s_format_id, text); } return dib; } static BOOL DLL_CALLCONV Save(FreeImageIO *io, FIBITMAP *dib, fi_handle handle, int page, int flags, void *data) { if (!dib) return FALSE; if(FreeImage_GetImageType(dib) != FIT_RGBF) { return FALSE; } unsigned width = FreeImage_GetWidth(dib); unsigned height = FreeImage_GetHeight(dib); // write the header rgbeHeaderInfo header_info; memset(&header_info, 0, sizeof(rgbeHeaderInfo)); // fill the header with correct gamma and exposure rgbe_WriteMetadata(dib, &header_info); // fill a comment sprintf(header_info.comment, "# Made with FreeImage %s", FreeImage_GetVersion()); if (!rgbe_WriteHeader(io, handle, width, height, &header_info)) { return FALSE; } // write each scanline for(unsigned y = 0; y < height; y++) { FIRGBF *scanline = (FIRGBF*)FreeImage_GetScanLine(dib, height - 1 - y); if (!rgbe_WritePixels_RLE(io, handle, scanline, width, 1)) { return FALSE; } } return TRUE; } // ========================================================== // Init // ========================================================== void DLL_CALLCONV InitHDR(Plugin *plugin, int format_id) { s_format_id = format_id; plugin->format_proc = Format; plugin->description_proc = Description; plugin->extension_proc = Extension; plugin->regexpr_proc = RegExpr; plugin->open_proc = NULL; plugin->close_proc = NULL; plugin->pagecount_proc = NULL; plugin->pagecapability_proc = NULL; plugin->load_proc = Load; plugin->save_proc = Save; plugin->validate_proc = Validate; plugin->mime_proc = MimeType; plugin->supports_export_bpp_proc = SupportsExportDepth; plugin->supports_export_type_proc = SupportsExportType; plugin->supports_icc_profiles_proc = NULL; plugin->supports_no_pixels_proc = SupportsNoPixels; }