SDL: Add SDL_LoadSurface and SDL_LoadSurface_IO (#14374)

From a01d6f109dee9126f5ac283dab7b0b8c3d08fc8d Mon Sep 17 00:00:00 2001
From: Maia <[EMAIL REDACTED]>
Date: Thu, 13 Nov 2025 23:50:37 +0100
Subject: [PATCH] Add SDL_LoadSurface and SDL_LoadSurface_IO (#14374)

---
 include/SDL3/SDL_surface.h        | 40 +++++++++++++++++++++++++++++++
 src/dynapi/SDL_dynapi.sym         |  2 ++
 src/dynapi/SDL_dynapi_overrides.h |  2 ++
 src/dynapi/SDL_dynapi_procs.h     |  2 ++
 src/video/SDL_bmp.c               | 30 ++++++++++++++++++-----
 src/video/SDL_stb.c               | 36 +++++++++++++++++-----------
 src/video/SDL_surface.c           | 25 +++++++++++++++++++
 src/video/SDL_surface_c.h         |  2 ++
 test/testcustomcursor.c           |  2 +-
 test/testshape.c                  |  2 +-
 test/testutils.c                  |  2 +-
 11 files changed, 122 insertions(+), 23 deletions(-)

diff --git a/include/SDL3/SDL_surface.h b/include/SDL3/SDL_surface.h
index 880115288316b..053557fc2fd05 100644
--- a/include/SDL3/SDL_surface.h
+++ b/include/SDL3/SDL_surface.h
@@ -502,6 +502,46 @@ extern SDL_DECLSPEC bool SDLCALL SDL_LockSurface(SDL_Surface *surface);
  */
 extern SDL_DECLSPEC void SDLCALL SDL_UnlockSurface(SDL_Surface *surface);
 
+/**
+ * Load a BMP or PNG image from a seekable SDL data stream.
+ *
+ * The new surface should be freed with SDL_DestroySurface(). Not doing so
+ * will result in a memory leak.
+ *
+ * \param src the data stream for the surface.
+ * \param closeio if true, calls SDL_CloseIO() on `src` before returning, even
+ *                in the case of an error.
+ * \returns a pointer to a new SDL_Surface structure or NULL on failure; call
+ *          SDL_GetError() for more information.
+ *
+ * \threadsafety It is safe to call this function from any thread.
+ *
+ * \since This function is available since SDL 3.4.0.
+ *
+ * \sa SDL_DestroySurface
+ * \sa SDL_LoadSurface
+ */
+extern SDL_DECLSPEC SDL_Surface * SDLCALL SDL_LoadSurface_IO(SDL_IOStream *src, bool closeio);
+
+/**
+ * Load a BMP or PNG image from a file.
+ *
+ * The new surface should be freed with SDL_DestroySurface(). Not doing so
+ * will result in a memory leak.
+ *
+ * \param file the file to load.
+ * \returns a pointer to a new SDL_Surface structure or NULL on failure; call
+ *          SDL_GetError() for more information.
+ *
+ * \threadsafety It is safe to call this function from any thread.
+ *
+ * \since This function is available since SDL 3.4.0.
+ *
+ * \sa SDL_DestroySurface
+ * \sa SDL_LoadSurface_IO
+ */
+extern SDL_DECLSPEC SDL_Surface * SDLCALL SDL_LoadSurface(const char *file);
+
 /**
  * Load a BMP image from a seekable SDL data stream.
  *
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index 9feb8a0fc6f7d..de64a3efad92a 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -1268,6 +1268,8 @@ SDL3_0.0.0 {
     SDL_GetPenDeviceType;
     SDL_CreateAnimatedCursor;
     SDL_RotateSurface;
+    SDL_LoadSurface_IO;
+    SDL_LoadSurface;
     # extra symbols go here (don't modify this line)
   local: *;
 };
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index 1d17c7fcf0a2f..cc53044efb606 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -1294,3 +1294,5 @@
 #define SDL_GetPenDeviceType SDL_GetPenDeviceType_REAL
 #define SDL_CreateAnimatedCursor SDL_CreateAnimatedCursor_REAL
 #define SDL_RotateSurface SDL_RotateSurface_REAL
+#define SDL_LoadSurface_IO SDL_LoadSurface_IO_REAL
+#define SDL_LoadSurface SDL_LoadSurface_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index a2fc06fd7f14d..9e3c9cdbef2bd 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1302,3 +1302,5 @@ SDL_DYNAPI_PROC(int,SDL_GetSystemPageSize,(void),(),return)
 SDL_DYNAPI_PROC(SDL_PenDeviceType,SDL_GetPenDeviceType,(SDL_PenID a),(a),return)
 SDL_DYNAPI_PROC(SDL_Cursor*,SDL_CreateAnimatedCursor,(SDL_CursorFrameInfo *a,int b,int c,int d),(a,b,c,d),return)
 SDL_DYNAPI_PROC(SDL_Surface*,SDL_RotateSurface,(SDL_Surface *a,float b),(a,b),return)
+SDL_DYNAPI_PROC(SDL_Surface*,SDL_LoadSurface_IO,(SDL_IOStream *a,bool b),(a,b),return)
+SDL_DYNAPI_PROC(SDL_Surface*,SDL_LoadSurface,(const char *a),(a),return)
diff --git a/src/video/SDL_bmp.c b/src/video/SDL_bmp.c
index e9c21333da317..ad9837f69e554 100644
--- a/src/video/SDL_bmp.c
+++ b/src/video/SDL_bmp.c
@@ -177,6 +177,26 @@ static void CorrectAlphaChannel(SDL_Surface *surface)
     }
 }
 
+bool SDL_IsBMP(SDL_IOStream *src)
+{
+    Sint64 start;
+    Uint8 magic[2];
+    bool is_BMP;
+
+    is_BMP = false;
+    start = SDL_TellIO(src);
+    if (start >= 0) {
+        if (SDL_ReadIO(src, magic, sizeof(magic)) == sizeof(magic)) {
+            if (magic[0] == 'B' && magic[1] == 'M') {
+                is_BMP = true;
+            }
+        }
+        SDL_SeekIO(src, start, SDL_IO_SEEK_SET);
+    }
+
+    return is_BMP;
+}
+
 SDL_Surface *SDL_LoadBMP_IO(SDL_IOStream *src, bool closeio)
 {
     bool was_error = true;
@@ -195,7 +215,7 @@ SDL_Surface *SDL_LoadBMP_IO(SDL_IOStream *src, bool closeio)
     bool correctAlpha = false;
 
     // The Win32 BMP file header (14 bytes)
-    char magic[2];
+    // char magic[2];
     // Uint32 bfSize;
     // Uint16 bfReserved1;
     // Uint16 bfReserved2;
@@ -227,14 +247,12 @@ SDL_Surface *SDL_LoadBMP_IO(SDL_IOStream *src, bool closeio)
         goto done;
     }
     SDL_ClearError();
-    if (SDL_ReadIO(src, magic, 2) != 2) {
-        goto done;
-    }
-    if (SDL_strncmp(magic, "BM", 2) != 0) {
+    if (!SDL_IsBMP(src)) {
         SDL_SetError("File is not a Windows BMP file");
         goto done;
     }
-    if (!SDL_ReadU32LE(src, NULL /* bfSize */) ||
+    if (!SDL_ReadU16LE(src, NULL /* magic (already checked) */) ||
+        !SDL_ReadU32LE(src, NULL /* bfSize */) ||
         !SDL_ReadU16LE(src, NULL /* bfReserved1 */) ||
         !SDL_ReadU16LE(src, NULL /* bfReserved2 */) ||
         !SDL_ReadU32LE(src, &bfOffBits)) {
diff --git a/src/video/SDL_stb.c b/src/video/SDL_stb.c
index 1073ba658a7ab..99696c65ac805 100644
--- a/src/video/SDL_stb.c
+++ b/src/video/SDL_stb.c
@@ -328,11 +328,31 @@ static SDL_Surface *SDL_LoadSTB_IO(SDL_IOStream *src)
 }
 #endif // SDL_HAVE_STB
 
-SDL_Surface *SDL_LoadPNG_IO(SDL_IOStream *src, bool closeio)
+bool SDL_IsPNG(SDL_IOStream *src)
 {
     Sint64 start;
     Uint8 magic[4];
     bool is_PNG;
+
+    is_PNG = false;
+    start = SDL_TellIO(src);
+    if (start >= 0) {
+        if (SDL_ReadIO(src, magic, sizeof(magic)) == sizeof(magic)) {
+            if (magic[0] == 0x89 &&
+                magic[1] == 'P' &&
+                magic[2] == 'N' &&
+                magic[3] == 'G') {
+                is_PNG = true;
+            }
+        }
+        SDL_SeekIO(src, start, SDL_IO_SEEK_SET);
+    }
+
+    return is_PNG;
+}
+
+SDL_Surface *SDL_LoadPNG_IO(SDL_IOStream *src, bool closeio)
+{
     SDL_Surface *surface = NULL;
 
     CHECK_PARAM(!src) {
@@ -340,19 +360,7 @@ SDL_Surface *SDL_LoadPNG_IO(SDL_IOStream *src, bool closeio)
         goto done;
     }
 
-    start = SDL_TellIO(src);
-    is_PNG = false;
-    if (SDL_ReadIO(src, magic, sizeof(magic)) == sizeof(magic)) {
-        if (magic[0] == 0x89 &&
-            magic[1] == 'P' &&
-            magic[2] == 'N' &&
-            magic[3] == 'G') {
-            is_PNG = true;
-        }
-    }
-    SDL_SeekIO(src, start, SDL_IO_SEEK_SET);
-
-    if (!is_PNG) {
+    if (!SDL_IsPNG(src)) {
         SDL_SetError("File is not a PNG file");
         goto done;
     }
diff --git a/src/video/SDL_surface.c b/src/video/SDL_surface.c
index 99c1318ff5876..22709207d93ce 100644
--- a/src/video/SDL_surface.c
+++ b/src/video/SDL_surface.c
@@ -3083,3 +3083,28 @@ void SDL_DestroySurface(SDL_Surface *surface)
         SDL_free(surface);
     }
 }
+
+SDL_Surface *SDL_LoadSurface_IO(SDL_IOStream *src, bool closeio)
+{
+    if (SDL_IsBMP(src)) {
+        return SDL_LoadBMP_IO(src, closeio);
+    } else if (SDL_IsPNG(src)) {
+        return SDL_LoadPNG_IO(src, closeio);
+    } else {
+        if (closeio) {
+            SDL_CloseIO(src);
+        }
+        SDL_SetError("Unsupported image format");
+        return NULL;
+    }
+}
+
+SDL_Surface *SDL_LoadSurface(const char *file)
+{
+    SDL_IOStream *stream = SDL_IOFromFile(file, "rb");
+    if (!stream) {
+        return NULL;
+    }
+
+    return SDL_LoadSurface_IO(stream, true);
+}
diff --git a/src/video/SDL_surface_c.h b/src/video/SDL_surface_c.h
index 3b38fe00709e5..c1917936ade66 100644
--- a/src/video/SDL_surface_c.h
+++ b/src/video/SDL_surface_c.h
@@ -93,5 +93,7 @@ extern float SDL_GetDefaultHDRHeadroom(SDL_Colorspace colorspace);
 extern float SDL_GetSurfaceHDRHeadroom(SDL_Surface *surface, SDL_Colorspace colorspace);
 extern SDL_Surface *SDL_GetSurfaceImage(SDL_Surface *surface, float display_scale);
 extern SDL_Surface *SDL_ConvertSurfaceRect(SDL_Surface *surface, const SDL_Rect *rect, SDL_PixelFormat format);
+extern bool SDL_IsBMP(SDL_IOStream *src);
+extern bool SDL_IsPNG(SDL_IOStream *src);
 
 #endif // SDL_surface_c_h_
diff --git a/test/testcustomcursor.c b/test/testcustomcursor.c
index b3ac20bfdfc63..b5c6a09f06bea 100644
--- a/test/testcustomcursor.c
+++ b/test/testcustomcursor.c
@@ -111,7 +111,7 @@ static const char *cross[] = {
 
 static SDL_Surface *load_image_file(const char *file)
 {
-    SDL_Surface *surface = SDL_strstr(file, ".png") ? SDL_LoadPNG(file) : SDL_LoadBMP(file);
+    SDL_Surface *surface = SDL_LoadSurface(file);
     if (surface) {
         if (SDL_GetSurfacePalette(surface)) {
             const Uint8 bpp = SDL_BITSPERPIXEL(surface->format);
diff --git a/test/testshape.c b/test/testshape.c
index 3198d3870d83f..4386e6a7c004b 100644
--- a/test/testshape.c
+++ b/test/testshape.c
@@ -54,7 +54,7 @@ int main(int argc, char *argv[])
     }
 
     if (image_file) {
-        shape = SDL_strstr(image_file, ".png") ? SDL_LoadPNG(image_file) : SDL_LoadBMP(image_file);
+        shape = SDL_LoadSurface(image_file);
         if (!shape) {
             SDL_Log("Couldn't load %s: %s", image_file, SDL_GetError());
             goto quit;
diff --git a/test/testutils.c b/test/testutils.c
index 4acf985fac447..bbe809081070f 100644
--- a/test/testutils.c
+++ b/test/testutils.c
@@ -84,7 +84,7 @@ SDL_Texture *LoadTexture(SDL_Renderer *renderer, const char *file, bool transpar
         file = path;
     }
 
-    temp = SDL_strstr(file, ".png") ? SDL_LoadPNG(file) : SDL_LoadBMP(file);
+    temp = SDL_LoadSurface(file);
     if (!temp) {
         SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't load %s: %s", file, SDL_GetError());
     } else {