SDL_image: Load single GIF images as 8-bit indexed images

From 329d61949b64d1b8557661bd13f838775c7eb9a1 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sun, 20 Sep 2026 11:26:42 -0700
Subject: [PATCH] Load single GIF images as 8-bit indexed images

Fixes https://github.com/libsdl-org/SDL_image/issues/765
---
 src/IMG_gif.c | 102 ++++++++++++++++++++++++++++++--------------------
 1 file changed, 61 insertions(+), 41 deletions(-)

diff --git a/src/IMG_gif.c b/src/IMG_gif.c
index a13bc3de6..46555383e 100644
--- a/src/IMG_gif.c
+++ b/src/IMG_gif.c
@@ -28,6 +28,8 @@
 #include "IMG_anim_encoder.h"
 #include "IMG_anim_decoder.h"
 
+#define IMG_PROP_ANIMATION_DECODER_CREATE_GIF_SINGLE_IMAGE_BOOLEAN "SDL_image.animation_decoder.create.gif.single_image"
+
 // We will have the saving GIF feature by default
 #if !defined(SAVE_GIF)
 #define SAVE_GIF 1
@@ -528,6 +530,8 @@ struct IMG_AnimationDecoderContext
     int frame_count;             /* Total number of frames seen */
     int current_frame;           /* Current frame index */
 
+    bool single_frame;           /* Whether this decoder will return a single frame */
+
     bool got_header;             /* Whether we've read the GIF header */
     bool got_eof;                /* Whether we've reached the end of the GIF */
 
@@ -550,7 +554,7 @@ struct IMG_AnimationDecoderContext
     bool ignore_props;
 };
 
-static bool IMG_AnimationDecoderGetGIFHeader(IMG_AnimationDecoder *decoder, char**comment, int *loopCount)
+static bool IMG_AnimationDecoderGetGIFHeader(IMG_AnimationDecoder *decoder, char **comment, int *loopCount)
 {
     if (comment) {
         *comment = NULL;
@@ -723,29 +727,31 @@ static bool IMG_AnimationDecoderGetGIFHeader(IMG_AnimationDecoder *decoder, char
             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->single_frame) {
             if (!ctx->canvas) {
-                return SDL_SetError("Failed to create canvas surface");
-            }
+                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 (!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");
-            }
+                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");
+                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");
+                }
             }
         }
 
@@ -902,31 +908,36 @@ static bool IMG_AnimationDecoderGetNextFrame_Internal(IMG_AnimationDecoder *deco
                               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.
+        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 the
+            // 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;
-      }
+            // Only set the error if ReadImage did not do it.
+            if (SDL_GetError()[0] == '\0') {
+                return SDL_SetError("Failed to decode frame.");
+            }
 
-        /* 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");
+            return false;
         }
 
-        /* Store the frame in the output array */
-        retval = SDL_DuplicateSurface(ctx->canvas);
-        if (!retval) {
+        if (ctx->single_frame &&
+            left == 0 && top == 0 && image->w == ctx->width && image->h == ctx->height) {
+            retval = image;
+        } else {
+            /* 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);
             SDL_DestroySurface(image);
-            return SDL_SetError("Failed to duplicate frame surface");
+            if (!retval) {
+                return SDL_SetError("Failed to duplicate frame surface");
+            }
         }
 
         if (ctx->state.Gif89.delayTime < 0 && ctx->last_duration) {
@@ -941,8 +952,6 @@ static bool IMG_AnimationDecoderGetNextFrame_Internal(IMG_AnimationDecoder *deco
 
         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;
@@ -1004,6 +1013,7 @@ bool IMG_CreateGIFAnimationDecoder(IMG_AnimationDecoder *decoder, SDL_Properties
     ctx->last_disposal = GIF_DISPOSE_NONE;
     SDL_Rect r = {0};
     ctx->restore_area = r;
+    ctx->single_frame = SDL_GetBooleanProperty(props, IMG_PROP_ANIMATION_DECODER_CREATE_GIF_SINGLE_IMAGE_BOOLEAN, false);
 
     decoder->ctx = ctx;
     decoder->Reset = IMG_AnimationDecoderReset_Internal;
@@ -1073,7 +1083,17 @@ bool IMG_isGIF(SDL_IOStream *src)
 /* 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");
+    SDL_PropertiesID props = SDL_CreateProperties();
+    if (!props) {
+        return NULL;
+    }
+
+    SDL_SetPointerProperty(props, IMG_PROP_ANIMATION_DECODER_CREATE_IOSTREAM_POINTER, src);
+    SDL_SetBooleanProperty(props, IMG_PROP_ANIMATION_DECODER_CREATE_IOSTREAM_AUTOCLOSE_BOOLEAN, false);
+    SDL_SetStringProperty(props, IMG_PROP_ANIMATION_DECODER_CREATE_TYPE_STRING, "gif");
+    SDL_SetBooleanProperty(props, IMG_PROP_ANIMATION_DECODER_CREATE_GIF_SINGLE_IMAGE_BOOLEAN, true);
+    IMG_AnimationDecoder *decoder = IMG_CreateAnimationDecoderWithProperties(props);
+    SDL_DestroyProperties(props);
     if (!decoder) {
         return NULL;
     }