singe/thirdparty/SDL3_image/src/IMG_gif.c
2026-09-03 22:01:35 -05:00

2898 lines
89 KiB
C

/*
SDL_image: An example image loading library for use with SDL
Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
/* This is a GIF image file loading framework */
#include <SDL3_image/SDL_image.h>
#include "IMG.h"
#include "IMG_gif.h"
#include "IMG_anim_encoder.h"
#include "IMG_anim_decoder.h"
// We will have the saving GIF feature by default
#if !defined(SAVE_GIF)
#define SAVE_GIF 1
#endif
// By default, non-indexed surfaces will be converted to indexed pixels using octree quantization.
#if SAVE_GIF
#ifndef SAVE_GIF_OCTREE
#define SAVE_GIF_OCTREE 1
#endif
#endif
#ifdef LOAD_GIF
/* Some parts of the code have been adapted from XPaint: */
/* +-------------------------------------------------------------------+ */
/* | Copyright 1990, 1991, 1993 David Koblas. | */
/* | Copyright 1996 Torsten Martinsen. | */
/* | Permission to use, copy, modify, and distribute this software | */
/* | and its documentation for any purpose and without fee is hereby | */
/* | granted, provided that the above copyright notice appear in all | */
/* | copies and that both that copyright notice and this permission | */
/* | notice appear in supporting documentation. This software is | */
/* | provided "as is" without express or implied warranty. | */
/* +-------------------------------------------------------------------+ */
/* Adapted for use in SDL by Sam Lantinga -- 7/20/98 */
/* Changes to work with SDL:
Include SDL header file
Use SDL_Surface rather than xpaint Image structure
Define SDL versions of RWSetMsg(), ImageNewCmap() and ImageSetCmap()
*/
/* Has been overwhelmingly modified by Xen (@lordofxen) and Sam Lantinga (@slouken) as of 11/9/2025. */
#include <SDL3/SDL.h>
#define Image SDL_Surface
#define RWSetMsg SDL_SetError
#define ImageNewCmap(w, h, s) SDL_CreateSurface(w, h, SDL_PIXELFORMAT_INDEX8)
#define ImageSetCmap(s, i, R, G, B) do { \
palette->colors[i].r = R; \
palette->colors[i].g = G; \
palette->colors[i].b = B; \
} while (0)
/* * * * * */
#define GIF_DISPOSE_NA 0 /* No disposal specified */
#define GIF_DISPOSE_NONE 1 /* Do not dispose */
#define GIF_DISPOSE_RESTORE_BACKGROUND 2 /* Restore to background */
#define GIF_DISPOSE_RESTORE_PREVIOUS 3 /* Restore to previous */
#define MAXCOLORMAPSIZE 256
#define TRUE 1
#define FALSE 0
#define CM_RED 0
#define CM_GREEN 1
#define CM_BLUE 2
#define MAX_LWZ_BITS 12
#define INTERLACE 0x40
#define LOCALCOLORMAP 0x80
#define BitSet(byte, bit) (((byte) & (bit)) == (bit))
#define ReadOK(file,buffer,len) (SDL_ReadIO(file, buffer, len) == len)
#define LM_to_uint(a,b) (((b)<<8)|(a))
typedef struct {
struct {
unsigned int Width;
unsigned int Height;
unsigned char ColorMap[3][MAXCOLORMAPSIZE];
unsigned int BitPixel;
unsigned int ColorResolution;
unsigned int Background;
unsigned int AspectRatio;
int GrayScale;
} GifScreen;
struct {
int transparent;
int delayTime;
int inputFlag;
int disposal;
} Gif89;
unsigned char buf[280];
int curbit, lastbit, done, last_byte;
int fresh;
int code_size, set_code_size;
int max_code, max_code_size;
int firstcode, oldcode;
int clear_code, end_code;
int table[2][(1 << MAX_LWZ_BITS)];
int stack[(1 << (MAX_LWZ_BITS)) * 2], *sp;
int ZeroDataBlock;
} State_t;
typedef struct
{
Image *image;
int x, y;
int disposal;
int delay;
} Frame_t;
typedef struct
{
int count;
Frame_t *frames;
} Anim_t;
static int ReadColorMap(SDL_IOStream * src, int number,
unsigned char buffer[3][MAXCOLORMAPSIZE], int *flag);
static int DoExtension(SDL_IOStream * src, int label, State_t * state);
static int GetDataBlock(SDL_IOStream * src, unsigned char *buf, State_t * state);
static int GetCode(SDL_IOStream * src, int code_size, int flag, State_t * state);
static int LWZReadByte(SDL_IOStream * src, int flag, int input_code_size, State_t * state);
static Image *ReadImage(SDL_IOStream *src, int len, int height, int cmapSize,
unsigned char cmap[3][MAXCOLORMAPSIZE],
int gray, int interlace, int ignore, State_t *state);
static int
ReadColorMap(SDL_IOStream *src, int number,
unsigned char buffer[3][MAXCOLORMAPSIZE], int *gray)
{
int i;
unsigned char rgb[3];
int flag;
flag = 1;
for (i = 0; i < number; ++i) {
if (!ReadOK(src, rgb, sizeof(rgb))) {
RWSetMsg("bad colormap");
return 1;
}
buffer[CM_RED][i] = rgb[0];
buffer[CM_GREEN][i] = rgb[1];
buffer[CM_BLUE][i] = rgb[2];
flag &= (rgb[0] == rgb[1] && rgb[1] == rgb[2]);
}
#if 0
if (flag)
*gray = (number == 2) ? PBM_TYPE : PGM_TYPE;
else
*gray = PPM_TYPE;
#else
(void) flag;
*gray = 0;
#endif
return FALSE;
}
static int
DoExtension(SDL_IOStream *src, int label, State_t * state)
{
unsigned char buf[256];
switch (label) {
case 0x01: /* Plain Text Extension */
break;
case 0xff: /* Application Extension */
break;
case 0xfe: /* Comment Extension */
while (GetDataBlock(src, buf, state) > 0)
;
return FALSE;
case 0xf9: /* Graphic Control Extension */
(void) GetDataBlock(src, buf, state);
state->Gif89.disposal = (buf[0] >> 2) & 0x7;
state->Gif89.inputFlag = (buf[0] >> 1) & 0x1;
state->Gif89.delayTime = LM_to_uint(buf[1], buf[2]);
if ((buf[0] & 0x1) != 0)
state->Gif89.transparent = buf[3];
while (GetDataBlock(src, buf, state) > 0)
;
return FALSE;
default:
break;
}
while (GetDataBlock(src, buf, state) > 0)
;
return FALSE;
}
static int
GetDataBlock(SDL_IOStream *src, unsigned char *buf, State_t * state)
{
unsigned char count;
if (!ReadOK(src, &count, 1)) {
/* pm_message("error in getting DataBlock size" ); */
return -1;
}
state->ZeroDataBlock = count == 0;
if ((count != 0) && (!ReadOK(src, buf, count))) {
/* pm_message("error in reading DataBlock" ); */
return -1;
}
return count;
}
static int
GetCode(SDL_IOStream *src, int code_size, int flag, State_t * state)
{
int i, j, ret;
unsigned char count;
if (flag) {
state->curbit = 0;
state->lastbit = 0;
state->done = FALSE;
return 0;
}
if ((state->curbit + code_size) >= state->lastbit) {
if (state->done) {
if (state->curbit >= state->lastbit)
RWSetMsg("ran off the end of my bits");
return -1;
}
if (state->last_byte > 2) {
state->buf[0] = state->buf[state->last_byte - 2];
state->buf[1] = state->buf[state->last_byte - 1];
}
if ((ret = GetDataBlock(src, &state->buf[2], state)) > 0)
count = (unsigned char) ret;
else {
count = 0;
state->done = TRUE;
}
state->last_byte = 2 + count;
state->curbit = (state->curbit - state->lastbit) + 16;
state->lastbit = (2 + count) * 8;
}
ret = 0;
for (i = state->curbit, j = 0; j < code_size; ++i, ++j)
ret |= ((state->buf[i / 8] & (1 << (i % 8))) != 0) << j;
state->curbit += code_size;
return ret;
}
static int
LWZReadByte(SDL_IOStream *src, int flag, int input_code_size, State_t * state)
{
int i, code, incode;
/* Fixed buffer overflow found by Michael Skladnikiewicz */
if (input_code_size > MAX_LWZ_BITS)
return -1;
if (flag) {
state->set_code_size = input_code_size;
state->code_size = state->set_code_size + 1;
state->clear_code = 1 << state->set_code_size;
state->end_code = state->clear_code + 1;
state->max_code_size = 2 * state->clear_code;
state->max_code = state->clear_code + 2;
GetCode(src, 0, TRUE, state);
state->fresh = TRUE;
for (i = 0; i < state->clear_code; ++i) {
state->table[0][i] = 0;
state->table[1][i] = i;
}
state->table[1][0] = 0;
for (; i < (1 << MAX_LWZ_BITS); ++i)
state->table[0][i] = 0;
state->sp = state->stack;
return 0;
} else if (state->fresh) {
state->fresh = FALSE;
do {
state->firstcode = state->oldcode = GetCode(src, state->code_size, FALSE, state);
} while (state->firstcode == state->clear_code);
return state->firstcode;
}
if (state->sp > state->stack)
return *--state->sp;
while ((code = GetCode(src, state->code_size, FALSE, state)) >= 0) {
if (code == state->clear_code) {
for (i = 0; i < state->clear_code; ++i) {
state->table[0][i] = 0;
state->table[1][i] = i;
}
for (; i < (1 << MAX_LWZ_BITS); ++i)
state->table[0][i] = state->table[1][i] = 0;
state->code_size = state->set_code_size + 1;
state->max_code_size = 2 * state->clear_code;
state->max_code = state->clear_code + 2;
state->sp = state->stack;
state->firstcode = state->oldcode = GetCode(src, state->code_size, FALSE, state);
return state->firstcode;
} else if (code == state->end_code) {
int count;
unsigned char buf[260];
if (state->ZeroDataBlock)
return -2;
while ((count = GetDataBlock(src, buf, state)) > 0)
;
if (count != 0) {
/*
* pm_message("missing EOD in data stream (common occurrence)");
*/
}
return -2;
}
incode = code;
if (code >= state->max_code) {
*state->sp++ = state->firstcode;
code = state->oldcode;
}
while (code >= state->clear_code) {
/* Guard against buffer overruns */
if (code < 0 || code >= (1 << MAX_LWZ_BITS)) {
RWSetMsg("invalid LWZ data");
return -3;
}
if (state->sp == &state->stack[SDL_arraysize(state->stack)]) {
RWSetMsg("invalid LWZ data");
return -3;
}
*state->sp++ = state->table[1][code];
if (code == state->table[0][code]) {
RWSetMsg("circular table entry BIG ERROR");
return -3;
}
code = state->table[0][code];
}
/* Guard against buffer overruns */
if (code < 0 || code >= (1 << MAX_LWZ_BITS)) {
RWSetMsg("invalid LWZ data");
return -4;
}
if (state->sp == &state->stack[SDL_arraysize(state->stack)]) {
RWSetMsg("invalid LWZ data");
return -4;
}
*state->sp++ = state->firstcode = state->table[1][code];
if ((code = state->max_code) < (1 << MAX_LWZ_BITS)) {
state->table[0][code] = state->oldcode;
state->table[1][code] = state->firstcode;
++state->max_code;
if ((state->max_code >= state->max_code_size) &&
(state->max_code_size < (1 << MAX_LWZ_BITS))) {
state->max_code_size *= 2;
++state->code_size;
}
}
state->oldcode = incode;
if (state->sp > state->stack)
return *--state->sp;
}
return code;
}
static Image *
ReadImage(SDL_IOStream * src, int len, int height, int cmapSize,
unsigned char cmap[3][MAXCOLORMAPSIZE],
int gray, int interlace, int ignore, State_t * state)
{
Image *image;
SDL_Palette *palette;
unsigned char c;
int i, v;
int xpos = 0, ypos = 0, pass = 0;
/*
** Initialize the compression routines
*/
if (!ReadOK(src, &c, 1)) {
RWSetMsg("EOF / read error on image data");
return NULL;
}
if (LWZReadByte(src, TRUE, c, state) < 0) {
RWSetMsg("error reading image");
return NULL;
}
/*
** If this is an "uninteresting picture" ignore it.
*/
if (ignore) {
while (LWZReadByte(src, FALSE, c, state) >= 0)
;
return NULL;
}
image = ImageNewCmap(len, height, cmapSize);
if (!image) {
return NULL;
}
palette = SDL_CreateSurfacePalette(image);
if (!palette) {
return NULL;
}
if (cmapSize > palette->ncolors) {
cmapSize = palette->ncolors;
}
palette->ncolors = cmapSize;
for (i = 0; i < cmapSize; i++) {
ImageSetCmap(image, i, cmap[CM_RED][i], cmap[CM_GREEN][i], cmap[CM_BLUE][i]);
}
if (state->Gif89.transparent >= 0 && state->Gif89.transparent < cmapSize) {
SDL_SetSurfaceColorKey(image, true, state->Gif89.transparent);
}
while ((v = LWZReadByte(src, FALSE, c, state)) >= 0) {
((Uint8 *)image->pixels)[xpos + ypos * image->pitch] = (Uint8)v;
++xpos;
if (xpos == len) {
xpos = 0;
if (interlace) {
switch (pass) {
case 0:
case 1:
ypos += 8;
break;
case 2:
ypos += 4;
break;
case 3:
ypos += 2;
break;
}
if (ypos >= height) {
++pass;
switch (pass) {
case 1:
ypos = 4;
break;
case 2:
ypos = 2;
break;
case 3:
ypos = 1;
break;
default:
goto fini;
}
}
} else {
++ypos;
}
}
if (ypos >= height)
break;
}
fini:
return image;
}
struct IMG_AnimationDecoderContext
{
State_t state; /* GIF decoding state */
unsigned char buf[256]; /* Buffer for reading chunks */
char version[4]; /* GIF version */
int width; /* Width of the GIF */
int height; /* Height of the GIF */
SDL_Surface *canvas; /* Canvas for compositing frames */
SDL_Surface *prev_canvas; /* Previous canvas for DISPOSE_PREVIOUS */
int frame_count; /* Total number of frames seen */
int current_frame; /* Current frame index */
bool got_header; /* Whether we've read the GIF header */
bool got_eof; /* Whether we've reached the end of the GIF */
/* Current frame info */
int current_disposal; /* Disposal method for current frame */
int current_delay; /* Delay time for current frame */
int transparent_index; /* Transparent color index for current frame */
/* Global color map */
unsigned char global_colormap[3][MAXCOLORMAPSIZE];
int global_colormap_size;
bool has_global_colormap;
bool global_grayscale;
/* Frame info */
Uint64 last_duration; /* The duration of the previous frame */
int last_disposal; /* Disposal method from previous frame */
SDL_Rect restore_area; /* Area to restore when using DISPOSE_RESTORE_BACKGROUND */
bool ignore_props;
};
static bool IMG_AnimationDecoderGetGIFHeader(IMG_AnimationDecoder *decoder, char**comment, int *loopCount)
{
if (comment) {
*comment = NULL;
}
if (loopCount) {
*loopCount = 1;
}
IMG_AnimationDecoderContext *ctx = decoder->ctx;
SDL_IOStream *src = decoder->src;
if (!ctx->got_header) {
if (!ReadOK(src, ctx->buf, 6)) {
return SDL_SetError("Error reading GIF magic number");
}
if (SDL_strncmp((char *)ctx->buf, "GIF", 3) != 0) {
return SDL_SetError("Not a GIF file");
}
SDL_memcpy(ctx->version, (char *)ctx->buf + 3, 3);
ctx->version[3] = '\0';
if ((SDL_strcmp(ctx->version, "87a") != 0) && (SDL_strcmp(ctx->version, "89a") != 0)) {
return SDL_SetError("Bad version number, not '87a' or '89a'");
}
if (!ReadOK(src, ctx->buf, 7)) {
return SDL_SetError("Failed to read screen descriptor");
}
ctx->width = LM_to_uint(ctx->buf[0], ctx->buf[1]);
ctx->height = LM_to_uint(ctx->buf[2], ctx->buf[3]);
ctx->state.GifScreen.Width = ctx->width;
ctx->state.GifScreen.Height = ctx->height;
ctx->state.GifScreen.BitPixel = 2 << (ctx->buf[4] & 0x07);
ctx->state.GifScreen.ColorResolution = (((ctx->buf[4] & 0x70) >> 3) + 1);
ctx->state.GifScreen.Background = ctx->buf[5];
ctx->state.GifScreen.AspectRatio = ctx->buf[6];
ctx->has_global_colormap = BitSet(ctx->buf[4], LOCALCOLORMAP);
if (ctx->has_global_colormap) {
ctx->global_colormap_size = ctx->state.GifScreen.BitPixel;
int g = ctx->global_grayscale ? 1 : 0;
if (ReadColorMap(src, ctx->global_colormap_size, ctx->global_colormap, &g)) {
return SDL_SetError("Error reading global colormap");
}
SDL_memcpy(ctx->state.GifScreen.ColorMap, ctx->global_colormap, sizeof(ctx->global_colormap));
ctx->state.GifScreen.GrayScale = ctx->global_grayscale;
}
if (!ctx->ignore_props) {
Uint64 stream_pos = SDL_TellIO(src);
bool processing_extensions = true;
while (processing_extensions) {
uint8_t block_type;
if (!ReadOK(src, &block_type, 1)) {
return SDL_SetError("Error reading GIF block type");
}
switch (block_type) {
case 0x21: // Extension Introducer
{
uint8_t extension_label;
if (!ReadOK(src, &extension_label, 1)) {
return SDL_SetError("Error reading GIF extension label");
}
switch (extension_label) {
case 0xFF: // Application Extension
{
// Read the application block size first (should be 11 for "NETSCAPE2.0")
Uint8 app_block_size;
if (!ReadOK(src, &app_block_size, 1)) {
return SDL_SetError("Error reading application extension block size");
}
Uint8 app_data[256];
if (!ReadOK(src, app_data, app_block_size)) {
return SDL_SetError("Error reading GIF application extension block");
}
// Check for NETSCAPE2.0 extension (loop count)
if (app_block_size == 11 &&
SDL_strncmp((char *)app_data, "NETSCAPE2.0", 11) == 0) {
Uint8 sub_block_size;
if (!ReadOK(src, &sub_block_size, 1)) {
return SDL_SetError("Error reading Netscape sub-block size");
}
if (sub_block_size == 3) {
Uint8 sub_block_data[3];
if (!ReadOK(src, sub_block_data, 3)) {
return SDL_SetError("Error reading Netscape sub-block data");
}
if (sub_block_data[0] == 0x01 && loopCount) {
Uint16 repeatCount = LM_to_uint(sub_block_data[1], sub_block_data[2]);
*loopCount = (repeatCount ? (repeatCount + 1) : 0);
}
// Terminator
if (!ReadOK(src, &sub_block_size, 1) || sub_block_size != 0x00) {
return SDL_SetError("Netscape extension block not terminated correctly");
}
} else {
// Skip unexpected sub-block sizes
SDL_SeekIO(src, sub_block_size, SDL_IO_SEEK_CUR);
Uint8 terminator;
if (!ReadOK(src, &terminator, 1) || terminator != 0) {
return SDL_SetError("Extension block not terminated correctly");
}
}
} else {
// Skip all sub-blocks for non-Netscape extensions
Uint8 sub_block_size;
while (ReadOK(src, &sub_block_size, 1) && sub_block_size > 0) {
SDL_SeekIO(src, sub_block_size, SDL_IO_SEEK_CUR);
}
}
} break;
case 0xFE: // Comment Extension
{
if (comment) {
char *c = NULL;
Uint8 sub_block_size;
size_t current_len = 0;
while (ReadOK(src, &sub_block_size, 1) && sub_block_size > 0) {
size_t new_len = current_len + sub_block_size;
char *temp_comment = SDL_realloc(c, new_len + 1);
if (!temp_comment) {
return SDL_SetError("Failed to allocate memory for GIF comment");
}
c = temp_comment;
if (!ReadOK(src, c + current_len, sub_block_size)) {
return SDL_SetError("Error reading GIF comment data");
}
current_len = new_len;
}
if (c && current_len > 0) {
c[current_len] = '\0';
}
*comment = c;
}
} break;
default: // Other extensions
{
Uint8 sub_block_size;
while (ReadOK(src, &sub_block_size, 1) && sub_block_size > 0) {
SDL_SeekIO(src, sub_block_size, SDL_IO_SEEK_CUR);
}
} break;
}
} break;
case 0x2C: // Image Descriptor
processing_extensions = false;
break;
case 0x3B: // Trailer
return SDL_SetError("GIF file contains no images");
default: // Unknown block type
return SDL_SetError("Unknown GIF block type: 0x%02X", block_type);
}
}
SDL_SeekIO(src, stream_pos, SDL_IO_SEEK_SET);
}
if (!ctx->canvas) {
ctx->canvas = SDL_CreateSurface(ctx->width, ctx->height, SDL_PIXELFORMAT_RGBA32);
if (!ctx->canvas) {
return SDL_SetError("Failed to create canvas surface");
}
if (!SDL_FillSurfaceRect(ctx->canvas, NULL, 0)) {
SDL_DestroySurface(ctx->canvas);
ctx->canvas = NULL;
return SDL_SetError("Failed to fill canvas surface with transparent color");
}
}
if (!ctx->prev_canvas) {
ctx->prev_canvas = SDL_CreateSurface(ctx->width, ctx->height, SDL_PIXELFORMAT_RGBA32);
if (!ctx->prev_canvas) {
return SDL_SetError("Failed to create previous canvas surface");
}
if (!SDL_FillSurfaceRect(ctx->prev_canvas, NULL, 0)) {
SDL_DestroySurface(ctx->prev_canvas);
ctx->prev_canvas = NULL;
return SDL_SetError("Failed to fill previous canvas surface with transparent color");
}
}
ctx->got_header = true;
}
return true;
}
static bool IMG_AnimationDecoderReset_Internal(IMG_AnimationDecoder *decoder)
{
IMG_AnimationDecoderContext* ctx = decoder->ctx;
if (SDL_SeekIO(decoder->src, decoder->start, SDL_IO_SEEK_SET) != decoder->start) {
return SDL_SetError("Failed to seek to beginning of GIF file");
}
SDL_memset(&ctx->state, 0, sizeof(ctx->state));
ctx->state.Gif89.transparent = -1;
ctx->state.Gif89.delayTime = -1;
ctx->state.Gif89.inputFlag = -1;
ctx->state.Gif89.disposal = GIF_DISPOSE_NA;
ctx->current_frame = 0;
ctx->current_disposal = GIF_DISPOSE_NA;
ctx->current_delay = 100;
ctx->transparent_index = -1;
ctx->got_header = false;
ctx->got_eof = false;
ctx->last_disposal = GIF_DISPOSE_NONE;
SDL_Rect r = {0};
ctx->restore_area = r;
// We don't care about metadata when resetting to re-read.
ctx->ignore_props = true;
if (ctx->canvas) {
SDL_FillSurfaceRect(ctx->canvas, NULL, 0);
}
if (ctx->prev_canvas) {
SDL_FillSurfaceRect(ctx->prev_canvas, NULL, 0);
}
return IMG_AnimationDecoderGetGIFHeader(decoder, NULL, NULL);
}
static bool IMG_AnimationDecoderGetNextFrame_Internal(IMG_AnimationDecoder *decoder, SDL_Surface **frame, Uint64 *duration)
{
IMG_AnimationDecoderContext *ctx = decoder->ctx;
SDL_IOStream *src = decoder->src;
unsigned char c;
int framesLoaded = 0;
if (ctx->got_eof) {
decoder->status = IMG_DECODER_STATUS_COMPLETE;
return false;
}
if (!IMG_AnimationDecoderGetGIFHeader(decoder, NULL, NULL)) {
return false;
}
int framesToLoad = 1;
SDL_Surface *retval = NULL;
while (framesLoaded < framesToLoad) {
if (!ReadOK(src, &c, 1)) {
ctx->got_eof = true;
break;
}
if (c == ';') {
ctx->got_eof = true;
break;
}
if (c == '!') {
if (!ReadOK(src, &c, 1)) {
ctx->got_eof = true;
break;
}
DoExtension(src, c, &ctx->state);
continue;
}
if (c != ',') {
continue;
}
if (!ReadOK(src, ctx->buf, 9)) {
ctx->got_eof = true;
break;
}
int useGlobalColormap = !BitSet(ctx->buf[8], LOCALCOLORMAP);
int bitPixel = 1 << ((ctx->buf[8] & 0x07) + 1);
int left = LM_to_uint(ctx->buf[0], ctx->buf[1]);
int top = LM_to_uint(ctx->buf[2], ctx->buf[3]);
int width = LM_to_uint(ctx->buf[4], ctx->buf[5]);
int height = LM_to_uint(ctx->buf[6], ctx->buf[7]);
unsigned char localColorMap[3][MAXCOLORMAPSIZE];
int grayScale = 0;
if (!useGlobalColormap) {
if (ReadColorMap(src, bitPixel, localColorMap, &grayScale)) {
ctx->got_eof = true;
break;
}
}
switch (ctx->last_disposal) {
case GIF_DISPOSE_NONE:
/* Leave canvas as is */
break;
case GIF_DISPOSE_RESTORE_BACKGROUND:
{
if (!SDL_FillSurfaceRect(ctx->canvas, &ctx->restore_area, 0)) {
return SDL_SetError("Failed to fill canvas with background color");
}
} break;
case GIF_DISPOSE_RESTORE_PREVIOUS:
/* Restore canvas to previous state */
if (ctx->prev_canvas) {
if (!SDL_BlitSurface(ctx->prev_canvas, NULL, ctx->canvas, NULL)) {
return SDL_SetError("Failed to restore previous canvas");
}
}
break;
default:
/* Default is to do nothing */
break;
}
/* If current disposal method is RESTORE_PREVIOUS, save current canvas */
if (ctx->state.Gif89.disposal == GIF_DISPOSE_RESTORE_PREVIOUS) {
if (!SDL_BlitSurface(ctx->canvas, NULL, ctx->prev_canvas, NULL)) {
return SDL_SetError("Failed to save current canvas for restoration");
}
} else if (ctx->state.Gif89.disposal == GIF_DISPOSE_RESTORE_BACKGROUND) {
SDL_Rect r = { left, top, width, height };
ctx->restore_area = r;
}
Image *image;
if (!useGlobalColormap) {
image = ReadImage(src, width, height, bitPixel, localColorMap, grayScale,
BitSet(ctx->buf[8], INTERLACE), 0, &ctx->state);
} else {
image = ReadImage(src, width, height, ctx->state.GifScreen.BitPixel,
ctx->state.GifScreen.ColorMap, ctx->state.GifScreen.GrayScale,
BitSet(ctx->buf[8], INTERLACE), 0, &ctx->state);
}
if (!image) {
// Incorrect animation is harder to detect than a direct failure,
// so it's better to fail than try to animate a GIF without a,
// full set of frames it has in the file.
// Only set the error if ReadImage did not do it.
if (SDL_GetError()[0] == '\0') {
return SDL_SetError("Failed to decode frame.");
}
return false;
}
/* Composite the frame onto the canvas */
SDL_Rect dest = { left, top, width, height };
if (!SDL_BlitSurface(image, NULL, ctx->canvas, &dest)) {
SDL_DestroySurface(image);
return SDL_SetError("Failed to blit frame onto canvas");
}
/* Store the frame in the output array */
retval = SDL_DuplicateSurface(ctx->canvas);
if (!retval) {
SDL_DestroySurface(image);
return SDL_SetError("Failed to duplicate frame surface");
}
if (ctx->state.Gif89.delayTime < 0 && ctx->last_duration) {
*duration = ctx->last_duration;
} else if (ctx->state.Gif89.delayTime < 2) {
/* Default animation delay, matching browser and Qt */
*duration = IMG_GetDecoderDuration(decoder, 10, 100);
} else {
*duration = IMG_GetDecoderDuration(decoder, ctx->state.Gif89.delayTime, 100);
}
ctx->last_duration = *duration;
ctx->last_disposal = ctx->state.Gif89.disposal;
SDL_DestroySurface(image);
ctx->state.Gif89.transparent = -1;
ctx->state.Gif89.delayTime = -1;
ctx->state.Gif89.inputFlag = -1;
ctx->state.Gif89.disposal = GIF_DISPOSE_NA;
framesLoaded++;
ctx->current_frame++;
ctx->frame_count++;
}
if (framesLoaded == 0) {
if (ctx->got_eof) {
decoder->status = IMG_DECODER_STATUS_COMPLETE;
return false;
}
return SDL_SetError("Failed to load any frames");
}
*frame = retval;
return true;
}
static bool IMG_AnimationDecoderClose_Internal(IMG_AnimationDecoder *decoder)
{
IMG_AnimationDecoderContext* ctx = decoder->ctx;
if (ctx->canvas) {
SDL_DestroySurface(ctx->canvas);
}
if (ctx->prev_canvas) {
SDL_DestroySurface(ctx->prev_canvas);
}
SDL_free(ctx);
decoder->ctx = NULL;
return true;
}
bool IMG_CreateGIFAnimationDecoder(IMG_AnimationDecoder *decoder, SDL_PropertiesID props)
{
IMG_AnimationDecoderContext *ctx = (IMG_AnimationDecoderContext*)SDL_calloc(1, sizeof(IMG_AnimationDecoderContext));
if (!ctx) {
return SDL_SetError("Out of memory for GIF decoder context");
}
ctx->state.Gif89.transparent = -1;
ctx->state.Gif89.delayTime = -1;
ctx->state.Gif89.inputFlag = -1;
ctx->state.Gif89.disposal = GIF_DISPOSE_NA;
ctx->transparent_index = -1;
ctx->got_header = false;
ctx->got_eof = false;
ctx->current_frame = 0;
ctx->frame_count = 0;
ctx->current_delay = 100;
ctx->current_disposal = GIF_DISPOSE_NA;
ctx->last_disposal = GIF_DISPOSE_NONE;
SDL_Rect r = {0};
ctx->restore_area = r;
decoder->ctx = ctx;
decoder->Reset = IMG_AnimationDecoderReset_Internal;
decoder->GetNextFrame = IMG_AnimationDecoderGetNextFrame_Internal;
decoder->Close = IMG_AnimationDecoderClose_Internal;
char *comment = NULL;
int loop_count = 1;
if (!IMG_AnimationDecoderGetGIFHeader(decoder, &comment, &loop_count)) {
return false;
}
bool ignoreProps = SDL_GetBooleanProperty(props, IMG_PROP_METADATA_IGNORE_PROPS_BOOLEAN, false);
ctx->ignore_props = ignoreProps;
if (!ignoreProps) {
// Set well-defined properties.
SDL_SetNumberProperty(decoder->props, IMG_PROP_METADATA_LOOP_COUNT_NUMBER, loop_count);
// Get other well-defined properties and set them in our props.
if (comment) {
SDL_SetStringProperty(decoder->props, IMG_PROP_METADATA_DESCRIPTION_STRING, comment);
}
}
SDL_free(comment);
return true;
}
#else
bool IMG_CreateGIFAnimationDecoder(IMG_AnimationDecoder *decoder, SDL_PropertiesID props)
{
SDL_SetError("SDL_image built without GIF support");
return false;
}
#endif /* LOAD_GIF */
#if !defined(__APPLE__) || defined(SDL_IMAGE_USE_COMMON_BACKEND)
#ifdef LOAD_GIF
/* See if an image is contained in a data source */
bool IMG_isGIF(SDL_IOStream *src)
{
Sint64 start;
bool is_GIF;
char magic[6];
if (!src) {
return false;
}
start = SDL_TellIO(src);
is_GIF = false;
if (SDL_ReadIO(src, magic, sizeof(magic)) == sizeof(magic) ) {
if ( (SDL_strncmp(magic, "GIF", 3) == 0) &&
((SDL_memcmp(magic + 3, "87a", 3) == 0) ||
(SDL_memcmp(magic + 3, "89a", 3) == 0)) ) {
is_GIF = true;
}
}
SDL_SeekIO(src, start, SDL_IO_SEEK_SET);
return is_GIF;
}
/* Load a GIF type image from an SDL datasource */
SDL_Surface *IMG_LoadGIF_IO(SDL_IOStream *src)
{
IMG_AnimationDecoder *decoder = IMG_CreateAnimationDecoder_IO(src, false, "gif");
if (!decoder) {
return NULL;
}
Uint64 pts = 0;
SDL_Surface *frame = NULL;
IMG_GetAnimationDecoderFrame(decoder, &frame, &pts);
IMG_CloseAnimationDecoder(decoder);
return frame;
}
#else
/* See if an image is contained in a data source */
bool IMG_isGIF(SDL_IOStream *src)
{
return false;
}
/* Load a GIF type image from an SDL datasource */
SDL_Surface *IMG_LoadGIF_IO(SDL_IOStream *src)
{
SDL_SetError("SDL_image built without GIF support");
return NULL;
}
#endif /* LOAD_GIF */
#endif /* !defined(__APPLE__) || defined(SDL_IMAGE_USE_COMMON_BACKEND) */
#if SAVE_GIF
#pragma pack(push,1)
// GIF Header (6 bytes) + Logical Screen Descriptor (7 bytes)
// Total 13 bytes
typedef struct
{
char signature[3]; // "GIF"
char version[3]; // "89a"
uint16_t screenWidth;
uint16_t screenHeight;
uint8_t globalColorTableInfo; // Packed field
uint8_t backgroundColorIndex;
uint8_t pixelAspectRatio;
} GifHeaderAndLSD;
// Graphics Control Extension (8 bytes)
typedef struct
{
uint8_t blockSeparator; // 0x21
uint8_t label; // 0xF9
uint8_t blockSize; // 0x04
uint8_t packedFields; // Packed field (disposal method, user input, transparency)
uint16_t delayTime; // Delay in 1/100ths of a second
uint8_t transparentColorIndex;
uint8_t blockTerminator; // 0x00
} GraphicsControlExtension;
// Image Descriptor (10 bytes)
typedef struct
{
uint8_t imageSeparator; // 0x2C
uint16_t imageLeft;
uint16_t imageTop;
uint16_t imageWidth;
uint16_t imageHeight;
uint8_t packedFields; // Packed field (local color table, interlace, sort, reserved)
} ImageDescriptor;
// Netscape Application Extension (19 bytes + 1 byte terminator)
typedef struct
{
uint8_t blockSeparator; // 0x21
uint8_t label; // 0xFF
uint8_t blockSize; // 0x0B
char applicationIdentifier[8]; // "NETSCAPE"
char applicationAuthenticationCode[3]; // "2.0"
uint8_t subBlockSize; // 0x03
uint8_t loopCountExtension; // 0x01
uint16_t loopCount; // 0 for infinite loop
uint8_t blockTerminator; // 0x00
} NetscapeExtension;
#pragma pack(pop)
// Function to write a byte to the SDL_IOStream
static bool writeByte(SDL_IOStream *io, uint8_t byte)
{
return SDL_WriteIO(io, &byte, 1) == 1;
}
// Function to write a 16-bit word (little-endian) to the SDL_IOStream
static bool writeWord(SDL_IOStream *io, uint16_t word)
{
uint8_t bytes[2];
bytes[0] = word & 0xFF; // Low byte
bytes[1] = (word >> 8) & 0xFF; // High byte
return SDL_WriteIO(io, bytes, 2) == 2;
}
struct IMG_AnimationEncoderContext
{
uint16_t width;
uint16_t height;
uint8_t globalColorTable[256][3];
uint16_t numGlobalColors;
int transparentColorIndex;
bool firstFrame;
uint8_t colorMapLUT[32][32][32];
bool lut_initialized;
bool use_lut;
SDL_PropertiesID metadata;
};
#define LZW_MAX_CODES 4096
#define LZW_MAX_BITS 12
typedef struct
{
int prefix;
uint8_t suffix;
} LZWEntry;
typedef struct
{
LZWEntry entries[LZW_MAX_CODES];
int nextCode;
int initialCodeSize;
int currentCodeSize;
int clearCode;
int eoiCode;
} LZWDictionary;
typedef struct
{
uint8_t *buffer;
size_t currentByte;
int currentBit;
size_t allocatedSize;
} BitStream;
static bool BitStream_Init(BitStream *bs)
{
bs->buffer = (uint8_t *)SDL_calloc(1, bs->allocatedSize);
if (!bs->buffer) {
return false;
}
bs->currentByte = 0;
bs->currentBit = 0;
return true;
}
static bool BitStream_WriteCode(BitStream *bs, int code, int numBits)
{
if (!bs->buffer) {
SDL_SetError("BitStream buffer not initialized.");
return false;
}
size_t requiredBytes = bs->currentByte + ((bs->currentBit + numBits + 7) / 8) + 1;
if (requiredBytes >= bs->allocatedSize) {
// TODO: For now we basically allocate 2x the current size, but we could implement a more sophisticated resizing strategy for large images.
size_t newSize = bs->allocatedSize * 2;
if (newSize < requiredBytes) {
newSize = requiredBytes + 256;
}
uint8_t *newBuffer = (uint8_t *)SDL_realloc(bs->buffer, newSize);
if (!newBuffer) {
SDL_SetError("Failed to reallocate BitStream buffer.");
return false;
}
SDL_memset(newBuffer + bs->allocatedSize, 0, newSize - bs->allocatedSize);
bs->buffer = newBuffer;
bs->allocatedSize = newSize;
}
for (int i = 0; i < numBits; i++) {
if (code & (1 << i)) {
bs->buffer[bs->currentByte] |= (1 << bs->currentBit);
}
bs->currentBit++;
if (bs->currentBit == 8) {
bs->currentBit = 0;
bs->currentByte++;
bs->buffer[bs->currentByte] = 0;
}
}
return true;
}
static size_t BitStream_Flush(BitStream *bs)
{
if (bs->currentBit > 0) {
bs->currentByte++;
}
return bs->currentByte;
}
static void BitStream_Free(BitStream *bs)
{
if (bs->buffer) {
SDL_free(bs->buffer);
bs->buffer = NULL;
}
}
static int lzwCompress(const uint8_t *indexedPixels, uint16_t width, uint16_t height,
uint8_t minCodeSize, uint8_t **compressedData, size_t *compressedSize,
int quality)
{
if (!indexedPixels || !compressedData || !compressedSize) {
SDL_SetError("Invalid NULL pointer passed to lzwCompress");
return -1;
}
// Validate minCodeSize is in reasonable range
if (minCodeSize < 2 || minCodeSize >= LZW_MAX_BITS) {
SDL_SetError("Invalid minCodeSize %d (must be between 2 and %d)",
minCodeSize, LZW_MAX_BITS - 1);
return -1;
}
const int HASH_TABLE_SIZE = 32771;
// Please do not lower the threshold for <50, lowering threshold too much will result in corrupted frames after lzw compression.
const int qualityThreshold = (quality < 50) ? 3072 : (quality < 75) ? 3584
: 4096;
BitStream bs;
bs.allocatedSize = width * height;
if (bs.allocatedSize < 4096) {
bs.allocatedSize = 4096;
}
if (!BitStream_Init(&bs)) {
SDL_SetError("Failed to allocate bitstream buffer");
return -1;
}
typedef struct
{
int prefix;
uint8_t suffix;
int next_in_chain;
} DictEntry;
DictEntry *dict = (DictEntry *)SDL_calloc(LZW_MAX_CODES, sizeof(DictEntry));
if (!dict) {
BitStream_Free(&bs);
SDL_SetError("Failed to allocate LZW dictionary");
return -1;
}
int *hashTable = (int *)SDL_calloc(HASH_TABLE_SIZE, sizeof(int));
if (!hashTable) {
SDL_free(dict);
BitStream_Free(&bs);
SDL_SetError("Failed to allocate hash table");
return -1;
}
const int clearCode = 1 << minCodeSize;
const int eoiCode = clearCode + 1;
int nextCode;
int curCodeSize;
const size_t pixelCount = (size_t)width * height;
#define HASH_FUNC(prefix, suffix) \
(((((unsigned int)(prefix) << 13) ^ ((unsigned int)(suffix) << 5)) * 2654435761u) ^ \
(((unsigned int)(prefix) >> 7) * 16777619u)) % \
HASH_TABLE_SIZE
SDL_memset(hashTable, 0xFF, HASH_TABLE_SIZE * sizeof(int));
nextCode = eoiCode + 1;
curCodeSize = minCodeSize + 1;
if (!BitStream_WriteCode(&bs, clearCode, curCodeSize)) {
SDL_free(dict);
SDL_free(hashTable);
BitStream_Free(&bs);
return -1;
}
if (pixelCount > 0) {
int curString = indexedPixels[0];
for (size_t i = 1; i < pixelCount; i++) {
const uint8_t pixel = indexedPixels[i];
int hashKey = HASH_FUNC(curString, pixel);
int code = hashTable[hashKey];
bool found = false;
while (code != -1) {
if (dict[code].prefix == curString && dict[code].suffix == pixel) {
curString = code;
found = true;
break;
}
code = dict[code].next_in_chain;
}
if (!found) {
if (!BitStream_WriteCode(&bs, curString, curCodeSize)) {
SDL_free(dict);
SDL_free(hashTable);
BitStream_Free(&bs);
return -1;
}
if (nextCode < qualityThreshold) {
dict[nextCode].prefix = curString;
dict[nextCode].suffix = pixel;
dict[nextCode].next_in_chain = hashTable[hashKey];
hashTable[hashKey] = nextCode;
if (nextCode == (1 << curCodeSize) && curCodeSize < LZW_MAX_BITS) {
curCodeSize++;
}
nextCode++;
} else {
// Dictionary reset is controlled by quality threshold
if (!BitStream_WriteCode(&bs, clearCode, curCodeSize)) {
SDL_free(dict);
SDL_free(hashTable);
BitStream_Free(&bs);
return -1;
}
curCodeSize = minCodeSize + 1;
nextCode = eoiCode + 1;
SDL_memset(hashTable, 0xFF, HASH_TABLE_SIZE * sizeof(int));
}
curString = pixel;
}
}
if (!BitStream_WriteCode(&bs, curString, curCodeSize)) {
SDL_free(dict);
SDL_free(hashTable);
BitStream_Free(&bs);
return -1;
}
}
if (!BitStream_WriteCode(&bs, eoiCode, curCodeSize)) {
SDL_free(dict);
SDL_free(hashTable);
BitStream_Free(&bs);
return -1;
}
*compressedData = bs.buffer;
*compressedSize = BitStream_Flush(&bs);
SDL_free(dict);
SDL_free(hashTable);
return 0;
#undef HASH_FUNC
}
#if SAVE_GIF_OCTREE
#ifndef OCTREE_MAX_LEVELS
#define OCTREE_MAX_LEVELS 8
#endif
typedef struct OctreeNode
{
struct OctreeNode *children[8];
struct OctreeNode *parent;
uint32_t pixelCount;
uint64_t rSum, gSum, bSum;
int paletteIndex;
int level;
bool isLeaf;
bool inLeafList;
struct OctreeNode *next_leaf;
struct OctreeNode *prev_leaf;
} OctreeNode;
typedef struct
{
OctreeNode *root;
OctreeNode *head_leaf_list[OCTREE_MAX_LEVELS];
OctreeNode *tail_leaf_list[OCTREE_MAX_LEVELS];
uint32_t leafCount;
uint32_t maxColors;
uint8_t *palette;
uint32_t paletteSize;
} Octree;
static OctreeNode *OctreeNode_Create(int level, OctreeNode *parent)
{
OctreeNode *node = (OctreeNode *)SDL_calloc(1, sizeof(OctreeNode));
if (node) {
node->level = level;
node->parent = parent;
node->isLeaf = true;
node->inLeafList = false;
node->paletteIndex = -1;
node->next_leaf = NULL;
node->prev_leaf = NULL;
}
return node;
}
static void OctreeNode_Free(OctreeNode *node)
{
if (!node)
return;
for (int i = 0; i < 8; ++i) {
if (node->children[i]) {
OctreeNode_Free(node->children[i]);
}
}
SDL_free(node);
}
static bool Octree_Init(Octree *octree, uint32_t maxColors)
{
octree->root = OctreeNode_Create(0, NULL);
if (!octree->root) {
SDL_SetError("Failed to create Octree root node.");
return false;
}
octree->leafCount = 0;
octree->maxColors = maxColors;
octree->palette = NULL;
octree->paletteSize = 0;
for (int i = 0; i < OCTREE_MAX_LEVELS; ++i) {
octree->head_leaf_list[i] = NULL;
octree->tail_leaf_list[i] = NULL;
}
return true;
}
static void Octree_AddLeaf(Octree *octree, OctreeNode *node)
{
if (node->inLeafList) {
return;
}
int level = node->level;
node->next_leaf = NULL;
node->prev_leaf = octree->tail_leaf_list[level];
if (octree->tail_leaf_list[level]) {
octree->tail_leaf_list[level]->next_leaf = node;
} else {
octree->head_leaf_list[level] = node;
}
octree->tail_leaf_list[level] = node;
node->inLeafList = true;
octree->leafCount++;
}
static void Octree_RemoveLeaf(Octree *octree, OctreeNode *node)
{
if (!node->inLeafList) {
return;
}
int level = node->level;
if (node->prev_leaf) {
node->prev_leaf->next_leaf = node->next_leaf;
} else {
octree->head_leaf_list[level] = node->next_leaf;
}
if (node->next_leaf) {
node->next_leaf->prev_leaf = node->prev_leaf;
} else {
octree->tail_leaf_list[level] = node->prev_leaf;
}
node->next_leaf = NULL;
node->prev_leaf = NULL;
node->inLeafList = false;
octree->leafCount--;
}
static void Octree_InsertColor(Octree *octree, OctreeNode *node, uint8_t r, uint8_t g, uint8_t b, int level)
{
if (node->isLeaf && level < OCTREE_MAX_LEVELS - 1) {
if (node->pixelCount > 0) {
Octree_RemoveLeaf(octree, node);
}
node->isLeaf = false;
}
node->pixelCount++;
node->rSum += r;
node->gSum += g;
node->bSum += b;
if (level == OCTREE_MAX_LEVELS - 1) {
Octree_AddLeaf(octree, node);
return;
}
int index = 0;
if (r & (1 << (7 - level)))
index |= 4;
if (g & (1 << (7 - level)))
index |= 2;
if (b & (1 << (7 - level)))
index |= 1;
if (!node->children[index]) {
node->children[index] = OctreeNode_Create(level + 1, node);
if (!node->children[index]) {
// If allocation fails, revert to leaf node
node->isLeaf = true;
Octree_AddLeaf(octree, node);
return;
}
}
Octree_InsertColor(octree, node->children[index], r, g, b, level + 1);
}
static bool OctreeNode_AllChildrenAreLeaves(OctreeNode *node)
{
if (node->isLeaf)
return false;
bool hasAnyChildren = false;
for (int i = 0; i < 8; ++i) {
if (node->children[i]) {
hasAnyChildren = true;
if (!node->children[i]->isLeaf) {
return false;
}
}
}
return hasAnyChildren;
}
static bool Octree_Reduce(Octree *octree)
{
OctreeNode *nodeToCollapse = NULL;
uint32_t minPixelCount = (uint32_t)-1;
for (int level = OCTREE_MAX_LEVELS - 2; level >= 0; --level) {
OctreeNode *currentLeaf = octree->head_leaf_list[level + 1];
while (currentLeaf) {
OctreeNode *parentNode = currentLeaf->parent;
if (parentNode && parentNode->level == level && !parentNode->isLeaf) {
if (OctreeNode_AllChildrenAreLeaves(parentNode)) {
uint32_t currentPixelCount = 0;
for (int i = 0; i < 8; ++i) {
if (parentNode->children[i]) {
currentPixelCount += parentNode->children[i]->pixelCount;
}
}
if (currentPixelCount < minPixelCount) {
minPixelCount = currentPixelCount;
nodeToCollapse = parentNode;
}
}
}
currentLeaf = currentLeaf->next_leaf;
}
}
if (!nodeToCollapse) {
SDL_SetError("Octree_Reduce: No suitable node found to collapse to reduce leafCount.");
return false;
}
nodeToCollapse->rSum = 0;
nodeToCollapse->gSum = 0;
nodeToCollapse->bSum = 0;
nodeToCollapse->pixelCount = 0;
for (int i = 0; i < 8; ++i) {
if (nodeToCollapse->children[i]) {
OctreeNode *child = nodeToCollapse->children[i];
Octree_RemoveLeaf(octree, child);
nodeToCollapse->rSum += child->rSum;
nodeToCollapse->gSum += child->gSum;
nodeToCollapse->bSum += child->bSum;
nodeToCollapse->pixelCount += child->pixelCount;
SDL_free(child);
nodeToCollapse->children[i] = NULL;
}
}
nodeToCollapse->isLeaf = true;
Octree_AddLeaf(octree, nodeToCollapse);
return true;
}
static bool Octree_BuildPalette(Octree *octree, OctreeNode *node, int *paletteIndexCounter)
{
if (!node)
return false;
if (node->isLeaf) {
if (node->pixelCount > 0) {
octree->palette[*paletteIndexCounter * 3 + 0] = (uint8_t)(node->rSum / node->pixelCount);
octree->palette[*paletteIndexCounter * 3 + 1] = (uint8_t)(node->gSum / node->pixelCount);
octree->palette[*paletteIndexCounter * 3 + 2] = (uint8_t)(node->bSum / node->pixelCount);
} else {
octree->palette[*paletteIndexCounter * 3 + 0] = 0;
octree->palette[*paletteIndexCounter * 3 + 1] = 0;
octree->palette[*paletteIndexCounter * 3 + 2] = 0;
}
node->paletteIndex = *paletteIndexCounter;
(*paletteIndexCounter)++;
octree->paletteSize++;
return true;
}
for (int i = 0; i < 8; ++i) {
if (node->children[i]) {
if (!Octree_BuildPalette(octree, node->children[i], paletteIndexCounter)) {
return false;
}
}
}
return true;
}
static int Octree_GetPaletteIndex(OctreeNode *node, uint8_t r, uint8_t g, uint8_t b, int level)
{
if (node->isLeaf) {
return node->paletteIndex;
}
int index = 0;
if (r & (1 << (7 - level)))
index |= 4;
if (g & (1 << (7 - level)))
index |= 2;
if (b & (1 << (7 - level)))
index |= 1;
if (node->children[index]) {
return Octree_GetPaletteIndex(node->children[index], r, g, b, level + 1);
} else {
int bestDist = 196608; // 3 * 256 * 256, larger than any possible distance
int bestIndex = -1;
// Find the first valid child to initialize the search
for (int i = 0; i < 8; ++i) {
if (node->children[i] && node->children[i]->pixelCount > 0) {
int dr = r - (uint8_t)(node->children[i]->rSum / node->children[i]->pixelCount);
int dg = g - (uint8_t)(node->children[i]->gSum / node->children[i]->pixelCount);
int db = b - (uint8_t)(node->children[i]->bSum / node->children[i]->pixelCount);
bestDist = dr * dr + dg * dg + db * db;
bestIndex = node->children[i]->paletteIndex;
break;
}
}
// Now search the rest of the children for a better match
for (int i = 0; i < 8; ++i) {
if (node->children[i] && node->children[i]->pixelCount > 0) {
int dr = r - (uint8_t)(node->children[i]->rSum / node->children[i]->pixelCount);
int dg = g - (uint8_t)(node->children[i]->gSum / node->children[i]->pixelCount);
int db = b - (uint8_t)(node->children[i]->bSum / node->children[i]->pixelCount);
int dist = dr * dr + dg * dg + db * db;
if (dist < bestDist) {
bestDist = dist;
bestIndex = node->children[i]->paletteIndex;
}
}
}
return (bestIndex >= 0) ? bestIndex : 0;
}
}
static void Octree_Free(Octree *octree)
{
if (octree->root) {
OctreeNode_Free(octree->root);
octree->root = NULL;
}
if (octree->palette) {
SDL_free(octree->palette);
octree->palette = NULL;
}
octree->paletteSize = 0;
octree->leafCount = 0;
for (int i = 0; i < OCTREE_MAX_LEVELS; ++i) {
octree->head_leaf_list[i] = NULL;
octree->tail_leaf_list[i] = NULL;
}
}
#endif /* SAVE_GIF_OCTREE */
static int count_set_bits(uint32_t n)
{
int count = 0;
while (n > 0) {
n &= (n - 1);
count++;
}
return count;
}
static void buildColorMapLUT(uint8_t lut[32][32][32], uint8_t palette[][3], uint16_t numColors, bool hasTransparency)
{
const int color_count = hasTransparency ? numColors - 1 : numColors;
for (int r = 0; r < 32; ++r) {
for (int g = 0; g < 32; ++g) {
for (int b = 0; b < 32; ++b) {
// Map the 5-bit color back to 8-bit to find the closest match
const uint8_t r8 = (r << 3) | (r >> 2);
const uint8_t g8 = (g << 3) | (g >> 2);
const uint8_t b8 = (b << 3) | (b >> 2);
int best_match = 0;
int min_dist = 195076; // 3 * 255*255 + 1
for (uint16_t i = 0; i < color_count; ++i) {
const int dr = r8 - palette[i][0];
const int dg = g8 - palette[i][1];
const int db = b8 - palette[i][2];
const int dist = dr * dr + dg * dg + db * db;
if (dist < min_dist) {
min_dist = dist;
best_match = i;
if (min_dist == 0) {
break; // Exact match
}
}
}
lut[r][g][b] = (uint8_t)best_match;
}
}
}
}
static int mapSurfaceToExistingPalette(SDL_Surface *psurf, uint8_t lut[32][32][32], uint8_t *indexedPixels, int transparentIndex)
{
SDL_Surface *surf = psurf;
bool surface_converted = false;
bool surface_locked = false;
Uint32 colorKey = 0;
bool hasTransparency = SDL_SurfaceHasColorKey(surf);
if (hasTransparency) {
SDL_GetSurfaceColorKey(surf, &colorKey);
}
if (surf->format != SDL_PIXELFORMAT_RGBA32 && surf->format != SDL_PIXELFORMAT_ARGB32) {
surf = SDL_ConvertSurface(surf, SDL_PIXELFORMAT_RGBA32);
if (!surf) {
SDL_SetError("Failed to convert surface to RGBA32 for palette mapping.");
return -1;
}
surface_converted = true;
}
const SDL_PixelFormatDetails *pixelFormatDetails = SDL_GetPixelFormatDetails(surf->format);
if (!pixelFormatDetails) {
SDL_SetError("Failed to get pixel format details for surface.");
if (surface_converted) {
SDL_DestroySurface(surf);
}
return -1;
}
if (SDL_MUSTLOCK(surf)) {
if (!SDL_LockSurface(surf)) {
if (surface_converted) {
SDL_DestroySurface(surf);
}
SDL_SetError("Failed to lock surface for palette mapping.");
return -1;
}
surface_locked = true;
}
const int r_bpp = count_set_bits(pixelFormatDetails->Rmask);
const int g_bpp = count_set_bits(pixelFormatDetails->Gmask);
const int b_bpp = count_set_bits(pixelFormatDetails->Bmask);
for (int y = 0; y < surf->h; ++y) {
uint32_t *src_row = (uint32_t *)((uint8_t *)surf->pixels + y * surf->pitch);
uint8_t *dst_row = indexedPixels + y * surf->w;
for (int x = 0; x < surf->w; ++x) {
uint32_t pixel = src_row[x];
if (hasTransparency && pixel == colorKey) {
dst_row[x] = (uint8_t)transparentIndex;
continue;
}
uint8_t r = (uint8_t)(((pixel & pixelFormatDetails->Rmask) >> pixelFormatDetails->Rshift) << (8 - r_bpp));
uint8_t g = (uint8_t)(((pixel & pixelFormatDetails->Gmask) >> pixelFormatDetails->Gshift) << (8 - g_bpp));
uint8_t b = (uint8_t)(((pixel & pixelFormatDetails->Bmask) >> pixelFormatDetails->Bshift) << (8 - b_bpp));
// Use the pre-calculated LUT for an O(1) color lookup.
dst_row[x] = lut[r >> 3][g >> 3][b >> 3];
}
}
if (surface_locked) {
SDL_UnlockSurface(surf);
}
if (surface_converted) {
SDL_DestroySurface(surf);
}
return 0;
}
static int quantizeSurfaceToIndexedPixels(SDL_Surface *psurf, uint8_t palette[][3], uint16_t numPaletteColors, uint8_t *indexedPixels, int transparentIndex)
{
if (!psurf || !palette || !indexedPixels || numPaletteColors == 0 || (numPaletteColors & (numPaletteColors - 1)) != 0) {
SDL_SetError("Invalid arguments for quantizeSurfaceToIndexedPixels: numPaletteColors must be a power of 2.");
return -1;
}
bool surface_converted = false;
bool surface_locked = false;
bool hasTransparency = false;
SDL_Surface *surf = psurf;
#if !SAVE_GIF_OCTREE
// Without octree, we rely on SDL to convert non-indexed surfaces to indexed format.
// Since SDL does a fast conversion, this may result in lower quality than octree quantization.
if (surf->format != SDL_PIXELFORMAT_INDEX8) {
surf = SDL_ConvertSurface(surf, SDL_PIXELFORMAT_INDEX8);
if (!surf) {
SDL_SetError("Failed to convert surface to INDEX8 format.");
return -1;
}
surface_converted = true;
}
Uint32 colorKey = 0;
if (transparentIndex >= 0 && SDL_SurfaceHasColorKey(surf)) {
SDL_GetSurfaceColorKey(surf, &colorKey);
hasTransparency = true;
}
SDL_Palette *surface_palette = SDL_GetSurfacePalette(surf);
if (!surface_palette) {
SDL_SetError("INDEX8 surface has no palette.");
if (surface_converted) {
SDL_DestroySurface(surf);
}
return -1;
}
if (surface_palette->ncolors > (hasTransparency ? numPaletteColors - 1 : numPaletteColors)) {
SDL_SetError("INDEX8 surface palette has too many colors for target.");
if (surface_converted) {
SDL_DestroySurface(surf);
}
return -1;
}
for (int i = 0; i < surface_palette->ncolors; ++i) {
palette[i][0] = surface_palette->colors[i].r;
palette[i][1] = surface_palette->colors[i].g;
palette[i][2] = surface_palette->colors[i].b;
}
for (int i = surface_palette->ncolors; i < numPaletteColors; ++i) {
palette[i][0] = 0;
palette[i][1] = 0;
palette[i][2] = 0;
}
if (hasTransparency) {
palette[transparentIndex][0] = 0;
palette[transparentIndex][1] = 0;
palette[transparentIndex][2] = 0;
}
if (SDL_MUSTLOCK(surf)) {
if (!SDL_LockSurface(surf)) {
SDL_SetError("Failed to lock INDEX8 surface");
if (surface_converted) {
SDL_DestroySurface(surf);
}
return -1;
}
surface_locked = true;
}
for (int y = 0; y < surf->h; ++y) {
uint8_t *src_row = (uint8_t *)surf->pixels + y * surf->pitch;
uint8_t *dst_row = indexedPixels + y * surf->w;
if (hasTransparency) {
for (int x = 0; x < surf->w; ++x) {
uint8_t index = src_row[x];
uint32_t pixel = index;
if (pixel == colorKey) {
dst_row[x] = transparentIndex;
} else {
dst_row[x] = index;
}
}
} else {
SDL_memcpy(dst_row, src_row, surf->w);
}
}
if (surface_converted) {
SDL_DestroySurface(surf);
} else {
if (surface_locked) {
SDL_UnlockSurface(surf);
}
}
return 0;
#else
const SDL_PixelFormatDetails *pixelFormatDetails = SDL_GetPixelFormatDetails(surf->format);
if (!pixelFormatDetails) {
SDL_SetError("Failed to get pixel format details for original surface.");
return -1;
}
Uint32 colorKey = 0;
if (transparentIndex >= 0) {
if (SDL_SurfaceHasColorKey(psurf)) {
SDL_GetSurfaceColorKey(psurf, &colorKey);
}
hasTransparency = true;
}
if (surf->format == SDL_PIXELFORMAT_INDEX8) {
SDL_Palette *surface_palette = SDL_GetSurfacePalette(surf);
if (!surface_palette) {
SDL_SetError("INDEX8 surface has no palette.");
return -1;
}
if (surface_palette->ncolors > (hasTransparency ? numPaletteColors - 1 : numPaletteColors)) {
SDL_SetError("INDEX8 surface palette has too many colors for target.");
return -1;
}
for (int i = 0; i < surface_palette->ncolors; ++i) {
palette[i][0] = surface_palette->colors[i].r;
palette[i][1] = surface_palette->colors[i].g;
palette[i][2] = surface_palette->colors[i].b;
}
for (int i = surface_palette->ncolors; i < numPaletteColors; ++i) {
palette[i][0] = 0;
palette[i][1] = 0;
palette[i][2] = 0;
}
if (hasTransparency) {
palette[transparentIndex][0] = 0;
palette[transparentIndex][1] = 0;
palette[transparentIndex][2] = 0;
}
if (SDL_MUSTLOCK(surf)) {
if (!SDL_LockSurface(surf)) {
SDL_SetError("Failed to lock INDEX8 surface");
return -1;
}
surface_locked = true;
}
for (int y = 0; y < surf->h; ++y) {
uint8_t *src_row = (uint8_t *)surf->pixels + y * surf->pitch;
uint8_t *dst_row = indexedPixels + y * surf->w;
if (hasTransparency) {
for (int x = 0; x < surf->w; ++x) {
uint8_t index = src_row[x];
uint32_t pixel = index;
if (pixel == colorKey) {
dst_row[x] = transparentIndex;
} else {
dst_row[x] = index;
}
}
} else {
SDL_memcpy(dst_row, src_row, surf->w);
}
}
if (surface_locked) {
SDL_UnlockSurface(surf);
}
return 0;
} else {
if (surf->format != SDL_PIXELFORMAT_RGBA32 && surf->format != SDL_PIXELFORMAT_ARGB32) {
surf = SDL_ConvertSurface(surf, SDL_PIXELFORMAT_RGBA32);
if (!surf) {
SDL_SetError("Failed to convert surface to RGBA32 format.");
return -1;
}
surface_converted = true;
pixelFormatDetails = SDL_GetPixelFormatDetails(surf->format);
if (!pixelFormatDetails) {
SDL_SetError("Failed to get pixel format details for surface.");
SDL_DestroySurface(surf);
return -1;
}
}
if (SDL_MUSTLOCK(surf)) {
if (!SDL_LockSurface(surf)) {
SDL_SetError("Failed to lock RGBA32 surface");
if (surface_converted) {
SDL_DestroySurface(surf);
}
return -1;
}
surface_locked = true;
}
Octree octree;
if (!Octree_Init(&octree, hasTransparency ? numPaletteColors - 1 : numPaletteColors)) {
if (surface_locked) {
SDL_UnlockSurface(surf);
}
if (surface_converted) {
SDL_DestroySurface(surf);
}
return -1;
}
octree.palette = (uint8_t *)SDL_calloc(numPaletteColors, 3);
if (!octree.palette) {
Octree_Free(&octree);
if (surface_locked) {
SDL_UnlockSurface(surf);
}
if (surface_converted) {
SDL_DestroySurface(surf);
}
SDL_SetError("Failed to allocate palette memory.");
return -1;
}
octree.paletteSize = 0;
octree.maxColors = hasTransparency ? numPaletteColors - 1 : numPaletteColors;
octree.leafCount = 0;
for (int i = 0; i < OCTREE_MAX_LEVELS; ++i) {
octree.head_leaf_list[i] = NULL;
octree.tail_leaf_list[i] = NULL;
}
OctreeNode *root = octree.root;
if (!root) {
Octree_Free(&octree);
if (surface_locked) {
SDL_UnlockSurface(surf);
}
if (surface_converted) {
SDL_DestroySurface(surf);
}
SDL_SetError("Failed to create Octree root node.");
return -1;
}
root->pixelCount = 0;
root->rSum = 0;
root->gSum = 0;
root->bSum = 0;
root->paletteIndex = -1;
root->isLeaf = true;
root->inLeafList = false;
root->next_leaf = NULL;
root->prev_leaf = NULL;
const int r_bpp = count_set_bits(pixelFormatDetails->Rmask);
const int g_bpp = count_set_bits(pixelFormatDetails->Gmask);
const int b_bpp = count_set_bits(pixelFormatDetails->Bmask);
const Uint32 current_Amask = pixelFormatDetails->Amask;
const Uint8 current_Ashift = pixelFormatDetails->Ashift;
const int current_a_bpp = (current_Amask == 0) ? 0 : count_set_bits(current_Amask);
for (int y = 0; y < surf->h; ++y) {
uint32_t *src_row = (uint32_t *)((uint8_t *)surf->pixels + y * surf->pitch);
for (int x = 0; x < surf->w; ++x) {
uint32_t pixel = src_row[x];
bool isTransparent = false;
if (hasTransparency) {
if (current_Amask != 0 && current_a_bpp > 0) {
uint8_t a = (uint8_t)(((pixel & current_Amask) >> current_Ashift) << (8 - current_a_bpp));
if (a < 128) { // Alpha threshold
isTransparent = true;
}
} else if (SDL_SurfaceHasColorKey(psurf) && (pixel == colorKey)) {
isTransparent = true;
}
}
if (isTransparent) {
continue;
} else {
uint8_t r = (uint8_t)(((pixel & pixelFormatDetails->Rmask) >> pixelFormatDetails->Rshift) << (8 - r_bpp));
uint8_t g = (uint8_t)(((pixel & pixelFormatDetails->Gmask) >> pixelFormatDetails->Gshift) << (8 - g_bpp));
uint8_t b = (uint8_t)(((pixel & pixelFormatDetails->Bmask) >> pixelFormatDetails->Bshift) << (8 - b_bpp));
Octree_InsertColor(&octree, root, r, g, b, 0);
}
}
}
while (octree.leafCount > octree.maxColors && octree.leafCount > 1) {
if (!Octree_Reduce(&octree)) {
Octree_Free(&octree);
if (surface_converted) {
SDL_DestroySurface(surf);
} else if (surface_locked) {
SDL_UnlockSurface(surf);
}
return -1;
}
}
int paletteIndexCounter = 0;
octree.paletteSize = 0;
if (!Octree_BuildPalette(&octree, octree.root, &paletteIndexCounter)) {
SDL_SetError("Failed to build palette from Octree.");
Octree_Free(&octree);
if (surface_converted) {
SDL_DestroySurface(surf);
} else if (surface_locked) {
SDL_UnlockSurface(surf);
}
return -1;
}
if (paletteIndexCounter > (int)octree.maxColors) {
SDL_SetError("Octree built more colors than expected.");
Octree_Free(&octree);
if (surface_converted) {
SDL_DestroySurface(surf);
} else if (surface_locked) {
SDL_UnlockSurface(surf);
}
return -1;
}
uint32_t destIndex = 0;
for (uint32_t i = 0; i < octree.paletteSize; ++i) {
if (hasTransparency && destIndex == (uint32_t)transparentIndex) {
destIndex++;
}
palette[destIndex][0] = octree.palette[i * 3 + 0];
palette[destIndex][1] = octree.palette[i * 3 + 1];
palette[destIndex][2] = octree.palette[i * 3 + 2];
destIndex++;
}
for (uint32_t i = destIndex; i < numPaletteColors; ++i) {
palette[i][0] = 0;
palette[i][1] = 0;
palette[i][2] = 0;
}
if (hasTransparency) {
palette[transparentIndex][0] = 0;
palette[transparentIndex][1] = 0;
palette[transparentIndex][2] = 0;
}
for (int y = 0; y < surf->h; ++y) {
uint32_t *src_row = (uint32_t *)((uint8_t *)surf->pixels + y * surf->pitch);
uint8_t *dst_row = indexedPixels + y * surf->w;
for (int x = 0; x < surf->w; ++x) {
uint32_t pixel = src_row[x];
bool isTransparent = false;
if (hasTransparency) {
if (current_Amask != 0 && current_a_bpp > 0) {
uint8_t a = (uint8_t)(((pixel & current_Amask) >> current_Ashift) << (8 - current_a_bpp));
if (a < 128) { // Alpha threshold
isTransparent = true;
}
} else if (SDL_SurfaceHasColorKey(psurf) && (pixel == colorKey)) {
isTransparent = true;
}
}
if (isTransparent) {
dst_row[x] = transparentIndex;
} else {
uint8_t r = (uint8_t)(((pixel & pixelFormatDetails->Rmask) >> pixelFormatDetails->Rshift) << (8 - r_bpp));
uint8_t g = (uint8_t)(((pixel & pixelFormatDetails->Gmask) >> pixelFormatDetails->Gshift) << (8 - g_bpp));
uint8_t b = (uint8_t)(((pixel & pixelFormatDetails->Bmask) >> pixelFormatDetails->Bshift) << (8 - b_bpp));
int index = Octree_GetPaletteIndex(octree.root, r, g, b, 0);
if (hasTransparency && index >= transparentIndex) {
index++;
}
dst_row[x] = index;
}
}
}
// Clean up the octree
Octree_Free(&octree);
if (surface_converted) {
SDL_DestroySurface(surf);
} else if (surface_locked) {
SDL_UnlockSurface(surf);
}
return 0;
}
#endif /* !defined(SAVE_GIF_OCTREE) */
}
static int writeGifHeader(SDL_IOStream *io, uint16_t width, uint16_t height,
bool hasGlobalColorTable, uint8_t colorResolution, bool sortedColorTable,
uint8_t backgroundColorIndex, uint8_t pixelAspectRatio,
uint8_t gctSizeFieldValue)
{
// Write signature and version (6 bytes)
if (SDL_WriteIO(io, "GIF89a", 6) != 6) {
SDL_SetError("Failed to write GIF signature and version.");
return -1;
}
// Write width (2 bytes, little-endian)
if (!writeWord(io, width)) {
SDL_SetError("Failed to write GIF width.");
return -1;
}
// Write height (2 bytes, little-endian)
if (!writeWord(io, height)) {
SDL_SetError("Failed to write GIF height.");
return -1;
}
// Pack Global Color Table Info byte
uint8_t globalColorTableInfo = 0;
if (hasGlobalColorTable) {
globalColorTableInfo |= 0x80; // Set GCT Flag (bit 7)
globalColorTableInfo |= (gctSizeFieldValue & 0x07); // Set GCT Size (bits 0-2)
}
globalColorTableInfo |= ((colorResolution - 1) & 0x07) << 4; // Set Color Resolution (bits 4-6)
if (sortedColorTable) {
globalColorTableInfo |= 0x08; // Set Sorted Flag (bit 3)
}
// Write packed fields byte
if (!writeByte(io, globalColorTableInfo)) {
SDL_SetError("Failed to write GIF packed fields byte.");
return -1;
}
// Write background color index
if (!writeByte(io, backgroundColorIndex)) {
SDL_SetError("Failed to write GIF background color index.");
return -1;
}
// Write pixel aspect ratio
if (!writeByte(io, pixelAspectRatio)) {
SDL_SetError("Failed to write GIF pixel aspect ratio.");
return -1;
}
return 0;
}
static int writeColorTable(SDL_IOStream *io, uint8_t colors[][3], uint16_t numColors)
{
if (!io || !colors || numColors == 0)
return -1;
if (SDL_WriteIO(io, colors, numColors * 3) != numColors * 3u) {
SDL_SetError("Failed to write color table data");
return -1;
}
return 0;
}
static int writeGraphicsControlExtension(SDL_IOStream *io, uint16_t delayTime, int transparentColorIndex, uint8_t disposalMethod)
{
if (!io)
return -1;
// Extension Introducer
if (!writeByte(io, 0x21)) {
return -1;
}
// Graphic Control Label
if (!writeByte(io, 0xF9)) {
return -1;
}
// Block Size
if (!writeByte(io, 0x04)) {
return -1;
}
uint8_t packedFields = 0;
// Reserved (3 bits) - 0
// Disposal Method (3 bits)
packedFields |= (disposalMethod & 0x07) << 2;
// User Input Flag (1 bit) - 0 (no user input)
// Transparent Color Flag (1 bit)
if (transparentColorIndex != -1) {
packedFields |= (1 << 0);
}
if (!writeByte(io, packedFields)) {
return -1;
}
if (!writeWord(io, delayTime)) {
return -1;
}
if (!writeByte(io, (transparentColorIndex != -1) ? (uint8_t)transparentColorIndex : 0x00)) {
return -1;
}
// Block Terminator
if (!writeByte(io, 0x00)) {
return -1;
}
return 0;
}
static int writeImageDescriptor(SDL_IOStream *io, uint16_t left, uint16_t top,
uint16_t width, uint16_t height,
int hasLocalColorTable, int interlace,
int sortedColorTable, uint8_t localColorTableSize)
{
if (!io)
return -1;
// Image Separator
if (!writeByte(io, 0x2C)) {
return -1;
}
if (!writeWord(io, left)) {
return -1;
}
if (!writeWord(io, top)) {
return -1;
}
if (!writeWord(io, width)) {
return -1;
}
if (!writeWord(io, height)) {
return -1;
}
uint8_t packedFields = 0;
// Local Color Table Flag (1 bit)
if (hasLocalColorTable) {
packedFields |= (1 << 7);
}
// Interlace Flag (1 bit)
if (interlace) {
packedFields |= (1 << 6);
}
// Sort Flag (1 bit)
if (sortedColorTable) {
packedFields |= (1 << 5);
}
// Reserved (2 bits) - 0
// Size of Local Color Table (3 bits)
if (hasLocalColorTable) {
packedFields |= (localColorTableSize & 0x07);
}
if (!writeByte(io, packedFields)) {
return -1;
}
return 0;
}
static int writeImageData(SDL_IOStream *io, uint8_t minCodeSize, const uint8_t *compressedData, size_t dataSize)
{
if (!io || !compressedData)
return -1; // dataSize can be 0 for empty images
// LZW Minimum Code Size
if (!writeByte(io, minCodeSize)) {
return -1;
}
if (dataSize == 0) {
// For empty images, write a zero-sized data block
// Empty block
writeByte(io, 0x00);
} else {
// Write data sub-blocks (max 255 bytes per sub-block)
size_t bytesWritten = 0;
while (bytesWritten < dataSize) {
uint8_t subBlockSize = (uint8_t)((dataSize - bytesWritten > 255) ? 255 : (dataSize - bytesWritten));
// Block Size
if (!writeByte(io, subBlockSize)) {
return -1;
}
if (!SDL_WriteIO(io, compressedData + bytesWritten, subBlockSize)) {
return -1;
}
bytesWritten += subBlockSize;
}
}
// Block Terminator (end of image data)
if (!writeByte(io, 0x00)) {
return -1;
}
return 0;
}
static int writeNetscapeLoopExtension(SDL_IOStream *io, uint16_t loopCount)
{
if (!io)
return -1;
// Omit the extension if the loop count is 1, since 1 can't be represented
if (loopCount == 1) {
return 0;
}
// Extension Introducer
if (!writeByte(io, 0x21)) {
return -1;
}
// Application Extension Label
if (!writeByte(io, 0xFF)) {
return -1;
}
// Block Size
if (!writeByte(io, 0x0B)) {
return -1;
}
// Application Identifier and Authentication Code
if (SDL_WriteIO(io, "NETSCAPE2.0", 11) != 11) {
return -1;
}
// Sub-block Size
if (!writeByte(io, 0x03)) {
return -1;
}
// Sub-block ID
if (!writeByte(io, 0x01)) {
return -1;
}
// Loop Count
uint16_t repeatCount = (loopCount > 0) ? (loopCount - 1) : 0;
if (!writeWord(io, repeatCount)) {
return -1;
}
// Block Terminator
if (!writeByte(io, 0x00)) {
return -1;
}
return 0;
}
static int writeCommentExtension(SDL_IOStream *io, const char *comment)
{
if (!io || !comment || SDL_strlen(comment) == 0) {
return 0;
}
size_t remaining = SDL_strlen(comment);
const char *current_pos = comment;
// Extension Introducer: 0x21
if (!writeByte(io, 0x21)) {
return -1;
}
// Comment Extension Label: 0xFE
if (!writeByte(io, 0xFE)) {
return -1;
}
// Write the comment in sub-blocks of up to 255 bytes
while (remaining > 0) {
Uint8 sub_block_size = (remaining > 255) ? 255 : (Uint8)remaining;
if (!writeByte(io, sub_block_size)) {
return -1;
}
if (SDL_WriteIO(io, current_pos, sub_block_size) != sub_block_size) {
return -1;
}
remaining -= sub_block_size;
current_pos += sub_block_size;
}
// Block Terminator: 0x00
if (!writeByte(io, 0x00)) {
return -1;
}
return 0;
}
static int writeGifTrailer(SDL_IOStream *io)
{
if (!io)
return -1;
// GIF Trailer
if (!writeByte(io, 0x3B)) {
return -1;
}
return 0;
}
static bool AnimationEncoder_AddFrame(IMG_AnimationEncoder *encoder, SDL_Surface *surface, Uint64 duration)
{
IMG_AnimationEncoderContext *ctx = encoder->ctx;
SDL_IOStream *io = encoder->dst;
uint8_t *indexedPixels = NULL;
uint8_t *compressedData = NULL;
uint16_t numColors = ctx->numGlobalColors;
uint8_t palette_bits_per_pixel = 0;
uint8_t localColorTable[256][3];
bool useLocalColorTable = !ctx->firstFrame;
if (!io) {
SDL_SetError("SDL_IOStream pointer (stream->dst) is NULL.");
return false;
}
if (numColors > 1) {
uint16_t temp_colors = numColors;
while (temp_colors > 1 && palette_bits_per_pixel < 8) {
temp_colors >>= 1;
palette_bits_per_pixel++;
}
} else {
palette_bits_per_pixel = 1;
}
size_t pixel_buffer_size = (size_t)surface->w * surface->h;
if (pixel_buffer_size == 0 || pixel_buffer_size / (size_t)surface->w != (size_t)surface->h) {
SDL_SetError("Surface dimensions too large for GIF encoding");
return false;
}
indexedPixels = (uint8_t *)SDL_malloc(pixel_buffer_size);
if (!indexedPixels) {
SDL_SetError("Failed to allocate indexed pixel buffer.");
goto error;
}
if (ctx->firstFrame) {
ctx->width = (uint16_t)surface->w;
ctx->height = (uint16_t)surface->h;
if (quantizeSurfaceToIndexedPixels(surface, ctx->globalColorTable, numColors, indexedPixels, ctx->transparentColorIndex) != 0) {
goto error;
}
if (ctx->use_lut) {
// Build the fast lookup table for subsequent frames.
if (!ctx->lut_initialized) {
buildColorMapLUT(ctx->colorMapLUT, ctx->globalColorTable, numColors, (ctx->transparentColorIndex != -1));
ctx->lut_initialized = true;
}
}
uint8_t gct_size_field_value = (palette_bits_per_pixel > 0) ? (palette_bits_per_pixel - 1) : 0;
if (writeGifHeader(io, ctx->width, ctx->height, true, 8, false, 0, 0, gct_size_field_value) != 0) {
goto error;
}
if (writeColorTable(io, ctx->globalColorTable, numColors) != 0) {
goto error;
}
int loopCount = 0;
const char *description = NULL;
if (ctx->metadata) {
loopCount = (int)SDL_max(SDL_GetNumberProperty(ctx->metadata, IMG_PROP_METADATA_LOOP_COUNT_NUMBER, 0), 0);
description = SDL_GetStringProperty(ctx->metadata, IMG_PROP_METADATA_DESCRIPTION_STRING, NULL);
}
if (writeNetscapeLoopExtension(io, loopCount) != 0) {
goto error;
}
if (description) {
if (writeCommentExtension(io, description) != 0) {
goto error;
}
}
} else {
if (surface->w != ctx->width || surface->h != ctx->height) {
SDL_SetError("Frame dimensions (%dx%d) do not match GIF canvas dimensions (%dx%d).",
surface->w, surface->h, ctx->width, ctx->height);
goto error;
}
if (ctx->use_lut) {
// For subsequent frames, map pixels to the existing global palette using the fast LUT.
if (mapSurfaceToExistingPalette(surface, ctx->colorMapLUT, indexedPixels, ctx->transparentColorIndex) != 0) {
goto error;
}
} else {
// For subsequent frames, create a new optimal palette
if (quantizeSurfaceToIndexedPixels(surface, localColorTable, numColors, indexedPixels, ctx->transparentColorIndex) != 0) {
goto error;
}
}
}
uint16_t resolvedDuration = (uint16_t)IMG_GetEncoderDuration(encoder, duration, 100);
uint8_t disposalMethod = (ctx->transparentColorIndex != -1) ? 2 : 1;
if (writeGraphicsControlExtension(io, resolvedDuration, ctx->transparentColorIndex, disposalMethod) != 0) {
goto error;
}
if (ctx->use_lut) {
// Write image descriptor, indicating we are NOT using a local color table.
if (writeImageDescriptor(io, 0, 0, surface->w, surface->h, false, 0, 0, 0) != 0) {
goto error;
}
} else {
// Write image descriptor with local color table for non-first frames
if (writeImageDescriptor(io, 0, 0, surface->w, surface->h,
useLocalColorTable, 0, 0,
useLocalColorTable ? palette_bits_per_pixel - 1 : 0) != 0) {
goto error;
}
// Write local color table for non-first frames
if (useLocalColorTable) {
if (writeColorTable(io, localColorTable, numColors) != 0) {
goto error;
}
}
}
size_t compressedSize = 0;
uint8_t lzwMinCodeSize = SDL_max(2, palette_bits_per_pixel);
if (lzwCompress(indexedPixels, surface->w, surface->h, lzwMinCodeSize,
&compressedData, &compressedSize, encoder->quality) != 0) {
goto error;
}
if (writeImageData(io, lzwMinCodeSize, compressedData, compressedSize) != 0) {
goto error;
}
SDL_free(indexedPixels);
SDL_free(compressedData);
if (ctx->firstFrame) {
ctx->firstFrame = false;
}
return true;
error:
SDL_free(indexedPixels);
SDL_free(compressedData);
return false;
}
static bool AnimationEncoder_End(IMG_AnimationEncoder *encoder)
{
IMG_AnimationEncoderContext *ctx = encoder->ctx;
SDL_IOStream *io = encoder->dst;
bool success = true;
if (ctx->metadata) {
SDL_DestroyProperties(ctx->metadata);
ctx->metadata = 0;
}
if (io) {
if (writeGifTrailer(io) != 0) {
SDL_SetError("Failed to write GIF trailer.");
success = false;
}
} else {
SDL_SetError("SDL_IOStream pointer (stream->dst) is NULL.");
success = false;
}
SDL_free(ctx);
encoder->ctx = NULL;
return success;
}
bool IMG_CreateGIFAnimationEncoder(IMG_AnimationEncoder *encoder, SDL_PropertiesID props)
{
IMG_AnimationEncoderContext *ctx;
int transparent_index = -1;
uint16_t num_global_colors = 256;
transparent_index = (int)SDL_GetNumberProperty(props, IMG_PROP_ANIMATION_DECODER_CREATE_GIF_TRANSPARENT_COLOR_INDEX_NUMBER, -1);
Sint64 globalcolors = SDL_GetNumberProperty(props, IMG_PROP_ANIMATION_DECODER_CREATE_GIF_NUM_COLORS_NUMBER, 256);
if (globalcolors <= 1 || (globalcolors & (globalcolors - 1)) != 0 || globalcolors > 256) {
return SDL_SetError("GIF stream property 'num_colors' must be a power of 2 (starting from 2, up to 256).");
}
num_global_colors = (uint16_t)globalcolors;
if (transparent_index >= (int)num_global_colors) {
return SDL_SetError("Transparent color index %d exceeds palette size %d", transparent_index, num_global_colors);
}
if (transparent_index < 0) {
transparent_index = num_global_colors - 1;
}
ctx = (IMG_AnimationEncoderContext *)SDL_calloc(1, sizeof(IMG_AnimationEncoderContext));
if (!ctx) {
return false;
}
if (encoder->quality < 0)
encoder->quality = 75;
else if (encoder->quality > 100)
encoder->quality = 100;
bool ignoreProps = SDL_GetBooleanProperty(props, IMG_PROP_METADATA_IGNORE_PROPS_BOOLEAN, false);
if (!ignoreProps) {
ctx->metadata = SDL_CreateProperties();
if (!ctx->metadata) {
SDL_free(ctx);
return false;
}
if (!SDL_CopyProperties(props, ctx->metadata)) {
SDL_DestroyProperties(ctx->metadata);
SDL_free(ctx);
return false;
}
}
ctx->width = 0;
ctx->height = 0;
ctx->numGlobalColors = num_global_colors;
ctx->transparentColorIndex = transparent_index;
ctx->firstFrame = true;
ctx->use_lut = SDL_GetBooleanProperty(props, IMG_PROP_ANIMATION_ENCODER_CREATE_GIF_USE_LUT_BOOLEAN, false);
encoder->ctx = ctx;
encoder->AddFrame = AnimationEncoder_AddFrame;
encoder->Close = AnimationEncoder_End;
return true;
}
bool IMG_SaveGIF_IO(SDL_Surface *surface, SDL_IOStream *dst, bool closeio)
{
if (!IMG_VerifyCanSaveSurface(surface)) {
return false;
}
IMG_AnimationEncoder *encoder = IMG_CreateAnimationEncoder_IO(dst, closeio, "gif");
if (!encoder) {
return false;
}
if (!IMG_AddAnimationEncoderFrame(encoder, surface, 0)) {
IMG_CloseAnimationEncoder(encoder);
return false;
}
if (!IMG_CloseAnimationEncoder(encoder)) {
return false;
}
return true;
}
bool IMG_SaveGIF(SDL_Surface *surface, const char *file)
{
if (!IMG_VerifyCanSaveSurface(surface)) {
return false;
}
SDL_IOStream *dst = SDL_IOFromFile(file, "wb");
if (dst) {
return IMG_SaveGIF_IO(surface, dst, true);
} else {
return false;
}
}
#else
bool IMG_CreateGIFAnimationEncoder(IMG_AnimationEncoder *encoder, SDL_PropertiesID props)
{
return SDL_SetError("SDL_image built without GIF save support");
}
bool IMG_SaveGIF_IO(SDL_Surface *surface, SDL_IOStream *dst, bool closeio)
{
return SDL_SetError("SDL_image built without GIF save support");
}
bool IMG_SaveGIF(SDL_Surface *surface, const char *file)
{
return SDL_SetError("SDL_image built without GIF save support");
}
#endif /* SAVE_GIF */