SDL_image: Added support for loading ANI animated cursors

From 0a54c70db319d920544c601c98c1807c7dce0363 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sun, 19 Oct 2025 23:14:00 -0700
Subject: [PATCH] Added support for loading ANI animated cursors

---
 Android.mk                        |   5 +-
 CMakeLists.txt                    |  16 +
 VisualC/SDL_image.vcxproj         |  10 +-
 VisualC/SDL_image.vcxproj.filters |   6 +
 include/SDL3_image/SDL_image.h    | 139 +++++++--
 src/IMG.c                         |   1 +
 src/IMG_ani.c                     | 482 ++++++++++++++++++++++++++++++
 src/IMG_ani.h                     |  23 ++
 src/IMG_anim_decoder.c            |   5 +-
 9 files changed, 661 insertions(+), 26 deletions(-)
 create mode 100644 src/IMG_ani.c
 create mode 100644 src/IMG_ani.h

diff --git a/Android.mk b/Android.mk
index 35164da8..9b9f1ada 100644
--- a/Android.mk
+++ b/Android.mk
@@ -73,6 +73,7 @@ LOCAL_MODULE := SDL3_image
 
 LOCAL_SRC_FILES :=  		\
     src/IMG.c           	\
+    src/IMG_ani.c               \
     src/IMG_anim_encoder.c      \
     src/IMG_anim_decoder.c      \
     src/IMG_avif.c      	\
@@ -99,8 +100,8 @@ LOCAL_SRC_FILES :=  		\
 
 LOCAL_C_INCLUDES += $(LOCAL_PATH)/include
 
-LOCAL_CFLAGS := -DLOAD_BMP -DSAVE_BMP -DLOAD_GIF -DSAVE_GIF -DLOAD_LBM \
-                -DLOAD_PCX -DLOAD_PNM -DLOAD_SVG -DLOAD_TGA -DSAVE_TGA \
+LOCAL_CFLAGS := -DLOAD_ANI -DLOAD_BMP -DLOAD_GIF -DLOAD_LBM \
+                -DLOAD_PCX -DLOAD_PNM -DLOAD_SVG -DLOAD_TGA \
                 -DLOAD_XCF -DLOAD_XPM -DLOAD_XV -DLOAD_QOI
 LOCAL_LDLIBS :=
 LOCAL_LDFLAGS := -Wl,--no-undefined -Wl,--version-script=$(LOCAL_PATH)/src/SDL_image.sym
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c888178f..ccf05a97 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -110,6 +110,7 @@ option(SDLIMAGE_BACKEND_STB "Use stb_image for loading JPEG and PNG files" ON)
 cmake_dependent_option(SDLIMAGE_BACKEND_WIC "Add WIC backend (Windows Imaging Component)" OFF WIN32 OFF)
 cmake_dependent_option(SDLIMAGE_BACKEND_IMAGEIO "Use native Mac OS X frameworks for loading images" ON APPLE OFF)
 
+option(SDLIMAGE_ANI "Support loading ANI animations" ON)
 option(SDLIMAGE_AVIF "Support loading AVIF images" ON)
 option(SDLIMAGE_BMP "Support loading BMP images" ON)
 option(SDLIMAGE_GIF "Support loading GIF images" ON)
