Skip to content
Larry Bank edited this page Mar 8, 2024 · 8 revisions

About GIF Decoding

GIF compression was created by CompuServe in the mid 1980's. They were basically the first internet service provider and wanted to make images less burdensome to slow 300 and 1200 baud connections. They chose LZW (Lempel-Ziv-Welsh) compression and supported palette color images of 1 to 8 bits per pixel (2 to 256 unique colors). Single frame GIF images are relatively easy to understand, a simple 2D image composed of symbols that reference a palette of 24-bit RGB colors. Soon after, Netscape Navigator added another layer on top of the humble GIF file - animation. Instead of creating a video-like codec, animated GIF uses transparent pixels to allow partial overpaint of sub-rectangles on the canvas (full size image). To properly render the current frame, a full sized canvas image must be kept in memory along with the new sub-frame to be merged on top. I originally created this library to leverage the framebuffer memory of SPI LCD displays so that microcontrollers with minimal RAM could decode animated GIFs. The way this works is that SPI displays allow the current write position (pixel cursor) to be placed anywhere, so as you decode each new line of image, transparent pixels just move the write cursor without changing the pixel color underneath. This allows a MCU to decode animated GIF images correctly without having to have enough memory to hold the full sized canvas. The down side of this technique is that every time you switch from "move cursor" to "write pixel" mode on the LCD, you add a delay. If your GIF animation has a lot of small groups of transparent pixels, this can have a dramatically bad effect on the performance. This latest release of AnimatedGIF adds code to simplify all of these different situations - if you have additional RAM, the work of merging the transparent pixels and creating fully-rendered output can be done within the library. I even added support for generating 1-bit output suitable for OLED displays like the SSD1306.

Normal vs Turbo Mode

I recently added a new 'Turbo' option for decoding that runs faster in exchange for needing more RAM. Traditional LZW decoding uses a dynamic dictionary that keeps a linked list of repeated patterns (strings). As each variable length code is decoded, a new entry is added to the list with the new code added on. At the same time, the decoder traverses the list backwards to output the current string. This list traversal is what slows down the decoding process because each pixel must be fetched in reverse order. After the list terminator is reached, the string is output (in forward order) to the decoded image. With Turbo mode, I use the output image as the dictionary instead of a linked list. This requires additional memory to hold the (32-bit) offset of the older version of each string. It's faster because to generate the output pixels, it's just a simple forward copy instead of having to traverse a list backwards and then output the data forwards. For images with long strings (lots of repeating patterns), the speed difference between Turbo and Normal mode can be dramatic.

Public API

The AnimatedGIF class exposes a few simple methods detailed below. The more challenging part of using it is when you're using a new type of media/file and/or display and need to write your own callback functions. Those are documented below the API section.

This is the entire class definition for AnimatedGIF:

class AnimatedGIF {
  public:
    int open(uint8_t *pData, int iDataSize, GIF_DRAW_CALLBACK *pfnDraw);
    int open(char *szFilename, GIF_OPEN_CALLBACK *pfnOpen, GIF_CLOSE_CALLBACK *pfnClose, GIF_READ_CALLBACK *pfnRead, GIF_SEEK_CALLBACK *pfnSeek, GIF_DRAW_CALLBACK *pfnDraw);
    int openFLASH(uint8_t *pData, int iDataSize, GIF_DRAW_CALLBACK *pfnDraw);
    void close();
    void reset();
    void begin(uint8_t ucPaletteType);
    int playFrame(bool bSync, int *delayMilliseconds, void *pUser);
    int getCanvasWidth();
    int getCanvasHeight();
    int getLastError();
    int getInfo(GIFINFO *pInfo);
    int allocTurboBuf(GIF_ALLOC_CALLBACK *pfnAlloc);
    int freeTurboBuf(GIF_FREE_CALLBACK *pfnFree);
    void setTurboBuf(void *pTurboBuffer);
    int allocFrameBuf(GIF_ALLOC_CALLBACK *pfnAlloc);
    int freeFrameBuf(GIF_FREE_CALLBACK *pfnFree);
    void setFrameBuf(void *pFrameBuffer);
    uint8_t *getFrameBuf();
    uint8_t *getTurboBuf();
    int setDrawType(int iType);
    int getLoopCount();
    int getComment(char *destBuffer);

  private:
    GIFIMAGE _gif;
};

The class is a thin C++ wrapper on top of C99 code which does all of the actual work. The GIFIMAGE structure is kept private, but is passed as a pointer to the C 'worker' functions. This makes it easy to lift out the C code if you want to use it in a pure C project. Here are details of each method:

