blob: 0d805bc85b74ad0cd239d9522a5f053829246d24 [file] [log] [blame]
/*********************************************************
*
* 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 <windowsx.h>
#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;
}