@@ -260,6 +261,7 @@ set(BUILD_SHARED_LIBS ${SDLIMAGE_BUILD_SHARED_LIBS})
 add_library(${sdl3_image_target_name}
     src/IMG.c
     src/IMG_WIC.c
+    src/IMG_ani.c
     src/IMG_anim_encoder.c
     src/IMG_anim_decoder.c
     src/IMG_avif.c
@@ -594,6 +596,20 @@ if(SDLIMAGE_AVIF)
     endif()
 endif()
 
+list(APPEND SDLIMAGE_BACKENDS ANI)
+set(SDLIMAGE_ANI_ENABLED FALSE)
+if(SDLIMAGE_ANI)
+    set(SDLIMAGE_ANI_ENABLED TRUE)
+    if(SDLIMAGE_ANI_ENABLED)
+        target_compile_definitions(${sdl3_image_target_name} PRIVATE
+            LOAD_ANI
+        )
+    else()
+        # Variable is used by test suite
+        set(SDLIMAGE_ANI_SAVE OFF)
+    endif()
+endif()
+
 list(APPEND SDLIMAGE_BACKENDS BMP)
 set(SDLIMAGE_BMP_ENABLED FALSE)
 if(SDLIMAGE_BMP)
diff --git a/VisualC/SDL_image.vcxproj b/VisualC/SDL_image.vcxproj
index e0cc754c..629db25b 100644
--- a/VisualC/SDL_image.vcxproj
+++ b/VisualC/SDL_image.vcxproj
@@ -100,7 +100,7 @@
     <ClCompile>
       <Optimization>Disabled</Optimization>
       <AdditionalIncludeDirectories>external\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
-      <PreprocessorDefinitions>DLL_EXPORT;_DEBUG;WIN32;_WINDOWS;USE_STBIMAGE;LOAD_AVIF;LOAD_AVIF_DYNAMIC="libavif-16.dll";LOAD_BMP;LOAD_GIF;LOAD_JPG;LOAD_LBM;LOAD_PCX;LOAD_PNG;LOAD_PNM;LOAD_QOI;LOAD_SVG;LOAD_TGA;LOAD_TIF;LOAD_TIF_DYNAMIC="libtiff-6.dll";LOAD_WEBP;LOAD_WEBP_DYNAMIC="libwebp-7.dll";LOAD_WEBPDEMUX_DYNAMIC="libwebpdemux-2.dll";LOAD_WEBPMUX_DYNAMIC="libwebpmux-3.dll";LOAD_XCF;LOAD_XPM;LOAD_XV;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <PreprocessorDefinitions>DLL_EXPORT;_DEBUG;WIN32;_WINDOWS;USE_STBIMAGE;LOAD_ANI;LOAD_AVIF;LOAD_AVIF_DYNAMIC="libavif-16.dll";LOAD_BMP;LOAD_GIF;LOAD_JPG;LOAD_LBM;LOAD_PCX;LOAD_PNG;LOAD_PNM;LOAD_QOI;LOAD_SVG;LOAD_TGA;LOAD_TIF;LOAD_TIF_DYNAMIC="libtiff-6.dll";LOAD_WEBP;LOAD_WEBP_DYNAMIC="libwebp-7.dll";LOAD_WEBPDEMUX_DYNAMIC="libwebpdemux-2.dll";LOAD_WEBPMUX_DYNAMIC="libwebpmux-3.dll";LOAD_XCF;LOAD_XPM;LOAD_XV;%(PreprocessorDefinitions)</PreprocessorDefinitions>
       <RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
       <WarningLevel>Level3</WarningLevel>
       <DebugInformationFormat>OldStyle</DebugInformationFormat>
@@ -127,7 +127,7 @@
     </Midl>
     <ClCompile>
       <AdditionalIncludeDirectories>external\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
-      <PreprocessorDefinitions>DLL_EXPORT;_DEBUG;WIN32;_WINDOWS;USE_STBIMAGE;LOAD_AVIF;LOAD_AVIF_DYNAMIC="libavif-16.dll";LOAD_BMP;LOAD_GIF;LOAD_JPG;LOAD_LBM;LOAD_PCX;LOAD_PNG;LOAD_PNM;LOAD_QOI;LOAD_SVG;LOAD_TGA;LOAD_TIF;LOAD_TIF_DYNAMIC="libtiff-6.dll";LOAD_WEBP;LOAD_WEBP_DYNAMIC="libwebp-7.dll";LOAD_WEBPDEMUX_DYNAMIC="libwebpdemux-2.dll";LOAD_WEBPMUX_DYNAMIC="libwebpmux-3.dll";LOAD_LIBPNG_DYNAMIC="libpng16-16.dll";SDL_IMAGE_LIBPNG;LOAD_XCF;LOAD_XPM;LOAD_XV;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <PreprocessorDefinitions>DLL_EXPORT;_DEBUG;WIN32;_WINDOWS;USE_STBIMAGE;LOAD_ANI;LOAD_AVIF;LOAD_AVIF_DYNAMIC="libavif-16.dll";LOAD_BMP;LOAD_GIF;LOAD_JPG;LOAD_LBM;LOAD_PCX;LOAD_PNG;LOAD_PNM;LOAD_QOI;LOAD_SVG;LOAD_TGA;LOAD_TIF;LOAD_TIF_DYNAMIC="libtiff-6.dll";LOAD_WEBP;LOAD_WEBP_DYNAMIC="libwebp-7.dll";LOAD_WEBPDEMUX_DYNAMIC="libwebpdemux-2.dll";LOAD_WEBPMUX_DYNAMIC="libwebpmux-3.dll";LOAD_LIBPNG_DYNAMIC="libpng16-16.dll";SDL_IMAGE_LIBPNG;LOAD_XCF;LOAD_XPM;LOAD_XV;%(PreprocessorDefinitions)</PreprocessorDefinitions>
       <RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
       <WarningLevel>Level3</WarningLevel>
       <Optimization>Disabled</Optimization>
@@ -154,7 +154,7 @@
     </Midl>
     <ClCompile>
       <AdditionalIncludeDirectories>external\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
-      <PreprocessorDefinitions>DLL_EXPORT;NDEBUG;WIN32;_WINDOWS;USE_STBIMAGE;LOAD_AVIF;LOAD_AVIF_DYNAMIC="libavif-16.dll";LOAD_BMP;LOAD_GIF;LOAD_JPG;LOAD_LBM;LOAD_PCX;LOAD_PNG;LOAD_PNM;LOAD_QOI;LOAD_SVG;LOAD_TGA;LOAD_TIF;LOAD_TIF_DYNAMIC="libtiff-6.dll";LOAD_WEBP;LOAD_WEBP_DYNAMIC="libwebp-7.dll";LOAD_WEBPDEMUX_DYNAMIC="libwebpdemux-2.dll";LOAD_XCF;LOAD_XPM;LOAD_XV;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <PreprocessorDefinitions>DLL_EXPORT;NDEBUG;WIN32;_WINDOWS;USE_STBIMAGE;LOAD_ANI;LOAD_AVIF;LOAD_AVIF_DYNAMIC="libavif-16.dll";LOAD_BMP;LOAD_GIF;LOAD_JPG;LOAD_LBM;LOAD_PCX;LOAD_PNG;LOAD_PNM;LOAD_QOI;LOAD_SVG;LOAD_TGA;LOAD_TIF;LOAD_TIF_DYNAMIC="libtiff-6.dll";LOAD_WEBP;LOAD_WEBP_DYNAMIC="libwebp-7.dll";LOAD_WEBPDEMUX_DYNAMIC="libwebpdemux-2.dll";LOAD_XCF;LOAD_XPM;LOAD_XV;%(PreprocessorDefinitions)</PreprocessorDefinitions>
       <RuntimeLibrary>MultiThreaded</RuntimeLibrary>
       <WarningLevel>Level3</WarningLevel>
       <EnableEnhancedInstructionSet>StreamingSIMDExtensions</EnableEnhancedInstructionSet>
@@ -181,7 +181,7 @@
     </Midl>
     <ClCompile>
       <AdditionalIncludeDirectories>external\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
-      <PreprocessorDefinitions>DLL_EXPORT;NDEBUG;WIN32;_WINDOWS;USE_STBIMAGE;LOAD_AVIF;LOAD_AVIF_DYNAMIC="libavif-16.dll";LOAD_BMP;LOAD_GIF;LOAD_JPG;LOAD_LBM;LOAD_PCX;LOAD_PNG;LOAD_PNM;LOAD_QOI;LOAD_SVG;LOAD_TGA;LOAD_TIF;LOAD_TIF_DYNAMIC="libtiff-6.dll";LOAD_WEBP;LOAD_WEBP_DYNAMIC="libwebp-7.dll";LOAD_WEBPDEMUX_DYNAMIC="libwebpdemux-2.dll";LOAD_WEBPMUX_DYNAMIC="libwebpmux-3.dll";LOAD_LIBPNG_DYNAMIC="libpng16-16.dll";SDL_IMAGE_LIBPNG;LOAD_XCF;LOAD_XPM;LOAD_XV;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <PreprocessorDefinitions>DLL_EXPORT;NDEBUG;WIN32;_WINDOWS;USE_STBIMAGE;LOAD_ANI;LOAD_AVIF;LOAD_AVIF_DYNAMIC="libavif-16.dll";LOAD_BMP;LOAD_GIF;LOAD_JPG;LOAD_LBM;LOAD_PCX;LOAD_PNG;LOAD_PNM;LOAD_QOI;LOAD_SVG;LOAD_TGA;LOAD_TIF;LOAD_TIF_DYNAMIC="libtiff-6.dll";LOAD_WEBP;LOAD_WEBP_DYNAMIC="libwebp-7.dll";LOAD_WEBPDEMUX_DYNAMIC="libwebpdemux-2.dll";LOAD_WEBPMUX_DYNAMIC="libwebpmux-3.dll";LOAD_LIBPNG_DYNAMIC="libpng16-16.dll";SDL_IMAGE_LIBPNG;LOAD_XCF;LOAD_XPM;LOAD_XV;%(PreprocessorDefinitions)</PreprocessorDefinitions>
       <RuntimeLibrary>MultiThreaded</RuntimeLibrary>
       <WarningLevel>Level3</WarningLevel>
     </ClCompile>
@@ -197,6 +197,7 @@
   </ItemDefinitionGroup>
   <ItemGroup>
     <ClCompile Include="..\src\IMG.c" />
+    <ClCompile Include="..\src\IMG_ani.c" />
     <ClCompile Include="..\src\IMG_anim_decoder.c" />
     <ClCompile Include="..\src\IMG_anim_encoder.c" />
     <ClCompile Include="..\src\IMG_avif.c" />
@@ -226,6 +227,7 @@
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="..\include\SDL3_image\SDL_image.h" />
+    <ClInclude Include="..\src\IMG_ani.h" />
     <ClInclude Include="..\src\IMG_anim_decoder.h" />
     <ClInclude Include="..\src\IMG_anim_encoder.h" />
     <ClInclude Include="..\src\IMG_libpng.h" />
diff --git a/VisualC/SDL_image.vcxproj.filters b/VisualC/SDL_image.vcxproj.filters
index 5ac3ea57..0fdfe327 100644
--- a/VisualC/SDL_image.vcxproj.filters
+++ b/VisualC/SDL_image.vcxproj.filters
@@ -73,6 +73,9 @@
     <ClCompile Include="..\src\xmlman.c">
       <Filter>Sources</Filter>
     </ClCompile>
+    <ClCompile Include="..\src\IMG_ani.c">
+      <Filter>Sources</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <Filter Include="Sources">
@@ -110,6 +113,9 @@
     <ClInclude Include="..\src\xmlman.h">
       <Filter>Sources</Filter>
     </ClInclude>
+    <ClInclude Include="..\src\IMG_ani.h">
+      <Filter>Sources</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="..\src\version.rc">
diff --git a/include/SDL3_image/SDL_image.h b/include/SDL3_image/SDL_image.h
index f3a8d1b2..4f95311a 100644
--- a/include/SDL3_image/SDL_image.h
+++ b/include/SDL3_image/SDL_image.h
@@ -376,7 +376,7 @@ extern SDL_DECLSPEC SDL_Texture * SDLCALL IMG_LoadTextureTyped_IO(SDL_Renderer *
 extern SDL_DECLSPEC SDL_Surface * SDLCALL IMG_GetClipboardImage(void);
 
 /**
- * Detect AVIF image data on a readable/seekable SDL_IOStream.
+ * Detect ANI animated cursor data on a readable/seekable SDL_IOStream.
  *
  * This function attempts to determine if a file is a given filetype, reading
  * the least amount possible from the SDL_IOStream (usually a few bytes).
@@ -394,7 +394,7 @@ extern SDL_DECLSPEC SDL_Surface * SDLCALL IMG_GetClipboardImage(void);
  * determine file type in many cases in its standard load functions.
  *
  * \param src a seekable/readable SDL_IOStream to provide image data.
- * \returns non-zero if this is AVIF data, zero otherwise.
+ * \returns true if this is ANI animated cursor data, false otherwise.
  *
  * \since This function is available since SDL_image 3.0.0.
  *
@@ -417,6 +417,50 @@ extern SDL_DECLSPEC SDL_Surface * SDLCALL IMG_GetClipboardImage(void);
  * \sa IMG_isXV
  * \sa IMG_isWEBP
  */
+extern SDL_DECLSPEC bool SDLCALL IMG_isANI(SDL_IOStream *src);
+
+/**
+ * Detect AVIF image data on a readable/seekable SDL_IOStream.
+ *
+ * This function attempts to determine if a file is a given filetype, reading
+ * the least amount possible from the SDL_IOStream (usually a few bytes).
+ *
+ * There is no distinction made between "not the filetype in question" and
+ * basic i/o errors.
+ *
+ * This function will always attempt to seek `src` back to where it started
+ * when this function was called, but it will not report any errors in doing
+ * so, but assuming seeking works, this means you can immediately use this
+ * with a different IMG_isTYPE function, or load the image without further
+ * seeking.
+ *
+ * You do not need to call this function to load data; SDL_image can work to
+ * determine file type in many cases in its standard load functions.
+ *
+ * \param src a seekable/readable SDL_IOStream to provide image data.
+ * \returns true if this is AVIF data, false otherwise.
+ *
+ * \since This function is available since SDL_image 3.0.0.
+ *
+ * \sa IMG_isANI
+ * \sa IMG_isICO
+ * \sa IMG_isCUR
+ * \sa IMG_isBMP
+ * \sa IMG_isGIF
+ * \sa IMG_isJPG
+ * \sa IMG_isJXL
+ * \sa IMG_isLBM
+ * \sa IMG_isPCX
+ * \sa IMG_isPNG
+ * \sa IMG_isPNM
+ * \sa IMG_isSVG
+ * \sa IMG_isQOI
+ * \sa IMG_isTIF
+ * \sa IMG_isXCF
+ * \sa IMG_isXPM
+ * \sa IMG_isXV
+ * \sa IMG_isWEBP
+ */
 extern SDL_DECLSPEC bool SDLCALL IMG_isAVIF(SDL_IOStream *src);
 
 /**
@@ -438,10 +482,11 @@ extern SDL_DECLSPEC bool SDLCALL IMG_isAVIF(SDL_IOStream *src);
  * determine file type in many cases in its standard load functions.
  *
  * \param src a seekable/readable SDL_IOStream to provide image data.
- * \returns non-zero if this is ICO data, zero otherwise.
+ * \returns true if this is ICO data, false otherwise.
  *
  * \since This function is available since SDL_image 3.0.0.
  *
+ * \sa IMG_isANI
  * \sa IMG_isAVIF
  * \sa IMG_isCUR
  * \sa IMG_isBMP
@@ -481,10 +526,11 @@ extern SDL_DECLSPEC bool SDLCALL IMG_isICO(SDL_IOStream *src);
  * determine file type in many cases in its standard load functions.
  *
  * \param src a seekable/readable SDL_IOStream to provide image data.
- * \returns non-zero if this is CUR data, zero otherwise.
+ * \returns true if this is CUR data, false otherwise.
  *
  * \since This function is available since SDL_image 3.0.0.
  *
+ * \sa IMG_isANI
  * \sa IMG_isAVIF
  * \sa IMG_isICO
  * \sa IMG_isBMP
@@ -524,10 +570,11 @@ extern SDL_DECLSPEC bool SDLCALL IMG_isCUR(SDL_IOStream *src);
  * determine file type in many cases in its standard load functions.
  *
  * \param src a seekable/readable SDL_IOStream to provide image data.
- * \returns non-zero if this is BMP data, zero otherwise.
+ * \returns true if this is BMP data, false otherwise.
  *
  * \since This function is available since SDL_image 3.0.0.
  *
+ * \sa IMG_isANI
  * \sa IMG_isAVIF
  * \sa IMG_isICO
  * \sa IMG_isCUR
@@ -567,10 +614,11 @@ extern SDL_DECLSPEC bool SDLCALL IMG_isBMP(SDL_IOStream *src);
  * determine file type in many cases in its standard load functions.
  *
  * \param src a seekable/readable SDL_IOStream to provide image data.
- * \returns non-zero if this is GIF data, zero otherwise.
+ * \returns true if this is GIF data, false otherwise.
  *
  * \since This function is available since SDL_image 3.0.0.
  *
+ * \sa IMG_isANI
  * \sa IMG_isAVIF
  * \sa IMG_isICO
  * \sa IMG_isCUR
@@ -610,10 +658,11 @@ extern SDL_DECLSPEC bool SDLCALL IMG_isGIF(SDL_IOStream *src);
  * determine file type in many cases in its standard load functions.
  *
  * \param src a seekable/readable SDL_IOStream to provide image data.
- * \returns non-zero if this is JPG data, zero otherwise.
+ * \returns true if this is JPG data, false otherwise.
  *
  * \since This function is available since SDL_image 3.0.0.
  *
+ * \sa IMG_isANI
  * \sa IMG_isAVIF
  * \sa IMG_isICO
  * \sa IMG_isCUR
@@ -653,10 +702,11 @@ extern SDL_DECLSPEC bool SDLCALL IMG_isJPG(SDL_IOStream *src);
  * determine file type in many cases in its standard load functions.
  *
  * \param src a seekable/readable SDL_IOStream to provide image data.
- * \returns non-zero if this is JXL data, zero otherwise.
+ * \returns true if this is JXL data, false otherwise.
  *
  * \since This function is available since SDL_image 3.0.0.
  *
+ * \sa IMG_isANI
  * \sa IMG_isAVIF
  * \sa IMG_isICO
  * \sa IMG_isCUR
@@ -696,10 +746,11 @@ extern SDL_DECLSPEC bool SDLCALL IMG_isJXL(SDL_IOStream *src);
  * determine file type in many cases in its standard load functions.
  *
  * \param src a seekable/readable SDL_IOStream to provide image data.
- * \returns non-zero if this is LBM data, zero otherwise.
+ * \returns true if this is LBM data, false otherwise.
  *
  * \since This function is available since SDL_image 3.0.0.
  *
+ * \sa IMG_isANI
  * \sa IMG_isAVIF
  * \sa IMG_isICO
  * \sa IMG_isCUR
@@ -739,10 +790,11 @@ extern SDL_DECLSPEC bool SDLCALL IMG_isLBM(SDL_IOStream *src);
  * determine file type in many cases in its standard load functions.
  *
  * \param src a seekable/readable SDL_IOStream to provide image data.
- * \returns non-zero if this is PCX data, zero otherwise.
+ * \returns true if this is PCX data, false otherwise.
  *
  * \since This function is available since SDL_image 3.0.0.
  *
+ * \sa IMG_isANI
  * \sa IMG_isAVIF
  * \sa IMG_isICO
  * \sa IMG_isCUR
@@ -782,10 +834,11 @@ extern SDL_DECLSPEC bool SDLCALL IMG_isPCX(SDL_IOStream *src);
  * determine file type in many cases in its standard load functions.
  *
  * \param src a seekable/readable SDL_IOStream to provide image data.
- * \returns non-zero if this is PNG data, zero otherwise.
+ * \returns true if this is PNG data, false otherwise.
  *
  * \since This function is available since SDL_image 3.0.0.
  *
+ * \sa IMG_isANI
  * \sa IMG_isAVIF
  * \sa IMG_isICO
  * \sa IMG_isCUR
@@ -825,10 +878,11 @@ extern SDL_DECLSPEC bool SDLCALL IMG_isPNG(SDL_IOStream *src);
  * determine file type in many cases in its standard load functions.
  *
  * \param src a seekable/readable SDL_IOStream to provide image data.
- * \returns non-zero if this is PNM data, zero otherwise.
+ * \returns true if this is PNM data, false otherwise.
  *
  * \since This function is available since SDL_image 3.0.0.
  *
+ * \sa IMG_isANI
  * \sa IMG_isAVIF
  * \sa IMG_isICO
  * \sa IMG_isCUR
@@ -868,10 +922,11 @@ extern SDL_DECLSPEC bool SDLCALL IMG_isPNM(SDL_IOStream *src);
  * determine file type in many cases in its standard load functions.
  *
  * \param src a seekable/readable SDL_IOStream to provide image data.
- * \returns non-zero if this is SVG data, zero otherwise.
+ * \returns true if this is SVG data, false otherwise.
  *
  * \since This function is available since SDL_image 3.0.0.
  *
+ * \sa IMG_isANI
  * \sa IMG_isAVIF
  * \sa IMG_isICO
  * \sa IMG_isCUR
@@ -911,10 +966,11 @@ extern SDL_DECLSPEC bool SDLCALL IMG_isSVG(SDL_IOStream *src);
  * determine file type in many cases in its standard load functions.
  *
  * \param src a seekable/readable SDL_IOStream to provide image data.
- * \returns non-zero if this is QOI data, zero otherwise.
+ * \returns true if this is QOI data, false otherwise.
  *
  * \since This function is available since SDL_image 3.0.0.
  *
+ * \sa IMG_isANI
  * \sa IMG_isAVIF
  * \sa IMG_isICO
  * \sa IMG_isCUR
@@ -954,10 +1010,11 @@ extern SDL_DECLSPEC bool SDLCALL IMG_isQOI(SDL_IOStream *src);
  * determine file type in many cases in its standard load functions.
  *
  * \param src a seekable/readable SDL_IOStream to provide image data.
- * \returns non-zero if this is TIFF data, zero otherwise.
+ * \returns true if this is TIFF data, false otherwise.
  *
  * \since This function is available since SDL_image 3.0.0.
  *
+ * \sa IMG_isANI
  * \sa IMG_isAVIF
  * \sa IMG_isICO
  * \sa IMG_isCUR
@@ -997,10 +1054,11 @@ extern SDL_DECLSPEC bool SDLCALL IMG_isTIF(SDL_IOStream *src);
  * determine file type in many cases in its standard load functions.
  *
  * \param src a seekable/readable SDL_IOStream to provide image data.
- * \returns non-zero if this is XCF data, zero otherwise.
+ * \returns true if this is XCF data, false otherwise.
  *
  * \since This function is available since SDL_image 3.0.0.
  *
+ * \sa IMG_isANI
  * \sa IMG_isAVIF
  * \sa IMG_isICO
  * \sa IMG_isCUR
@@ -1040,10 +1098,11 @@ extern SDL_DECLSPEC bool SDLCALL IMG_isXCF(SDL_IOStream *src);
  * determine file type in many cases in its standard load functions.
  *
  * \param src a seekable/readable SDL_IOStream to provide image data.
- * \returns non-zero if this is XPM data, zero otherwise.
+ * \returns true if this is XPM data, false otherwise.
  *
  * \since This function is available since SDL_image 3.0.0.
  *
+ * \sa IMG_isANI
  * \sa IMG_isAVIF
  * \sa IMG_isICO
  * \sa IMG_isCUR
@@ -1083,10 +1142,11 @@ extern SDL_DECLSPEC bool SDLCALL IMG_isXPM(SDL_IOStream *src);
  * determine file type in many cases in its standard load functions.
  *
  * \param src a seekable/readable SDL_IOStream to provide image data.
- * \returns non-zero if this is XV data, zero otherwise.
+ * \returns true if this is XV data, false otherwise.
  *
  * \since This function is available since SDL_image 3.0.0.
  *
+ * \sa IMG_isANI
  * \sa IMG_isAVIF
  * \sa IMG_isICO
  * \sa IMG_isCUR
@@ -1126,10 +1186,11 @@ extern SDL_DECLSPEC bool SDLCALL IMG_isXV(SDL_IOStream *src);
  * determine file type in many cases in its standard load functions.
  *
  * \param src a seekable/readable SDL_IOStream to provide image data.
- * \returns non-zero if this is WEBP data, zero otherwise.
+ * \returns true if this is WEBP data, false otherwise.
  *
  * \since This function is available since SDL_image 3.0.0.
  *
+ * \sa IMG_isANI
  * \sa IMG_isAVIF
  * \sa IMG_isICO
  * \sa IMG_isCUR
@@ -2225,6 +2286,7 @@ typedef struct IMG_Animation
  * \sa IMG_CreateAnimatedCursor
  * \sa IMG_LoadAnimation_IO
  * \sa IMG_LoadAnimationTyped_IO
+ * \sa IMG_LoadANIAnimation_IO
  * \sa IMG_LoadAPNGAnimation_IO
  * \sa IMG_LoadAVIFAnimation_IO
  * \sa IMG_LoadGIFAnimation_IO
@@ -2253,6 +2315,7 @@ extern SDL_DECLSPEC IMG_Animation * SDLCALL IMG_LoadAnimation(const char *file);
  * \sa IMG_CreateAnimatedCursor
  * \sa IMG_LoadAnimation
  * \sa IMG_LoadAnimationTyped_IO
+ * \sa IMG_LoadANIAnimation_IO
  * \sa IMG_LoadAPNGAnimation_IO
  * \sa IMG_LoadAVIFAnimation_IO
  * \sa IMG_LoadGIFAnimation_IO
@@ -2288,6 +2351,7 @@ extern SDL_DECLSPEC IMG_Animation * SDLCALL IMG_LoadAnimation_IO(SDL_IOStream *s
  * \sa IMG_CreateAnimatedCursor
  * \sa IMG_LoadAnimation
  * \sa IMG_LoadAnimation_IO
+ * \sa IMG_LoadANIAnimation_IO
  * \sa IMG_LoadAPNGAnimation_IO
  * \sa IMG_LoadAVIFAnimation_IO
  * \sa IMG_LoadGIFAnimation_IO
@@ -2308,6 +2372,7 @@ extern SDL_DECLSPEC IMG_Animation * SDLCALL IMG_LoadAnimationTyped_IO(SDL_IOStre
  * \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
@@ -2315,6 +2380,34 @@ extern SDL_DECLSPEC IMG_Animation * SDLCALL IMG_LoadAnimationTyped_IO(SDL_IOStre
  */
 extern SDL_DECLSPEC void SDLCALL IMG_FreeAnimation(IMG_Animation *anim);
 
+/**
+ * Load an ANI animation directly from an SDL_IOStream.
+ *
+ * If you know you definitely have an ANI image, you can call this function,
+ * which will skip SDL_image's file format detection routines. Generally, it's
+ * better to use the abstract interfaces; also, there is only an SDL_IOStream
+ * interface available here.
+ *
+ * When done with the returned animation, the app should dispose of it with a
+ * call to IMG_FreeAnimation().
+ *
+ * \param src an SDL_IOStream from which data will be read.
+ * \returns a new IMG_Animation, or NULL on error.
+ *
+ * \since This function is available since SDL_image 3.4.0.
+ *
+ * \sa IMG_isANI
+ * \sa IMG_LoadAnimation
+ * \sa IMG_LoadAnimation_IO
+ * \sa IMG_LoadAnimationTyped_IO
+ * \sa IMG_LoadAPNGAnimation_IO
+ * \sa IMG_LoadAVIFAnimation_IO
+ * \sa IMG_LoadGIFAnimation_IO
+ * \sa IMG_LoadWEBPAnimation_IO
+ * \sa IMG_FreeAnimation
+ */
+extern SDL_DECLSPEC IMG_Animation *SDLCALL IMG_LoadANIAnimation_IO(SDL_IOStream *src);
+
 /**
  * Load an APNG animation directly from an SDL_IOStream.
  *
@@ -2331,9 +2424,11 @@ extern SDL_DECLSPEC void SDLCALL IMG_FreeAnimation(IMG_Animation *anim);
  *
  * \since This function is available since SDL_image 3.4.0.
  *
+ * \sa IMG_isPNG
  * \sa IMG_LoadAnimation
  * \sa IMG_LoadAnimation_IO
  * \sa IMG_LoadAnimationTyped_IO
+ * \sa IMG_LoadANIAnimation_IO
  * \sa IMG_LoadAVIFAnimation_IO
  * \sa IMG_LoadGIFAnimation_IO
  * \sa IMG_LoadWEBPAnimation_IO
@@ -2357,9 +2452,11 @@ extern SDL_DECLSPEC IMG_Animation *SDLCALL IMG_LoadAPNGAnimation_IO(SDL_IOStream
  *
  * \since This function is available since SDL_image 3.4.0.
  *
+ * \sa IMG_isAVIF
  * \sa IMG_LoadAnimation
  * \sa IMG_LoadAnimation_IO
  * \sa IMG_LoadAnimationTyped_IO
+ * \sa IMG_LoadANIAnimation_IO
  * \sa IMG_LoadAPNGAnimation_IO
  * \sa IMG_LoadGIFAnimation_IO
  * \sa IMG_LoadWEBPAnimation_IO
@@ -2380,9 +2477,11 @@ extern SDL_DECLSPEC IMG_Animation *SDLCALL IMG_LoadAVIFAnimation_IO(SDL_IOStream
  *
  * \since This function is available since SDL_image 3.0.0.
  *
+ * \sa IMG_isGIF
  * \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_LoadWEBPAnimation_IO
@@ -2403,9 +2502,11 @@ extern SDL_DECLSPEC IMG_Animation * SDLCALL IMG_LoadGIFAnimation_IO(SDL_IOStream
  *
  * \since This function is available since SDL_image 3.0.0.
  *
+ * \sa IMG_isWEBP
  * \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
diff --git a/src/IMG.c b/src/IMG.c
index 8809de7b..95117162 100644
--- a/src/IMG.c
+++ b/src/IMG.c
@@ -83,6 +83,7 @@ static struct {
     { "WEBP", IMG_isWEBP, IMG_LoadWEBPAnimation_IO  },
     { "APNG", IMG_isPNG, IMG_LoadAPNGAnimation_IO   },
     { "AVIFS", IMG_isAVIF, IMG_LoadAVIFAnimation_IO },
+    { "ANI", IMG_isANI, IMG_LoadANIAnimation_IO },
 };
 
 int IMG_Version(void)
diff --git a/src/IMG_ani.c b/src/IMG_ani.c
new file mode 100644
index 00000000..476b36ab
--- /dev/null
+++ b/src/IMG_ani.c
@@ -0,0 +1,482 @@
+/*
+  SDL_image:  An example image loading library for use with SDL
+  Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#include <SDL3_image/SDL_image.h>
+
+#include "IMG_ani.h"
+#include "IMG_anim_decoder.h"
+
+#ifdef LOAD_ANI
+
+#define RIFF_FOURCC(c0, c1, c2, c3)                 \
+    ((Uint32)(Uint8)(c0) | ((Uint32)(Uint8)(c1) << 8) | \
+     ((Uint32)(Uint8)(c2) << 16) | ((Uint32)(Uint8)(c3) << 24))
+
+#define ANI_FLAG_ICON       0x1
+#define ANI_FLAG_SEQUENCE   0x2
+
+typedef struct
+{
+    Uint32 riffID;
+    Uint32 cbSize;
+    Uint32 chunkID;
+} RIFFHEADER;
+
+typedef struct {
+    Uint32 cbSizeof; // sizeof(ANIHEADER) = 36 bytes.
+    Uint32 frames;   // Number of frames in the frame list.
+    Uint32 steps;    // Number of steps in the animation loop.
+    Uint32 width;    // Width
+    Uint32 height;   // Height
+    Uint32 bpp;      // bpp
+    Uint32 planes;   // Not used
+    Uint32 jifRate;  // Default display rate, in jiffies (1/60s)
+    Uint32 fl;       // AF_ICON should be set. AF_SEQUENCE is optional
+} ANIHEADER;
+
+
+bool IMG_isANI(SDL_IOStream* src)
+{
+    Sint64 start;
+    bool is_ANI;
+    RIFFHEADER header;
+
+    if (!src) {
+        return false;
+    }
+
+    start = SDL_TellIO(src);
+    is_ANI = false;
+    if (SDL_ReadU32LE(src, &header.riffID) &&
+        SDL_ReadU32LE(src, &header.cbSize) &&
+        SDL_ReadU32LE(src, &header.chunkID) &&
+        header.riffID == RIFF_FOURCC('R', 'I', 'F', 'F') &&
+        header.chunkID == RIFF_FOURCC('A', 'C', 'O', 'N')) {
+        is_ANI = true;
+    }
+    SDL_SeekIO(src, start, SDL_IO_SEEK_SET);
+    return is_ANI;
+}
+
+IMG_Animation *IMG_LoadANIAnimation_IO(SDL_IOStream *src)
+{
+    return IMG_DecodeAsAnimation(src, "ani", 0);
+}
+
+struct IMG_AnimationDecoderContext
+{
+    Uint32 frame_index;
+    Uint32 frame_count;
+    Sint64 *frame_offsets;
+    Uint32 *frame_durations;
+    Uint32 *frame_sequence;
+};
+
+typedef struct
+{
+    IMG_AnimationDecoderContext *ctx;
+    SDL_IOStream *src;
+    bool has_anih;
+    ANIHEADER anih;
+    char *author;
+    char *title;
+} IMG_AnimationParseContext;
+
+static bool IMG_AnimationDecoderReset_Internal(IMG_AnimationDecoder* decoder)
+{
+    IMG_AnimationDecoderContext* ctx = decoder->ctx;
+
+    ctx->frame_index = 0;
+
+    return true;
+}
+
+static bool IMG_AnimationDecoderGetNextFrame_Internal(IMG_AnimationDecoder *decoder, SDL_Surface **frame, Uint64 *duration)
+{
+    IMG_AnimationDecoderContext *ctx = decoder->ctx;
+
+    if (ctx->frame_index == ctx->frame_count) {
+        decoder->status = IMG_DECODER_STATUS_COMPLETE;
+        return false;
+    }
+
+    *duration = IMG_GetDecoderDuration(decoder, ctx->frame_durations[ctx->frame_index], 60);
+
+    Sint64 offset = ctx->frame_offsets[ctx->frame_sequence[ctx->frame_index]];
+    if (SDL_SeekIO(decoder->src, offset, SDL_IO_SEEK_SET) < 0) {
+        return SDL_SetError("Failed to seek to frame offset");
+    }
+    if (IMG_isCUR(decoder->src)) {
+        *frame = IMG_LoadCUR_IO(decoder->src);
+    } else if (IMG_isICO(decoder->src)) {
+        *frame = IMG_LoadICO_IO(decoder->src);
+    } else {
+        SDL_SetError("Unrecognized frame type");
+        *frame = NULL;
+    }
+    ++ctx->frame_index;
+
+    return (*frame != NULL);
+}
+
+static bool IMG_AnimationDecoderClose_Internal(IMG_AnimationDecoder *decoder)
+{
+    IMG_AnimationDecoderContext *ctx = decoder->ctx;
+
+    SDL_free(ctx->frame_offsets);
+    SDL_free(ctx->frame_durations);
+    SDL_free(ctx->frame_sequence);
+    SDL_free(ctx);
+    decoder->ctx = NULL;
+    return true;
+}
+
+static bool ParseANIHeader(IMG_AnimationParseContext *parse, Uint32 size)
+{
+    SDL_IOStream *src = parse->src;
+    IMG_AnimationDecoderContext *ctx = parse->ctx;
+    ANIHEADER *anih = &parse->anih;
+
+    if (size != sizeof(*anih)) {
+        return SDL_SetError("Invalid ANI header");
+    }
+
+    if (parse->has_anih) {
+        // Ignore duplicate 'anih' chunk
+        return true;
+    }
+
+    if (!SDL_ReadU32LE(src, &anih->cbSizeof) ||
+        !SDL_ReadU32LE(src, &anih->frames) ||
+        !SDL_ReadU32LE(src, &anih->steps) ||
+        !SDL_ReadU32LE(src, &anih->width) ||
+        !SDL_ReadU32LE(src, &anih->height) ||
+        !SDL_ReadU32LE(src, &anih->bpp) ||
+        !SDL_ReadU32LE(src, &anih->planes) ||
+        !SDL_ReadU32LE(src, &anih->jifRate) ||
+        !SDL_ReadU32LE(src, &anih->fl)) {
+        return SDL_SetError("Couldn't read ANI header");
+    }
+    parse->has_anih = true;
+
+    if (anih->cbSizeof != sizeof(*anih) ||
+        anih->frames == 0 ||
+        anih->steps == 0) {
+        return SDL_SetError("Invalid ANI header");
+    }
+
+    // We could support raw frames if we get an example of this
+    if (!(anih->fl & ANI_FLAG_ICON)) {
+        return SDL_SetError("Raw ANI frames are unsupported");
+    }
+
+    ctx->frame_count = anih->steps;
+    ctx->frame_offsets = (Sint64 *)SDL_calloc(anih->frames, sizeof(*ctx->frame_offsets));
+    ctx->frame_durations = (Uint32 *)SDL_calloc(ctx->frame_count, sizeof(*ctx->frame_durations));
+    ctx->frame_sequence = (Uint32 *)SDL_calloc(ctx->frame_count, sizeof(*ctx->frame_durations));
+    if (!ctx->frame_offsets || !ctx->frame_durations || !ctx->frame_sequence) {
+        return false;
+    }
+
+    for (Uint32 i = 0; i < ctx->frame_count; ++i) {
+        ctx->frame_sequence[i] = i;
+

(Patch may be truncated, please check the link at the top of this post.)