SDL_image: Added functions to save animations

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: *;
 };