From a02c124940b6c5d61ffcbfb2c7e4d493775c3192 Mon Sep 17 00:00:00 2001 From: Andrew Hannam Date: Sat, 20 Apr 2013 21:19:26 +1000 Subject: GIF image handling GIF image handling Updates to Image structure to make memory accounting optional Add set image background color to handle animated transparency. --- src/gdisp/image.c | 33 ++ src/gdisp/image_bmp.c | 21 +- src/gdisp/image_gif.c | 1170 ++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 1201 insertions(+), 23 deletions(-) (limited to 'src/gdisp') diff --git a/src/gdisp/image.c b/src/gdisp/image.c index 3da0844d..061db5fe 100644 --- a/src/gdisp/image.c +++ b/src/gdisp/image.c @@ -167,6 +167,7 @@ bool_t gdispImageSetBaseFileStreamReader(gdispImage *img, void *BaseFileStreamPt gdispImageError gdispImageOpen(gdispImage *img) { gdispImageError err; + img->bgcolor = White; for(img->fns = ImageHandlers; img->fns < ImageHandlers+sizeof(ImageHandlers)/sizeof(ImageHandlers[0]); img->fns++) { err = img->fns->open(img); if (err != GDISP_IMAGE_ERR_BADFORMAT) { @@ -190,6 +191,10 @@ void gdispImageClose(gdispImage *img) { img->io.fns->close(&img->io); } +void gdispImageSetBgColor(gdispImage *img, color_t bgcolor) { + img->bgcolor = bgcolor; +} + gdispImageError gdispImageCache(gdispImage *img) { if (!img->fns) return GDISP_IMAGE_ERR_BADFORMAT; return img->fns->cache(img); @@ -205,6 +210,34 @@ systime_t gdispImageNext(gdispImage *img) { return img->fns->next(img); } +// Helper Routines +void *gdispImageAlloc(gdispImage *img, size_t sz) { + #if GDISP_NEED_IMAGE_ACCOUNTING + void *ptr; + + ptr = chHeapAlloc(NULL, sz); + if (ptr) { + img->memused += sz; + if (img->memused > img->maxmemused) + img->maxmemused = img->memused; + } + return ptr; + #else + (void) img; + return chHeapAlloc(NULL, sz); + #endif +} + +void gdispImageFree(gdispImage *img, void *ptr, size_t sz) { + #if GDISP_NEED_IMAGE_ACCOUNTING + chHeapFree(ptr); + img->memused -= sz; + #else + (void) img; + (void) sz; + chHeapFree(ptr); + #endif +} #endif /* GFX_USE_GDISP && GDISP_NEED_IMAGE */ /** @} */ diff --git a/src/gdisp/image_bmp.c b/src/gdisp/image_bmp.c index 4ddfc7f8..bab9b4e9 100644 --- a/src/gdisp/image_bmp.c +++ b/src/gdisp/image_bmp.c @@ -53,6 +53,12 @@ #define GDISP_NEED_IMAGE_BMP_32 TRUE #endif +/** + * Helper Routines Needed + */ +void *gdispImageAlloc(gdispImage *img, size_t sz); +void gdispImageFree(gdispImage *img, void *ptr, size_t sz); + /** * How big a pixel array to allocate for blitting (in pixels) * Bigger is faster but uses more RAM. @@ -148,9 +154,8 @@ gdispImageError gdispImageOpen_BMP(gdispImage *img) { img->flags = 0; /* Allocate our private area */ - if (!(img->priv = (gdispImagePrivate *)chHeapAlloc(NULL, sizeof(gdispImagePrivate)))) + if (!(img->priv = (gdispImagePrivate *)gdispImageAlloc(img, sizeof(gdispImagePrivate)))) return GDISP_IMAGE_ERR_NOMEMORY; - img->membytes = sizeof(gdispImagePrivate); /* Initialise the essential bits in the private area */ priv = img->priv; @@ -336,9 +341,8 @@ gdispImageError gdispImageOpen_BMP(gdispImage *img) { if (priv->bmpflags & BMP_PALETTE) { img->io.fns->seek(&img->io, offsetColorTable); - if (!(priv->palette = (color_t *)chHeapAlloc(NULL, priv->palsize*sizeof(color_t)))) + if (!(priv->palette = (color_t *)gdispImageAlloc(img, priv->palsize*sizeof(color_t)))) return GDISP_IMAGE_ERR_NOMEMORY; - img->membytes += priv->palsize * sizeof(color_t); if (priv->bmpflags & BMP_V2) { for(aword = 0; aword < priv->palsize; aword++) { if (img->io.fns->read(&img->io, &priv->buf, 3) != 3) goto baddatacleanup; @@ -430,14 +434,13 @@ void gdispImageClose_BMP(gdispImage *img) { if (img->priv) { #if GDISP_NEED_IMAGE_BMP_1 || GDISP_NEED_IMAGE_BMP_4 || GDISP_NEED_IMAGE_BMP_4_RLE || GDISP_NEED_IMAGE_BMP_8 || GDISP_NEED_IMAGE_BMP_8_RLE if (img->priv->palette) - chHeapFree((void *)img->priv->palette); + gdispImageFree(img, (void *)img->priv->palette, img->priv->palsize*sizeof(color_t)); #endif if (img->priv->frame0cache) - chHeapFree((void *)img->priv->frame0cache); - chHeapFree((void *)img->priv); + gdispImageFree(img, (void *)img->priv->frame0cache, img->width*img->height*sizeof(pixel_t)); + gdispImageFree(img, (void *)img->priv, sizeof(gdispImagePrivate)); img->priv = 0; } - img->membytes = 0; img->io.fns->close(&img->io); } @@ -794,7 +797,7 @@ gdispImageError gdispImageCache_BMP(gdispImage *img) { /* We need to allocate the cache */ len = img->width * img->height * sizeof(pixel_t); - priv->frame0cache = (pixel_t *)chHeapAlloc(NULL, len); + priv->frame0cache = (pixel_t *)gdispImageAlloc(img, len); if (!priv->frame0cache) return GDISP_IMAGE_ERR_NOMEMORY; img->membytes += len; diff --git a/src/gdisp/image_gif.c b/src/gdisp/image_gif.c index b7a940ad..2672333f 100644 --- a/src/gdisp/image_gif.c +++ b/src/gdisp/image_gif.c @@ -28,20 +28,1162 @@ #if GFX_USE_GDISP && GDISP_NEED_IMAGE && GDISP_NEED_IMAGE_GIF -#error "GIF support not implemented yet" - -/* A pallete structure */ -typedef struct gdispImagePallete { - uint8_t flags; - #define GDISP_IMAGE_FLG_INT_TRANSPARENT 0x01 - uint8_t idxtrans; /* The transparent idx */ - uint8_t maxidx; /* The maximum index (0..255) */ - uint8_t repidx; /* The index to use if the image data > maxidx */ - color_t pal[256]; /* The pallete entries - not all may actually be allocated */ -} gdispImagePallete; - -/* Draw a single palletized line (or partial line) */ -static void gdispDrawPalleteLine(const gdispImagePallete *pal, const uint8_t *line, coord_t x, coord_t y, coord_t cx); +/** + * Helper Routines Needed + */ +void *gdispImageAlloc(gdispImage *img, size_t sz); +void gdispImageFree(gdispImage *img, void *ptr, size_t sz); + +/** + * How big an array to allocate for blitting (in pixels) + * Bigger is faster but uses more RAM. + */ +#define BLIT_BUFFER_SIZE 32 + +/* + * Determining endianness as at compile time is not guaranteed or compiler portable. + * We use the best test we can. If we can't guarantee little endianness we do things the + * hard way. + */ +#define GUARANTEED_LITTLE_ENDIAN (!defined(SAFE_ENDIAN) && !defined(SAFE_ALIGNMENT) && (\ + (defined(__BYTE_ORDER__)&&(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) \ + || defined(__LITTLE_ENDIAN__) \ + || defined(__LITTLE_ENDIAN) \ + || defined(_LITTLE_ENDIAN) \ +/* || (1 == *(unsigned char *)&(const int){1})*/ \ + )) + + +/* This is a runtime test */ +static const uint8_t dwordOrder[4] = { 1, 2, 3, 4 }; + +#define isWordLittleEndian() (*(uint16_t *)&dwordOrder == 0x0201) +#define isDWordLittleEndian() (*(uint32_t *)&dwordOrder == 0x04030201) + +#if GUARANTEED_LITTLE_ENDIAN + /* These are fast routines for guaranteed little endian machines */ + #define CONVERT_FROM_WORD_LE(w) + #define CONVERT_FROM_DWORD_LE(dw) +#else + /* These are slower routines for when little endianness cannot be guaranteed at compile time */ + #define CONVERT_FROM_WORD_LE(w) { if (!isWordLittleEndian()) w = ((((uint16_t)(w))>>8)|(((uint16_t)(w))<<8)); } + #define CONVERT_FROM_DWORD_LE(dw) { if (!isDWordLittleEndian()) dw = (((uint32_t)(((const uint8_t *)(&dw))[0]))|(((uint32_t)(((const uint8_t *)(&dw))[1]))<<8)|(((uint32_t)(((const uint8_t *)(&dw))[2]))<<16)|(((uint32_t)(((const uint8_t *)(&dw))[3]))<<24)); } +#endif + +// We need a special error to indicate the end of file (which may not actually be an error) +#define GDISP_IMAGE_EOF ((gdispImageError)-1) +#define GDISP_IMAGE_LOOP ((gdispImageError)-2) + +#define MAX_CODE_BITS 12 +#define CODE_MAX ((1<priv; + + // We need the decode ram, and possibly a palette + if (!(decode = (imgdecode *)gdispImageAlloc(img, sizeof(imgdecode)+priv->frame.palsize*sizeof(color_t)))) + return GDISP_IMAGE_ERR_NOMEMORY; + + // We currently have not read any image data block + decode->blocksz = 0; + + // Set the palette + if (priv->frame.palsize) { + // Local palette + decode->maxpixel = priv->frame.palsize-1; + decode->palette = (color_t *)(decode+1); + img->io.fns->seek(&img->io, priv->frame.pospal); + for(cnt = 0; cnt < priv->frame.palsize; cnt++) { + if (img->io.fns->read(&img->io, &decode->buf, 3) != 3) + goto baddatacleanup; + decode->palette[cnt] = RGB2COLOR(decode->buf[0], decode->buf[1], decode->buf[2]); + } + } else if (priv->palette) { + // Global palette + decode->maxpixel = priv->palsize-1; + decode->palette = priv->palette; + } else { + // Oops - we must have a palette + goto baddatacleanup; + } + + // Get the initial lzw code size and values + img->io.fns->seek(&img->io, priv->frame.posimg); + if (img->io.fns->read(&img->io, &decode->bitsperpixel, 1) != 1 || decode->bitsperpixel >= MAX_CODE_BITS) + goto baddatacleanup; + decode->code_clear = 1 << decode->bitsperpixel; + decode->code_eof = decode->code_clear + 1; + decode->code_max = decode->code_clear + 2; + decode->code_last = CODE_NONE; + decode->bitspercode = decode->bitsperpixel+1; + decode->maxcodesz = 1 << decode->bitspercode; + decode->shiftbits = 0; + decode->shiftdata = 0; + decode->stackcnt = 0; + for(cnt = 0; cnt <= CODE_MAX; cnt++) + decode->prefix[cnt] = CODE_NONE; + + // All ready to go + priv->decode = decode; + return GDISP_IMAGE_ERR_OK; + +baddatacleanup: + gdispImageFree(img, decode, sizeof(imgdecode)+priv->frame.palsize*sizeof(color_t)); + return GDISP_IMAGE_ERR_BADDATA; +} + +/** + * Stop decoding a frame. + * + * Pre: Frame info has been read. + */ +static void stopDecode(gdispImage *img) { + gdispImagePrivate * priv; + + priv = img->priv; + + // Free the decode data + if (priv->decode) { + gdispImageFree(img, (void *)priv->decode, sizeof(imgdecode)+priv->frame.palsize*sizeof(color_t)); + priv->decode = 0; + } +} + +static uint16_t getPrefix(imgdecode *decode, uint16_t code) { + uint16_t i; + + for(i=0; code > decode->code_clear && i <= CODE_MAX; i++, code = decode->prefix[code]) { + if (code > CODE_MAX) + return CODE_NONE; + } + return code; +} + +/** + * Decode some pixels from a frame. + * + * Pre: We are ready for decoding. + * + * Return: The number of pixels decoded 0 .. BLIT_BUFFER_SIZE-1. 0 means EOF + * + * Note: The resulting pixels are stored in decode->buf + */ +static uint16_t getbytes(gdispImage *img) { + gdispImagePrivate * priv; + imgdecode * decode; + uint16_t cnt; + uint16_t code, prefix; + uint8_t bdata; + + priv = img->priv; + decode = priv->decode; + cnt = 0; + + // At EOF + if (decode->code_last == decode->code_eof) + return 0; + + while(cnt < sizeof(decode->buf)) { + // Use the stack up first + if (decode->stackcnt > 0) { + decode->buf[cnt++] = decode->stack[--decode->stackcnt]; + continue; + } + + // Get another code - a code is made up of decode->bitspercode bits. + while (decode->shiftbits < decode->bitspercode) { + // Get a byte - we may have to start a new data block + if ((!decode->blocksz && (img->io.fns->read(&img->io, &decode->blocksz, 1) != 1 || !decode->blocksz)) + || img->io.fns->read(&img->io, &bdata, 1) != 1) { + // Pretend we got the EOF code - some encoders seem to just end the file + decode->code_last = decode->code_eof; + return cnt; + } + decode->blocksz--; + + decode->shiftdata |= ((unsigned long)bdata) << decode->shiftbits; + decode->shiftbits += 8; + } + code = decode->shiftdata & BitMask[decode->bitspercode]; + decode->shiftdata >>= decode->bitspercode; + decode->shiftbits -= decode->bitspercode; + /** + * If code cannot fit into bitspercode bits we must raise its size. + * Note that codes above CODE_MAX are used for special signaling. + * If we're using MAX_CODE_BITS bits already and we're at the max code, just + * keep using the table as it is, don't increment decode->bitspercode. + */ + if (decode->code_max < CODE_MAX + 2 && ++decode->code_max > decode->maxcodesz && decode->bitspercode < MAX_CODE_BITS) { + decode->maxcodesz <<= 1; + decode->bitspercode++; + } + + // EOF - the appropriate way to stop decoding + if (code == decode->code_eof) { + // Skip to the end of the data blocks + do { + img->io.fns->seek(&img->io, img->io.pos+decode->blocksz); + } while (img->io.fns->read(&img->io, &decode->blocksz, 1) == 1 && decode->blocksz); + + // Mark the end + decode->code_last = decode->code_eof; + break; + } + + if (code == decode->code_clear) { + // Start again + for(prefix = 0; prefix <= CODE_MAX; prefix++) + decode->prefix[prefix] = CODE_NONE; + decode->code_max = decode->code_eof + 1; + decode->bitspercode = decode->bitsperpixel + 1; + decode->maxcodesz = 1 << decode->bitspercode; + decode->code_last = CODE_NONE; + continue; + } + + if (code < decode->code_clear) { + // Simple unencoded pixel - add it + decode->buf[cnt++] = code; + + } else { + /** + * Its a LZW code - trace the linked list until the prefix is a + * valid pixel while pushing the suffix pixels on the stack. + * If done, pop the stack in reverse order adding the pixels + */ + if (decode->prefix[code] != CODE_NONE) + prefix = code; + + /** + * Only allowed if the code equals the partial code. + * In that case code = XXXCode, CrntCode or the + * prefix code is last code and the suffix char is + * exactly the prefix of last code! + */ + else if (code == decode->code_max - 2 && decode->stackcnt < sizeof(decode->stack)) { + prefix = decode->code_last; + decode->suffix[decode->code_max - 2] = decode->stack[decode->stackcnt++] = getPrefix(decode, decode->code_last); + } else + return 0; + + /** + * If the image is OK we should not get a CODE_NONE while tracing. + * To prevent looping with a bad image we use StackPtr as loop counter + * and stop before overflowing Stack[]. + */ + while (decode->stackcnt < sizeof(decode->stack) && prefix > decode->code_clear && prefix <= CODE_MAX) { + decode->stack[decode->stackcnt++] = decode->suffix[prefix]; + prefix = decode->prefix[prefix]; + } + if (decode->stackcnt >= sizeof(decode->stack) || prefix > CODE_MAX) + return 0; + + /* Push the last character on stack: */ + decode->stack[decode->stackcnt++] = prefix; + } + + if (decode->code_last != CODE_NONE && decode->prefix[decode->code_max - 2] == CODE_NONE) { + decode->prefix[decode->code_max - 2] = decode->code_last; + + /* Only allowed if code is exactly the running code: + * In that case code = XXXCode, CrntCode or the + * prefix code is last code and the suffix char is + * exactly the prefix of last code! */ + decode->suffix[decode->code_max - 2] = getPrefix(decode, code == decode->code_max - 2 ? decode->code_last : code); + } + decode->code_last = code; + } + return cnt; +} + +/** + * Read the info on a frame. + * + * Pre: The file position is at the start of the frame. + */ +static gdispImageError initFrame(gdispImage *img) { + gdispImagePrivate * priv; + imgcache * cache; + uint8_t blocktype; + uint8_t blocksz; + + priv = img->priv; + + // Save the dispose info from the existing frame + priv->dispose.flags = priv->frame.flags; + priv->dispose.paltrans = priv->frame.paltrans; + priv->dispose.x = priv->frame.x; + priv->dispose.y = priv->frame.y; + priv->dispose.width = priv->frame.width; + priv->dispose.height = priv->frame.height; + + // Check for a cached version of this image + for(cache=priv->cache; cache && cache->frame.posstart <= img->io.pos; cache=cache->next) { + if (cache->frame.posstart == img->io.pos) { + priv->frame = cache->frame; + priv->curcache = cache; + return GDISP_IMAGE_ERR_OK; + } + } + + // Get ready for a new image + priv->curcache = 0; + priv->frame.posstart = img->io.pos; + priv->frame.flags = 0; + priv->frame.delay = 0; + priv->frame.palsize = 0; + + // Process blocks until we reach the image descriptor + while(1) { + if (img->io.fns->read(&img->io, &blocktype, 1) != 1) + return GDISP_IMAGE_ERR_BADDATA; + + switch(blocktype) { + case 0x2C: //',' - IMAGE_DESC_RECORD_TYPE; + // Read the Image Descriptor + if (img->io.fns->read(&img->io, priv->buf, 9) != 9) + return GDISP_IMAGE_ERR_BADDATA; + priv->frame.x = *(uint16_t *)(((uint8_t *)priv->buf)+0); + CONVERT_FROM_WORD_LE(priv->frame.x); + priv->frame.y = *(uint16_t *)(((uint8_t *)priv->buf)+2); + CONVERT_FROM_WORD_LE(priv->frame.y); + priv->frame.width = *(uint16_t *)(((uint8_t *)priv->buf)+4); + CONVERT_FROM_WORD_LE(priv->frame.width); + priv->frame.height = *(uint16_t *)(((uint8_t *)priv->buf)+6); + CONVERT_FROM_WORD_LE(priv->frame.height); + if (((uint8_t *)priv->buf)[8] & 0x80) // Local color table? + priv->frame.palsize = 2 << (((uint8_t *)priv->buf)[8] & 0x07); + if (((uint8_t *)priv->buf)[8] & 0x40) // Interlaced? + priv->frame.flags |= GIFL_INTERLACE; + + // We are ready to go for the actual palette read and image decode + priv->frame.pospal = img->io.pos; + priv->frame.posimg = priv->frame.pospal+priv->frame.palsize*3; + priv->frame.posend = 0; + + // Mark this as an animated image if more than 1 frame. + if (priv->frame.posstart != priv->frame0pos) + img->flags |= GDISP_IMAGE_FLG_ANIMATED; + return GDISP_IMAGE_ERR_OK; + + case 0x21: //'!' - EXTENSION_RECORD_TYPE; + // Read the extension type + if (img->io.fns->read(&img->io, &blocktype, 1) != 1) + return GDISP_IMAGE_ERR_BADDATA; + + switch(blocktype) { + case 0xF9: // EXTENSION - Graphics Control Block + // Read the GCB + if (img->io.fns->read(&img->io, priv->buf, 6) != 6) + return GDISP_IMAGE_ERR_BADDATA; + // Check we have read a 4 byte data block and a data block terminator (0) + if (((uint8_t *)priv->buf)[0] != 4 || ((uint8_t *)priv->buf)[5] != 0) + return GDISP_IMAGE_ERR_BADDATA; + // Process the flags + switch(((uint8_t *)priv->buf)[1] & 0x1C) { + case 0x00: case 0x04: break; // Dispose = do nothing + case 0x08: priv->frame.flags |= GIFL_DISPOSECLEAR; break; // Dispose = clear + case 0x0C: case 0x10: priv->frame.flags |= GIFL_DISPOSEREST; break; // Dispose = restore. Value 0x10 is a hack for bad encoders + default: return GDISP_IMAGE_ERR_UNSUPPORTED; + } + if (((uint8_t *)priv->buf)[1] & 0x01) { + priv->frame.flags |= GIFL_TRANSPARENT; + img->flags |= GDISP_IMAGE_FLG_TRANSPARENT; // We set this but never clear it + } + if (((uint8_t *)priv->buf)[1] & 0x02) // Wait for user input? + img->flags |= GDISP_IMAGE_FLG_MULTIPAGE; + else + img->flags &= ~GDISP_IMAGE_FLG_MULTIPAGE; + // Process frame delay and the transparent color (if any) + priv->frame.delay = *(uint16_t *)(((uint8_t *)priv->buf)+2); + CONVERT_FROM_WORD_LE(priv->frame.delay); + priv->frame.paltrans = ((uint8_t *)priv->buf)[4]; + break; + + case 0xFF: // EXTENSION - Application + // We only handle this for the special Netscape loop counter for animation + if (priv->flags & GIF_LOOP) + goto skipdatablocks; + // Read the Application header + if (img->io.fns->read(&img->io, priv->buf, 16) != 16) + return GDISP_IMAGE_ERR_BADDATA; + // Check we have read a 11 byte data block + if (((uint8_t *)priv->buf)[0] != 11 && ((uint8_t *)priv->buf)[12] != 3) + return GDISP_IMAGE_ERR_BADDATA; + // Check the vendor + if (((uint8_t *)priv->buf)[1] == 'N' && ((uint8_t *)priv->buf)[2] == 'E' && ((uint8_t *)priv->buf)[3] == 'T' + && ((uint8_t *)priv->buf)[4] == 'S' && ((uint8_t *)priv->buf)[5] == 'C' && ((uint8_t *)priv->buf)[6] == 'A' + && ((uint8_t *)priv->buf)[7] == 'P' && ((uint8_t *)priv->buf)[8] == 'E' && ((uint8_t *)priv->buf)[9] == '2' + && ((uint8_t *)priv->buf)[10] == '.' && ((uint8_t *)priv->buf)[11] == '0') { + if (((uint8_t *)priv->buf)[13] == 1) { + priv->loops = *(uint16_t *)(((uint8_t *)priv->buf)+14); + CONVERT_FROM_WORD_LE(priv->loops); + priv->flags |= GIF_LOOP; + if (!priv->loops) + priv->flags |= GIF_LOOPFOREVER; + } + } + goto skipdatablocks; + + case 0x01: // EXTENSION - Plain Text (Graphics Rendering) + case 0xFE: // EXTENSION - Comment + default: + // 0x00-0x7F (0-127) are the Graphic Rendering blocks + if (blocktype <= 0x7F) + return GDISP_IMAGE_ERR_UNSUPPORTED; + // 0x80-0xF9 (128-249) are the Control blocks + // 0xFA-0xFF (250-255) are the Special Purpose blocks + // We don't understand this extension - just skip it by skipping data blocks + skipdatablocks: + while(1) { + if (img->io.fns->read(&img->io, &blocksz, 1) != 1) + return GDISP_IMAGE_ERR_BADDATA; + if (!blocksz) + break; + img->io.fns->seek(&img->io, img->io.pos + blocksz); + } + break; + } + break; + + case 0x3B: //';' - TERMINATE_RECORD_TYPE; + // Are we an looping animation + if (!(priv->flags & GIF_LOOP)) + return GDISP_IMAGE_EOF; + if (!(priv->flags & GIF_LOOPFOREVER)) { + if (!priv->loops) + return GDISP_IMAGE_EOF; + priv->loops--; + } + + // Seek back to frame0 + img->io.fns->seek(&img->io, priv->frame0pos); + return GDISP_IMAGE_LOOP; + + default: // UNDEFINED_RECORD_TYPE; + return GDISP_IMAGE_ERR_UNSUPPORTED; + } + } +} + +gdispImageError gdispImageOpen_GIF(gdispImage *img) { + gdispImagePrivate *priv; + uint8_t hdr[6]; + uint16_t aword; + + /* Read the file identifier */ + if (img->io.fns->read(&img->io, hdr, 6) != 6) + return GDISP_IMAGE_ERR_BADFORMAT; // It can't be us + + /* Process the GIFFILEHEADER structure */ + + if (hdr[0] != 'G' || hdr[1] != 'I' || hdr[2] != 'F' + || hdr[3] != '8' || (hdr[4] != '7' && hdr[4] != '9') || hdr[5] != 'a') + return GDISP_IMAGE_ERR_BADFORMAT; // It can't be us + + /* We know we are a GIF format image */ + img->flags = 0; + + /* Allocate our private area */ + if (!(img->priv = (gdispImagePrivate *)gdispImageAlloc(img, sizeof(gdispImagePrivate)))) + return GDISP_IMAGE_ERR_NOMEMORY; + + /* Initialise the essential bits in the private area */ + priv = img->priv; + priv->flags = 0; + priv->palsize = 0; + priv->palette = 0; + priv->frame.flags = 0; + + /* Process the Screen Descriptor structure */ + + // Read the screen descriptor + if (img->io.fns->read(&img->io, priv->buf, 7) != 7) + goto baddatacleanup; + // Get the width + img->width = *(uint16_t *)(((uint8_t *)priv->buf)+0); + CONVERT_FROM_WORD_LE(img->width); + // Get the height + img->height = *(uint16_t *)(((uint8_t *)priv->buf)+2); + CONVERT_FROM_WORD_LE(img->height); + if (((uint8_t *)priv->buf)[4] & 0x80) { + // Global color table + priv->palsize = 2 << (((uint8_t *)priv->buf)[4] & 0x07); + // Allocate the global palette + if (!(priv->palette = (color_t *)gdispImageAlloc(img, priv->palsize*sizeof(color_t)))) + goto nomemcleanup; + // Read the global palette + for(aword = 0; aword < priv->palsize; aword++) { + if (img->io.fns->read(&img->io, &priv->buf, 3) != 3) + goto baddatacleanup; + priv->palette[aword] = RGB2COLOR(((uint8_t *)priv->buf)[0], ((uint8_t *)priv->buf)[1], ((uint8_t *)priv->buf)[2]); + } + } + priv->bgcolor = ((uint8_t *)priv->buf)[5]; + + // Save the fram0pos + priv->frame0pos = img->io.pos; + + // Read the first frame descriptor + switch(initFrame(img)) { + case GDISP_IMAGE_ERR_OK: // Everything OK + return GDISP_IMAGE_ERR_OK; + case GDISP_IMAGE_ERR_UNSUPPORTED: // Unsupported + gdispImageClose_GIF(img); // Clean up the private data area + return GDISP_IMAGE_ERR_UNSUPPORTED; + case GDISP_IMAGE_ERR_NOMEMORY: // Out of Memory + nomemcleanup: + gdispImageClose_GIF(img); // Clean up the private data area + return GDISP_IMAGE_ERR_NOMEMORY; + case GDISP_IMAGE_EOF: // We should have a frame but we don't seem to + case GDISP_IMAGE_LOOP: // We should have a frame but we don't seem to + case GDISP_IMAGE_ERR_BADDATA: // Oops - something wrong with the data + default: + baddatacleanup: + gdispImageClose_GIF(img); // Clean up the private data area + return GDISP_IMAGE_ERR_BADDATA; + } +} + +void gdispImageClose_GIF(gdispImage *img) { + gdispImagePrivate * priv; + imgcache * cache; + imgcache * ncache; + + priv = img->priv; + if (priv) { + // Free any stored frames + cache = priv->cache; + while(cache) { + ncache = cache->next; + gdispImageFree(img, (void *)cache, sizeof(imgcache)+cache->frame.width*cache->frame.height+cache->frame.palsize*sizeof(color_t)); + cache = ncache; + } + if (priv->palette) + gdispImageFree(img, (void *)priv->palette, priv->palsize*sizeof(color_t)); + gdispImageFree(img, (void *)img->priv, sizeof(gdispImagePrivate)); + img->priv = 0; + } + img->io.fns->close(&img->io); +} + +gdispImageError gdispImageCache_GIF(gdispImage *img) { + gdispImagePrivate * priv; + imgcache * cache; + imgdecode * decode; + uint8_t * p; + uint8_t * q; + coord_t mx, my; + uint16_t cnt; + + /* If we are already cached - just return OK */ + priv = img->priv; + if (priv->curcache) + return GDISP_IMAGE_ERR_OK; + + /* We need to allocate the frame, the palette and bits for the image */ + if (!(cache = (imgcache *)gdispImageAlloc(img, sizeof(imgcache) + priv->frame.palsize*sizeof(color_t) + priv->frame.width*priv->frame.height))) + return GDISP_IMAGE_ERR_NOMEMORY; + + /* Initialise the cache */ + decode = 0; + cache->frame = priv->frame; + cache->imagebits = (uint8_t *)(cache+1) + cache->frame.palsize*sizeof(color_t); + cache->next = 0; + + /* Start the decode */ + switch(startDecode(img)) { + case GDISP_IMAGE_ERR_OK: break; + case GDISP_IMAGE_ERR_NOMEMORY: goto nomemcleanup; + case GDISP_IMAGE_ERR_BADDATA: + default: goto baddatacleanup; + } + decode = priv->decode; + + // Save the palette + if (cache->frame.palsize) { + cache->palette = (color_t *)(cache+1); + + /* Copy the local palette into the cache */ + for(cnt = 0; cnt < cache->frame.palsize; cnt++) + cache->palette[cnt] = decode->palette[cnt]; + } else + cache->palette = priv->palette; + + // Check for interlacing + cnt = 0; + if (cache->frame.flags & GIFL_INTERLACE) { + // Every 8th row starting at row 0 + for(p=cache->imagebits, my=0; my < cache->frame.height; my+=8, p += cache->frame.width*7) { + for(mx=0; mx < cache->frame.width; mx++) { + if (!cnt) { + if (!(cnt = getbytes(img))) { + // Sometimes the image EOF is a bit early - treat the rest as transparent + if (decode->code_last != decode->code_eof) + goto baddatacleanup; + while(cnt < sizeof(decode->buf)) + decode->buf[cnt++] = (cache->frame.flags & GIFL_TRANSPARENT) ? cache->frame.paltrans : 0; + } + q = decode->buf; + } + *p++ = *q++; + cnt--; + } + } + // Every 8th row starting at row 4 + for(p=cache->imagebits+cache->frame.width*4, my=4; my < cache->frame.height; my+=8, p += cache->frame.width*7) { + for(mx=0; mx < cache->frame.width; mx++) { + if (!cnt) { + if (!(cnt = getbytes(img))) { + // Sometimes the image EOF is a bit early - treat the rest as transparent + if (decode->code_last != decode->code_eof) + goto baddatacleanup; + while(cnt < sizeof(decode->buf)) + decode->buf[cnt++] = (cache->frame.flags & GIFL_TRANSPARENT) ? cache->frame.paltrans : 0; + } + q = decode->buf; + } + *p++ = *q++; + cnt--; + } + } + // Every 4th row starting at row 2 + for(p=cache->imagebits+cache->frame.width*2, my=2; my < cache->frame.height; my+=4, p += cache->frame.width*3) { + for(mx=0; mx < cache->frame.width; mx++) { + if (!cnt) { + if (!(cnt = getbytes(img))) { + // Sometimes the image EOF is a bit early - treat the rest as transparent + if (decode->code_last != decode->code_eof) + goto baddatacleanup; + while(cnt < sizeof(decode->buf)) + decode->buf[cnt++] = (cache->frame.flags & GIFL_TRANSPARENT) ? cache->frame.paltrans : 0; + } + q = decode->buf; + } + *p++ = *q++; + cnt--; + } + } + // Every 2nd row starting at row 1 + for(p=cache->imagebits+cache->frame.width, my=1; my < cache->frame.height; my+=2, p += cache->frame.width) { + for(mx=0; mx < cache->frame.width; mx++) { + if (!cnt) { + if (!(cnt = getbytes(img))) { + // Sometimes the image EOF is a bit early - treat the rest as transparent + if (decode->code_last != decode->code_eof) + goto baddatacleanup; + while(cnt < sizeof(decode->buf)) + decode->buf[cnt++] = (cache->frame.flags & GIFL_TRANSPARENT) ? cache->frame.paltrans : 0; + } + q = decode->buf; + } + *p++ = *q++; + cnt--; + } + } + } else { + // Every row in sequence + p=cache->imagebits; + for(my=0; my < cache->frame.height; my++) { + for(mx=0; mx < cache->frame.width; mx++) { + if (!cnt) { + if (!(cnt = getbytes(img))) { + // Sometimes the image EOF is a bit early - treat the rest as transparent + if (decode->code_last != decode->code_eof) + goto baddatacleanup; + while(cnt < sizeof(decode->buf)) + decode->buf[cnt++] = (cache->frame.flags & GIFL_TRANSPARENT) ? cache->frame.paltrans : 0; + } + q = decode->buf; + } + *p++ = *q++; + cnt--; + } + } + } + // We could be pedantic here but extra bytes won't hurt us + while(getbytes(img)); + priv->frame.posend = cache->frame.posend = img->io.pos; + + // Save everything + priv->curcache = cache; + if (!priv->cache) + priv->cache = cache; + else if (priv->cache->frame.posstart > cache->frame.posstart) { + cache->next = priv->cache; + priv->cache = cache; + } else { + imgcache *pc; + + for(pc = priv->cache; pc; pc = pc->next) { + if (!pc->next || pc->next->frame.posstart > cache->frame.posstart) { + cache->next = pc->next; + pc->next = cache; + break; + } + } + } + stopDecode(img); + return GDISP_IMAGE_ERR_OK; + +nomemcleanup: + stopDecode(img); + gdispImageFree(img, cache, sizeof(imgcache) + priv->frame.palsize*sizeof(color_t) + priv->frame.width*priv->frame.height); + return GDISP_IMAGE_ERR_NOMEMORY; + +baddatacleanup: + stopDecode(img); + gdispImageFree(img, cache, sizeof(imgcache) + priv->frame.palsize*sizeof(color_t) + priv->frame.width*priv->frame.height); + return GDISP_IMAGE_ERR_BADDATA; +} + +gdispImageError gdispImageDraw_GIF(gdispImage *img, coord_t x, coord_t y, coord_t cx, coord_t cy, coord_t sx, coord_t sy) { + gdispImagePrivate * priv; + imgdecode * decode; + uint8_t * q; + coord_t mx, my, fx, fy; + uint16_t cnt, gcnt; + uint8_t col; + + priv = img->priv; + + /* Handle previous frame disposing */ + if (priv->dispose.flags & (GIFL_DISPOSECLEAR|GIFL_DISPOSEREST)) { + // Clip to the disposal area - clip area = mx,my -> fx, fy (sx,sy,cx,cy are unchanged) + mx = priv->dispose.x; + my = priv->dispose.y; + fx = priv->dispose.x+priv->dispose.width; + fy = priv->dispose.y+priv->dispose.height; + if (sx > mx) mx = sx; + if (sy > my) my = sy; + if (sx+cx <= fx) fx = sx+cx; + if (sy+cy <= fy) fy = sy+cy; + if (fx > mx && fy > my) { + // We only support clearing (not restoring). The specification says that we are allowed to do this. + // Calculate the bgcolor + // The spec says to restore the backgound color (priv->bgcolor) but in practice if there is transparency + // image decoders tend to assume that a restore to the transparent color is required instead + if (((priv->dispose.flags & GIFL_TRANSPARENT) /*&& priv->dispose.paltrans == priv->bgcolor*/) || priv->bgcolor >= priv->palsize) + gdispFillArea(x+mx, y+my, fx-mx, fy-my, img->bgcolor); + else + gdispFillArea(x+mx, y+my, fx-mx, fy-my, priv->palette[priv->bgcolor]); + } + } + + /* Clip to just this frame - clip area = sx,sy -> fx, fy */ + fx = priv->frame.x+priv->frame.width; + fy = priv->frame.y+priv->frame.height; + if (sx >= fx || sy >= fy || sx+cx < priv->frame.x || sy+cy < priv->frame.y) return GDISP_IMAGE_ERR_OK; + if (sx < priv->frame.x) { mx = priv->frame.x - sx; x += mx; cx -= mx; sx = priv->frame.x; } + if (sy < priv->frame.y) { my = priv->frame.y - sy; y += my; cy -= my; sy = priv->frame.y; } + if (sx+cx > fx) cx = fx-sx; + if (sy+cy > fy) cy = fy-sy; + + // Make sx, sy relative to this frame so we are not adding priv->frame.x & priv->frame.y each time + sx -= priv->frame.x; sy -= priv->frame.y; + fx = sx + cx; + fy = sy + cy; + + /* Draw from the image cache - if it exists */ + if (priv->curcache) { + imgcache * cache; + + cache = priv->curcache; + q = cache->imagebits+priv->frame.width*sy+sx; + + for(my=sy; my < fy; my++, q += priv->frame.width - cx) { + for(gcnt=0, mx=sx, cnt=0; mx < fx; mx++) { + col = *q++; + if ((priv->frame.flags & GIFL_TRANSPARENT) && col == priv->frame.paltrans) { + // We have a transparent pixel - dump the buffer to the display + switch(gcnt) { + case 0: break; + case 1: gdispDrawPixel(x+mx-gcnt, y+my, priv->buf[0]); break; + default: gdispBlitAreaEx(x+mx-gcnt, y+my, gcnt, 1, 0, 0, gcnt, priv->buf); break; + } + gcnt = 0; + continue; + } + priv->buf[gcnt++] = cache->palette[col]; + if (gcnt >= BLIT_BUFFER_SIZE) { + // We have run out of buffer - dump it to the display + gdispBlitAreaEx(x+mx-gcnt+1, y+my, gcnt, 1, 0, 0, gcnt, priv->buf); + gcnt = 0; + } + } + // We have finished the line - dump the buffer to the display + switch(gcnt) { + case 0: break; + case 1: gdispDrawPixel(x+mx-gcnt, y+my, priv->buf[0]); break; + default: gdispBlitAreaEx(x+mx-gcnt, y+my, gcnt, 1, 0, 0, gcnt, priv->buf); break; + } + } + + return GDISP_IMAGE_ERR_OK; + } + + /* Start the decode */ + switch(startDecode(img)) { + case GDISP_IMAGE_ERR_OK: break; + case GDISP_IMAGE_ERR_NOMEMORY: return GDISP_IMAGE_ERR_NOMEMORY; + case GDISP_IMAGE_ERR_BADDATA: + default: return GDISP_IMAGE_ERR_BADDATA; + } + decode = priv->decode; + + // Check for interlacing + cnt = 0; + if (priv->frame.flags & GIFL_INTERLACE) { + // Every 8th row starting at row 0 + for(my=0; my < priv->frame.height; my+=8) { + for(gcnt=0, mx=0; mx < priv->frame.width; mx++, q++, cnt--) { + if (!cnt) { + if (!(cnt = getbytes(img))) { + // Sometimes the image EOF is a bit early - treat the rest as transparent + if (decode->code_last != decode->code_eof) + goto baddatacleanup; + mx++; + break; + } + q = decode->buf; + } + if (my >= sy && my < fy && mx >= sx && mx < fx) { + col = *q; + if ((priv->frame.flags & GIFL_TRANSPARENT) && col == priv->frame.paltrans) { + // We have a transparent pixel - dump the buffer to the display + switch(gcnt) { + case 0: break; + case 1: gdispDrawPixel(x+mx-gcnt, y+my, priv->buf[0]); break; + default: gdispBlitAreaEx(x+mx-gcnt, y+my, gcnt, 1, 0, 0, gcnt, priv->buf); break; + } + gcnt = 0; + continue; + } + priv->buf[gcnt++] = decode->palette[col]; + if (gcnt >= BLIT_BUFFER_SIZE) { + // We have run out of buffer - dump it to the display + gdispBlitAreaEx(x+mx-gcnt+1, y+my, gcnt, 1, 0, 0, gcnt, priv->buf); + gcnt = 0; + } + } + } + // We have finished the line - dump the buffer to the display + switch(gcnt) { + case 0: break; + case 1: gdispDrawPixel(x+mx-gcnt, y+my, priv->buf[0]); break; + default: gdispBlitAreaEx(x+mx-gcnt, y+my, gcnt, 1, 0, 0, gcnt, priv->buf); break; + } + } + // Every 8th row starting at row 4 + for(my=4; my < priv->frame.height; my+=8) { + for(gcnt=0, mx=0; mx < priv->frame.width; mx++, q++, cnt--) { + if (!cnt) { + if (!(cnt = getbytes(img))) { + // Sometimes the image EOF is a bit early - treat the rest as transparent + if (decode->code_last != decode->code_eof) + goto baddatacleanup; + mx++; + break; + } + q = decode->buf; + } + if (my >= sy && my < fy && mx >= sx && mx < fx) { + col = *q; + if ((priv->frame.flags & GIFL_TRANSPARENT) && col == priv->frame.paltrans) { + // We have a transparent pixel - dump the buffer to the display + switch(gcnt) { + case 0: break; + case 1: gdispDrawPixel(x+mx-gcnt, y+my, priv->buf[0]); break; + default: gdispBlitAreaEx(x+mx-gcnt, y+my, gcnt, 1, 0, 0, gcnt, priv->buf); break; + } + gcnt = 0; + continue; + } + priv->buf[gcnt++] = decode->palette[col]; + if (gcnt >= BLIT_BUFFER_SIZE) { + // We have run out of buffer - dump it to the display + gdispBlitAreaEx(x+mx-gcnt+1, y+my, gcnt, 1, 0, 0, gcnt, priv->buf); + gcnt = 0; + } + } + } + // We have finished the line - dump the buffer to the display + switch(gcnt) { + case 0: break; + case 1: gdispDrawPixel(x+mx-gcnt, y+my, priv->buf[0]); break; + default: gdispBlitAreaEx(x+mx-gcnt, y+my, gcnt, 1, 0, 0, gcnt, priv->buf); break; + } + } + // Every 4th row starting at row 2 + for(my=2; my < priv->frame.height; my+=4) { + for(gcnt=0, mx=0; mx < priv->frame.width; mx++, q++, cnt--) { + if (!cnt) { + if (!(cnt = getbytes(img))) { + // Sometimes the image EOF is a bit early - treat the rest as transparent + if (decode->code_last != decode->code_eof) + goto baddatacleanup; + mx++; + break; + } + q = decode->buf; + } + if (my >= sy && my < fy && mx >= sx && mx < fx) { + col = *q; + if ((priv->frame.flags & GIFL_TRANSPARENT) && col == priv->frame.paltrans) { + // We have a transparent pixel - dump the buffer to the display + switch(gcnt) { + case 0: break; + case 1: gdispDrawPixel(x+mx-gcnt, y+my, priv->buf[0]); break; + default: gdispBlitAreaEx(x+mx-gcnt, y+my, gcnt, 1, 0, 0, gcnt, priv->buf); break; + } + gcnt = 0; + continue; + } + priv->buf[gcnt++] = decode->palette[col]; + if (gcnt >= BLIT_BUFFER_SIZE) { + // We have run out of buffer - dump it to the display + gdispBlitAreaEx(x+mx-gcnt+1, y+my, gcnt, 1, 0, 0, gcnt, priv->buf); + gcnt = 0; + } + } + } + // We have finished the line - dump the buffer to the display + switch(gcnt) { + case 0: break; + case 1: gdispDrawPixel(x+mx-gcnt, y+my, priv->buf[0]); break; + default: gdispBlitAreaEx(x+mx-gcnt, y+my, gcnt, 1, 0, 0, gcnt, priv->buf); break; + } + } + // Every 2nd row starting at row 1 + for(my=1; my < priv->frame.height; my+=2) { + for(gcnt=0, mx=0; mx < priv->frame.width; mx++, q++, cnt--) { + if (!cnt) { + if (!(cnt = getbytes(img))) { + // Sometimes the image EOF is a bit early - treat the rest as transparent + if (decode->code_last != decode->code_eof) + goto baddatacleanup; + mx++; + break; + } + q = decode->buf; + } + if (my >= sy && my < fy && mx >= sx && mx < fx) { + col = *q; + if ((priv->frame.flags & GIFL_TRANSPARENT) && col == priv->frame.paltrans) { + // We have a transparent pixel - dump the buffer to the display + switch(gcnt) { + case 0: break; + case 1: gdispDrawPixel(x+mx-gcnt, y+my, priv->buf[0]); break; + default: gdispBlitAreaEx(x+mx-gcnt, y+my, gcnt, 1, 0, 0, gcnt, priv->buf); break; + } + gcnt = 0; + continue; + } + priv->buf[gcnt++] = decode->palette[col]; + if (gcnt >= BLIT_BUFFER_SIZE) { + // We have run out of buffer - dump it to the display + gdispBlitAreaEx(x+mx-gcnt+1, y+my, gcnt, 1, 0, 0, gcnt, priv->buf); + gcnt = 0; + } + } + } + // We have finished the line - dump the buffer to the display + switch(gcnt) { + case 0: break; + case 1: gdispDrawPixel(x+mx-gcnt, y+my, priv->buf[0]); break; + default: gdispBlitAreaEx(x+mx-gcnt, y+my, gcnt, 1, 0, 0, gcnt, priv->buf); break; + } + } + } else { + // Every row in sequence + for(my=0; my < priv->frame.height; my++) { + for(gcnt=0, mx=0; mx < priv->frame.width; mx++, q++, cnt--) { + if (!cnt) { + if (!(cnt = getbytes(img))) { + // Sometimes the image EOF is a bit early - treat the rest as transparent + if (decode->code_last != decode->code_eof) + goto baddatacleanup; + mx++; + break; + } + q = decode->buf; + } + if (my >= sy && my < fy && mx >= sx && mx < fx) { + col = *q; + if ((priv->frame.flags & GIFL_TRANSPARENT) && col == priv->frame.paltrans) { + // We have a transparent pixel - dump the buffer to the display + switch(gcnt) { + case 0: break; + case 1: gdispDrawPixel(x+mx-gcnt, y+my, priv->buf[0]); break; + default: gdispBlitAreaEx(x+mx-gcnt, y+my, gcnt, 1, 0, 0, gcnt, priv->buf); break; + } + gcnt = 0; + continue; + } + priv->buf[gcnt++] = decode->palette[col]; + if (gcnt >= BLIT_BUFFER_SIZE) { + // We have run out of buffer - dump it to the display + gdispBlitAreaEx(x+mx-gcnt+1, y+my, gcnt, 1, 0, 0, gcnt, priv->buf); + gcnt = 0; + } + } + } + // We have finished the line - dump the buffer to the display + switch(gcnt) { + case 0: break; + case 1: gdispDrawPixel(x+mx-gcnt, y+my, priv->buf[0]); break; + default: gdispBlitAreaEx(x+mx-gcnt, y+my, gcnt, 1, 0, 0, gcnt, priv->buf); break; + } + } + } + // We could be pedantic here but extra bytes won't hurt us + while (getbytes(img)); + priv->frame.posend = img->io.pos; + + stopDecode(img); + return GDISP_IMAGE_ERR_OK; + +baddatacleanup: + stopDecode(img); + return GDISP_IMAGE_ERR_BADDATA; +} + +systime_t gdispImageNext_GIF(gdispImage *img) { + gdispImagePrivate * priv; + systime_t delay; + uint8_t blocksz; + + priv = img->priv; + + // Save the delay and convert to millisecs + delay = (systime_t)priv->frame.delay * 10; + + // We need to get to the end of this frame + if (!priv->frame.posend) { + // We don't know where the end of the frame is yet - find it! + img->io.fns->seek(&img->io, priv->frame.posimg+1); // Skip the code size byte too + while(1) { + if (img->io.fns->read(&img->io, &blocksz, 1) != 1) + return TIME_INFINITE; + if (!blocksz) + break; + img->io.fns->seek(&img->io, img->io.pos + blocksz); + } + priv->frame.posend = img->io.pos; + } + + // Seek to the end of this frame + img->io.fns->seek(&img->io, priv->frame.posend); + + // Read the next frame descriptor + for(blocksz=0; blocksz < 2; blocksz++) { // 2 loops max to prevent cycling forever with a bad file + switch(initFrame(img)) { + case GDISP_IMAGE_ERR_OK: // Everything OK + return delay; + case GDISP_IMAGE_LOOP: // Back to the beginning + break; + case GDISP_IMAGE_EOF: // The real End-Of-File + case GDISP_IMAGE_ERR_BADDATA: // Oops - something wrong with the data + case GDISP_IMAGE_ERR_NOMEMORY: // Out of Memory + case GDISP_IMAGE_ERR_UNSUPPORTED: // Unsupported + default: + return TIME_INFINITE; + } + } + return TIME_INFINITE; +} #endif /* GFX_USE_GDISP && GDISP_NEED_IMAGE && GDISP_NEED_IMAGE_GIF */ /** @} */ -- cgit v1.2.3