From 1e622fb6f94e4d965febaa31691d469e1b175e2f Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Wed, 22 Oct 2025 14:33:44 -0700
Subject: [PATCH] Added functions to save animations
---
examples/showanim.c | 22 +---
include/SDL3_image/SDL_image.h | 218 +++++++++++++++++++++++++++++----
src/IMG.c | 66 ++++++++++
src/IMG_anim_encoder.c | 67 ++++++++++
src/SDL_image.sym | 7 ++
5 files changed, 340 insertions(+), 40 deletions(-)
diff --git a/examples/showanim.c b/examples/showanim.c
index 2ca7398e..a85dadb7 100644
--- a/examples/showanim.c
+++ b/examples/showanim.c
@@ -50,24 +50,6 @@ static void draw_background(SDL_Renderer *renderer, int w, int h)
}
}
-static void SaveAnimation(IMG_Animation *anim, const char *file)
-{
- int i;
- IMG_AnimationEncoder *encoder = IMG_CreateAnimationEncoder(file);
- if (!encoder) {
- SDL_Log("Couldn't save anim: %s\n", SDL_GetError());
- return;
- }
-
- for (i = 0; i < anim->count; ++i) {
- if (!IMG_AddAnimationEncoderFrame(encoder, anim->frames[i], anim->delays[i])) {
- SDL_Log("Couldn't add anim frame: %s\n", SDL_GetError());
- break;
- }
- }
- IMG_CloseAnimationEncoder(encoder);
-}
-
int main(int argc, char *argv[])
{
SDL_Window *window;
@@ -133,7 +115,9 @@ int main(int argc, char *argv[])
h = anim->h;
if (saveFile) {
- SaveAnimation(anim, saveFile);
+ if (!IMG_SaveAnimation(anim, saveFile)) {
+ SDL_Log("Couldn't save animation: %s", SDL_GetError());
+ }
}
textures = (SDL_Texture **)SDL_calloc(anim->count, sizeof(*textures));
diff --git a/include/SDL3_image/SDL_image.h b/include/SDL3_image/SDL_image.h
index 4c72b97c..5e7b9b2a 100644
--- a/include/SDL3_image/SDL_image.h
+++ b/include/SDL3_image/SDL_image.h
@@ -2394,7 +2394,7 @@ extern SDL_DECLSPEC IMG_Animation * SDLCALL IMG_LoadAnimation(const char *file);
extern SDL_DECLSPEC IMG_Animation * SDLCALL IMG_LoadAnimation_IO(SDL_IOStream *src, bool closeio);
/**
- * Load an animation from an SDL datasource
+ * Load an animation from an SDL_IOStream.
*
* Even though this function accepts a file type, SDL_image may still try
* other decoders that are capable of detecting file type from the contents of
@@ -2429,26 +2429,6 @@ extern SDL_DECLSPEC IMG_Animation * SDLCALL IMG_LoadAnimation_IO(SDL_IOStream *s
*/
extern SDL_DECLSPEC IMG_Animation * SDLCALL IMG_LoadAnimationTyped_IO(SDL_IOStream *src, bool closeio, const char *type);
-/**
- * Dispose of an IMG_Animation and free its resources.
- *
- * The provided `anim` pointer is not valid once this call returns.
- *
- * \param anim IMG_Animation to dispose of.
- *
- * \since This function is available since SDL_image 3.0.0.
- *
- * \sa IMG_LoadAnimation
- * \sa IMG_LoadAnimation_IO
- * \sa IMG_LoadAnimationTyped_IO
- * \sa IMG_LoadANIAnimation_IO
- * \sa IMG_LoadAPNGAnimation_IO
- * \sa IMG_LoadAVIFAnimation_IO
- * \sa IMG_LoadGIFAnimation_IO
- * \sa IMG_LoadWEBPAnimation_IO
- */
-extern SDL_DECLSPEC void SDLCALL IMG_FreeAnimation(IMG_Animation *anim);
-
/**
* Load an ANI animation directly from an SDL_IOStream.
*
@@ -2583,6 +2563,182 @@ extern SDL_DECLSPEC IMG_Animation * SDLCALL IMG_LoadGIFAnimation_IO(SDL_IOStream
*/
extern SDL_DECLSPEC IMG_Animation * SDLCALL IMG_LoadWEBPAnimation_IO(SDL_IOStream *src);
+/**
+ * Save an animation to a file.
+ *
+ * For formats that accept a quality, a default quality of 90 will be used.
+ *
+ * \param anim the animation to save.
+ * \param file path on the filesystem containing an animated image.
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ * information.
+ *
+ * \since This function is available since SDL_image 3.4.0.
+ *
+ * \sa IMG_SaveAnimationTyped_IO
+ * \sa IMG_SaveANIAnimation_IO
+ * \sa IMG_SaveAPNGAnimation_IO
+ * \sa IMG_SaveAVIFAnimation_IO
+ * \sa IMG_SaveGIFAnimation_IO
+ * \sa IMG_SaveWEBPAnimation_IO
+ */
+extern SDL_DECLSPEC bool SDLCALL IMG_SaveAnimation(IMG_Animation *anim, const char *file);
+
+/**
+ * Save an animation to an SDL_IOStream.
+ *
+ * If you just want to save to a filename, you can use IMG_SaveAnimation() instead.
+ *
+ * If `closeio` is true, `dst` will be closed before returning, whether this
+ * function succeeds or not.
+ *
+ * For formats that accept a quality, a default quality of 90 will be used.
+ *
+ * \param anim the animation to save.
+ * \param dst an SDL_IOStream that data will be written to.
+ * \param closeio true to close/free the SDL_IOStream before returning, false
+ * to leave it open.
+ * \param type a filename extension that represent this data ("GIF", etc).
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ * information.
+ *
+ * \since This function is available since SDL_image 3.4.0.
+ *
+ * \sa IMG_SaveAnimation
+ * \sa IMG_SaveANIAnimation_IO
+ * \sa IMG_SaveAPNGAnimation_IO
+ * \sa IMG_SaveAVIFAnimation_IO
+ * \sa IMG_SaveGIFAnimation_IO
+ * \sa IMG_SaveWEBPAnimation_IO
+ */
+extern SDL_DECLSPEC bool SDLCALL IMG_SaveAnimationTyped_IO(IMG_Animation *anim, SDL_IOStream *dst, bool closeio, const char *type);
+
+/**
+ * Save an animation in ANI format to an SDL_IOStream.
+ *
+ * If `closeio` is true, `dst` will be closed before returning, whether this
+ * function succeeds or not.
+ *
+ * \param anim the animation to save.
+ * \param dst an SDL_IOStream from which data will be written to.
+ * \param closeio true to close/free the SDL_IOStream before returning, false
+ * to leave it open.
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ * information.
+ *
+ * \since This function is available since SDL_image 3.4.0.
+ *
+ * \sa IMG_SaveAnimation
+ * \sa IMG_SaveAnimationTyped_IO
+ * \sa IMG_SaveAPNGAnimation_IO
+ * \sa IMG_SaveAVIFAnimation_IO
+ * \sa IMG_SaveGIFAnimation_IO
+ * \sa IMG_SaveWEBPAnimation_IO
+ */
+extern SDL_DECLSPEC bool SDLCALL IMG_SaveANIAnimation_IO(IMG_Animation *anim, SDL_IOStream *dst, bool closeio);
+
+/**
+ * Save an animation in APNG format to an SDL_IOStream.
+ *
+ * If `closeio` is true, `dst` will be closed before returning, whether this
+ * function succeeds or not.
+ *
+ * \param anim the animation to save.
+ * \param dst an SDL_IOStream from which data will be written to.
+ * \param closeio true to close/free the SDL_IOStream before returning, false
+ * to leave it open.
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ * information.
+ *
+ * \since This function is available since SDL_image 3.4.0.
+ *
+ * \sa IMG_SaveAnimation
+ * \sa IMG_SaveAnimationTyped_IO
+ * \sa IMG_SaveANIAnimation_IO
+ * \sa IMG_SaveAVIFAnimation_IO
+ * \sa IMG_SaveGIFAnimation_IO
+ * \sa IMG_SaveWEBPAnimation_IO
+ */
+extern SDL_DECLSPEC bool SDLCALL IMG_SaveAPNGAnimation_IO(IMG_Animation *anim, SDL_IOStream *dst, bool closeio);
+
+/**
+ * Save an animation in AVIF format to an SDL_IOStream.
+ *
+ * If `closeio` is true, `dst` will be closed before returning, whether this
+ * function succeeds or not.
+ *
+ * \param anim the animation to save.
+ * \param dst an SDL_IOStream from which data will be written to.
+ * \param closeio true to close/free the SDL_IOStream before returning, false
+ * to leave it open.
+ * \param quality the desired quality, ranging between 0 (lowest) and 100
+ * (highest).
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ * information.
+ *
+ * \since This function is available since SDL_image 3.4.0.
+ *
+ * \sa IMG_SaveAnimation
+ * \sa IMG_SaveAnimationTyped_IO
+ * \sa IMG_SaveANIAnimation_IO
+ * \sa IMG_SaveAPNGAnimation_IO
+ * \sa IMG_SaveGIFAnimation_IO
+ * \sa IMG_SaveWEBPAnimation_IO
+ */
+extern SDL_DECLSPEC bool SDLCALL IMG_SaveAVIFAnimation_IO(IMG_Animation *anim, SDL_IOStream *dst, bool closeio, int quality);
+
+/**
+ * Save an animation in GIF format to an SDL_IOStream.
+ *
+ * If `closeio` is true, `dst` will be closed before returning, whether this
+ * function succeeds or not.
+ *
+ * \param anim the animation to save.
+ * \param dst an SDL_IOStream from which data will be written to.
+ * \param closeio true to close/free the SDL_IOStream before returning, false
+ * to leave it open.
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ * information.
+ *
+ * \since This function is available since SDL_image 3.4.0.
+ *
+ * \sa IMG_SaveAnimation
+ * \sa IMG_SaveAnimationTyped_IO
+ * \sa IMG_SaveANIAnimation_IO
+ * \sa IMG_SaveAPNGAnimation_IO
+ * \sa IMG_SaveAVIFAnimation_IO
+ * \sa IMG_SaveWEBPAnimation_IO
+ */
+extern SDL_DECLSPEC bool SDLCALL IMG_SaveGIFAnimation_IO(IMG_Animation *anim, SDL_IOStream *dst, bool closeio);
+
+/**
+ * Save an animation in WEBP format to an SDL_IOStream.
+ *
+ * If `closeio` is true, `dst` will be closed before returning, whether this
+ * function succeeds or not.
+ *
+ * \param anim the animation to save.
+ * \param dst an SDL_IOStream from which data will be written to.
+ * \param closeio true to close/free the SDL_IOStream before returning, false
+ * to leave it open.
+ * \param quality between 0 and 100. For lossy, 0 gives the smallest size and
+ * 100 the largest. For lossless, this parameter is the amount
+ * of effort put into the compression: 0 is the fastest but
+ * gives larger files compared to the slowest, but best, 100.
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ * information.
+ *
+ * \since This function is available since SDL_image 3.4.0.
+ *
+ * \sa IMG_SaveAnimation
+ * \sa IMG_SaveAnimationTyped_IO
+ * \sa IMG_SaveANIAnimation_IO
+ * \sa IMG_SaveAPNGAnimation_IO
+ * \sa IMG_SaveAVIFAnimation_IO
+ * \sa IMG_SaveGIFAnimation_IO
+ */
+extern SDL_DECLSPEC bool SDLCALL IMG_SaveWEBPAnimation_IO(IMG_Animation *anim, SDL_IOStream *dst, bool closeio, int quality);
+
/**
* Create an animated cursor from an animation.
*
@@ -2600,6 +2756,26 @@ extern SDL_DECLSPEC IMG_Animation * SDLCALL IMG_LoadWEBPAnimation_IO(SDL_IOStrea
*/
extern SDL_DECLSPEC SDL_Cursor * SDLCALL IMG_CreateAnimatedCursor(IMG_Animation *anim, int hot_x, int hot_y);
+/**
+ * Dispose of an IMG_Animation and free its resources.
+ *
+ * The provided `anim` pointer is not valid once this call returns.
+ *
+ * \param anim IMG_Animation to dispose of.
+ *
+ * \since This function is available since SDL_image 3.0.0.
+ *
+ * \sa IMG_LoadAnimation
+ * \sa IMG_LoadAnimation_IO
+ * \sa IMG_LoadAnimationTyped_IO
+ * \sa IMG_LoadANIAnimation_IO
+ * \sa IMG_LoadAPNGAnimation_IO
+ * \sa IMG_LoadAVIFAnimation_IO
+ * \sa IMG_LoadGIFAnimation_IO
+ * \sa IMG_LoadWEBPAnimation_IO
+ */
+extern SDL_DECLSPEC void SDLCALL IMG_FreeAnimation(IMG_Animation *anim);
+
/**
* An object representing the encoder context.
*/
diff --git a/src/IMG.c b/src/IMG.c
index c67a69e9..54f158b4 100644
--- a/src/IMG.c
+++ b/src/IMG.c
@@ -401,6 +401,72 @@ bool IMG_SaveTyped_IO(SDL_Surface *surface, SDL_IOStream *dst, bool closeio, con
return result;
}
+bool IMG_SaveAnimation(IMG_Animation *anim, const char *file)
+{
+ if (!anim) {
+ return SDL_InvalidParamError("anim");
+ }
+
+ if (!file || !*file) {
+ return SDL_InvalidParamError("file");
+ }
+
+ const char *type = SDL_strrchr(file, '.');
+ if (type) {
+ // Skip the '.' in the file extension
+ ++type;
+ } else {
+ return SDL_SetError("Couldn't determine file type");
+ }
+
+ SDL_IOStream *dst = SDL_IOFromFile(file, "wb");
+ if (!dst) {
+ return false;
+ }
+
+ return IMG_SaveAnimationTyped_IO(anim, dst, true, type);
+}
+
+bool IMG_SaveAnimationTyped_IO(IMG_Animation *anim, SDL_IOStream *dst, bool closeio, const char *type)
+{
+ bool result = false;
+
+ if (!anim) {
+ SDL_InvalidParamError("anim");
+ goto done;
+ }
+
+ if (!dst) {
+ SDL_InvalidParamError("dst");
+ goto done;
+ }
+
+ if (!type || !*type) {
+ SDL_InvalidParamError("type");
+ goto done;
+ }
+
+ if (SDL_strcasecmp(type, "ani") == 0) {
+ result = IMG_SaveANIAnimation_IO(anim, dst, false);
+ } else if (SDL_strcasecmp(type, "apng") == 0 || SDL_strcasecmp(type, "png") == 0) {
+ result = IMG_SaveAPNGAnimation_IO(anim, dst, false);
+ } else if (SDL_strcasecmp(type, "avif") == 0) {
+ result = IMG_SaveAVIFAnimation_IO(anim, dst, false, 90);
+ } else if (SDL_strcasecmp(type, "gif") == 0) {
+ result = IMG_SaveGIFAnimation_IO(anim, dst, false);
+ } else if (SDL_strcasecmp(type, "webp") == 0) {
+ result = IMG_SaveWEBPAnimation_IO(anim, dst, false, 90);
+ } else {
+ result = SDL_SetError("Unsupported image format");
+ }
+
+done:
+ if (dst && closeio) {
+ result &= SDL_CloseIO(dst);
+ }
+ return result;
+}
+
SDL_Surface *IMG_GetClipboardImage(void)
{
SDL_Surface *surface = NULL;
diff --git a/src/IMG_anim_encoder.c b/src/IMG_anim_encoder.c
index ceec0056..d46bd5f7 100644
--- a/src/IMG_anim_encoder.c
+++ b/src/IMG_anim_encoder.c
@@ -219,3 +219,70 @@ bool IMG_HasMetadata(SDL_PropertiesID props)
}
return has_metadata;
}
+
+static bool IMG_EncodeAnimation(IMG_Animation *anim, SDL_IOStream *dst, bool closeio, const char *type, int quality)
+{
+ IMG_AnimationEncoder *encoder = NULL;
+ bool result = false;
+
+ if (!anim || !anim->count || !anim->frames || !anim->delays) {
+ SDL_InvalidParamError("anim");
+ goto done;
+ }
+
+ SDL_PropertiesID props = SDL_CreateProperties();
+ if (!props) {
+ goto done;
+ }
+
+ SDL_SetPointerProperty(props, IMG_PROP_ANIMATION_ENCODER_CREATE_IOSTREAM_POINTER, dst);
+ SDL_SetStringProperty(props, IMG_PROP_ANIMATION_ENCODER_CREATE_TYPE_STRING, type);
+ SDL_SetNumberProperty(props, IMG_PROP_ANIMATION_ENCODER_CREATE_QUALITY_NUMBER, quality);
+ encoder = IMG_CreateAnimationEncoderWithProperties(props);
+ SDL_DestroyProperties(props);
+ if (!encoder) {
+ goto done;
+ }
+
+ result = true;
+ for (int i = 0; i < anim->count; ++i) {
+ if (!IMG_AddAnimationEncoderFrame(encoder, anim->frames[i], anim->delays[i])) {
+ result = false;
+ break;
+ }
+ }
+
+done:
+ if (encoder) {
+ result &= IMG_CloseAnimationEncoder(encoder);
+ }
+ if (closeio) {
+ result &= SDL_CloseIO(dst);
+ }
+ return result;
+}
+
+bool IMG_SaveANIAnimation_IO(IMG_Animation *anim, SDL_IOStream *dst, bool closeio)
+{
+ return IMG_EncodeAnimation(anim, dst, closeio, "ani", -1);
+}
+
+bool IMG_SaveAPNGAnimation_IO(IMG_Animation *anim, SDL_IOStream *dst, bool closeio)
+{
+ return IMG_EncodeAnimation(anim, dst, closeio, "png", -1);
+}
+
+bool IMG_SaveAVIFAnimation_IO(IMG_Animation *anim, SDL_IOStream *dst, bool closeio, int quality)
+{
+ return IMG_EncodeAnimation(anim, dst, closeio, "avifs", quality);
+}
+
+bool IMG_SaveGIFAnimation_IO(IMG_Animation *anim, SDL_IOStream *dst, bool closeio)
+{
+ return IMG_EncodeAnimation(anim, dst, closeio, "gif", -1);
+}
+
+bool IMG_SaveWEBPAnimation_IO(IMG_Animation *anim, SDL_IOStream *dst, bool closeio, int quality)
+{
+ return IMG_EncodeAnimation(anim, dst, closeio, "webp", quality);
+}
diff --git a/src/SDL_image.sym b/src/SDL_image.sym
index 0f47d4bc..9a7ab628 100644
--- a/src/SDL_image.sym
+++ b/src/SDL_image.sym
@@ -90,5 +90,12 @@ SDL3_image_0.0.0 {
IMG_SaveCUR_IO;
IMG_SaveICO;
IMG_SaveICO_IO;
+ IMG_SaveAnimation;
+ IMG_SaveAnimationTyped_IO;
+ IMG_SaveANIAnimation_IO;
+ IMG_SaveAPNGAnimation_IO;
+ IMG_SaveAVIFAnimation_IO;
+ IMG_SaveGIFAnimation_IO;
+ IMG_SaveWEBPAnimation_IO;
local: *;
};