// ========================================================== // GIF Loader and Writer // // Design and implementation by // - Ryan Rubley // - Raphaël Gaquer // - Aaron Shumate // // 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! // ========================================================== #ifdef _MSC_VER #pragma warning (disable : 4786) // identifier was truncated to 'number' characters #endif #include "FreeImage.h" #include "Utilities.h" #include "../Metadata/FreeImageTag.h" // ========================================================== // Metadata declarations // ========================================================== #define GIF_DISPOSAL_UNSPECIFIED 0 #define GIF_DISPOSAL_LEAVE 1 #define GIF_DISPOSAL_BACKGROUND 2 #define GIF_DISPOSAL_PREVIOUS 3 // ========================================================== // Constant/Typedef declarations // ========================================================== struct GIFinfo { BOOL read; //only really used when reading size_t global_color_table_offset; int global_color_table_size; BYTE background_color; std::vector application_extension_offsets; std::vector comment_extension_offsets; std::vector graphic_control_extension_offsets; std::vector image_descriptor_offsets; GIFinfo() : read(0), global_color_table_offset(0), global_color_table_size(0), background_color(0) { } }; struct PageInfo { PageInfo(int d, int l, int t, int w, int h) { disposal_method = d; left = (WORD)l; top = (WORD)t; width = (WORD)w; height = (WORD)h; } int disposal_method; WORD left, top, width, height; }; //GIF defines a max of 12 bits per code #define MAX_LZW_CODE 4096 class StringTable { public: StringTable(); ~StringTable(); void Initialize(int minCodeSize); BYTE *FillInputBuffer(int len); void CompressStart(int bpp, int width); int CompressEnd(BYTE *buf); //0-4 bytes bool Compress(BYTE *buf, int *len); bool Decompress(BYTE *buf, int *len); void Done(void); protected: bool m_done; int m_minCodeSize, m_clearCode, m_endCode, m_nextCode; int m_bpp, m_slack; //Compressor information int m_prefix; //Compressor state variable int m_codeSize, m_codeMask; //Compressor/Decompressor state variables int m_oldCode; //Decompressor state variable int m_partial, m_partialSize; //Compressor/Decompressor bit buffer int firstPixelPassed; // A specific flag that indicates if the first pixel // of the whole image had already been read std::string m_strings[MAX_LZW_CODE]; //This is what is really the "string table" data for the Decompressor int* m_strmap; //input buffer BYTE *m_buffer; int m_bufferSize, m_bufferRealSize, m_bufferPos, m_bufferShift; void ClearCompressorTable(void); void ClearDecompressorTable(void); }; #define GIF_PACKED_LSD_HAVEGCT 0x80 #define GIF_PACKED_LSD_COLORRES 0x70 #define GIF_PACKED_LSD_GCTSORTED 0x08 #define GIF_PACKED_LSD_GCTSIZE 0x07 #define GIF_PACKED_ID_HAVELCT 0x80 #define GIF_PACKED_ID_INTERLACED 0x40 #define GIF_PACKED_ID_LCTSORTED 0x20 #define GIF_PACKED_ID_RESERVED 0x18 #define GIF_PACKED_ID_LCTSIZE 0x07 #define GIF_PACKED_GCE_RESERVED 0xE0 #define GIF_PACKED_GCE_DISPOSAL 0x1C #define GIF_PACKED_GCE_WAITINPUT 0x02 #define GIF_PACKED_GCE_HAVETRANS 0x01 #define GIF_BLOCK_IMAGE_DESCRIPTOR 0x2C #define GIF_BLOCK_EXTENSION 0x21 #define GIF_BLOCK_TRAILER 0x3B #define GIF_EXT_PLAINTEXT 0x01 #define GIF_EXT_GRAPHIC_CONTROL 0xF9 #define GIF_EXT_COMMENT 0xFE #define GIF_EXT_APPLICATION 0xFF #define GIF_INTERLACE_PASSES 4 static int g_GifInterlaceOffset[GIF_INTERLACE_PASSES] = {0, 4, 2, 1}; static int g_GifInterlaceIncrement[GIF_INTERLACE_PASSES] = {8, 8, 4, 2}; // ========================================================== // Helpers Functions // ========================================================== static BOOL FreeImage_SetMetadataEx(FREE_IMAGE_MDMODEL model, FIBITMAP *dib, const char *key, WORD id, FREE_IMAGE_MDTYPE type, DWORD count, DWORD length, const void *value) { BOOL bResult = FALSE; FITAG *tag = FreeImage_CreateTag(); if(tag) { FreeImage_SetTagKey(tag, key); FreeImage_SetTagID(tag, id); FreeImage_SetTagType(tag, type); FreeImage_SetTagCount(tag, count); FreeImage_SetTagLength(tag, length); FreeImage_SetTagValue(tag, value); if(model == FIMD_ANIMATION) { TagLib& s = TagLib::instance(); // get the tag description const char *description = s.getTagDescription(TagLib::ANIMATION, id); FreeImage_SetTagDescription(tag, description); } // store the tag bResult = FreeImage_SetMetadata(model, dib, key, tag); FreeImage_DeleteTag(tag); } return bResult; } static BOOL FreeImage_GetMetadataEx(FREE_IMAGE_MDMODEL model, FIBITMAP *dib, const char *key, FREE_IMAGE_MDTYPE type, FITAG **tag) { if( FreeImage_GetMetadata(model, dib, key, tag) ) { if( FreeImage_GetTagType(*tag) == type ) { return TRUE; } } return FALSE; } StringTable::StringTable() { m_buffer = NULL; firstPixelPassed = 0; // Still no pixel read // Maximum number of entries in the map is MAX_LZW_CODE * 256 // (aka 2**12 * 2**8 => a 20 bits key) // This Map could be optmized to only handle MAX_LZW_CODE * 2**(m_bpp) m_strmap = new(std::nothrow) int[1<<20]; } StringTable::~StringTable() { if( m_buffer != NULL ) { delete [] m_buffer; } if( m_strmap != NULL ) { delete [] m_strmap; m_strmap = NULL; } } void StringTable::Initialize(int minCodeSize) { m_done = false; m_bpp = 8; m_minCodeSize = minCodeSize; m_clearCode = 1 << m_minCodeSize; if(m_clearCode > MAX_LZW_CODE) { m_clearCode = MAX_LZW_CODE; } m_endCode = m_clearCode + 1; m_partial = 0; m_partialSize = 0; m_bufferSize = 0; ClearCompressorTable(); ClearDecompressorTable(); } BYTE *StringTable::FillInputBuffer(int len) { if( m_buffer == NULL ) { m_buffer = new(std::nothrow) BYTE[len]; m_bufferRealSize = len; } else if( len > m_bufferRealSize ) { delete [] m_buffer; m_buffer = new(std::nothrow) BYTE[len]; m_bufferRealSize = len; } m_bufferSize = len; m_bufferPos = 0; m_bufferShift = 8 - m_bpp; return m_buffer; } void StringTable::CompressStart(int bpp, int width) { m_bpp = bpp; m_slack = (8 - ((width * bpp) % 8)) % 8; m_partial |= m_clearCode << m_partialSize; m_partialSize += m_codeSize; ClearCompressorTable(); } int StringTable::CompressEnd(BYTE *buf) { int len = 0; //output code for remaining prefix m_partial |= m_prefix << m_partialSize; m_partialSize += m_codeSize; while( m_partialSize >= 8 ) { *buf++ = (BYTE)m_partial; m_partial >>= 8; m_partialSize -= 8; len++; } //add the end of information code and flush the entire buffer out m_partial |= m_endCode << m_partialSize; m_partialSize += m_codeSize; while( m_partialSize > 0 ) { *buf++ = (BYTE)m_partial; m_partial >>= 8; m_partialSize -= 8; len++; } //most this can be is 4 bytes. 7 bits in m_partial to start + 12 for the //last code + 12 for the end code = 31 bits total. return len; } bool StringTable::Compress(BYTE *buf, int *len) { if( m_bufferSize == 0 || m_done ) { return false; } int mask = (1 << m_bpp) - 1; BYTE *bufpos = buf; while( m_bufferPos < m_bufferSize ) { //get the current pixel value char ch = (char)((m_buffer[m_bufferPos] >> m_bufferShift) & mask); // The next prefix is : // | int nextprefix = (((m_prefix)<<8)&0xFFF00) + (ch & 0x000FF); if(firstPixelPassed) { if( m_strmap[nextprefix] > 0) { m_prefix = m_strmap[nextprefix]; } else { m_partial |= m_prefix << m_partialSize; m_partialSize += m_codeSize; //grab full bytes for the output buffer while( m_partialSize >= 8 && bufpos - buf < *len ) { *bufpos++ = (BYTE)m_partial; m_partial >>= 8; m_partialSize -= 8; } //add the code to the "table map" m_strmap[nextprefix] = m_nextCode; //increment the next highest valid code, increase the code size if( m_nextCode == (1 << m_codeSize) ) { m_codeSize++; } m_nextCode++; //if we're out of codes, restart the string table if( m_nextCode == MAX_LZW_CODE ) { m_partial |= m_clearCode << m_partialSize; m_partialSize += m_codeSize; ClearCompressorTable(); } // Only keep the 8 lowest bits (prevent problems with "negative chars") m_prefix = ch & 0x000FF; } //increment to the next pixel if( m_bufferShift > 0 && !(m_bufferPos + 1 == m_bufferSize && m_bufferShift <= m_slack) ) { m_bufferShift -= m_bpp; } else { m_bufferPos++; m_bufferShift = 8 - m_bpp; } //jump out here if the output buffer is full if( bufpos - buf == *len ) { return true; } } else { // Specific behavior for the first pixel of the whole image firstPixelPassed=1; // Only keep the 8 lowest bits (prevent problems with "negative chars") m_prefix = ch & 0x000FF; //increment to the next pixel if( m_bufferShift > 0 && !(m_bufferPos + 1 == m_bufferSize && m_bufferShift <= m_slack) ) { m_bufferShift -= m_bpp; } else { m_bufferPos++; m_bufferShift = 8 - m_bpp; } //jump out here if the output buffer is full if( bufpos - buf == *len ) { return true; } } } m_bufferSize = 0; *len = (int)(bufpos - buf); return true; } bool StringTable::Decompress(BYTE *buf, int *len) { if( m_bufferSize == 0 || m_done ) { return false; } BYTE *bufpos = buf; for( ; m_bufferPos < m_bufferSize; m_bufferPos++ ) { m_partial |= (int)m_buffer[m_bufferPos] << m_partialSize; m_partialSize += 8; while( m_partialSize >= m_codeSize ) { int code = m_partial & m_codeMask; m_partial >>= m_codeSize; m_partialSize -= m_codeSize; if( code > m_nextCode || /*(m_nextCode == MAX_LZW_CODE && code != m_clearCode) || */code == m_endCode ) { m_done = true; *len = (int)(bufpos - buf); return true; } if( code == m_clearCode ) { ClearDecompressorTable(); continue; } //add new string to string table, if not the first pass since a clear code if( m_oldCode != MAX_LZW_CODE && m_nextCode < MAX_LZW_CODE) { m_strings[m_nextCode] = m_strings[m_oldCode] + m_strings[code == m_nextCode ? m_oldCode : code][0]; } if( (int)m_strings[code].size() > *len - (bufpos - buf) ) { //out of space, stuff the code back in for next time m_partial <<= m_codeSize; m_partialSize += m_codeSize; m_partial |= code; m_bufferPos++; *len = (int)(bufpos - buf); return true; } //output the string into the buffer memcpy(bufpos, m_strings[code].data(), m_strings[code].size()); bufpos += m_strings[code].size(); //increment the next highest valid code, add a bit to the mask if we need to increase the code size if( m_oldCode != MAX_LZW_CODE && m_nextCode < MAX_LZW_CODE ) { if( ++m_nextCode < MAX_LZW_CODE ) { if( (m_nextCode & m_codeMask) == 0 ) { m_codeSize++; m_codeMask |= m_nextCode; } } } m_oldCode = code; } } m_bufferSize = 0; *len = (int)(bufpos - buf); return true; } void StringTable::Done(void) { m_done = true; } void StringTable::ClearCompressorTable(void) { if(m_strmap) { memset(m_strmap, 0xFF, sizeof(unsigned int)*(1<<20)); } m_nextCode = m_endCode + 1; m_prefix = 0; m_codeSize = m_minCodeSize + 1; } void StringTable::ClearDecompressorTable(void) { for( int i = 0; i < m_clearCode; i++ ) { m_strings[i].resize(1); m_strings[i][0] = (char)i; } m_nextCode = m_endCode + 1; m_codeSize = m_minCodeSize + 1; m_codeMask = (1 << m_codeSize) - 1; m_oldCode = MAX_LZW_CODE; } // ========================================================== // Plugin Interface // ========================================================== static int s_format_id; // ========================================================== // Plugin Implementation // ========================================================== static const char * DLL_CALLCONV Format() { return "GIF"; } static const char * DLL_CALLCONV Description() { return "Graphics Interchange Format"; } static const char * DLL_CALLCONV Extension() { return "gif"; } static const char * DLL_CALLCONV RegExpr() { return "^GIF"; } static const char * DLL_CALLCONV MimeType() { return "image/gif"; } static BOOL DLL_CALLCONV Validate(FreeImageIO *io, fi_handle handle) { char buf[6]; if( io->read_proc(buf, 6, 1, handle) < 1 ) { return FALSE; } BOOL bResult = FALSE; if( !strncmp(buf, "GIF", 3) ) { if( buf[3] >= '0' && buf[3] <= '9' && buf[4] >= '0' && buf[4] <= '9' && buf[5] >= 'a' && buf[5] <= 'z' ) { bResult = TRUE; } } io->seek_proc(handle, -6, SEEK_CUR); return bResult; } static BOOL DLL_CALLCONV SupportsExportDepth(int depth) { return (depth == 1) || (depth == 4) || (depth == 8); } static BOOL DLL_CALLCONV SupportsExportType(FREE_IMAGE_TYPE type) { return (type == FIT_BITMAP) ? TRUE : FALSE; } // ---------------------------------------------------------- static void *DLL_CALLCONV Open(FreeImageIO *io, fi_handle handle, BOOL read) { GIFinfo *info = new(std::nothrow) GIFinfo; if( info == NULL ) { return NULL; } // 25/02/2008 MDA: Not safe to memset GIFinfo structure with VS 2008 (safe iterators), // perform initialization in constructor instead. // memset(info, 0, sizeof(GIFinfo)); info->read = read; if( read ) { try { //Header if( !Validate(io, handle) ) { throw FI_MSG_ERROR_MAGIC_NUMBER; } io->seek_proc(handle, 6, SEEK_CUR); //Logical Screen Descriptor io->seek_proc(handle, 4, SEEK_CUR); BYTE packed; if( io->read_proc(&packed, 1, 1, handle) < 1 ) { throw "EOF reading Logical Screen Descriptor"; } if( io->read_proc(&info->background_color, 1, 1, handle) < 1 ) { throw "EOF reading Logical Screen Descriptor"; } io->seek_proc(handle, 1, SEEK_CUR); //Global Color Table if( packed & GIF_PACKED_LSD_HAVEGCT ) { info->global_color_table_offset = io->tell_proc(handle); info->global_color_table_size = 2 << (packed & GIF_PACKED_LSD_GCTSIZE); io->seek_proc(handle, 3 * info->global_color_table_size, SEEK_CUR); } //Scan through all the rest of the blocks, saving offsets size_t gce_offset = 0; BYTE block = 0; while( block != GIF_BLOCK_TRAILER ) { if( io->read_proc(&block, 1, 1, handle) < 1 ) { throw "EOF reading blocks"; } if( block == GIF_BLOCK_IMAGE_DESCRIPTOR ) { info->image_descriptor_offsets.push_back(io->tell_proc(handle)); //GCE may be 0, meaning no GCE preceded this ID info->graphic_control_extension_offsets.push_back(gce_offset); gce_offset = 0; io->seek_proc(handle, 8, SEEK_CUR); if( io->read_proc(&packed, 1, 1, handle) < 1 ) { throw "EOF reading Image Descriptor"; } //Local Color Table if( packed & GIF_PACKED_ID_HAVELCT ) { io->seek_proc(handle, 3 * (2 << (packed & GIF_PACKED_ID_LCTSIZE)), SEEK_CUR); } //LZW Minimum Code Size io->seek_proc(handle, 1, SEEK_CUR); } else if( block == GIF_BLOCK_EXTENSION ) { BYTE ext; if( io->read_proc(&ext, 1, 1, handle) < 1 ) { throw "EOF reading extension"; } if( ext == GIF_EXT_GRAPHIC_CONTROL ) { //overwrite previous offset if more than one GCE found before an ID gce_offset = io->tell_proc(handle); } else if( ext == GIF_EXT_COMMENT ) { info->comment_extension_offsets.push_back(io->tell_proc(handle)); } else if( ext == GIF_EXT_APPLICATION ) { info->application_extension_offsets.push_back(io->tell_proc(handle)); } } else if( block == GIF_BLOCK_TRAILER ) { continue; } else { throw "Invalid GIF block found"; } //Data Sub-blocks BYTE len; if( io->read_proc(&len, 1, 1, handle) < 1 ) { throw "EOF reading sub-block"; } while( len != 0 ) { io->seek_proc(handle, len, SEEK_CUR); if( io->read_proc(&len, 1, 1, handle) < 1 ) { throw "EOF reading sub-block"; } } } } catch (const char *msg) { FreeImage_OutputMessageProc(s_format_id, msg); delete info; return NULL; } } else { //Header io->write_proc((void *)"GIF89a", 6, 1, handle); } return info; } static void DLL_CALLCONV Close(FreeImageIO *io, fi_handle handle, void *data) { if( data == NULL ) { return; } GIFinfo *info = (GIFinfo *)data; if( !info->read ) { //Trailer BYTE b = GIF_BLOCK_TRAILER; io->write_proc(&b, 1, 1, handle); } delete info; } static int DLL_CALLCONV PageCount(FreeImageIO *io, fi_handle handle, void *data) { if( data == NULL ) { return 0; } GIFinfo *info = (GIFinfo *)data; return (int) info->image_descriptor_offsets.size(); } static FIBITMAP * DLL_CALLCONV Load(FreeImageIO *io, fi_handle handle, int page, int flags, void *data) { if( data == NULL ) { return NULL; } GIFinfo *info = (GIFinfo *)data; if( page == -1 ) { page = 0; } if( page < 0 || page >= (int)info->image_descriptor_offsets.size() ) { return NULL; } FIBITMAP *dib = NULL; try { bool have_transparent = false, no_local_palette = false, interlaced = false; int disposal_method = GIF_DISPOSAL_LEAVE, delay_time = 0, transparent_color = 0; WORD left, top, width, height; BYTE packed, b; WORD w; //playback pages to generate what the user would see for this frame if( (flags & GIF_PLAYBACK) == GIF_PLAYBACK ) { //Logical Screen Descriptor io->seek_proc(handle, 6, SEEK_SET); WORD logicalwidth, logicalheight; io->read_proc(&logicalwidth, 2, 1, handle); io->read_proc(&logicalheight, 2, 1, handle); #ifdef FREEIMAGE_BIGENDIAN SwapShort(&logicalwidth); SwapShort(&logicalheight); #endif //set the background color with 0 alpha RGBQUAD background; if( info->global_color_table_offset != 0 && info->background_color < info->global_color_table_size ) { io->seek_proc(handle, (long)(info->global_color_table_offset + (info->background_color * 3)), SEEK_SET); io->read_proc(&background.rgbRed, 1, 1, handle); io->read_proc(&background.rgbGreen, 1, 1, handle); io->read_proc(&background.rgbBlue, 1, 1, handle); } else { background.rgbRed = 0; background.rgbGreen = 0; background.rgbBlue = 0; } background.rgbReserved = 0; //allocate entire logical area dib = FreeImage_Allocate(logicalwidth, logicalheight, 32); if( dib == NULL ) { throw FI_MSG_ERROR_DIB_MEMORY; } //fill with background color to start int x, y; RGBQUAD *scanline; for( y = 0; y < logicalheight; y++ ) { scanline = (RGBQUAD *)FreeImage_GetScanLine(dib, y); for( x = 0; x < logicalwidth; x++ ) { *scanline++ = background; } } //cache some info about each of the pages so we can avoid decoding as many of them as possible std::vector pageinfo; int start = page, end = page; while( start >= 0 ) { //Graphic Control Extension io->seek_proc(handle, (long)(info->graphic_control_extension_offsets[start] + 1), SEEK_SET); io->read_proc(&packed, 1, 1, handle); have_transparent = (packed & GIF_PACKED_GCE_HAVETRANS) ? true : false; disposal_method = (packed & GIF_PACKED_GCE_DISPOSAL) >> 2; //Image Descriptor io->seek_proc(handle, (long)(info->image_descriptor_offsets[start]), SEEK_SET); io->read_proc(&left, 2, 1, handle); io->read_proc(&top, 2, 1, handle); io->read_proc(&width, 2, 1, handle); io->read_proc(&height, 2, 1, handle); #ifdef FREEIMAGE_BIGENDIAN SwapShort(&left); SwapShort(&top); SwapShort(&width); SwapShort(&height); #endif pageinfo.push_back(PageInfo(disposal_method, left, top, width, height)); if( start != end ) { if( left == 0 && top == 0 && width == logicalwidth && height == logicalheight ) { if( disposal_method == GIF_DISPOSAL_BACKGROUND ) { pageinfo.pop_back(); start++; break; } else if( disposal_method != GIF_DISPOSAL_PREVIOUS ) { if( !have_transparent ) { break; } } } } start--; } if( start < 0 ) { start = 0; } //draw each page into the logical area delay_time = 0; for( page = start; page <= end; page++ ) { PageInfo &info = pageinfo[end - page]; //things we can skip having to decode if( page != end ) { if( info.disposal_method == GIF_DISPOSAL_PREVIOUS ) { continue; } if( info.disposal_method == GIF_DISPOSAL_BACKGROUND ) { for( y = 0; y < info.height; y++ ) { const int scanidx = logicalheight - (y + info.top) - 1; if ( scanidx < 0 ) { break; // If data is corrupt, don't calculate in invalid scanline } scanline = (RGBQUAD *)FreeImage_GetScanLine(dib, scanidx) + info.left; for( x = 0; x < info.width; x++ ) { *scanline++ = background; } } continue; } } //decode page FIBITMAP *pagedib = Load(io, handle, page, GIF_LOAD256, data); if( pagedib != NULL ) { RGBQUAD *pal = FreeImage_GetPalette(pagedib); have_transparent = false; if( FreeImage_IsTransparent(pagedib) ) { int count = FreeImage_GetTransparencyCount(pagedib); BYTE *table = FreeImage_GetTransparencyTable(pagedib); for( int i = 0; i < count; i++ ) { if( table[i] == 0 ) { have_transparent = true; transparent_color = i; break; } } } //copy page data into logical buffer, with full alpha opaqueness for( y = 0; y < info.height; y++ ) { const int scanidx = logicalheight - (y + info.top) - 1; if ( scanidx < 0 ) { break; // If data is corrupt, don't calculate in invalid scanline } scanline = (RGBQUAD *)FreeImage_GetScanLine(dib, scanidx) + info.left; BYTE *pageline = FreeImage_GetScanLine(pagedib, info.height - y - 1); for( x = 0; x < info.width; x++ ) { if( !have_transparent || *pageline != transparent_color ) { *scanline = pal[*pageline]; scanline->rgbReserved = 255; } scanline++; pageline++; } } //copy frame time if( page == end ) { FITAG *tag; if( FreeImage_GetMetadataEx(FIMD_ANIMATION, pagedib, "FrameTime", FIDT_LONG, &tag) ) { delay_time = *(LONG *)FreeImage_GetTagValue(tag); } } FreeImage_Unload(pagedib); } } //setup frame time FreeImage_SetMetadataEx(FIMD_ANIMATION, dib, "FrameTime", ANIMTAG_FRAMETIME, FIDT_LONG, 1, 4, &delay_time); return dib; } //get the actual frame image data for a single frame //Image Descriptor io->seek_proc(handle, (long)info->image_descriptor_offsets[page], SEEK_SET); io->read_proc(&left, 2, 1, handle); io->read_proc(&top, 2, 1, handle); io->read_proc(&width, 2, 1, handle); io->read_proc(&height, 2, 1, handle); #ifdef FREEIMAGE_BIGENDIAN SwapShort(&left); SwapShort(&top); SwapShort(&width); SwapShort(&height); #endif io->read_proc(&packed, 1, 1, handle); interlaced = (packed & GIF_PACKED_ID_INTERLACED) ? true : false; no_local_palette = (packed & GIF_PACKED_ID_HAVELCT) ? false : true; int bpp = 8; if( (flags & GIF_LOAD256) == 0 ) { if( !no_local_palette ) { int size = 2 << (packed & GIF_PACKED_ID_LCTSIZE); if( size <= 2 ) bpp = 1; else if( size <= 16 ) bpp = 4; } else if( info->global_color_table_offset != 0 ) { if( info->global_color_table_size <= 2 ) bpp = 1; else if( info->global_color_table_size <= 16 ) bpp = 4; } } dib = FreeImage_Allocate(width, height, bpp); if( dib == NULL ) { throw FI_MSG_ERROR_DIB_MEMORY; } FreeImage_SetMetadataEx(FIMD_ANIMATION, dib, "FrameLeft", ANIMTAG_FRAMELEFT, FIDT_SHORT, 1, 2, &left); FreeImage_SetMetadataEx(FIMD_ANIMATION, dib, "FrameTop", ANIMTAG_FRAMETOP, FIDT_SHORT, 1, 2, &top); b = no_local_palette ? 1 : 0; FreeImage_SetMetadataEx(FIMD_ANIMATION, dib, "NoLocalPalette", ANIMTAG_NOLOCALPALETTE, FIDT_BYTE, 1, 1, &b); b = interlaced ? 1 : 0; FreeImage_SetMetadataEx(FIMD_ANIMATION, dib, "Interlaced", ANIMTAG_INTERLACED, FIDT_BYTE, 1, 1, &b); //Palette RGBQUAD *pal = FreeImage_GetPalette(dib); if( !no_local_palette ) { int size = 2 << (packed & GIF_PACKED_ID_LCTSIZE); int i = 0; while( i < size ) { io->read_proc(&pal[i].rgbRed, 1, 1, handle); io->read_proc(&pal[i].rgbGreen, 1, 1, handle); io->read_proc(&pal[i].rgbBlue, 1, 1, handle); i++; } } else if( info->global_color_table_offset != 0 ) { long pos = io->tell_proc(handle); io->seek_proc(handle, (long)info->global_color_table_offset, SEEK_SET); int i = 0; while( i < info->global_color_table_size ) { io->read_proc(&pal[i].rgbRed, 1, 1, handle); io->read_proc(&pal[i].rgbGreen, 1, 1, handle); io->read_proc(&pal[i].rgbBlue, 1, 1, handle); i++; } io->seek_proc(handle, pos, SEEK_SET); } else { //its legal to have no palette, but we're going to generate *something* for( int i = 0; i < 256; i++ ) { pal[i].rgbRed = (BYTE)i; pal[i].rgbGreen = (BYTE)i; pal[i].rgbBlue = (BYTE)i; } } //LZW Minimum Code Size io->read_proc(&b, 1, 1, handle); StringTable *stringtable = new(std::nothrow) StringTable; stringtable->Initialize(b); //Image Data Sub-blocks int x = 0, xpos = 0, y = 0, shift = 8 - bpp, mask = (1 << bpp) - 1, interlacepass = 0; BYTE *scanline = FreeImage_GetScanLine(dib, height - 1); BYTE buf[4096]; io->read_proc(&b, 1, 1, handle); while( b ) { io->read_proc(stringtable->FillInputBuffer(b), b, 1, handle); int size = sizeof(buf); while( stringtable->Decompress(buf, &size) ) { for( int i = 0; i < size; i++ ) { scanline[xpos] |= (buf[i] & mask) << shift; if( shift > 0 ) { shift -= bpp; } else { xpos++; shift = 8 - bpp; } if( ++x >= width ) { if( interlaced ) { y += g_GifInterlaceIncrement[interlacepass]; if( y >= height && ++interlacepass < GIF_INTERLACE_PASSES ) { y = g_GifInterlaceOffset[interlacepass]; } } else { y++; } if( y >= height ) { stringtable->Done(); break; } x = xpos = 0; shift = 8 - bpp; scanline = FreeImage_GetScanLine(dib, height - y - 1); } } size = sizeof(buf); } io->read_proc(&b, 1, 1, handle); } if( page == 0 ) { size_t idx; //Logical Screen Descriptor io->seek_proc(handle, 6, SEEK_SET); WORD logicalwidth, logicalheight; io->read_proc(&logicalwidth, 2, 1, handle); io->read_proc(&logicalheight, 2, 1, handle); #ifdef FREEIMAGE_BIGENDIAN SwapShort(&logicalwidth); SwapShort(&logicalheight); #endif FreeImage_SetMetadataEx(FIMD_ANIMATION, dib, "LogicalWidth", ANIMTAG_LOGICALWIDTH, FIDT_SHORT, 1, 2, &logicalwidth); FreeImage_SetMetadataEx(FIMD_ANIMATION, dib, "LogicalHeight", ANIMTAG_LOGICALHEIGHT, FIDT_SHORT, 1, 2, &logicalheight); //Global Color Table if( info->global_color_table_offset != 0 ) { RGBQUAD globalpalette[256]; io->seek_proc(handle, (long)info->global_color_table_offset, SEEK_SET); int i = 0; while( i < info->global_color_table_size ) { io->read_proc(&globalpalette[i].rgbRed, 1, 1, handle); io->read_proc(&globalpalette[i].rgbGreen, 1, 1, handle); io->read_proc(&globalpalette[i].rgbBlue, 1, 1, handle); globalpalette[i].rgbReserved = 0; i++; } FreeImage_SetMetadataEx(FIMD_ANIMATION, dib, "GlobalPalette", ANIMTAG_GLOBALPALETTE, FIDT_PALETTE, info->global_color_table_size, info->global_color_table_size * 4, globalpalette); //background color if( info->background_color < info->global_color_table_size ) { FreeImage_SetBackgroundColor(dib, &globalpalette[info->background_color]); } } //Application Extension LONG loop = 1; //If no AE with a loop count is found, the default must be 1 for( idx = 0; idx < info->application_extension_offsets.size(); idx++ ) { io->seek_proc(handle, (long)info->application_extension_offsets[idx], SEEK_SET); io->read_proc(&b, 1, 1, handle); if( b == 11 ) { //All AEs start with an 11 byte sub-block to determine what type of AE it is char buf[11]; io->read_proc(buf, 11, 1, handle); if( !memcmp(buf, "NETSCAPE2.0", 11) || !memcmp(buf, "ANIMEXTS1.0", 11) ) { //Not everybody recognizes ANIMEXTS1.0 but it is valid io->read_proc(&b, 1, 1, handle); if( b == 3 ) { //we're supposed to have a 3 byte sub-block now io->read_proc(&b, 1, 1, handle); //this should be 0x01 but isn't really important io->read_proc(&w, 2, 1, handle); #ifdef FREEIMAGE_BIGENDIAN SwapShort(&w); #endif loop = w; if( loop > 0 ) loop++; break; } } } } FreeImage_SetMetadataEx(FIMD_ANIMATION, dib, "Loop", ANIMTAG_LOOP, FIDT_LONG, 1, 4, &loop); // Comment Extension for (idx = 0; idx < info->comment_extension_offsets.size(); idx++) { io->seek_proc(handle, (long)info->comment_extension_offsets[idx], SEEK_SET); std::string comment; char buf[255]; io->read_proc(&b, 1, 1, handle); while (b) { io->read_proc(buf, b, 1, handle); comment.append(buf, b); io->read_proc(&b, 1, 1, handle); } comment.append(1, '\0'); sprintf(buf, "Comment%d", (int)idx); DWORD comment_size = (DWORD)comment.size(); FreeImage_SetMetadataEx(FIMD_COMMENTS, dib, buf, 1, FIDT_ASCII, comment_size, comment_size, comment.c_str()); } } //Graphic Control Extension if( info->graphic_control_extension_offsets[page] != 0 ) { io->seek_proc(handle, (long)(info->graphic_control_extension_offsets[page] + 1), SEEK_SET); io->read_proc(&packed, 1, 1, handle); io->read_proc(&w, 2, 1, handle); #ifdef FREEIMAGE_BIGENDIAN SwapShort(&w); #endif io->read_proc(&b, 1, 1, handle); have_transparent = (packed & GIF_PACKED_GCE_HAVETRANS) ? true : false; disposal_method = (packed & GIF_PACKED_GCE_DISPOSAL) >> 2; delay_time = w * 10; //convert cs to ms transparent_color = b; if( have_transparent ) { int size = 1 << bpp; if( transparent_color <= size ) { BYTE table[256]; memset(table, 0xFF, size); table[transparent_color] = 0; FreeImage_SetTransparencyTable(dib, table, size); } } } FreeImage_SetMetadataEx(FIMD_ANIMATION, dib, "FrameTime", ANIMTAG_FRAMETIME, FIDT_LONG, 1, 4, &delay_time); b = (BYTE)disposal_method; FreeImage_SetMetadataEx(FIMD_ANIMATION, dib, "DisposalMethod", ANIMTAG_DISPOSALMETHOD, FIDT_BYTE, 1, 1, &b); delete stringtable; } catch (const char *msg) { if( dib != NULL ) { FreeImage_Unload(dib); } FreeImage_OutputMessageProc(s_format_id, msg); return NULL; } return dib; } static BOOL DLL_CALLCONV Save(FreeImageIO *io, FIBITMAP *dib, fi_handle handle, int page, int flags, void *data) { if( data == NULL ) { return FALSE; } //GIFinfo *info = (GIFinfo *)data; if( page == -1 ) { page = 0; } try { BYTE packed, b; WORD w; FITAG *tag; int bpp = FreeImage_GetBPP(dib); if( bpp != 1 && bpp != 4 && bpp != 8 ) { throw "Only 1, 4, or 8 bpp images supported"; } bool have_transparent = false, no_local_palette = false, interlaced = false; int disposal_method = GIF_DISPOSAL_BACKGROUND, delay_time = 100, transparent_color = 0; WORD left = 0, top = 0, width = (WORD)FreeImage_GetWidth(dib), height = (WORD)FreeImage_GetHeight(dib); WORD output_height = height; if( FreeImage_GetMetadataEx(FIMD_ANIMATION, dib, "FrameLeft", FIDT_SHORT, &tag) ) { left = *(WORD *)FreeImage_GetTagValue(tag); } if( FreeImage_GetMetadataEx(FIMD_ANIMATION, dib, "FrameTop", FIDT_SHORT, &tag) ) { top = *(WORD *)FreeImage_GetTagValue(tag); } if( FreeImage_GetMetadataEx(FIMD_ANIMATION, dib, "NoLocalPalette", FIDT_BYTE, &tag) ) { no_local_palette = *(BYTE *)FreeImage_GetTagValue(tag) ? true : false; } if( FreeImage_GetMetadataEx(FIMD_ANIMATION, dib, "Interlaced", FIDT_BYTE, &tag) ) { interlaced = *(BYTE *)FreeImage_GetTagValue(tag) ? true : false; } if( FreeImage_GetMetadataEx(FIMD_ANIMATION, dib, "FrameTime", FIDT_LONG, &tag) ) { delay_time = *(LONG *)FreeImage_GetTagValue(tag); } if( FreeImage_GetMetadataEx(FIMD_ANIMATION, dib, "DisposalMethod", FIDT_BYTE, &tag) ) { disposal_method = *(BYTE *)FreeImage_GetTagValue(tag); } RGBQUAD *pal = FreeImage_GetPalette(dib); #ifdef FREEIMAGE_BIGENDIAN SwapShort(&left); SwapShort(&top); SwapShort(&width); SwapShort(&height); #endif if( page == 0 ) { //gather some info WORD logicalwidth = width; // width has already been swapped... if( FreeImage_GetMetadataEx(FIMD_ANIMATION, dib, "LogicalWidth", FIDT_SHORT, &tag) ) { logicalwidth = *(WORD *)FreeImage_GetTagValue(tag); #ifdef FREEIMAGE_BIGENDIAN SwapShort(&logicalwidth); #endif } WORD logicalheight = height; // height has already been swapped... if( FreeImage_GetMetadataEx(FIMD_ANIMATION, dib, "LogicalHeight", FIDT_SHORT, &tag) ) { logicalheight = *(WORD *)FreeImage_GetTagValue(tag); #ifdef FREEIMAGE_BIGENDIAN SwapShort(&logicalheight); #endif } RGBQUAD *globalpalette = NULL; int globalpalette_size = 0; if( FreeImage_GetMetadataEx(FIMD_ANIMATION, dib, "GlobalPalette", FIDT_PALETTE, &tag) ) { globalpalette_size = FreeImage_GetTagCount(tag); if( globalpalette_size >= 2 ) { globalpalette = (RGBQUAD *)FreeImage_GetTagValue(tag); } } //Logical Screen Descriptor io->write_proc(&logicalwidth, 2, 1, handle); io->write_proc(&logicalheight, 2, 1, handle); packed = GIF_PACKED_LSD_COLORRES; b = 0; RGBQUAD background_color; if( globalpalette != NULL ) { packed |= GIF_PACKED_LSD_HAVEGCT; if( globalpalette_size < 4 ) { globalpalette_size = 2; packed |= 0 & GIF_PACKED_LSD_GCTSIZE; } else if( globalpalette_size < 8 ) { globalpalette_size = 4; packed |= 1 & GIF_PACKED_LSD_GCTSIZE; } else if( globalpalette_size < 16 ) { globalpalette_size = 8; packed |= 2 & GIF_PACKED_LSD_GCTSIZE; } else if( globalpalette_size < 32 ) { globalpalette_size = 16; packed |= 3 & GIF_PACKED_LSD_GCTSIZE; } else if( globalpalette_size < 64 ) { globalpalette_size = 32; packed |= 4 & GIF_PACKED_LSD_GCTSIZE; } else if( globalpalette_size < 128 ) { globalpalette_size = 64; packed |= 5 & GIF_PACKED_LSD_GCTSIZE; } else if( globalpalette_size < 256 ) { globalpalette_size = 128; packed |= 6 & GIF_PACKED_LSD_GCTSIZE; } else { globalpalette_size = 256; packed |= 7 & GIF_PACKED_LSD_GCTSIZE; } if( FreeImage_GetBackgroundColor(dib, &background_color) ) { for( int i = 0; i < globalpalette_size; i++ ) { if( background_color.rgbRed == globalpalette[i].rgbRed && background_color.rgbGreen == globalpalette[i].rgbGreen && background_color.rgbBlue == globalpalette[i].rgbBlue ) { b = (BYTE)i; break; } } } } else { packed |= (bpp - 1) & GIF_PACKED_LSD_GCTSIZE; } io->write_proc(&packed, 1, 1, handle); io->write_proc(&b, 1, 1, handle); b = 0; io->write_proc(&b, 1, 1, handle); //Global Color Table if( globalpalette != NULL ) { int i = 0; while( i < globalpalette_size ) { io->write_proc(&globalpalette[i].rgbRed, 1, 1, handle); io->write_proc(&globalpalette[i].rgbGreen, 1, 1, handle); io->write_proc(&globalpalette[i].rgbBlue, 1, 1, handle); i++; } } //Application Extension LONG loop = 0; if( FreeImage_GetMetadataEx(FIMD_ANIMATION, dib, "Loop", FIDT_LONG, &tag) ) { loop = *(LONG *)FreeImage_GetTagValue(tag); } if( loop != 1 ) { //the Netscape extension is really "repeats" not "loops" if( loop > 1 ) loop--; if( loop > 0xFFFF ) loop = 0xFFFF; w = (WORD)loop; #ifdef FREEIMAGE_BIGENDIAN SwapShort(&w); #endif io->write_proc((void *)"\x21\xFF\x0BNETSCAPE2.0\x03\x01", 16, 1, handle); io->write_proc(&w, 2, 1, handle); b = 0; io->write_proc(&b, 1, 1, handle); } //Comment Extension FIMETADATA *mdhandle = NULL; FITAG *tag = NULL; mdhandle = FreeImage_FindFirstMetadata(FIMD_COMMENTS, dib, &tag); if( mdhandle ) { do { if( FreeImage_GetTagType(tag) == FIDT_ASCII ) { int length = FreeImage_GetTagLength(tag) - 1; char *value = (char *)FreeImage_GetTagValue(tag); io->write_proc((void *)"\x21\xFE", 2, 1, handle); while( length > 0 ) { b = (BYTE)(length >= 255 ? 255 : length); io->write_proc(&b, 1, 1, handle); io->write_proc(value, b, 1, handle); value += b; length -= b; } b = 0; io->write_proc(&b, 1, 1, handle); } } while(FreeImage_FindNextMetadata(mdhandle, &tag)); FreeImage_FindCloseMetadata(mdhandle); } } //Graphic Control Extension if( FreeImage_IsTransparent(dib) ) { int count = FreeImage_GetTransparencyCount(dib); BYTE *table = FreeImage_GetTransparencyTable(dib); for( int i = 0; i < count; i++ ) { if( table[i] == 0 ) { have_transparent = true; transparent_color = i; break; } } } io->write_proc((void *)"\x21\xF9\x04", 3, 1, handle); b = (BYTE)((disposal_method << 2) & GIF_PACKED_GCE_DISPOSAL); if( have_transparent ) b |= GIF_PACKED_GCE_HAVETRANS; io->write_proc(&b, 1, 1, handle); //Notes about delay time for GIFs: //IE5/IE6 have a minimum and default of 100ms //Mozilla/Firefox/Netscape 6+/Opera have a minimum of 20ms and a default of 100ms if <20ms is specified or the GCE is absent //Netscape 4 has a minimum of 10ms if 0ms is specified, but will use 0ms if the GCE is absent w = (WORD)(delay_time / 10); //convert ms to cs #ifdef FREEIMAGE_BIGENDIAN SwapShort(&w); #endif io->write_proc(&w, 2, 1, handle); b = (BYTE)transparent_color; io->write_proc(&b, 1, 1, handle); b = 0; io->write_proc(&b, 1, 1, handle); //Image Descriptor b = GIF_BLOCK_IMAGE_DESCRIPTOR; io->write_proc(&b, 1, 1, handle); io->write_proc(&left, 2, 1, handle); io->write_proc(&top, 2, 1, handle); io->write_proc(&width, 2, 1, handle); io->write_proc(&height, 2, 1, handle); packed = 0; if( !no_local_palette ) packed |= GIF_PACKED_ID_HAVELCT | ((bpp - 1) & GIF_PACKED_ID_LCTSIZE); if( interlaced ) packed |= GIF_PACKED_ID_INTERLACED; io->write_proc(&packed, 1, 1, handle); //Local Color Table if( !no_local_palette ) { int palsize = 1 << bpp; for( int i = 0; i < palsize; i++ ) { io->write_proc(&pal[i].rgbRed, 1, 1, handle); io->write_proc(&pal[i].rgbGreen, 1, 1, handle); io->write_proc(&pal[i].rgbBlue, 1, 1, handle); } } //LZW Minimum Code Size b = (BYTE)(bpp == 1 ? 2 : bpp); io->write_proc(&b, 1, 1, handle); StringTable *stringtable = new(std::nothrow) StringTable; stringtable->Initialize(b); stringtable->CompressStart(bpp, width); //Image Data Sub-blocks int y = 0, interlacepass = 0, line = FreeImage_GetLine(dib); BYTE buf[255], *bufptr = buf; //255 is the max sub-block length int size = sizeof(buf); b = sizeof(buf); while( y < output_height ) { memcpy(stringtable->FillInputBuffer(line), FreeImage_GetScanLine(dib, output_height - y - 1), line); while( stringtable->Compress(bufptr, &size) ) { bufptr += size; if( bufptr - buf == sizeof(buf) ) { io->write_proc(&b, 1, 1, handle); io->write_proc(buf, sizeof(buf), 1, handle); size = sizeof(buf); bufptr = buf; } else { size = (int)(sizeof(buf) - (bufptr - buf)); } } if( interlaced ) { y += g_GifInterlaceIncrement[interlacepass]; if( y >= output_height && ++interlacepass < GIF_INTERLACE_PASSES ) { y = g_GifInterlaceOffset[interlacepass]; } } else { y++; } } size = (int)(bufptr - buf); BYTE last[4]; w = (WORD)stringtable->CompressEnd(last); if( size + w >= sizeof(buf) ) { //one last full size sub-block io->write_proc(&b, 1, 1, handle); io->write_proc(buf, size, 1, handle); io->write_proc(last, sizeof(buf) - size, 1, handle); //and possibly a tiny additional sub-block b = (BYTE)(w - (sizeof(buf) - size)); if( b > 0 ) { io->write_proc(&b, 1, 1, handle); io->write_proc(last + w - b, b, 1, handle); } } else { //last sub-block less than full size b = (BYTE)(size + w); io->write_proc(&b, 1, 1, handle); io->write_proc(buf, size, 1, handle); io->write_proc(last, w, 1, handle); } //Block Terminator b = 0; io->write_proc(&b, 1, 1, handle); delete stringtable; } catch (const char *msg) { FreeImage_OutputMessageProc(s_format_id, msg); return FALSE; } return TRUE; } // ========================================================== // Init // ========================================================== void DLL_CALLCONV InitGIF(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 = Open; plugin->close_proc = Close; plugin->pagecount_proc = PageCount; 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; }