SDL_image: Support non-animated images as single frame animations

From d516bc507f51b418d091f0aafb23ccbf937c6fc8 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 16 Feb 2026 12:42:19 -0800
Subject: [PATCH] Support non-animated images as single frame animations

Fixes https://github.com/libsdl-org/SDL_image/issues/692
---
 src/IMG_anim_decoder.c | 81 ++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 78 insertions(+), 3 deletions(-)

diff --git a/src/IMG_anim_decoder.c b/src/IMG_anim_decoder.c
index b569ef96..84459e83 100644
--- a/src/IMG_anim_decoder.c
+++ b/src/IMG_anim_decoder.c
@@ -28,6 +28,75 @@
 #include "IMG_libpng.h"
 #include "IMG_webp.h"
 
+struct IMG_AnimationDecoderContext
+{
+    char *type;
+    bool frame_read;
+};
+
+static bool IMG_SingleFrameDecoderReset(IMG_AnimationDecoder *decoder)
+{
+    IMG_AnimationDecoderContext *ctx = decoder->ctx;
+
+    if (SDL_SeekIO(decoder->src, decoder->start, SDL_IO_SEEK_SET) != decoder->start) {
+        return false;
+    }
+
+    ctx->frame_read = false;
+    return true;
+}
+
+static bool IMG_SingleFrameDecoderGetNextFrame(IMG_AnimationDecoder *decoder, SDL_Surface **frame, Uint64 *duration)
+{
+    IMG_AnimationDecoderContext *ctx = decoder->ctx;
+
+    if (ctx->frame_read) {
+        decoder->status = IMG_DECODER_STATUS_COMPLETE;
+        return false;
+    }
+
+    *duration = 0;
+    *frame = IMG_LoadTyped_IO(decoder->src, false, ctx->type);
+    if (*frame) {
+        ctx->frame_read = true;
+        return true;
+    } else {
+        return false;
+    }
+}
+
+static bool IMG_SingleFrameDecoderClose(IMG_AnimationDecoder *decoder)
+{
+    IMG_AnimationDecoderContext *ctx = decoder->ctx;
+
+    SDL_free(ctx->type);
+    SDL_free(ctx);
+    decoder->ctx = NULL;
+
+    return true;
+}
+
+static bool IMG_CreateSingleFrameAnimationDecoder(IMG_AnimationDecoder *decoder, const char *type)
+{
+    IMG_AnimationDecoderContext *ctx = (IMG_AnimationDecoderContext*)SDL_calloc(1, sizeof(IMG_AnimationDecoderContext));
+    if (!ctx) {
+        return false;
+    }
+
+    ctx->type = SDL_strdup(type);
+    if (!ctx->type) {
+        SDL_free(ctx);
+        return false;
+    }
+
+    decoder->ctx = ctx;
+    decoder->Reset = IMG_SingleFrameDecoderReset;
+    decoder->GetNextFrame = IMG_SingleFrameDecoderGetNextFrame;
+    decoder->Close = IMG_SingleFrameDecoderClose;
+
+    return true;
+}
+
 IMG_AnimationDecoder *IMG_CreateAnimationDecoder(const char *file)
 {
     if (!file || !*file) {
@@ -111,7 +180,7 @@ IMG_AnimationDecoder *IMG_CreateAnimationDecoderWithProperties(SDL_PropertiesID
 
     if (!src) {
         if (!file) {
-            SDL_SetError("No output properties set");
+            SDL_SetError("No input properties set");
             return NULL;
         }
 
@@ -149,8 +218,14 @@ IMG_AnimationDecoder *IMG_CreateAnimationDecoderWithProperties(SDL_PropertiesID
         result = IMG_CreateGIFAnimationDecoder(decoder, props);
     } else if (SDL_strcasecmp(type, "webp") == 0) {
         result = IMG_CreateWEBPAnimationDecoder(decoder, props);
-    } else {
-        SDL_SetError("Unrecognized output type");
+    }
+
+    if (!result) {
+        if (SDL_SeekIO(decoder->src, decoder->start, SDL_IO_SEEK_SET) != decoder->start) {
+            goto error;
+        }
+
+        result = IMG_CreateSingleFrameAnimationDecoder(decoder, type);
     }
 
     if (result) {