From: Dimitri Schoolwerth Date: Wed, 19 Jan 2011 12:28:31 +0000 (+0000) Subject: Added GIF and animated GIF saving support. X-Git-Url: https://git.saurik.com/wxWidgets.git/commitdiff_plain/77b83d0a0f152e0eea6e6d357fced42319ecb118 Added GIF and animated GIF saving support. Applied (modified) patch by troelsk. Also added a basic unit test for checking the frames of a saved animated GIF (a previous unit test already handles content of a GIF with a single frame). Closes #8583. git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@66716 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775 --- diff --git a/docs/changes.txt b/docs/changes.txt index 0e564f755a..31750ae2f1 100644 --- a/docs/changes.txt +++ b/docs/changes.txt @@ -457,6 +457,7 @@ All (GUI): - Added wxArtProvider returning higher quality icons from Tango project. - wxPropertyGrid: Added "HasAlpha" attribute for wxColourProperty. - Added support for saving PNG files with palette (troelsk). +- Added support for saving as GIF and animated GIF (troelsk). GTK: diff --git a/include/wx/anidecod.h b/include/wx/anidecod.h index 86970ae771..15b3adfe11 100644 --- a/include/wx/anidecod.h +++ b/include/wx/anidecod.h @@ -12,7 +12,7 @@ #include "wx/defs.h" -#if wxUSE_STREAMS && wxUSE_ICO_CUR +#if wxUSE_STREAMS && (wxUSE_ICO_CUR || wxUSE_GIF) #include "wx/stream.h" #include "wx/image.h" @@ -76,6 +76,6 @@ private: }; -#endif // wxUSE_STREAMS && wxUSE_ICO_CUR +#endif // wxUSE_STREAMS && (wxUSE_ICO_CUR || wxUSE_GIF) #endif // _WX_ANIDECOD_H diff --git a/include/wx/imaggif.h b/include/wx/imaggif.h index 602c2e3233..103e934fb6 100644 --- a/include/wx/imaggif.h +++ b/include/wx/imaggif.h @@ -1,9 +1,9 @@ ///////////////////////////////////////////////////////////////////////////// // Name: wx/imaggif.h // Purpose: wxImage GIF handler -// Author: Vaclav Slavik & Guillermo Rodriguez Garcia +// Author: Vaclav Slavik, Guillermo Rodriguez Garcia, Gershon Elber, Troels K // RCS-ID: $Id$ -// Copyright: (c) Guillermo Rodriguez Garcia +// Copyright: (c) 1999-2011 Vaclav Slavik, Guillermo Rodriguez Garcia, Gershon Elber, Troels K // Licence: wxWindows licence ///////////////////////////////////////////////////////////////////////////// @@ -19,6 +19,10 @@ #if wxUSE_GIF +struct wxRGB; +struct GifHashTableType; +class WXDLLIMPEXP_FWD_CORE wxImageArray; // anidecod.h + class WXDLLIMPEXP_CORE wxGIFHandler : public wxImageHandler { public: @@ -28,6 +32,7 @@ public: m_extension = wxT("gif"); m_type = wxBITMAP_TYPE_GIF; m_mime = wxT("image/gif"); + m_hashTable = NULL; } #if wxUSE_STREAMS @@ -36,11 +41,49 @@ public: virtual bool SaveFile(wxImage *image, wxOutputStream& stream, bool verbose=true); + // Save animated gif + bool SaveAnimation(const wxImageArray& images, wxOutputStream *stream, + bool verbose = true, int delayMilliSecs = 1000, + const wxString& comment = wxEmptyString); + protected: virtual int DoGetImageCount(wxInputStream& stream); virtual bool DoCanRead(wxInputStream& stream); + + bool DoSaveFile(const wxImage&, wxOutputStream *, bool verbose, + bool first, int delayMilliSecs, bool loop, + const wxRGB *pal, int palCount, + int mask_index, const wxString& comment = wxEmptyString); #endif // wxUSE_STREAMS +protected: + + // Declarations for saving + unsigned long m_crntShiftDWord; /* For bytes decomposition into codes. */ + int m_pixelCount; + struct GifHashTableType *m_hashTable; + wxInt16 + m_EOFCode, /* The EOF LZ code. */ + m_clearCode, /* The CLEAR LZ code. */ + m_runningCode, /* The next code algorithm can generate. */ + m_runningBits, /* The number of bits required to represent RunningCode. */ + m_maxCode1, /* 1 bigger than max. possible code, in RunningBits bits. */ + m_crntCode, /* Current algorithm code. */ + m_crntShiftState; /* Number of bits in CrntShiftDWord. */ + wxUint8 m_LZBuf[256]; /* Compressed input is buffered here. */ + + bool InitHashTable(); + void ClearHashTable(); + void InsertHashTable(unsigned long key, int code); + int ExistsHashTable(unsigned long key); + +#if wxUSE_STREAMS + bool CompressOutput(wxOutputStream *, int code); + bool SetupCompress(wxOutputStream *, int bpp); + bool CompressLine(wxOutputStream *, const wxUint8 *line, int lineLen); +#endif +public: + static wxString ms_comment; private: DECLARE_DYNAMIC_CLASS(wxGIFHandler) }; diff --git a/interface/wx/image.h b/interface/wx/image.h index 5ac8bca173..e106e43061 100644 --- a/interface/wx/image.h +++ b/interface/wx/image.h @@ -334,7 +334,7 @@ const unsigned char wxIMAGE_ALPHA_OPAQUE = 0xff; - wxBMPHandler: For loading (including alpha support) and saving, always installed. - wxPNGHandler: For loading and saving. Includes alpha support. - wxJPEGHandler: For loading and saving. - - wxGIFHandler: Only for loading, due to legal issues. + - wxGIFHandler: For loading and saving (see below). - wxPCXHandler: For loading and saving (see below). - wxPNMHandler: For loading and saving (see below). - wxTIFFHandler: For loading (including alpha support) and saving. @@ -352,6 +352,8 @@ const unsigned char wxIMAGE_ALPHA_OPAQUE = 0xff; Loading PNMs only works for ASCII or raw RGB images. When saving in PNM format, wxPNMHandler will always save as raw RGB. + Saving GIFs requires images of maximum 8 bpp (see wxQuantize), and the alpha channel converted to a mask (see wxImage::ConvertAlphaToMask). + Saving an animated GIF requires images of the same size (see wxGIFHandler::SaveAnimation) @library{wxcore} @category{gdi} diff --git a/src/common/imaggif.cpp b/src/common/imaggif.cpp index fdb9a4971d..b0c4399d9c 100644 --- a/src/common/imaggif.cpp +++ b/src/common/imaggif.cpp @@ -1,9 +1,9 @@ ///////////////////////////////////////////////////////////////////////////// // Name: src/common/imaggif.cpp // Purpose: wxGIFHandler -// Author: Vaclav Slavik & Guillermo Rodriguez Garcia +// Author: Vaclav Slavik, Guillermo Rodriguez Garcia, Gershon Elber, Troels K // RCS-ID: $Id$ -// Copyright: (c) 1999 Vaclav Slavik & Guillermo Rodriguez Garcia +// Copyright: (c) 1999-2011 Vaclav Slavik, Guillermo Rodriguez Garcia, Gershon Elber, Troels K // Licence: wxWindows licence ///////////////////////////////////////////////////////////////////////////// @@ -19,14 +19,98 @@ #ifndef WX_PRECOMP #include "wx/intl.h" #include "wx/log.h" + #include "wx/palette.h" + #include "wx/utils.h" #endif #include "wx/imaggif.h" #include "wx/gifdecod.h" -#include "wx/wfstream.h" +#include "wx/stream.h" +#include "wx/anidecod.h" // wxImageArray + +#define GIF89_HDR "GIF89a" +#define NETSCAPE_LOOP "NETSCAPE2.0" + +// see members.aol.com/royalef/gifabout.htm +// members.aol.com/royalef/gif89a.txt + +enum +{ + GIF_MARKER_EXT = '!', // 0x21 + GIF_MARKER_SEP = ',', // 0x2C + GIF_MARKER_ENDOFDATA = ';', // 0x3B + + GIF_MARKER_EXT_GRAPHICS_CONTROL = 0xF9, + GIF_MARKER_EXT_COMMENT = 0xFE, + GIF_MARKER_EXT_APP = 0xFF +}; + +#define LZ_MAX_CODE 4095 // Biggest code possible in 12 bits. +#define FLUSH_OUTPUT 4096 // Impossible code, to signal flush. +#define FIRST_CODE 4097 // Impossible code, to signal first. + +#define HT_SIZE 8192 // 12bits = 4096 or twice as big! +#define HT_KEY_MASK 0x1FFF // 13bits keys + +#define HT_GET_KEY(l) (l >> 12) +#define HT_GET_CODE(l) (l & 0x0FFF) +#define HT_PUT_KEY(l) (l << 12) +#define HT_PUT_CODE(l) (l & 0x0FFF) + +struct wxRGB +{ + wxUint8 red; + wxUint8 green; + wxUint8 blue; +}; + +struct GifHashTableType +{ + wxUint32 HTable[HT_SIZE]; +}; + +/*static*/ wxString wxGIFHandler::ms_comment; IMPLEMENT_DYNAMIC_CLASS(wxGIFHandler,wxImageHandler) +//---------------------------------------------------------------------------- +// Forward declarations +//---------------------------------------------------------------------------- + +static int wxGIFHandler_KeyItem(unsigned long item); + +#if wxUSE_STREAMS + +static int wxGIFHandler_BitSize(int n); + +#if wxUSE_PALETTE +static bool wxGIFHandler_GetPalette(const wxImage& image, + wxRGB *pal, int *palCount, int *mask_index); +#endif +static +int wxGIFHandler_PaletteFind(const wxRGB& clr, const wxRGB *array, int count); + +static bool wxGIFHandler_Write(wxOutputStream *, const void *buf, size_t len); +static bool wxGIFHandler_WriteByte(wxOutputStream *, wxUint8); +static bool wxGIFHandler_WriteWord(wxOutputStream *, wxUint16); +static bool wxGIFHandler_WriteHeader(wxOutputStream *, int width, int height, + bool loop, const wxRGB *pal, int palCount, + const wxString& comment = wxEmptyString); +static bool wxGIFHandler_WriteRect(wxOutputStream *, int width, int height); +#if wxUSE_PALETTE +static bool wxGIFHandler_WriteTerm(wxOutputStream *); +#endif +static bool wxGIFHandler_WriteZero(wxOutputStream *); +static bool wxGIFHandler_WritePalette(wxOutputStream *, + const wxRGB *pal, size_t palCount, int bpp); +static bool wxGIFHandler_WriteControl(wxOutputStream *, + int maskIndex, int delayMilliSecs); +static bool wxGIFHandler_WriteComment(wxOutputStream *, const wxString&); +static bool wxGIFHandler_WriteLoop(wxOutputStream *); + +static bool wxGIFHandler_BufferedOutput(wxOutputStream *, wxUint8 *buf, int c); +#endif // wxUSE_STREAMS + //----------------------------------------------------------------------------- // wxGIFHandler //----------------------------------------------------------------------------- @@ -34,7 +118,7 @@ IMPLEMENT_DYNAMIC_CLASS(wxGIFHandler,wxImageHandler) #if wxUSE_STREAMS bool wxGIFHandler::LoadFile(wxImage *image, wxInputStream& stream, - bool verbose, int index) + bool verbose, int index) { wxGIFDecoder *decod; wxGIFErrorCode error; @@ -68,7 +152,7 @@ bool wxGIFHandler::LoadFile(wxImage *image, wxInputStream& stream, if ((error == wxGIF_TRUNCATED) && verbose) { wxLogError(_("GIF: data stream seems to be truncated.")); - /* go on; image data is OK */ + // go on; image data is OK } if (ok) @@ -85,15 +169,24 @@ bool wxGIFHandler::LoadFile(wxImage *image, wxInputStream& stream, return ok; } -bool wxGIFHandler::SaveFile( wxImage * WXUNUSED(image), - wxOutputStream& WXUNUSED(stream), bool verbose ) +bool wxGIFHandler::SaveFile(wxImage *image, + wxOutputStream& stream, bool verbose) { - if (verbose) - { - wxLogDebug(wxT("GIF: the handler is read-only!!")); - } +#if wxUSE_PALETTE + wxRGB pal[256]; + int palCount; + int maskIndex; + return wxGIFHandler_GetPalette(*image, pal, &palCount, &maskIndex) + && DoSaveFile(*image, &stream, verbose, true /*first?*/, 0, + false /*loop?*/, pal, palCount, maskIndex, ms_comment) + && wxGIFHandler_WriteTerm(&stream); +#else + wxUnusedVar(image); + wxUnusedVar(stream); + wxUnusedVar(verbose); return false; +#endif } bool wxGIFHandler::DoCanRead( wxInputStream& stream ) @@ -116,6 +209,603 @@ int wxGIFHandler::DoGetImageCount( wxInputStream& stream ) return decod.GetFrameCount(); } +bool wxGIFHandler::DoSaveFile(const wxImage& image, wxOutputStream *stream, + bool WXUNUSED(verbose), bool first, int delayMilliSecs, bool loop, + const wxRGB *pal, int palCount, int maskIndex, const wxString& comment) +{ + const unsigned long colorcount = image.CountColours(256+1); + bool ok = colorcount && (colorcount <= 256); + if (!ok) + { + return false; + } + + int width = image.GetWidth(); + int height = image.GetHeight(); + int width_even = width + ((width % 2) ? 1 : 0); + + if (first) + { + ok = wxGIFHandler_WriteHeader(stream, width, height, loop, + pal, palCount, comment); + } + + ok = ok && wxGIFHandler_WriteControl(stream, maskIndex, delayMilliSecs) + && wxGIFHandler_WriteByte(stream, GIF_MARKER_SEP) + && wxGIFHandler_WriteRect(stream, width, height); + + // local palette + if (first) + { + // we already saved the (global) palette + ok = ok && wxGIFHandler_WriteZero(stream); + } + else + { + const int bpp = wxGIFHandler_BitSize(palCount); + wxUint8 b; + + b = 0x80; + b |=(bpp - 1) << 5; + b |=(bpp - 1); + b &=~0x40; // clear interlaced + + ok = ok && wxGIFHandler_WriteByte(stream, b) + && wxGIFHandler_WritePalette(stream, pal, palCount, bpp); + } + + if (!ok) + { + return false; + } + + if (!InitHashTable()) + { + wxLogError(_("Couldn't initialize GIF hash table.")); + return false; + } + + const wxUint8 *src = image.GetData(); + wxUint8 *eightBitData = new wxUint8[width]; + + SetupCompress(stream, 8); + + m_pixelCount = height * width_even; + for (int y = 0; y < height; y++) + { + m_pixelCount -= width_even; + for (int x = 0; x < width; x++) + { + wxRGB rgb; + rgb.red = src[0]; + rgb.green = src[1]; + rgb.blue = src[2]; + int index = wxGIFHandler_PaletteFind(rgb, pal, palCount); + wxASSERT(index != wxNOT_FOUND); + eightBitData[x] = (wxUint8)index; + src+=3; + } + + ok = CompressLine(stream, eightBitData, width); + if (!ok) + { + break; + } + } + + delete [] eightBitData; + + wxDELETE(m_hashTable); + + return ok; +} + +bool wxGIFHandler::SaveAnimation(const wxImageArray& images, + wxOutputStream *stream, bool verbose, int delayMilliSecs, + const wxString& comment) +{ +#if wxUSE_PALETTE + bool ok = true; + size_t i; + + wxSize size(0,0); + for (i = 0; (i < images.GetCount()) && ok; i++) + { + const wxImage& image = images.Item(i); + wxSize temp(image.GetWidth(), image.GetHeight()); + ok = ok && image.HasPalette(); + if (i) + { + ok = ok && (size == temp); + } + else + { + size = temp; + } + } + + for (i = 0; (i < images.GetCount()) && ok; i++) + { + const wxImage& image = images.Item(i); + + wxRGB pal[256]; + int palCount; + int maskIndex; + + ok = wxGIFHandler_GetPalette(image, pal, &palCount, &maskIndex) + && DoSaveFile(image, stream, verbose, i == 0 /*first?*/, delayMilliSecs, + true /*loop?*/, pal, palCount, maskIndex, + comment.length() ? comment : ms_comment); + } + + return ok && wxGIFHandler_WriteTerm(stream); +#else + wxUnusedVar(images); + wxUnusedVar(stream); + wxUnusedVar(verbose); + wxUnusedVar(delayMilliSecs); + wxUnusedVar(comment); + + return false; +#endif +} + +bool wxGIFHandler::CompressOutput(wxOutputStream *stream, int code) +{ + if (code == FLUSH_OUTPUT) + { + while (m_crntShiftState > 0) + { + // Get rid of what is left in DWord, and flush it. + if (!wxGIFHandler_BufferedOutput(stream, m_LZBuf, + m_crntShiftDWord & 0xff)) + { + return false; + } + m_crntShiftDWord >>= 8; + m_crntShiftState -= 8; + } + m_crntShiftState = 0; // For next time. + if (!wxGIFHandler_BufferedOutput(stream, m_LZBuf, FLUSH_OUTPUT)) + { + return false; + } + } + else + { + m_crntShiftDWord |= ((long) code) << m_crntShiftState; + m_crntShiftState += m_runningBits; + while (m_crntShiftState >= 8) + { + // Dump out full bytes: + if (!wxGIFHandler_BufferedOutput(stream, m_LZBuf, + m_crntShiftDWord & 0xff)) + { + return false; + } + m_crntShiftDWord >>= 8; + m_crntShiftState -= 8; + } + } + + // If code can't fit into RunningBits bits, must raise its size. Note + // however that codes above LZ_MAX_CODE are used for special signaling. + if ( (m_runningCode >= m_maxCode1) && (code <= LZ_MAX_CODE)) + { + m_maxCode1 = 1 << ++m_runningBits; + } + return true; +} + +bool wxGIFHandler::SetupCompress(wxOutputStream *stream, int bpp) +{ + m_LZBuf[0] = 0; // Nothing was output yet. + m_clearCode = (1 << bpp); + m_EOFCode = m_clearCode + 1; + m_runningCode = m_EOFCode + 1; + m_runningBits = bpp + 1; // Number of bits per code. + m_maxCode1 = 1 << m_runningBits; // Max. code + 1. + m_crntCode = FIRST_CODE; // Signal that this is first one! + m_crntShiftState = 0; // No information in CrntShiftDWord. + m_crntShiftDWord = 0; + + // Clear hash table and send Clear to make sure the decoder does the same. + ClearHashTable(); + + return wxGIFHandler_WriteByte(stream, (wxUint8)bpp) + && CompressOutput(stream, m_clearCode); +} + +bool wxGIFHandler::CompressLine(wxOutputStream *stream, + const wxUint8 *line, int lineLen) +{ + int i = 0, crntCode, newCode; + unsigned long newKey; + wxUint8 pixel; + if (m_crntCode == FIRST_CODE) // It's first time! + crntCode = line[i++]; + else + crntCode = m_crntCode; // Get last code in compression. + + while (i < lineLen) + { + // Decode lineLen items. + pixel = line[i++]; // Get next pixel from stream. + // Form a new unique key to search hash table for the code combines + // crntCode as Prefix string with Pixel as postfix char. + newKey = (((unsigned long) crntCode) << 8) + pixel; + if ((newCode = ExistsHashTable(newKey)) >= 0) + { + // This Key is already there, or the string is old one, so + // simply take new code as our crntCode: + crntCode = newCode; + } + else + { + // Put it in hash table, output the prefix code, and make our + // crntCode equal to Pixel. + if (!CompressOutput(stream, crntCode)) + { + return false; + } + + crntCode = pixel; + + // If however the HashTable is full, we send a clear first and + // Clear the hash table. + if (m_runningCode >= LZ_MAX_CODE) + { + // Time to do some clearance: + if (!CompressOutput(stream, m_clearCode)) + { + return false; + } + + m_runningCode = m_EOFCode + 1; + m_runningBits = 8 + 1; + m_maxCode1 = 1 << m_runningBits; + ClearHashTable(); + } + else + { + // Put this unique key with its relative Code in hash table: + InsertHashTable(newKey, m_runningCode++); + } + } + } + // Preserve the current state of the compression algorithm: + m_crntCode = crntCode; + if (m_pixelCount == 0) + { + // We are done - output last Code and flush output buffers: + if (!CompressOutput(stream, crntCode) + || !CompressOutput(stream, m_EOFCode) + || !CompressOutput(stream, FLUSH_OUTPUT)) + { + return false; + } + } + + return true; +} + #endif // wxUSE_STREAMS -#endif // wxUSE_GIF +bool wxGIFHandler::InitHashTable() +{ + if (!m_hashTable) + { + m_hashTable = new GifHashTableType(); + } + + if (!m_hashTable) + { + return false; + } + + ClearHashTable(); + + return true; +} + +void wxGIFHandler::ClearHashTable() +{ + int index = HT_SIZE; + wxUint32 *HTable = m_hashTable->HTable; + + while (--index>=0) + { + HTable[index] = 0xfffffffful; + } +} + +void wxGIFHandler::InsertHashTable(unsigned long key, int code) +{ + int hKey = wxGIFHandler_KeyItem(key); + wxUint32 *HTable = m_hashTable->HTable; + + while (HT_GET_KEY(HTable[hKey]) != 0xFFFFFL) + { + hKey = (hKey + 1) & HT_KEY_MASK; + } + HTable[hKey] = HT_PUT_KEY(key) | HT_PUT_CODE(code); +} + + +int wxGIFHandler::ExistsHashTable(unsigned long key) +{ + int hKey = wxGIFHandler_KeyItem(key); + wxUint32 *HTable = m_hashTable->HTable, HTKey; + + while ((HTKey = HT_GET_KEY(HTable[hKey])) != 0xFFFFFL) + { + if (key == HTKey) + { + return HT_GET_CODE(HTable[hKey]); + } + hKey = (hKey + 1) & HT_KEY_MASK; + } + return -1; +} + +// --------------------------------------------------------------------------- +// implementation of global private functions +// --------------------------------------------------------------------------- + +int wxGIFHandler_KeyItem(unsigned long item) +{ + return ((item >> 12) ^ item) & HT_KEY_MASK; +} + +#if wxUSE_STREAMS + +int wxGIFHandler_BitSize(int n) +{ + int i; + for (i = 1; i <= 8; i++) + { + if ((1 << i) >= n) + { + break; + } + } + return i; +} + +#if wxUSE_PALETTE +bool wxGIFHandler_GetPalette(const wxImage& image, + wxRGB *pal, int *pPalCount, int *pMaskIndex) +{ + if (!image.HasPalette()) + { + return false; + } + + const wxPalette& palette = image.GetPalette(); + int palCount = palette.GetColoursCount(); + + for (int i = 0; i < palCount; ++i) + { + if (!palette.GetRGB(i, &pal[i].red, &pal[i].green, &pal[i].blue)) + { + break; + } + } + if (image.HasMask()) + { + wxRGB mask; + + mask.red = image.GetMaskRed(); + mask.green = image.GetMaskGreen(); + mask.blue = image.GetMaskBlue(); + *pMaskIndex = wxGIFHandler_PaletteFind(mask, pal, palCount); + if ( (*pMaskIndex == wxNOT_FOUND) && (palCount < 256)) + { + *pMaskIndex = palCount; + pal[palCount++] = mask; + } + } + else + { + *pMaskIndex = wxNOT_FOUND; + } + *pPalCount = palCount; + + return true; +} +#endif // wxUSE_PALETTE + +int wxGIFHandler_PaletteFind(const wxRGB& clr, const wxRGB *array, int count) +{ + for (int i = 0; i < count; i++) + { + if ( (clr.red == array[i].red) + && (clr.green == array[i].green) + && (clr.blue == array[i].blue)) + { + return i; + } + } + + return wxNOT_FOUND; +} + +bool wxGIFHandler_Write(wxOutputStream *stream, const void *buf, size_t len) +{ + return (len == stream->Write(buf, len).LastWrite()); +} + +bool wxGIFHandler_WriteByte(wxOutputStream *stream, wxUint8 byte) +{ + return wxGIFHandler_Write(stream, &byte, sizeof(byte)); +} + +bool wxGIFHandler_WriteWord(wxOutputStream *stream, wxUint16 word) +{ + wxUint8 buf[2]; + + buf[0] = word & 0xff; + buf[1] = (word >> 8) & 0xff; + return wxGIFHandler_Write(stream, &word, sizeof(word)); +} + +bool wxGIFHandler_WriteHeader(wxOutputStream *stream, int width, int height, + bool loop, const wxRGB *pal, int palCount, const wxString& comment) +{ + const int bpp = wxGIFHandler_BitSize(palCount); + wxUint8 buf[3]; + + bool ok = wxGIFHandler_Write(stream, GIF89_HDR, sizeof(GIF89_HDR)-1) + && wxGIFHandler_WriteWord(stream, (wxUint16) width) + && wxGIFHandler_WriteWord(stream, (wxUint16) height); + + buf[0] = 0x80; + buf[0] |=(bpp - 1) << 5; + buf[0] |=(bpp - 1); + buf[1] = 0; // background color == entry 0 + buf[2] = 0; // aspect ratio 1:1 + ok = ok && wxGIFHandler_Write(stream, buf, sizeof(buf)) + && wxGIFHandler_WritePalette(stream, pal, palCount, bpp); + + if (loop) + { + ok = ok && wxGIFHandler_WriteLoop(stream); + } + + if (comment.length()) + { + ok = ok && wxGIFHandler_WriteComment(stream, comment); + } + + return ok; +} + +bool wxGIFHandler_WriteRect(wxOutputStream *stream, int width, int height) +{ + return wxGIFHandler_WriteWord(stream, 0) // left + && wxGIFHandler_WriteWord(stream, 0) // top + && wxGIFHandler_WriteWord(stream, (wxUint16) width) + && wxGIFHandler_WriteWord(stream, (wxUint16) height); +} + +#if wxUSE_PALETTE +bool wxGIFHandler_WriteTerm(wxOutputStream *stream) +{ + return wxGIFHandler_WriteByte(stream, GIF_MARKER_ENDOFDATA); +} +#endif + +bool wxGIFHandler_WriteZero(wxOutputStream *stream) +{ + return wxGIFHandler_WriteByte(stream, 0); +} + +bool wxGIFHandler_WritePalette(wxOutputStream *stream, + const wxRGB *array, size_t count, int bpp) +{ + wxUint8 buf[3]; + for (int i = 0; (i < (1 << bpp)); i++) + { + if (i < (int)count) + { + buf[0] = array[i].red; + buf[1] = array[i].green; + buf[2] = array[i].blue; + } + else + { + buf[0] = buf[1] = buf[2] = 0; + } + + if ( !wxGIFHandler_Write(stream, buf, sizeof(buf)) ) + { + return false; + } + } + + return true; +} + +bool wxGIFHandler_WriteControl(wxOutputStream *stream, + int maskIndex, int delayMilliSecs) +{ + wxUint8 buf[8]; + + buf[0] = GIF_MARKER_EXT; // extension marker + buf[1] = GIF_MARKER_EXT_GRAPHICS_CONTROL; + buf[2] = 4; // length of block + buf[3] = (maskIndex != wxNOT_FOUND) ? 1 : 0; // has transparency + buf[4] = delayMilliSecs / 10; // delay time + buf[5] = 0; + buf[6] = (maskIndex != wxNOT_FOUND) ? (wxUint8) maskIndex : 0; + buf[7] = 0; + return wxGIFHandler_Write(stream, buf, sizeof(buf)); +} + +bool wxGIFHandler_WriteComment(wxOutputStream *stream, const wxString& comment) +{ + wxUint8 buf[3]; + wxCharBuffer text(comment.mb_str()); + size_t len = strlen(text.data()); + len = wxMin(len, 255); + + buf[0] = GIF_MARKER_EXT; + buf[1] = GIF_MARKER_EXT_COMMENT; + buf[2] = (wxUint8)len; + + return wxGIFHandler_Write(stream, buf, sizeof(buf)) + && wxGIFHandler_Write(stream, text.data(), len) + && wxGIFHandler_WriteZero(stream); +} + +bool wxGIFHandler_WriteLoop(wxOutputStream *stream) +{ + wxUint8 buf[4]; + const int loopcount = 0; // infinite + + buf[0] = GIF_MARKER_EXT; + buf[1] = GIF_MARKER_EXT_APP; + buf[2] = 0x0B; + bool ok = wxGIFHandler_Write(stream, buf, 3) + && wxGIFHandler_Write(stream, NETSCAPE_LOOP, sizeof(NETSCAPE_LOOP)-1); + + buf[0] = 3; + buf[1] = 1; + buf[2] = loopcount & 0xFF; + buf[3] = loopcount >> 8; + + return ok && wxGIFHandler_Write(stream, buf, 4) + && wxGIFHandler_WriteZero(stream); +} + +bool wxGIFHandler_BufferedOutput(wxOutputStream *stream, wxUint8 *buf, int c) +{ + bool ok = true; + + if (c == FLUSH_OUTPUT) + { + // Flush everything out. + if (buf[0]) + { + ok = wxGIFHandler_Write(stream, buf, buf[0]+1); + } + // Mark end of compressed data, by an empty block (see GIF doc): + wxGIFHandler_WriteZero(stream); + } + else + { + if (buf[0] == 255) + { + // Dump out this buffer - it is full: + ok = wxGIFHandler_Write(stream, buf, buf[0] + 1); + buf[0] = 0; + } + buf[++buf[0]] = c; + } + + return ok; +} + +#endif // wxUSE_STREAMS + +#endif // wxUSE_IMAGE && wxUSE_GIF diff --git a/tests/image/image.cpp b/tests/image/image.cpp index 8782d95a24..47114b52e8 100644 --- a/tests/image/image.cpp +++ b/tests/image/image.cpp @@ -23,6 +23,7 @@ #ifndef WX_PRECOMP #endif // WX_PRECOMP +#include "wx/anidecod.h" // wxImageArray #include "wx/image.h" #include "wx/palette.h" #include "wx/url.h" @@ -70,6 +71,7 @@ private: CPPUNIT_TEST( SizeImage ); CPPUNIT_TEST( CompareLoadedImage ); CPPUNIT_TEST( CompareSavedImage ); + CPPUNIT_TEST( SaveAnimatedGIF ); CPPUNIT_TEST_SUITE_END(); void LoadFromSocketStream(); @@ -78,6 +80,7 @@ private: void SizeImage(); void CompareLoadedImage(); void CompareSavedImage(); + void SaveAnimatedGIF(); DECLARE_NO_COPY_CLASS(ImageTestCase) }; @@ -1089,6 +1092,47 @@ void ImageTestCase::CompareSavedImage() #endif } +void ImageTestCase::SaveAnimatedGIF() +{ +#if wxUSE_PALETTE + wxImage image("horse.gif"); + CPPUNIT_ASSERT( image.IsOk() ); + + wxImageArray images; + images.Add(image); + int i; + for (i = 0; i < 4-1; ++i) + { + images.Add( images[i].Rotate90() ); + + images[i+1].SetPalette(images[0].GetPalette()); + } + + wxMemoryOutputStream memOut; + CPPUNIT_ASSERT( wxGIFHandler().SaveAnimation(images, &memOut) ); + + wxGIFHandler handler; + wxMemoryInputStream memIn(memOut); + CPPUNIT_ASSERT(memIn.IsOk()); + const int imageCount = handler.GetImageCount(memIn); + CPPUNIT_ASSERT_EQUAL(4, imageCount); + + for (i = 0; i < imageCount; ++i) + { + wxFileOffset pos = memIn.TellI(); + CPPUNIT_ASSERT( handler.LoadFile(&image, memIn, true, i) ); + memIn.SeekI(pos); + + WX_ASSERT_MESSAGE + ( + ("Compare test for GIF frame number %d failed", i), + memcmp(image.GetData(), images[i].GetData(), + images[i].GetWidth() * images[i].GetHeight() * 3) == 0 + ); + } +#endif // #if wxUSE_PALETTE +} + #endif //wxUSE_IMAGE