#include <SDL3_image/SDL_image.h>
#include "IMG.h"
#include "IMG_gif.h"
#include "IMG_anim_encoder.h"
#include "IMG_anim_decoder.h"
#if !defined(SAVE_GIF)
#define SAVE_GIF 1
#endif
#if SAVE_GIF
#ifndef SAVE_GIF_OCTREE
#define SAVE_GIF_OCTREE 1
#endif
#endif
#ifdef LOAD_GIF
#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
#define GIF_DISPOSE_NONE 1
#define GIF_DISPOSE_RESTORE_BACKGROUND 2
#define GIF_DISPOSE_RESTORE_PREVIOUS 3
#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#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:
break;
case 0xff:
break;
case 0xfe:
while (GetDataBlock(src, buf, state) > 0)
;
return FALSE;
case 0xf9:
(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)) {
return -1;
}
state->ZeroDataBlock = count == 0;
if ((count != 0) && (!ReadOK(src, buf, count))) {
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;
}
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;
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) {
}
return -2;
}
incode = code;
if (code >= state->max_code) {
*state->sp++ = state->firstcode;
code = state->oldcode;
}
while (code >= state->clear_code) {
if (code < 0 || code >= (1 << MAX_LWZ_BITS)) {
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];
}
if (code < 0 || code >= (1 << MAX_LWZ_BITS)) {
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;
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 (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;
unsigned char buf[256];
char version[4];
int width;
int height;
SDL_Surface *canvas;
SDL_Surface *prev_canvas;
int frame_count;
int current_frame;
bool got_header;
bool got_eof;
int current_disposal;
int current_delay;
int transparent_index;
unsigned char global_colormap[3][MAXCOLORMAPSIZE];
int global_colormap_size;
bool has_global_colormap;
bool global_grayscale;
Uint64 last_duration;
int last_disposal;
int restore_frame;
bool ignore_props;
};
static bool IMG_AnimationDecoderGetGIFHeader(IMG_AnimationDecoder *decoder, char**comment, int *loopCount)
{
if (comment) {
*comment = NULL;
}
if (loopCount) {
*loopCount = 0;
}
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: {
uint8_t extension_label;
if (!ReadOK(src, &extension_label, 1)) {
return SDL_SetError("Error reading GIF extension label");
}
switch (extension_label) {
case 0xFF: {
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");
}
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) {
*loopCount = LM_to_uint(sub_block_data[1], sub_block_data[2]);
}
if (!ReadOK(src, &sub_block_size, 1) || sub_block_size != 0x00) {
return SDL_SetError("Netscape extension block not terminated correctly");
}
} else {
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 {
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: {
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: {
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: processing_extensions = false;
break;
case 0x3B: return SDL_SetError("GIF file contains no images");
default: 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;
ctx->restore_frame = 0;
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:
break;
case GIF_DISPOSE_RESTORE_BACKGROUND:
{
SDL_Rect rect = { 0, 0, ctx->width, ctx->height };
if (!SDL_FillSurfaceRect(ctx->canvas, &rect, 0)) {
return SDL_SetError("Failed to fill canvas with background color");
}
} break;
case GIF_DISPOSE_RESTORE_PREVIOUS:
if (ctx->prev_canvas) {
if (!SDL_BlitSurface(ctx->prev_canvas, NULL, ctx->canvas, NULL)) {
return SDL_SetError("Failed to restore previous canvas");
}
}
break;
default:
break;
}
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");
}
}
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) {
if (SDL_GetError()[0] == '\0') {
return SDL_SetError("Failed to decode frame.");
}
return false;
}
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");
}
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) {
*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;
ctx->restore_frame = 0;
decoder->ctx = ctx;
decoder->Reset = IMG_AnimationDecoderReset_Internal;
decoder->GetNextFrame = IMG_AnimationDecoderGetNextFrame_Internal;
decoder->Close = IMG_AnimationDecoderClose_Internal;
char *comment = NULL;
int loop_count = 0;
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) {
SDL_SetNumberProperty(decoder->props, IMG_PROP_METADATA_LOOP_COUNT_NUMBER, (Sint64)loop_count);
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
#if !defined(__APPLE__) || defined(SDL_IMAGE_USE_COMMON_BACKEND)
#ifdef LOAD_GIF
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;
}
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
bool IMG_isGIF(SDL_IOStream *src)
{
return false;
}
SDL_Surface *IMG_LoadGIF_IO(SDL_IOStream *src)
{
SDL_SetError("SDL_image built without GIF support");
return NULL;
}
#endif
#endif
#if SAVE_GIF
#pragma pack(push,1)
typedef struct
{
char signature[3]; char version[3]; uint16_t screenWidth;
uint16_t screenHeight;
uint8_t globalColorTableInfo; uint8_t backgroundColorIndex;
uint8_t pixelAspectRatio;
} GifHeaderAndLSD;
typedef struct
{
uint8_t blockSeparator; uint8_t label; uint8_t blockSize; uint8_t packedFields; uint16_t delayTime; uint8_t transparentColorIndex;
uint8_t blockTerminator; } GraphicsControlExtension;
typedef struct
{
uint8_t imageSeparator; uint16_t imageLeft;
uint16_t imageTop;
uint16_t imageWidth;
uint16_t imageHeight;
uint8_t packedFields; } ImageDescriptor;
typedef struct
{
uint8_t blockSeparator; uint8_t label; uint8_t blockSize; char applicationIdentifier[8]; char applicationAuthenticationCode[3]; uint8_t subBlockSize; uint8_t loopCountExtension; uint16_t loopCount; uint8_t blockTerminator; } NetscapeExtension;
#pragma pack(pop)
static bool writeByte(SDL_IOStream *io, uint8_t byte)
{
return SDL_WriteIO(io, &byte, 1) == 1;
}
static bool writeWord(SDL_IOStream *io, uint16_t word)
{
uint8_t bytes[2];
bytes[0] = word & 0xFF; bytes[1] = (word >> 8) & 0xFF; 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) {
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;
}
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;
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 {
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]) {
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; int bestIndex = -1;
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;
}
}
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
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) {
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;
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; }
}
}
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));
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
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) { 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) { 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;
}
}
}
Octree_Free(&octree);
if (surface_converted) {
SDL_DestroySurface(surf);
} else if (surface_locked) {
SDL_UnlockSurface(surf);
}
return 0;
}
#endif
}
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)
{
if (SDL_WriteIO(io, "GIF89a", 6) != 6) {
SDL_SetError("Failed to write GIF signature and version.");
return -1;
}
if (!writeWord(io, width)) {
SDL_SetError("Failed to write GIF width.");
return -1;
}
if (!writeWord(io, height)) {
SDL_SetError("Failed to write GIF height.");
return -1;
}
uint8_t globalColorTableInfo = 0;
if (hasGlobalColorTable) {
globalColorTableInfo |= 0x80; globalColorTableInfo |= (gctSizeFieldValue & 0x07); }
globalColorTableInfo |= ((colorResolution - 1) & 0x07) << 4; if (sortedColorTable) {
globalColorTableInfo |= 0x08; }
if (!writeByte(io, globalColorTableInfo)) {
SDL_SetError("Failed to write GIF packed fields byte.");
return -1;
}
if (!writeByte(io, backgroundColorIndex)) {
SDL_SetError("Failed to write GIF background color index.");
return -1;
}
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;
if (!writeByte(io, 0x21)) {
return -1;
}
if (!writeByte(io, 0xF9)) {
return -1;
}
if (!writeByte(io, 0x04)) {
return -1;
}
uint8_t packedFields = 0;
packedFields |= (disposalMethod & 0x07) << 2;
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;
}
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;
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;
if (hasLocalColorTable) {
packedFields |= (1 << 7);
}
if (interlace) {
packedFields |= (1 << 6);
}
if (sortedColorTable) {
packedFields |= (1 << 5);
}
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;
if (!writeByte(io, minCodeSize)) {
return -1;
}
if (dataSize == 0) {
writeByte(io, 0x00);
} else {
size_t bytesWritten = 0;
while (bytesWritten < dataSize) {
uint8_t subBlockSize = (uint8_t)((dataSize - bytesWritten > 255) ? 255 : (dataSize - bytesWritten));
if (!writeByte(io, subBlockSize)) {
return -1;
}
if (!SDL_WriteIO(io, compressedData + bytesWritten, subBlockSize)) {
return -1;
}
bytesWritten += subBlockSize;
}
}
if (!writeByte(io, 0x00)) {
return -1;
}
return 0;
}
static int writeNetscapeLoopExtension(SDL_IOStream *io, uint16_t loopCount)
{
if (!io)
return -1;
if (!writeByte(io, 0x21)) {
return -1;
}
if (!writeByte(io, 0xFF)) {
return -1;
}
if (!writeByte(io, 0x0B)) {
return -1;
}
if (SDL_WriteIO(io, "NETSCAPE2.0", 11) != 11) {
return -1;
}
if (!writeByte(io, 0x03)) {
return -1;
}
if (!writeByte(io, 0x01)) {
return -1;
}
if (!writeWord(io, loopCount)) {
return -1;
}
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;
if (!writeByte(io, 0x21)) {
return -1;
}
if (!writeByte(io, 0xFE)) {
return -1;
}
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;
}
if (!writeByte(io, 0x00)) {
return -1;
}
return 0;
}
static int writeGifTrailer(SDL_IOStream *io)
{
if (!io)
return -1;
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) {
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) {
if (mapSurfaceToExistingPalette(surface, ctx->colorMapLUT, indexedPixels, ctx->transparentColorIndex) != 0) {
goto error;
}
} else {
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) {
if (writeImageDescriptor(io, 0, 0, surface->w, surface->h, false, 0, 0, 0) != 0) {
goto error;
}
} else {
if (writeImageDescriptor(io, 0, 0, surface->w, surface->h,
useLocalColorTable, 0, 0,
useLocalColorTable ? palette_bits_per_pixel - 1 : 0) != 0) {
goto error;
}
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