From df4d9edaf8e4ab1a8097a84869f47f394e55bc20 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Tue, 21 Oct 2025 12:25:00 -0700
Subject: [PATCH] Added support for saving ANI animated cursors
---
CMakeLists.txt | 2 +
include/SDL3_image/SDL_image.h | 48 +++++++
src/IMG_ani.c | 221 +++++++++++++++++++++++++++++++++
src/IMG_ani.h | 1 +
src/IMG_anim_decoder.c | 22 ++--
src/IMG_anim_encoder.c | 20 +--
test/CMakeLists.txt | 6 +-
test/rgbrgb.ani | Bin 0 -> 5404 bytes
test/testanimation.c | 29 +++--
9 files changed, 319 insertions(+), 30 deletions(-)
create mode 100755 test/rgbrgb.ani
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ccf05a97..8180300f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -129,6 +129,7 @@ option(SDLIMAGE_XCF "Support loading XCF images" ON)
option(SDLIMAGE_XPM "Support loading XPM images" ON)
option(SDLIMAGE_XV "Support loading XV images" ON)
+cmake_dependent_option(SDLIMAGE_ANI_SAVE "Add ANI save support" ON SDLIMAGE_ANI OFF)
cmake_dependent_option(SDLIMAGE_AVIF_SAVE "Add AVIF save support" ON SDLIMAGE_AVIF OFF)
cmake_dependent_option(SDLIMAGE_BMP_SAVE "Add BMP save support" ON SDLIMAGE_BMP OFF)
cmake_dependent_option(SDLIMAGE_GIF_SAVE "Add GIF save support" ON SDLIMAGE_GIF OFF)
@@ -603,6 +604,7 @@ if(SDLIMAGE_ANI)
if(SDLIMAGE_ANI_ENABLED)
target_compile_definitions(${sdl3_image_target_name} PRIVATE
LOAD_ANI
+ SAVE_ANI=$<BOOL:${SDLIMAGE_ANI_SAVE}>
)
else()
# Variable is used by test suite
diff --git a/include/SDL3_image/SDL_image.h b/include/SDL3_image/SDL_image.h
index df72e2fc..05213848 100644
--- a/include/SDL3_image/SDL_image.h
+++ b/include/SDL3_image/SDL_image.h
@@ -2605,6 +2605,14 @@ typedef struct IMG_AnimationEncoder IMG_AnimationEncoder;
/**
* Create an encoder to save a series of images to a file.
*
+ * These animation types are currently supported:
+ *
+ * - ANI
+ * - APNG
+ * - AVIFS
+ * - GIF
+ * - WEBP
+ *
* The file type is determined from the file extension, e.g. "file.webp" will
* be encoded using WEBP.
*
@@ -2624,6 +2632,14 @@ extern SDL_DECLSPEC IMG_AnimationEncoder * SDLCALL IMG_CreateAnimationEncoder(co
/**
* Create an encoder to save a series of images to an IOStream.
*
+ * These animation types are currently supported:
+ *
+ * - ANI
+ * - APNG
+ * - AVIFS
+ * - GIF
+ * - WEBP
+ *
* If `closeio` is true, `dst` will be closed before returning if this
* function fails, or when the animation encoder is closed if this function
* succeeds.
@@ -2647,6 +2663,14 @@ extern SDL_DECLSPEC IMG_AnimationEncoder * SDLCALL IMG_CreateAnimationEncoder_IO
/**
* Create an animation encoder with the specified properties.
*
+ * These animation types are currently supported:
+ *
+ * - ANI
+ * - APNG
+ * - AVIFS
+ * - GIF
+ * - WEBP
+ *
* These are the supported properties:
*
* - `IMG_PROP_ANIMATION_ENCODER_CREATE_FILENAME_STRING`: the file to save, if
@@ -2754,6 +2778,14 @@ typedef struct IMG_AnimationDecoder IMG_AnimationDecoder;
/**
* Create a decoder to read a series of images from a file.
*
+ * These animation types are currently supported:
+ *
+ * - ANI
+ * - APNG
+ * - AVIFS
+ * - GIF
+ * - WEBP
+ *
* The file type is determined from the file extension, e.g. "file.webp" will
* be decoded using WEBP.
*
@@ -2774,6 +2806,14 @@ extern SDL_DECLSPEC IMG_AnimationDecoder * SDLCALL IMG_CreateAnimationDecoder(co
/**
* Create a decoder to read a series of images from an IOStream.
*
+ * These animation types are currently supported:
+ *
+ * - ANI
+ * - APNG
+ * - AVIFS
+ * - GIF
+ * - WEBP
+ *
* If `closeio` is true, `src` will be closed before returning if this
* function fails, or when the animation decoder is closed if this function
* succeeds.
@@ -2798,6 +2838,14 @@ extern SDL_DECLSPEC IMG_AnimationDecoder * SDLCALL IMG_CreateAnimationDecoder_IO
/**
* Create an animation decoder with the specified properties.
*
+ * These animation types are currently supported:
+ *
+ * - ANI
+ * - APNG
+ * - AVIFS
+ * - GIF
+ * - WEBP
+ *
* These are the supported properties:
*
* - `IMG_PROP_ANIMATION_DECODER_CREATE_FILENAME_STRING`: the file to load, if
diff --git a/src/IMG_ani.c b/src/IMG_ani.c
index 476b36ab..f80cb2ab 100644
--- a/src/IMG_ani.c
+++ b/src/IMG_ani.c
@@ -22,8 +22,14 @@
#include <SDL3_image/SDL_image.h>
#include "IMG_ani.h"
+#include "IMG_anim_encoder.h"
#include "IMG_anim_decoder.h"
+// We will have the saving ANI feature by default
+#if !defined(SAVE_ANI)
+#define SAVE_ANI 1
+#endif
+
#ifdef LOAD_ANI
#define RIFF_FOURCC(c0, c1, c2, c3) \
@@ -480,3 +486,218 @@ bool IMG_CreateANIAnimationDecoder(IMG_AnimationDecoder *decoder, SDL_Properties
#endif // LOAD_ANI
+#if SAVE_ANI
+
+struct IMG_AnimationEncoderContext
+{
+ char *author;
+ char *title;
+ int num_frames;
+ int max_frames;
+ SDL_Surface **frames;
+ Uint64 *durations;
+};
+
+
+static bool AnimationEncoder_AddFrame(IMG_AnimationEncoder *encoder, SDL_Surface *surface, Uint64 duration)
+{
+ IMG_AnimationEncoderContext *ctx = encoder->ctx;
+
+ if (ctx->num_frames == ctx->max_frames) {
+ int max_frames = ctx->max_frames + 8;
+
+ SDL_Surface **frames = (SDL_Surface **)SDL_realloc(ctx->frames, max_frames * sizeof(*frames));
+ if (!frames) {
+ return false;
+ }
+
+ Uint64 *durations = (Uint64 *)SDL_realloc(ctx->durations, max_frames * sizeof(*durations));
+ if (!durations) {
+ SDL_free(frames);
+ return false;
+ }
+
+ ctx->frames = frames;
+ ctx->durations = durations;
+ ctx->max_frames = max_frames;
+ }
+
+ ctx->frames[ctx->num_frames] = surface;
+ ++surface->refcount;
+ ctx->durations[ctx->num_frames] = duration;
+ ++ctx->num_frames;
+
+ return true;
+}
+
+static bool SaveChunkSize(SDL_IOStream *dst, Sint64 offset)
+{
+ Sint64 here = SDL_TellIO(dst);
+ if (here < 0) {
+ return false;
+ }
+ if (SDL_SeekIO(dst, offset, SDL_IO_SEEK_SET) < 0) {
+ return false;
+ }
+
+ Uint32 size = (Uint32)(here - (offset + sizeof(Uint32)));
+ if (!SDL_WriteU32LE(dst, size)) {
+ return false;
+ }
+ return SDL_SeekIO(dst, here, SDL_IO_SEEK_SET);
+}
+
+static bool WriteIconFrame(SDL_Surface *surface, SDL_IOStream *dst)
+{
+ bool result = true;
+
+ result &= SDL_WriteU32LE(dst, RIFF_FOURCC('i', 'c', 'o', 'n'));
+ Sint64 icon_size_offset = SDL_TellIO(dst);
+ result &= SDL_WriteU32LE(dst, 0);
+ // Technically this could be ICO format, but it's generally animated cursors
+ result &= IMG_SaveCUR_IO(surface, dst, false);
+ result &= SaveChunkSize(dst, icon_size_offset);
+
+ return result;
+}
+
+static bool WriteAnimInfo(IMG_AnimationEncoderContext *ctx, SDL_IOStream *dst)
+{
+ bool result = true;
+
+ result &= SDL_WriteU32LE(dst, RIFF_FOURCC('L', 'I', 'S', 'T'));
+ Sint64 list_size_offset = SDL_TellIO(dst);
+ result &= SDL_WriteU32LE(dst, 0);
+ result &= SDL_WriteU32LE(dst, RIFF_FOURCC('I', 'N', 'F', 'O'));
+
+ if (ctx->title) {
+ Uint32 size = (Uint32)(SDL_strlen(ctx->title) + 1);
+ result &= SDL_WriteU32LE(dst, RIFF_FOURCC('I', 'N', 'A', 'M'));
+ result &= SDL_WriteU32LE(dst, size);
+ result &= (SDL_WriteIO(dst, ctx->title, size) == size);
+ }
+
+ if (ctx->author) {
+ Uint32 size = (Uint32)(SDL_strlen(ctx->author) + 1);
+ result &= SDL_WriteU32LE(dst, RIFF_FOURCC('I', 'A', 'R', 'T'));
+ result &= SDL_WriteU32LE(dst, size);
+ result &= (SDL_WriteIO(dst, ctx->author, size) == size);
+ }
+
+ result &= SaveChunkSize(dst, list_size_offset);
+
+ return result;
+}
+
+static bool WriteAnimation(IMG_AnimationEncoder *encoder)
+{
+ IMG_AnimationEncoderContext *ctx = encoder->ctx;
+ SDL_IOStream *dst = encoder->dst;
+ bool result = true;
+
+ // RIFF header
+ result &= SDL_WriteU32LE(dst, RIFF_FOURCC('R', 'I', 'F', 'F'));
+ Sint64 riff_size_offset = SDL_TellIO(dst);
+ result &= SDL_WriteU32LE(dst, 0);
+ result &= SDL_WriteU32LE(dst, RIFF_FOURCC('A', 'C', 'O', 'N'));
+
+ // anih header chunk
+ result &= SDL_WriteU32LE(dst, RIFF_FOURCC('a', 'n', 'i', 'h'));
+ result &= SDL_WriteU32LE(dst, sizeof(ANIHEADER));
+
+ ANIHEADER anih;
+ SDL_zero(anih);
+ anih.cbSizeof = sizeof(anih);
+ anih.frames = ctx->num_frames;
+ anih.steps = ctx->num_frames;
+ anih.jifRate = 1;
+ anih.fl = ANI_FLAG_ICON;
+ result &= (SDL_WriteIO(dst, &anih, sizeof(anih)) == sizeof(anih));
+
+ // Info list
+ if (ctx->author || ctx->title) {
+ WriteAnimInfo(ctx, dst);
+ }
+
+ // Rate chunk
+ result &= SDL_WriteU32LE(dst, RIFF_FOURCC('r', 'a', 't', 'e'));
+ result &= SDL_WriteU32LE(dst, sizeof(Uint32) * ctx->num_frames);
+ for (int i = 0; i < ctx->num_frames; ++i) {
+ Uint32 duration = (Uint32)IMG_GetEncoderDuration(encoder, ctx->durations[i], 60);
+ result &= SDL_WriteU32LE(dst, duration);
+ }
+
+ // Frame list
+ result &= SDL_WriteU32LE(dst, RIFF_FOURCC('L', 'I', 'S', 'T'));
+ Sint64 frame_list_size_offset = SDL_TellIO(dst);
+ result &= SDL_WriteU32LE(dst, 0);
+ result &= SDL_WriteU32LE(dst, RIFF_FOURCC('f', 'r', 'a', 'm'));
+
+ for (int i = 0; i < ctx->num_frames; ++i) {
+ result &= WriteIconFrame(ctx->frames[i], dst);
+ }
+ result &= SaveChunkSize(dst, frame_list_size_offset);
+
+ // All done!
+ result &= SaveChunkSize(dst, riff_size_offset);
+
+ return result;
+}
+
+static bool AnimationEncoder_End(IMG_AnimationEncoder *encoder)
+{
+ IMG_AnimationEncoderContext *ctx = encoder->ctx;
+ bool result = true;
+
+ if (ctx->num_frames > 0) {
+ result = WriteAnimation(encoder);
+
+ for (int i = 0; i < ctx->num_frames; ++i) {
+ SDL_DestroySurface(ctx->frames[i]);
+ }
+ SDL_free(ctx->frames);
+ SDL_free(ctx->durations);
+ }
+
+ SDL_free(ctx->author);
+ SDL_free(ctx->title);
+ SDL_free(ctx);
+ encoder->ctx = NULL;
+
+ return result;
+}
+
+bool IMG_CreateANIAnimationEncoder(IMG_AnimationEncoder *encoder, SDL_PropertiesID props)
+{
+ IMG_AnimationEncoderContext *ctx;
+
+ ctx = (IMG_AnimationEncoderContext *)SDL_calloc(1, sizeof(IMG_AnimationEncoderContext));
+ if (!ctx) {
+ return false;
+ }
+
+ const char *author = SDL_GetStringProperty(props, IMG_PROP_METADATA_AUTHOR_STRING, NULL);
+ if (author && *author) {
+ ctx->author = SDL_strdup(author);
+ }
+
+ const char *title = SDL_GetStringProperty(props, IMG_PROP_METADATA_TITLE_STRING, NULL);
+ if (title && *title) {
+ ctx->title = SDL_strdup(title);
+ }
+
+ encoder->ctx = ctx;
+ encoder->AddFrame = AnimationEncoder_AddFrame;
+ encoder->Close = AnimationEncoder_End;
+
+ return true;
+}
+
+#else
+
+bool IMG_CreateANIAnimationEncoder(IMG_AnimationEncoder *encoder, SDL_PropertiesID props)
+{
+ return SDL_SetError("SDL_image built without ANI save support");
+}
+
+#endif // SAVE_ANI
diff --git a/src/IMG_ani.h b/src/IMG_ani.h
index 45eb3abc..3197484b 100644
--- a/src/IMG_ani.h
+++ b/src/IMG_ani.h
@@ -19,5 +19,6 @@
3. This notice may not be removed or altered from any source distribution.
*/
+extern bool IMG_CreateANIAnimationEncoder(IMG_AnimationEncoder *encoder, SDL_PropertiesID props);
extern bool IMG_CreateANIAnimationDecoder(IMG_AnimationDecoder *decoder, SDL_PropertiesID props);
diff --git a/src/IMG_anim_decoder.c b/src/IMG_anim_decoder.c
index 72958b2d..c024f035 100644
--- a/src/IMG_anim_decoder.c
+++ b/src/IMG_anim_decoder.c
@@ -22,11 +22,11 @@
#include <SDL3_image/SDL_image.h>
#include "IMG_anim_decoder.h"
-#include "IMG_webp.h"
-#include "IMG_libpng.h"
-#include "IMG_gif.h"
-#include "IMG_avif.h"
#include "IMG_ani.h"
+#include "IMG_avif.h"
+#include "IMG_gif.h"
+#include "IMG_libpng.h"
+#include "IMG_webp.h"
IMG_AnimationDecoder *IMG_CreateAnimationDecoder(const char *file)
{
@@ -139,16 +139,16 @@ IMG_AnimationDecoder *IMG_CreateAnimationDecoderWithProperties(SDL_PropertiesID
}
bool result = false;
- if (SDL_strcasecmp(type, "webp") == 0) {
- result = IMG_CreateWEBPAnimationDecoder(decoder, props);
- } else if (SDL_strcasecmp(type, "png") == 0) {
+ if (SDL_strcasecmp(type, "ani") == 0) {
+ result = IMG_CreateANIAnimationDecoder(decoder, props);
+ } else if (SDL_strcasecmp(type, "apng") == 0 || SDL_strcasecmp(type, "png") == 0) {
result = IMG_CreateAPNGAnimationDecoder(decoder, props);
- } else if (SDL_strcasecmp(type, "gif") == 0) {
- result = IMG_CreateGIFAnimationDecoder(decoder, props);
} else if (SDL_strcasecmp(type, "avifs") == 0) {
result = IMG_CreateAVIFAnimationDecoder(decoder, props);
- } else if (SDL_strcasecmp(type, "ani") == 0) {
- result = IMG_CreateANIAnimationDecoder(decoder, props);
+ } else if (SDL_strcasecmp(type, "gif") == 0) {
+ result = IMG_CreateGIFAnimationDecoder(decoder, props);
+ } else if (SDL_strcasecmp(type, "webp") == 0) {
+ result = IMG_CreateWEBPAnimationDecoder(decoder, props);
} else {
SDL_SetError("Unrecognized output type");
}
diff --git a/src/IMG_anim_encoder.c b/src/IMG_anim_encoder.c
index 83aeb69c..ceec0056 100644
--- a/src/IMG_anim_encoder.c
+++ b/src/IMG_anim_encoder.c
@@ -22,10 +22,12 @@
#include <SDL3_image/SDL_image.h>
#include "IMG_anim_encoder.h"
-#include "IMG_webp.h"
-#include "IMG_libpng.h"
-#include "IMG_gif.h"
+#include "IMG_ani.h"
#include "IMG_avif.h"
+#include "IMG_gif.h"
+#include "IMG_libpng.h"
+#include "IMG_webp.h"
+
IMG_AnimationEncoder *IMG_CreateAnimationEncoder(const char *file)
{
@@ -134,14 +136,16 @@ IMG_AnimationEncoder *IMG_CreateAnimationEncoderWithProperties(SDL_PropertiesID
encoder->timebase_denominator = timebase_denominator;
bool result = false;
- if (SDL_strcasecmp(type, "webp") == 0) {
- result = IMG_CreateWEBPAnimationEncoder(encoder, props);
- } else if (SDL_strcasecmp(type, "png") == 0) {
+ if (SDL_strcasecmp(type, "ani") == 0) {
+ result = IMG_CreateANIAnimationEncoder(encoder, props);
+ } else if (SDL_strcasecmp(type, "apng") == 0 || SDL_strcasecmp(type, "png") == 0) {
result = IMG_CreateAPNGAnimationEncoder(encoder, props);
- } else if (SDL_strcasecmp(type, "gif") == 0) {
- result = IMG_CreateGIFAnimationEncoder(encoder, props);
} else if (SDL_strcasecmp(type, "avifs") == 0) {
result = IMG_CreateAVIFAnimationEncoder(encoder, props);
+ } else if (SDL_strcasecmp(type, "gif") == 0) {
+ result = IMG_CreateGIFAnimationEncoder(encoder, props);
+ } else if (SDL_strcasecmp(type, "webp") == 0) {
+ result = IMG_CreateWEBPAnimationEncoder(encoder, props);
} else {
SDL_SetError("Unrecognized output type");
}
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index e90d8c75..a28ca137 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -66,11 +66,15 @@ function(add_sdl_image_test NAME)
"SDL_TEST_SRCDIR=${CMAKE_CURRENT_SOURCE_DIR}"
"SDL_TEST_BUILDDIR=$<TARGET_FILE_DIR:${TARGET}>"
"SDL_VIDEO_DRIVER=dummy"
+ "SDL_IMAGE_SAVE_ANI=$<BOOL:${SDLIMAGE_ANI_SAVE}>"
"SDL_IMAGE_SAVE_AVIF=$<BOOL:${SDLIMAGE_AVIF_SAVE}>"
+ "SDL_IMAGE_SAVE_CUR=$<BOOL:${SDLIMAGE_BMP_SAVE}>"
+ "SDL_IMAGE_SAVE_ICO=$<BOOL:${SDLIMAGE_BMP_SAVE}>"
"SDL_IMAGE_SAVE_JPG=$<BOOL:${SDLIMAGE_JPG_SAVE}>"
"SDL_IMAGE_SAVE_PNG=$<BOOL:${SDLIMAGE_PNG_SAVE}>"
+ "SDL_IMAGE_ANIM_ANI=$<BOOL:${SDLIMAGE_ANI_ENABLED}>"
+ "SDL_IMAGE_ANIM_APNG=$<AND:$<BOOL:${SDLIMAGE_PNG_ENABLED}>,$<NOT:$<OR:$<BOOL:${SDLIMAGE_BACKEND_WIC}>,$<BOOL:${SDLIMAGE_BACKEND_STB}>,$<BOOL:${SDLIMAGE_BACKEND_IMAGEIO}>>>>"
"SDL_IMAGE_ANIM_AVIFS=$<BOOL:${SDLIMAGE_AVIF_ENABLED}>"
- "SDL_IMAGE_ANIM_PNG=$<AND:$<BOOL:${SDLIMAGE_PNG_ENABLED}>,$<NOT:$<OR:$<BOOL:${SDLIMAGE_BACKEND_WIC}>,$<BOOL:${SDLIMAGE_BACKEND_STB}>,$<BOOL:${SDLIMAGE_BACKEND_IMAGEIO}>>>>"
"SDL_IMAGE_ANIM_GIF=$<BOOL:${SDLIMAGE_GIF_ENABLED}>"
"SDL_IMAGE_ANIM_WEBP=$<BOOL:${SDLIMAGE_WEBP_ENABLED}>"
)
diff --git a/test/rgbrgb.ani b/test/rgbrgb.ani
new file mode 100755
index 0000000000000000000000000000000000000000..289156c607ec0074128d1a54ad9fd2ad7a5e5d6b
GIT binary patch
literal 5404
zcmWIYbaN9CWngf0_V-K7%gj(=U|;}YHYf&CD1Z^j1L2~?l2i#O8zu&#nV@1mp1~n|
zL>L&-iV|}(lk@X(n1K>NS&$kC2m^|U0hyfve(t<nQd~eLucwDg5X5{&1`d!DVv1Iz
zF)%R8db&7<RLpsM#gLIff#<-6#`^QlZ0^FWd#lRQ*gbabXKtwHVK_93Mnhl(hJf7K
zZ_NL0{4ZY*%2%GQelF{r5}Ih4=hD(g^W11*GonL4Zq0Y*e=4t3kI^m9m5t`P5uM^j
zV8w>!Wx3TqnE%;-+EYTWJU805M2mq@?nn!P(YEDiw{kQD=o<o~ZA<!w_ed+#NBxT#
I0$AIY0NSB|m;e9(
literal 0
HcmV?d00001
diff --git a/test/testanimation.c b/test/testanimation.c
index a610b6f1..36448af7 100644
--- a/test/testanimation.c
+++ b/test/testanimation.c
@@ -45,13 +45,15 @@ static const Sint64 default_loop_count = 1;
#define DEFAULT_COPYRIGHT "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat"
static const FormatInfo formatInfo[] = {
- { "PNG", { { IMG_PROP_METADATA_TITLE_STRING, (const void*)DEFAULT_TITLE, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_AUTHOR_STRING, (const void*)DEFAULT_AUTHOR, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_DESCRIPTION_STRING, (const void*)DEFAULT_DESCRIPTION, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_LOOP_COUNT_NUMBER, (const void *)&default_loop_count, SDL_PROPERTY_TYPE_NUMBER }, { IMG_PROP_METADATA_CREATION_TIME_STRING, (const void*)DEFAULT_CREATION_TIME, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_COPYRIGHT_STRING, (const void*)DEFAULT_COPYRIGHT, SDL_PROPERTY_TYPE_STRING } } },
+ { "ANI", { { IMG_PROP_METADATA_TITLE_STRING, (const void*)DEFAULT_TITLE, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_AUTHOR_STRING, (const void*)DEFAULT_AUTHOR, SDL_PROPERTY_TYPE_STRING } } },
+
+ { "APNG", { { IMG_PROP_METADATA_TITLE_STRING, (const void*)DEFAULT_TITLE, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_AUTHOR_STRING, (const void*)DEFAULT_AUTHOR, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_DESCRIPTION_STRING, (const void*)DEFAULT_DESCRIPTION, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_LOOP_COUNT_NUMBER, (const void *)&default_loop_count, SDL_PROPERTY_TYPE_NUMBER }, { IMG_PROP_METADATA_CREATION_TIME_STRING, (const void*)DEFAULT_CREATION_TIME, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_COPYRIGHT_STRING, (const void*)DEFAULT_COPYRIGHT, SDL_PROPERTY_TYPE_STRING } } },
+
+ { "AVIFS", { { IMG_PROP_METADATA_TITLE_STRING, (const void*)DEFAULT_TITLE, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_AUTHOR_STRING, (const void*)DEFAULT_AUTHOR, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_DESCRIPTION_STRING, (const void*)DEFAULT_DESCRIPTION, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_LOOP_COUNT_NUMBER, (const void *)&default_loop_count, SDL_PROPERTY_TYPE_NUMBER }, { IMG_PROP_METADATA_CREATION_TIME_STRING, (const void*)DEFAULT_CREATION_TIME, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_COPYRIGHT_STRING, (const void*)DEFAULT_COPYRIGHT, SDL_PROPERTY_TYPE_STRING } } },
{ "GIF", { { IMG_PROP_METADATA_DESCRIPTION_STRING, (const void*)DEFAULT_DESCRIPTION, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_LOOP_COUNT_NUMBER, (const void *)&default_loop_count, SDL_PROPERTY_TYPE_NUMBER } } },
{ "WEBP", { { IMG_PROP_METADATA_TITLE_STRING, (const void*)DEFAULT_TITLE, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_AUTHOR_STRING, (const void*)DEFAULT_AUTHOR, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_DESCRIPTION_STRING, (const void*)DEFAULT_DESCRIPTION, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_LOOP_COUNT_NUMBER, (const void *)&default_loop_count, SDL_PROPERTY_TYPE_NUMBER }, { IMG_PROP_METADATA_CREATION_TIME_STRING, (const void*)DEFAULT_CREATION_TIME, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_COPYRIGHT_STRING, (const void*)DEFAULT_COPYRIGHT, SDL_PROPERTY_TYPE_STRING } } },
-
- { "AVIFS", { { IMG_PROP_METADATA_TITLE_STRING, (const void*)DEFAULT_TITLE, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_AUTHOR_STRING, (const void*)DEFAULT_AUTHOR, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_DESCRIPTION_STRING, (const void*)DEFAULT_DESCRIPTION, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_LOOP_COUNT_NUMBER, (const void *)&default_loop_count, SDL_PROPERTY_TYPE_NUMBER }, { IMG_PROP_METADATA_CREATION_TIME_STRING, (const void*)DEFAULT_CREATION_TIME, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_COPYRIGHT_STRING, (const void*)DEFAULT_COPYRIGHT, SDL_PROPERTY_TYPE_STRING } } },
};
@@ -59,12 +61,13 @@ static const struct {
const char *format;
const char *filename;
} inputImages[] = {
- { "PNG", "rgbrgb.png" },
+ { "ANI", "rgbrgb.ani" },
+ { "APNG", "rgbrgb.png" },
+ { "AVIFS", "rgbrgb.avifs" },
{ "GIF", "rgbrgb.gif" },
- { "WEBP", "rgbrgb.webp" },
- { "AVIFS", "rgbrgb.avifs" }
+ { "WEBP", "rgbrgb.webp" }
};
-static const char *outputImageFormats[] = { "PNG", "GIF", "WEBP", "AVIFS" };
+static const char *outputImageFormats[] = { "ANI", "APNG", "AVIFS", "GIF", "WEBP" };
static const char *GetAnimationDecoderStatusString(IMG_AnimationDecoderStatus status)
{
@@ -341,7 +344,7 @@ static int SDLCALL testDecodeEncode(void *args) {
SDLTest_AssertPass("Encoder closed successfully after adding %i frames.", ii);
}
- SDLTest_AssertCheck(ii == i, "All frames were added teo the encoder. Added %i, Total: %i", ii, i);
+ SDLTest_AssertCheck(ii == i, "All frames were added to the encoder. Added %i, Total: %i", ii, i);
if (ii != i) {
for (int ai = 0; ai < arraySize; ++ai) {
SDL_free(decodedFrameData[ai]);
@@ -375,10 +378,16 @@ static int SDLCALL testDecodeEncode(void *args) {
outputImageFormat);
SDLTest_AssertCheck(decoder2 != NULL, "IMG_CreateAnimationDecoder_IO");
if (decoder2) {
-
+ bool check_duration = true;
Uint64 duration2;
SDL_Surface *frame2;
int j = 0;
+ if (SDL_strcmp(inputImages[cim].format, "ANI") == 0 || SDL_strcmp(outputImageFormat, "ANI") == 0) {
+ /* ANI uses 1/60 time units which can't represent our 20 ms sample frame durations.
+ * If we switched the sample data to be 100 FPS, that would work for both, but as-is...
+ */
+ check_duration = false;
+ }
while (IMG_GetAnimationDecoderFrame(decoder2, &frame2, &duration2)) {
SDLTest_Log("Reloaded Frame Duration (%i): %" SDL_PRIu64 " ms", j, duration2);
SDLTest_Log("Reloaded Frame Format (%i): %s", j, SDL_GetPixelFormatName(frame2->format));
@@ -398,7 +407,7 @@ static int SDLCALL testDecodeEncode(void *args) {
if (decodedFrameData[j]->width != frame2->w ||
decodedFrameData[j]->height != frame2->h ||
- decodedFrameData[j]->duration != duration2) {
+ (check_duration && decodedFrameData[j]->duration != duration2)) {
SDLTest_LogError("Frame data mismatch at index %i. Expected (%i, %i, %" SDL_PRIu64 "), Got (%i, %i, %" SDL_PRIu64 ")",
j, decodedFrameData[j]->width, decodedFrameData[j]->height, decodedFrameData[j]->duration, frame2->w, frame2->h, duration2);
for (int ai = 0; ai < arraySize; ++ai) {