open()
There are three versions of the open method - one for GIF images in memory (RAM + FLASH) and another for images coming from a file. All return 1 for success, 0 for failure. The success or failure depends not just on the file successfully opening, but also on the GIF image file header being parsed a little to gather the canvas size. Once this function returns successfully, you can use getCanvasWidth() and getCanvasHeight().

close()
You can call this any time to close the file handle associated with your GIF image. For GIF files coming from memory, this has no effect.

reset()
Use this function to reset a GIF animation to the first frame in the sequence. Each frame is displayed by calling playFrame(). It returns false when it has played the last frame in the sequence. If you want to start from the first frame again without closing and re-opening the file, call reset().

begin()
Call this before using the AnimatedGIF class methods. It clears the internal variables and allows you to choose whether to provide little-endian or big-endian RGB565 colors in the palette.

playFrame()
This method decodes the current frame, updates the internal file pointer to the start of the next frame and return 1 if there are additional frames to decode or 0 if the last frame has just been decoded. You can optionally have the AnimatedGIF code manage the timing of the frames by setting bSync to true. For example, if the GIF file frameDelay is set to 100ms (10 FPS) and it takes 20ms to decode+display the frame, bSync set to true will wait for the other 80ms to pass. If you want to draw frames as fast as possible or manage the timing in your code, set bSync to false. The delayMilliseconds parameter (optional - set to NULL to ignore) will return the number of milliseconds the frame is to be displayed. Each frame can have a unique delay value.
It's important to understand the each frame in a GIF animation only has to contain the changed pixels. In other words, if you want to jump forwards or backwards by N frames, you would have to re-decode the entire sequence from the start. This is why I don't offer a method to let you navigate the frames except in a forward direction, one at a time.

getCanvasWidth()
Returns the canvas width in pixels. This can be called any time after a successful open(). The canvas size determines the overall image size of an animated GIF, while each successive frame can be any size and only needs to be big enough to update the changed pixels for that frame.

allocFrameBuf(GIF_ALLOC_CALLBACK *pfnAlloc)
If your MCU/System has enough RAM to hold the entire canvas image, use this option to simplify and potentially speed up the drawing. By allocating a buffer, you allow the disposal options and transparent pixels to be drawn internally and then the draw function only needs to display the pixels instead of doing lower level operations on them. You provide a simple callback function that can allocate a block of memory.

setFrameBuf(void *pFrameBuffer)
If your MCU/System has enough RAM to hold the entire canvas image, use this option to simplify and potentially speed up the drawing. By providing a buffer, you allow the disposal options and transparent pixels to be drawn internally and then the draw function only needs to display the pixels instead of doing lower level operations on them.

freeFrameBuf(GIF_FREE_CALLBACK *pfnFree)
When you're finished decoding the current image, this will free the internal buffer using the method you provide.

getFrameBuf()
Returns the internal pointer to the canvas image. If you don't want to draw the GIF line by line, then after decoding a frame, this pointer will point to the start of the entire frame. You must successfully call allocFrameBuf() before using this.

allocTurboBuf(GIF_ALLOC_CALLBACK *pfnAlloc)
If your MCU/System has enough RAM to hold the entire canvas image plus about 32K, use this option to speed up decoding (see above). You provide a simple callback function that can allocate a block of memory.

setTurboBuf(void *pTurboBuffer)
Allows you to manage the memory if you allocate it outside of AnimatedGIF

freeTurboBuf(GIF_FREE_CALLBACK *pfnFree)
When you're finished decoding, this will free the internal buffer using the method you provide.

getTurboBuf()
Returns the internal pointer to the Turbo buffer. The first part of this buffer is the current frame as 8-bits per pixel.

setDrawType(int iDrawType)
Another option to simplify the GIF_DRAW_CALLBACK code. By default, the pixels passed to the draw callback are 'raw' in that they need to be translated through the current palette. Call this function with GIF_DRAW_COOKED to have the AnimatedGIF library do the palette translation for you and then the draw function will get pixels read to send to the display. This option is only available when a frame buffer has been allocated or given to the library.

getLastError()
Returns the last error or 0 (GIF_SUCCESS) if there was none. See AnimatedGIF.h for a list of error codes

getInfo(GIFINFO *pINFO)
Returns information about animated sequences (see structure below). This function may take a long time to return depending on the size of the file and if it's stored on a slow media such as a SDCard.

The GIFINFO structure:

