From d584893219e6bfc0b54072cf942f6c283178b145 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Wed, 29 Apr 2026 10:04:58 -0700
Subject: [PATCH] Clarify that the loop count is the number of times to play an
animation
showanim repeats animations the correct number of times
Corrected the loop count for AVIF animations, which was previously using the repeat count instead.
GIF animations always set the loop count property based on the netscape extension, which specifies the repeat count when available, or not repeating (loop count of 1) when not available.
Closes https://github.com/libsdl-org/SDL_image/pull/726
---
examples/showanim.c | 17 ++++++++++++++---
include/SDL3_image/SDL_image.h | 4 +++-
src/IMG_avif.c | 9 ++++-----
src/IMG_gif.c | 17 ++++++++++++-----
4 files changed, 33 insertions(+), 14 deletions(-)
diff --git a/examples/showanim.c b/examples/showanim.c
index 0fe85956e..676e9b545 100644
--- a/examples/showanim.c
+++ b/examples/showanim.c
@@ -74,6 +74,8 @@ int main(int argc, char *argv[])
Uint32 flags;
int i, j, w, h, done;
int once = 0;
+ int played = 0;
+ int loop_count = 0;
int current_frame, delay;
SDL_Event event;
const char *saveFile = NULL;
@@ -126,6 +128,7 @@ int main(int argc, char *argv[])
SDL_Log("Couldn't load %s: %s\n", argv[i], SDL_GetError());
continue;
}
+ loop_count = (int)SDL_GetNumberProperty(SDL_GetSurfaceProperties(anim->frames[0]), IMG_PROP_METADATA_LOOP_COUNT_NUMBER, -1);
w = anim->w;
h = anim->h;
@@ -144,6 +147,7 @@ int main(int argc, char *argv[])
for (j = 0; j < anim->count; ++j) {
textures[j] = SDL_CreateTextureFromSurface(renderer, anim->frames[j]);
}
+ played = 0;
current_frame = 0;
/* Show the window */
@@ -205,10 +209,17 @@ int main(int argc, char *argv[])
}
SDL_Delay(delay);
- current_frame = (current_frame + 1) % anim->count;
+ if (current_frame < (anim->count - 1)) {
+ ++current_frame;
+ } else {
+ if (played != (loop_count - 1)) {
+ ++played;
+ current_frame = 0;
+ }
- if (once && current_frame == 0) {
- break;
+ if (once) {
+ break;
+ }
}
}
diff --git a/include/SDL3_image/SDL_image.h b/include/SDL3_image/SDL_image.h
index 3865a797d..94bf554e2 100644
--- a/include/SDL3_image/SDL_image.h
+++ b/include/SDL3_image/SDL_image.h
@@ -3209,6 +3209,8 @@ extern SDL_DECLSPEC IMG_AnimationDecoder * SDLCALL IMG_CreateAnimationDecoderWit
* information about the underlying image such as description, copyright text
* and loop count.
*
+ * `IMG_PROP_METADATA_LOOP_COUNT_NUMBER`, if present, specifies the number of times to play the animation, with 0 meaning loop continuously.
+ *
* \param decoder the animation decoder.
* \returns the properties ID of the animation decoder, or 0 if there are no
* properties; call SDL_GetError() for more information.
@@ -3219,7 +3221,7 @@ extern SDL_DECLSPEC IMG_AnimationDecoder * SDLCALL IMG_CreateAnimationDecoderWit
* \sa IMG_CreateAnimationDecoder_IO
* \sa IMG_CreateAnimationDecoderWithProperties
*/
-extern SDL_DECLSPEC SDL_PropertiesID SDLCALL IMG_GetAnimationDecoderProperties(IMG_AnimationDecoder* decoder);
+extern SDL_DECLSPEC SDL_PropertiesID SDLCALL IMG_GetAnimationDecoderProperties(IMG_AnimationDecoder *decoder);
#define IMG_PROP_METADATA_IGNORE_PROPS_BOOLEAN "SDL_image.metadata.ignore_props"
#define IMG_PROP_METADATA_DESCRIPTION_STRING "SDL_image.metadata.description"
diff --git a/src/IMG_avif.c b/src/IMG_avif.c
index 73d20efd1..2840381c7 100644
--- a/src/IMG_avif.c
+++ b/src/IMG_avif.c
@@ -1145,7 +1145,9 @@ bool IMG_CreateAVIFAnimationDecoder(IMG_AnimationDecoder *decoder, SDL_Propertie
SDL_SetNumberProperty(decoder->props, IMG_PROP_METADATA_FRAME_COUNT_NUMBER, ctx->total_frames);
// Set well-defined properties.
- SDL_SetNumberProperty(decoder->props, IMG_PROP_METADATA_LOOP_COUNT_NUMBER, ctx->decoder->repetitionCount);
+ if (ctx->decoder->repetitionCount != AVIF_REPETITION_COUNT_UNKNOWN) {
+ SDL_SetNumberProperty(decoder->props, IMG_PROP_METADATA_LOOP_COUNT_NUMBER, ctx->decoder->repetitionCount + 1);
+ }
// Get other well-defined properties and set them in our props.
if (!ctx->decoder->ignoreXMP) {
@@ -1614,10 +1616,7 @@ bool IMG_CreateAVIFAnimationEncoder(IMG_AnimationEncoder *encoder, SDL_Propertie
bool ignoreProps = SDL_GetBooleanProperty(props, IMG_PROP_METADATA_IGNORE_PROPS_BOOLEAN, false);
if (!ignoreProps) {
- ctx->encoder->repetitionCount = (int)SDL_GetNumberProperty(props, IMG_PROP_METADATA_LOOP_COUNT_NUMBER, -1);
- if (ctx->encoder->repetitionCount < 1 && ctx->encoder->repetitionCount != -1) {
- ctx->encoder->repetitionCount = -1;
- }
+ ctx->encoder->repetitionCount = (int)SDL_max(SDL_GetNumberProperty(props, IMG_PROP_METADATA_LOOP_COUNT_NUMBER, 0), 0) - 1;
ctx->metadata = SDL_CreateProperties();
if (!ctx->metadata) {
diff --git a/src/IMG_gif.c b/src/IMG_gif.c
index 40a15d0c8..334384513 100644
--- a/src/IMG_gif.c
+++ b/src/IMG_gif.c
@@ -549,7 +549,7 @@ static bool IMG_AnimationDecoderGetGIFHeader(IMG_AnimationDecoder *decoder, char
}
if (loopCount) {
- *loopCount = 0;
+ *loopCount = 1;
}
IMG_AnimationDecoderContext *ctx = decoder->ctx;
@@ -641,7 +641,8 @@ static bool IMG_AnimationDecoderGetGIFHeader(IMG_AnimationDecoder *decoder, char
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]);
+ 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) {
@@ -1002,7 +1003,7 @@ bool IMG_CreateGIFAnimationDecoder(IMG_AnimationDecoder *decoder, SDL_Properties
decoder->Close = IMG_AnimationDecoderClose_Internal;
char *comment = NULL;
- int loop_count = 0;
+ int loop_count = 1;
if (!IMG_AnimationDecoderGetGIFHeader(decoder, &comment, &loop_count)) {
return false;
}
@@ -1011,7 +1012,7 @@ bool IMG_CreateGIFAnimationDecoder(IMG_AnimationDecoder *decoder, SDL_Properties
ctx->ignore_props = ignoreProps;
if (!ignoreProps) {
// Set well-defined properties.
- SDL_SetNumberProperty(decoder->props, IMG_PROP_METADATA_LOOP_COUNT_NUMBER, (Sint64)loop_count);
+ 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) {
@@ -2497,6 +2498,11 @@ 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;
@@ -2524,7 +2530,8 @@ static int writeNetscapeLoopExtension(SDL_IOStream *io, uint16_t loopCount)
return -1;
}
// Loop Count
- if (!writeWord(io, loopCount)) {
+ uint16_t repeatCount = (loopCount > 0) ? (loopCount - 1) : 0;
+ if (!writeWord(io, repeatCount)) {
return -1;
}
// Block Terminator