/********************************************************* * * File: GifWin.cpp * Title: Graphics Interchange Format implementation * * Author: Lennie Araki * Date: 24-Nov-1999 * * This class is a thin wrapper around the open source * giflib-1.4.0 for opening, parsing and displaying * Compuserve GIF files on Windows. * * The baseline code was derived from fragments extracted * from the sample programs gif2rgb.c and giftext.c. * Added support for local/global palettes, transparency * and "dispose" methods to improve display compliance * with GIF89a. * * Copyright (c) 1999 CallWave, Inc. * CallWave, Inc. * 136 W. Canon Perdido Suite A * Santa Barbara, CA 93101 * * Licensed under the terms laid out in the libungif * COPYING file. * *********************************************************/ #include "stdafx.h" #include #include "GifWin.h" extern "C" { #include "gif_lib.h" } #define LOCAL static // // Implements the GIF89a specification with the following omissions: // // Section 18. Logical Screen Descriptor: // Pixel Aspect Ratio is ignored - square pixels assumed (1:1) // Section 23. Graphic Control Extension: // User Input Flag is ignored - could be added but not very useful // Section 25. Plain Text Extension // Not implemented - would require embedding fonts and text drawing // code to be added // Section 26. Application Extension // Not implemented. Note: this includes Netscape 2.0 looping // extensions // // _______________________________ // | reserved | disposal |u_i| t | // |___|___|___|___|___|___|___|___| // #define GIF_TRANSPARENT 0x01 #define GIF_USER_INPUT 0x02 #define GIF_DISPOSE_MASK 0x07 #define GIF_DISPOSE_SHIFT 2 #define GIF_NOT_TRANSPARENT -1 #define GIF_DISPOSE_NONE 0 // No disposal specified. The decoder is // not required to take any action. #define GIF_DISPOSE_LEAVE 1 // Do not dispose. The graphic is to be left // in place. #define GIF_DISPOSE_BACKGND 2 // Restore to background color. The area used by the // graphic must be restored to the background color. #define GIF_DISPOSE_RESTORE 3 // Restore to previous. The decoder is required to // restore the area overwritten by the graphic with // what was there prior to rendering the graphic. // Initialize BITMAPINFO LOCAL void InitBitmapInfo(LPBITMAPINFO pBMI, int cx, int cy) { ::ZeroMemory(pBMI, sizeof(BITMAPINFOHEADER)); pBMI->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); pBMI->bmiHeader.biWidth = cx; pBMI->bmiHeader.biHeight = -cy; // negative for top-down bitmap pBMI->bmiHeader.biPlanes = 1; pBMI->bmiHeader.biBitCount = 24; pBMI->bmiHeader.biClrUsed = 256; } // Copy GIF ColorMap into Windows BITMAPINFO LOCAL void CopyColorMap(ColorMapObject* pColorMap, LPBITMAPINFO pBMI) { int iLen = pColorMap->ColorCount; ASSERT( iLen <= 256 ); int iCount = min(iLen, 256); for (int i = 0; i < iCount; i++) { BYTE red = pColorMap->Colors[i].Red; BYTE green = pColorMap->Colors[i].Green; BYTE blue = pColorMap->Colors[i].Blue; TRACE("%3d: %02xh %02xh %02xh ", i, red, green, blue); pBMI->bmiColors[i].rgbRed = red; pBMI->bmiColors[i].rgbGreen = green; pBMI->bmiColors[i].rgbBlue = blue; pBMI->bmiColors[i].rgbReserved = 0; if (i % 4 == 3) TRACE("\n"); } TRACE("\n"); } #define DWORD_PAD(x) (((x) + 3) & ~3) // Copy bytes from source to destination skipping transparent bytes LOCAL void CopyGIF(LPBYTE pDst, LPBYTE pSrc, int width, const int transparent, GifColorType* pColorTable) { ASSERT( pColorTable ); if (width) { do { BYTE b = *pSrc++; if (b != transparent) { // Translate to 24-bit RGB value if not transparent const GifColorType* pColor = pColorTable + b; pDst[0] = pColor->Blue; pDst[1] = pColor->Green; pDst[2] = pColor->Red; } // Skip to next pixel pDst += 3; } while (--width); } } // Fix pixels in 24-bit GIF buffer LOCAL void FillGIF(LPBYTE pDst, const COLORREF rgb, int width) { if (width) { do { pDst[0] = GetBValue(rgb); pDst[1] = GetGValue(rgb); pDst[2] = GetRValue(rgb); pDst += 3; } while (--width); } } // Constructor/destructor CGIFWin::CGIFWin() { m_pGifFile = NULL; m_pBits = NULL; // Clear bitmap information ::ZeroMemory(&m_bmiGlobal, sizeof(m_bmiGlobal)); ::ZeroMemory(&m_bmiDisplay, sizeof(m_bmiDisplay)); // // Per Section 11 of GIF spec: // If no color table is available at all, the decoder is free to use a // system color table or a table of its own. In that case, the decoder // may use a color table with as many colors as its hardware is able // to support; it is recommended that such a table have black and // white as its first two entries, so that monochrome images can be // rendered adequately. // const RGBQUAD rgbWhite = { 255, 255, 255, 0 }; const RGBQUAD rgbBlack = { 0, 0, 0, 0 }; m_bmiGlobal.bmi.bmiColors[0] = rgbBlack; for (int i = 1; i < 256; ++i) { m_bmiGlobal.bmi.bmiColors[i] = rgbWhite; } } CGIFWin::~CGIFWin() { TRACE("*** CGIFWin destructor called ***\n"); Close(); } // Open GIF file and allocate "screen" buffer int CGIFWin::Open(LPCTSTR pszGifFileName, COLORREF rgbTransparent) { m_rgbBackgnd = m_rgbTransparent = rgbTransparent; // First close and delete previous GIF (if open) Close(); m_pGifFile = ::DGifOpenFileName(pszGifFileName); int iResult = -1; if (m_pGifFile) { const int cxScreen = m_pGifFile->SWidth; const int cyScreen = m_pGifFile->SHeight; TRACE("\n%s:\n\n\tScreen Size - Width = %d, Height = %d.\n", pszGifFileName, cxScreen, cyScreen); TRACE("\tColorResolution = %d, BackGround = %d.\n", m_pGifFile->SColorResolution, m_pGifFile->SBackGroundColor); // Allocate buffer big enough for 2 screens + 1 line // Use 24-bit (3-bytes per pixel) to correctly handle local palettes const DWORD dwRowBytes = DWORD_PAD(cxScreen * 3); const DWORD dwScreen = dwRowBytes * cyScreen; m_pBits = (LPBYTE) GlobalAllocPtr(GHND, dwScreen * 2 + dwRowBytes); iResult = -2; if (m_pBits) { // Fill in current and next image with background color for (int y = 0; y < cyScreen * 2; ++y) { ::FillGIF(m_pBits + y * dwRowBytes, rgbTransparent, cxScreen); } ::InitBitmapInfo(&m_bmiGlobal.bmi, cxScreen, cyScreen); if (m_pGifFile->SColorMap) { TRACE("\tGlobal Color Map:\n"); ::CopyColorMap(m_pGifFile->SColorMap, &m_bmiGlobal.bmi); GifColorType* pColor = m_pGifFile->SColorMap->Colors + m_pGifFile->SBackGroundColor; m_rgbBackgnd = RGB(pColor->Red, pColor->Green, pColor->Blue); } iResult = 0; m_iImageNum = 0; m_uLoopCount = 0U; } } return iResult; } // Close the GIF file and free resources allocated by libgif void CGIFWin::Close() { // Close GIF file if opened if (m_pGifFile) { int iError = DGifCloseFile(m_pGifFile); if (iError == GIF_ERROR) { TRACE("DGifCloseFile error=%d\n", GifLastError()); } m_pGifFile = NULL; } // Free memory if allocated if (m_pBits) { GlobalFreePtr(m_pBits); m_pBits = NULL; } } // // Draw entire GIF to a Windows Device Context // iFactor Percent Ratio // -3 25% (1:4) // -2 33% (1:3) // -1 50% (1:2) // 0 100% (1:1) // 1 200% (2:1) // 2 300% (3:1) // 3 400% (4:1) // int CGIFWin::Draw(HDC hDC, LPCRECT pRect, int iFactor /*=0*/) { int iResult = 0; if (m_pGifFile && m_pBits) { const int Width = m_pGifFile->SWidth; const int Height = m_pGifFile->SHeight; int zoomWidth = Width; int zoomHeight = Height; if (iFactor < 0) { zoomWidth /= (1 - iFactor); zoomHeight /= (1 - iFactor); } else if (iFactor > 0) { zoomWidth *= (1 + iFactor); zoomHeight *= (1 + iFactor); } int x, y; if (pRect) { // Center image in rectangle x = (pRect->right - pRect->left - zoomWidth) / 2 + pRect->left; y = (pRect->bottom - pRect->top - zoomHeight) / 2 + pRect->top; } else { // Draw image at top-left x = y = 0; } if (Width && Height) { if (iFactor < 0) { HBITMAP hBitmap = CreateMappedBitmap(NULL, 0, 1 - iFactor); if (hBitmap) { HDC hdcMem = ::CreateCompatibleDC(hDC); if (hdcMem) { HBITMAP hOldBm = (HBITMAP) ::SelectObject(hdcMem, hBitmap); // Blast bits from memory DC to target DC. iResult = ::BitBlt(hDC, x, y, zoomWidth, zoomHeight, hdcMem, 0, 0, SRCCOPY); ::SelectObject(hdcMem, hOldBm); ::DeleteDC(hdcMem); } ::DeleteObject(hBitmap); } } else // (iFactor >= 0) { // Display bitmap on screen (-negative height to flip DIB upside down) iResult = ::StretchDIBits(hDC, x, y, zoomWidth, zoomHeight, 0, 0, Width, Height, m_pBits, &m_bmiDisplay.bmi, DIB_RGB_COLORS, SRCCOPY); } } } return iResult; } // Compute least squared color difference LOCAL COLORREF ColorDiff(COLORREF rgb1, COLORREF rgb2) { // If matching color, replace with Windows color const int rDiff = GetRValue(rgb1) - GetRValue(rgb2); const int gDiff = GetGValue(rgb1) - GetGValue(rgb2); const int bDiff = GetBValue(rgb1) - GetBValue(rgb2); // Use least squared difference const long lDiff = rDiff * rDiff + gDiff * gDiff + bDiff * bDiff; return lDiff; } LOCAL COLORREF AvePixel(LPBYTE pSrcRow, DWORD dwSrcRowBytes, LPCOLORMAP pColorMap, UINT uColors, int iScale) { const int iPower = iScale * iScale; const int iPower2 = iPower / 2; int red = iPower2; // For rounding int grn = iPower2; int blu = iPower2; for (int row = iScale; row > 0; --row) { LPBYTE pSrc = pSrcRow; for (int col = iScale; col > 0; --col) { COLORREF rgb = RGB(pSrc[2], pSrc[1], pSrc[0]); pSrc += 3; // Map color based on pColorMap, uColors long lClosest = 5; for (UINT u = 0; u < uColors; ++u) { const long lDiff = ColorDiff(pColorMap[u].from, rgb); if (lDiff < lClosest) { lClosest = lDiff; rgb = pColorMap[u].to; } } // Check for "solid" color flag (no pixel averaging) if (rgb & 0xff000000) { return rgb; } red += GetRValue(rgb); grn += GetGValue(rgb); blu += GetBValue(rgb); } pSrcRow += dwSrcRowBytes; } // Return "average" pixel return RGB(red / iPower, grn / iPower, blu / iPower); } // Copy (and resize) 24-bit Bitmap mapping colors LOCAL void CopyBitmap24(LPBYTE pDstRow, LPBYTE pSrcRow, int width, int height, LPCOLORMAP pColorMap, UINT uColors, int iScale) { ASSERT( iScale > 0 ); const DWORD dwSrcRowBytes = DWORD_PAD(width * 3); const DWORD dwDstRowBytes = DWORD_PAD(width / iScale * 3); for (int row = 0; row < height; row += iScale) { LPBYTE pDst = pDstRow; LPBYTE pSrc = pSrcRow + (row * dwSrcRowBytes); for (int col = 0; col < width; col += iScale) { const COLORREF rgb = AvePixel(pSrc, dwSrcRowBytes, pColorMap, uColors, iScale); *pDst++ = GetBValue(rgb); *pDst++ = GetGValue(rgb); *pDst++ = GetRValue(rgb); pSrc += (iScale * 3); } pDstRow += dwDstRowBytes; } } // Create a Device Independent Bitmap from current GIF image // Colorize bitmap to match Windows desktop colors // Returns NULL if error else handle to bitmap HBITMAP CGIFWin::CreateMappedBitmap(LPCOLORMAP pMap, UINT uCount, int iScale /*=1*/) { HBITMAP hBitmap = NULL; ASSERT( m_pGifFile && m_pBits ); // Create memory device context compatible with current screen HDC hDC = ::CreateCompatibleDC(NULL); if (hDC) { // Create bitmap from current image state LPVOID pBits = NULL; BMI256 bmiSize = m_bmiDisplay; if (iScale > 0) { bmiSize.bmi.bmiHeader.biWidth /= iScale; bmiSize.bmi.bmiHeader.biHeight /= iScale; } hBitmap = ::CreateDIBSection(hDC, &bmiSize.bmi, DIB_RGB_COLORS, &pBits, /*handle=*/ NULL, /*offset=*/ 0L); if (hBitmap && pBits) { const int cxScreen = m_pGifFile->SWidth; const int cyScreen = m_pGifFile->SHeight; ASSERT( m_bmiDisplay.bmi.bmiHeader.biBitCount == 24 ); ::CopyBitmap24((LPBYTE) pBits, (LPBYTE) m_pBits, cxScreen, cyScreen, pMap, uCount, iScale); } VERIFY( ::DeleteDC(hDC) ); } return hBitmap; } int CGIFWin::GetHeight() { return m_pGifFile ? m_pGifFile->SHeight : 0; } int CGIFWin::GetWidth() { return m_pGifFile ? m_pGifFile->SWidth : 0; } // Netscape 2.0 looping extension block LOCAL GifByteType szNetscape20ext[] = "\x0bNETSCAPE2.0"; #define NSEXT_LOOP 0x01 // Loop Count field code // // Appendix E. Interlaced Images. // // The rows of an Interlaced images are arranged in the following order: // // Group 1 : Every 8th. row, starting with row 0. (Pass 1) // Group 2 : Every 8th. row, starting with row 4. (Pass 2) // Group 3 : Every 4th. row, starting with row 2. (Pass 3) // Group 4 : Every 2nd. row, starting with row 1. (Pass 4) // const int InterlacedOffset[] = { 0, 4, 2, 1 }; /* The way Interlaced image should. */ const int InterlacedJumps[] = { 8, 8, 4, 2 }; /* be read - offsets and jumps... */ // // The Following example illustrates how the rows of an interlaced image are // ordered. // // Row Number Interlace Pass // // 0 ----------------------------------------- 1 // 1 ----------------------------------------- 4 // 2 ----------------------------------------- 3 // 3 ----------------------------------------- 4 // 4 ----------------------------------------- 2 // 5 ----------------------------------------- 4 // 6 ----------------------------------------- 3 // 7 ----------------------------------------- 4 // 8 ----------------------------------------- 1 // 9 ----------------------------------------- 4 // 10 ----------------------------------------- 3 // 11 ----------------------------------------- 4 // 12 ----------------------------------------- 2 // 13 ----------------------------------------- 4 // 14 ----------------------------------------- 3 // 15 ----------------------------------------- 4 // 16 ----------------------------------------- 1 // 17 ----------------------------------------- 4 // 18 ----------------------------------------- 3 // 19 ----------------------------------------- 4 // // Fetch next image from GIF file // Returns delay in msec, 0 for end-of-file, negative for error) int CGIFWin::NextImage() { // Error if no gif file! if (!m_pGifFile) { return -1; } const int cxScreen = m_pGifFile->SWidth; const int cyScreen = m_pGifFile->SHeight; // ___________ // pBits1 -> | | // | current | // | image | // |___________| // pBits2 -> | | // | next | // | image | // |___________| // pLine -> |___________| // const DWORD dwRowBytes = DWORD_PAD(cxScreen * 3); #define XYOFFSET(x,y) ((y) * dwRowBytes + (x) * 3) const DWORD dwScreen = dwRowBytes * cyScreen; LPBYTE pBits1 = m_pBits; LPBYTE pBits2 = pBits1 + dwScreen; GifPixelType *pLine = pBits2 + dwScreen; GifRecordType RecordType; GifByteType *pExtension; int delay = 10; // Default to 100 msec int dispose = 0; int transparent = GIF_NOT_TRANSPARENT; do { int i, ExtCode; if (DGifGetRecordType(m_pGifFile, &RecordType) == GIF_ERROR) { break; } switch (RecordType) { case IMAGE_DESC_RECORD_TYPE: if (DGifGetImageDesc(m_pGifFile) != GIF_ERROR) { const int x = m_pGifFile->Image.Left; const int y = m_pGifFile->Image.Top; ++m_iImageNum; TRACE("\nImage #%d:\n\n\tImage Size - Left = %d, Top = %d, Width = %d, Height = %d.\n", m_iImageNum, x, y, m_pGifFile->Image.Width, m_pGifFile->Image.Height); TRACE("\tImage is %s", m_pGifFile->Image.Interlace ? "Interlaced" : "Non Interlaced"); if (m_pGifFile->Image.ColorMap != NULL) TRACE(", BitsPerPixel = %d.\n", m_pGifFile->Image.ColorMap->BitsPerPixel); else TRACE(".\n"); GifColorType* pColorTable; if (m_pGifFile->Image.ColorMap == NULL) { TRACE("\tNo Image Color Map.\n"); // Copy global bitmap info for display memcpy(&m_bmiDisplay, &m_bmiGlobal, sizeof(m_bmiDisplay)); pColorTable = m_pGifFile->SColorMap->Colors; } else { TRACE("\tImage Has Color Map.\n"); ::InitBitmapInfo(&m_bmiDisplay.bmi, cxScreen, cyScreen); ::CopyColorMap(m_pGifFile->Image.ColorMap, &m_bmiDisplay.bmi); pColorTable = m_pGifFile->Image.ColorMap->Colors; } // Always copy next -> current image memcpy(pBits1, pBits2, dwScreen); const int Width = m_pGifFile->Image.Width; const int Height = m_pGifFile->Image.Height; if (m_pGifFile->Image.Interlace) { // Need to perform 4 passes on the images: for (int pass = 0; pass < 4; pass++) { for (i = InterlacedOffset[pass]; i < Height; i += InterlacedJumps[pass]) { if (DGifGetLine(m_pGifFile, pLine, Width) == GIF_ERROR) { TRACE("DGifGetLine error=%d\n", GifLastError()); return -1; } CopyGIF(pBits1 + XYOFFSET(x, y + i), pLine, Width, transparent, pColorTable); } } } else { // Non-interlaced image for (i = 0; i < Height; i++) { if (DGifGetLine(m_pGifFile, pLine, Width) == GIF_ERROR) { TRACE("DGifGetLine error=%d\n", GifLastError()); return -1; } CopyGIF(pBits1 + XYOFFSET(x, y + i), pLine, Width, transparent, pColorTable); } } // Prepare second image with next starting if (dispose == GIF_DISPOSE_BACKGND) { TRACE("*** GIF_DISPOSE_BACKGND ***\n"); const int x = m_pGifFile->Image.Left; const int y = m_pGifFile->Image.Top; const int Width = m_pGifFile->Image.Width; const int Height = m_pGifFile->Image.Height; // Clear next image to background index // Note: if transparent restore to transparent color (else use GIF background color) const COLORREF rgbFill = (transparent == GIF_NOT_TRANSPARENT) ? m_rgbBackgnd : m_rgbTransparent; for (int i = 0; i < Height; ++i) ::FillGIF(pBits2 + XYOFFSET(x, y + i), rgbFill, Width); } else if (dispose != GIF_DISPOSE_RESTORE) { // Copy current -> next (Update) memcpy(pBits2, pBits1, dwScreen); } dispose = 0; TRACE("\tdelay = %d msec\n", delay * 10); if (delay) { return delay * 10; } } break; case EXTENSION_RECORD_TYPE: { if (DGifGetExtension(m_pGifFile, &ExtCode, &pExtension) == GIF_ERROR) { TRACE("DGifGetExtension error=%d\n", GifLastError()); return -2; } TRACE("\n"); BOOL bNetscapeExt = FALSE; switch (ExtCode) { case COMMENT_EXT_FUNC_CODE: TRACE("GIF89 comment"); break; case GRAPHICS_EXT_FUNC_CODE: { TRACE("GIF89 graphics control"); ASSERT( pExtension[0] == 4 ); // int flag = pExtension[1]; delay = MAKEWORD(pExtension[2], pExtension[3]); transparent = (flag & GIF_TRANSPARENT) ? pExtension[4] : GIF_NOT_TRANSPARENT; dispose = (flag >> GIF_DISPOSE_SHIFT) & GIF_DISPOSE_MASK; TRACE(" delay = %d, dispose = %d transparent = %d\n", delay, dispose, transparent); break; } case PLAINTEXT_EXT_FUNC_CODE: TRACE("GIF89 plaintext"); break; case APPLICATION_EXT_FUNC_CODE: { TRACE("GIF89 application block\n"); ASSERT( pExtension ); if (memcmp(pExtension, szNetscape20ext, szNetscape20ext[0]) == 0) { TRACE("Netscape 2.0 extension\n"); bNetscapeExt = TRUE; } break; } default: TRACE("pExtension record of unknown type"); break; } TRACE(" (Ext Code = %d):\n", ExtCode); do { if (DGifGetExtensionNext(m_pGifFile, &pExtension) == GIF_ERROR) { TRACE("DGifGetExtensionNext error=%d\n", GifLastError()); return -3; } // Process Netscape 2.0 extension (GIF looping) if (pExtension && bNetscapeExt) { GifByteType bLength = pExtension[0]; int iSubCode = pExtension[1] & 0x07; if (bLength == 3 && iSubCode == NSEXT_LOOP) { UINT uLoopCount = MAKEWORD(pExtension[2], pExtension[3]); m_uLoopCount = uLoopCount - 1; TRACE("Looping extension, uLoopCount=%u\n", m_uLoopCount); } } } while (pExtension); break; } case TERMINATE_RECORD_TYPE: break; default: // Should be trapped by DGifGetRecordType break; } } while (RecordType != TERMINATE_RECORD_TYPE); return 0; }