typedef struct gif_info_tag
{
  int32_t iFrameCount; // total frames in file
  int32_t iDuration; // duration of animation in milliseconds
  int32_t iMaxDelay; // maximum frame delay
  int32_t iMinDelay; // minimum frame delay
} GIFINFO;

The Callback Functions

There are 7 callback functions defined by AnimatedGIF. If you're displaying a GIF from memory, then at a minimum you only need to provide a single function - GIF_DRAW_CALLBACK because the memory 'file' functions are provided for you inside the AnimatedGIF library code. The example sketches contain code which implements this sufficiently to display most GIFs properly on any LCD. Let's see what's involved in implementing it. Here's the function prototype:

typedef void (GIF_DRAW_CALLBACK)(GIFDRAW *pDraw);

The GIFDRAW structure:

typedef struct gif_draw_tag
{
  int iX, iY; // Corner offset of this frame on the canvas
  int y; // current line being drawn (0 = top line of image)
  int iWidth; // width of the current line
  int iCanvasWidth; // width of the whole canvas in case you're placing the new data into a framebuffer yourself
  uint8_t *pPixels; // 8-bit source pixels for this line
  uint16_t *pPalette; // little or big-endian RGB565 palette entries
  uint8_t ucTransparent; // transparent color
  uint8_t ucHasTransparency; // flag indicating the transparent color is in use
  uint8_t ucDisposalMethod; // frame disposal method
  uint8_t ucBackground; // background color
} GIFDRAW;

Hopefully you find the comments for each member variable pretty clear. The GIFDRAW callback is tasked with drawing the current scanline on the display. In 'RAW' mode, it's provided with the 8-bit pixel indexes and a palette to render those pixels as the desired output type (e.g. RGB565). It also must manage drawing (actually skipping) transparent pixels. In the example sketches you may see display library functions used which you're not accustomed to (e.g. setting the write position on the LCD). This is because working one pixel at a time on SPI LCD displays would slow the output down considerably. The only way to work efficiently on those serial displays is to write as many pixels as possible in each transaction. It also helps if your MCU can use DMA to write those pixels so that it doesn't have to wait for the each write transaction to finish. The examples which use my bb_spi_lcd library make use of DMA on a few systems. The code to manage DMA transactions hasn't been abstracted by the Arduino API, so it must be written uniquely for each MCU. Since we're trying to use as little RAM as possible on the target MCU, we need to make use of the memory built into the LCD controller. For transparent pixels, this presents a problem. We either need to have the pixels from the previous frame in our memory or "jump over" those pixels on the display. This causes quite a bit of extra delay if there are a lot of small runs of transparent pixels because switching to command mode and moving the LCD write pointer gums up the works. If you want to display frames as quickly as possible, create GIF sequences without transparent pixels.

The other 4 callback functions need to be implemented if you're working with files. They implement the standard functions of open, close, read, and seek:

typedef void * (GIF_OPEN_CALLBACK)(char *szFilename, int32_t *pFileSize);
typedef void (GIF_CLOSE_CALLBACK)(void *pHandle);
typedef int32_t (GIF_READ_CALLBACK)(GIFFILE *pFile, uint8_t *pBuf, int32_t iLen);
typedef int32_t (GIF_SEEK_CALLBACK)(GIFFILE *pFile, int32_t iPosition);
typedef void * (GIF_ALLOC_CALLBACK)(uint32_t iSize);
typedef void (GIF_FREE_CALLBACK)(void *buffer);

The challenge with the file system callbacks is that file access on Arduino is usually not associated with a simple file handle, but with a C++ class. In order to manage this in a generic way that will work for all possible systems, the AnimatedGIF class holds onto a void * pointer which you would like use to hold a class pointer. Let's look at the GIF_CLOSE_CALLBACK function I wrote for the Arduino SD library to understand how this is done:

void GIFCloseFile(void *pHandle)
{
  File *f = static_cast<File *>(pHandle);
  if (f != NULL)
    f->close();
}

1-Bpp Cooked Output

The latest version of the library adds support for generating ready-made 1-bit per pixel output that can be sent directly to OLED or LCD displays. There are two 1-bpp options depending on your display type: GIF_PALETTE_1BPP is arranged as horizontal bytes with the first pixel of each byte in the MSB (most significant bit), GIF_PALETTE_1BPP_OLED is arranged like OLED memory - vertical bytes with the LSB (least significant bit) in the topmost position of each byte.

Final Thoughts

The subject of bits, bytes and pixels can get complicated, especially for non-programmers. Hopefully the example code can point you in the right direction for how to use this library. I've tried to incorporate as much of the challenging parts into the library code to free you to use it without worrying too much about the details.

Clone this wiki locally