From c6b232f5d4478af7d2c2ebfe080450931c15f414 Mon Sep 17 00:00:00 2001
From: Cameron Cawley <[EMAIL REDACTED]>
Date: Wed, 13 May 2026 20:51:02 +0100
Subject: [PATCH] Support loading JPEG images through SDL_LoadSurface()
---
include/SDL3/SDL_surface.h | 60 ++++++++++++++--
src/dynapi/SDL_dynapi.exports | 2 +
src/dynapi/SDL_dynapi.sym | 2 +
src/dynapi/SDL_dynapi_overrides.h | 2 +
src/dynapi/SDL_dynapi_procs.h | 2 +
src/video/SDL_stb.c | 110 ++++++++++++++++++++++++++++++
src/video/SDL_surface.c | 2 +
src/video/SDL_surface_c.h | 1 +
8 files changed, 175 insertions(+), 6 deletions(-)
diff --git a/include/SDL3/SDL_surface.h b/include/SDL3/SDL_surface.h
index 885f8f5d982a5..1194596cd251e 100644
--- a/include/SDL3/SDL_surface.h
+++ b/include/SDL3/SDL_surface.h
@@ -29,10 +29,10 @@
* provides a reasonable toolbox for transforming the data, including copying
* between surfaces, filling rectangles in the image data, etc.
*
- * There is also a simple .bmp loader, SDL_LoadBMP(), and a simple .png
- * loader, SDL_LoadPNG(). SDL itself does not provide loaders for other file
- * formats, but there are several excellent external libraries that do,
- * including its own satellite library,
+ * There is also a simple .bmp loader, SDL_LoadBMP(), a simple .png loader,
+ * SDL_LoadPNG(), and a simple .jpg loader, SDL_LoadJPG(). SDL itself does not
+ * provide loaders for other file formats, but there are several excellent
+ * external libraries that do, including its own satellite library,
* [SDL_image](https://wiki.libsdl.org/SDL3_image)
* .
*
@@ -510,7 +510,7 @@ 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.
+ * Load a BMP, PNG or JPEG 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.
@@ -531,7 +531,7 @@ extern SDL_DECLSPEC void SDLCALL SDL_UnlockSurface(SDL_Surface *surface);
extern SDL_DECLSPEC SDL_Surface * SDLCALL SDL_LoadSurface_IO(SDL_IOStream *src, bool closeio);
/**
- * Load a BMP or PNG image from a file.
+ * Load a BMP, PNG or JPEG image from a file.
*
* The new surface should be freed with SDL_DestroySurface(). Not doing so
* will result in a memory leak.
@@ -729,6 +729,54 @@ extern SDL_DECLSPEC bool SDLCALL SDL_SavePNG_IO(SDL_Surface *surface, SDL_IOStre
*/
extern SDL_DECLSPEC bool SDLCALL SDL_SavePNG(SDL_Surface *surface, const char *file);
+/**
+ * Load a JPEG image from a seekable SDL data stream.
+ *
+ * This is intended as a convenience function for loading images from trusted
+ * sources. If you want to load arbitrary images you should use libjpeg or
+ * another image loading library designed with security in mind.
+ *
+ * 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.6.0.
+ *
+ * \sa SDL_DestroySurface
+ * \sa SDL_LoadJPG
+ */
+extern SDL_DECLSPEC SDL_Surface * SDLCALL SDL_LoadJPG_IO(SDL_IOStream *src, bool closeio);
+
+/**
+ * Load a JPEG image from a file.
+ *
+ * This is intended as a convenience function for loading images from trusted
+ * sources. If you want to load arbitrary images you should use libjpeg or
+ * another image loading library designed with security in mind.
+ *
+ * The new surface should be freed with SDL_DestroySurface(). Not doing so
+ * will result in a memory leak.
+ *
+ * \param file the JPG 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.6.0.
+ *
+ * \sa SDL_DestroySurface
+ * \sa SDL_LoadJPG_IO
+ */
+extern SDL_DECLSPEC SDL_Surface * SDLCALL SDL_LoadJPG(const char *file);
+
/**
* Set the RLE acceleration hint for a surface.
*
diff --git a/src/dynapi/SDL_dynapi.exports b/src/dynapi/SDL_dynapi.exports
index 5953db362273c..67600f2b7bdb6 100644
--- a/src/dynapi/SDL_dynapi.exports
+++ b/src/dynapi/SDL_dynapi.exports
@@ -1285,3 +1285,5 @@ _SDL_SetGPURenderStateStorageBuffers
_SDL_GDKSuspendRenderer
_SDL_GDKResumeRenderer
_SDL_IsPhone
+_SDL_LoadJPG_IO
+_SDL_LoadJPG
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index a16d0f79c32ce..3fdc470a33730 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -1286,6 +1286,8 @@ SDL3_0.0.0 {
SDL_GDKSuspendRenderer;
SDL_GDKResumeRenderer;
SDL_IsPhone;
+ SDL_LoadJPG_IO;
+ SDL_LoadJPG;
# 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 c60b5223d4724..7b88affdc65c7 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -1312,3 +1312,5 @@
#define SDL_GDKSuspendRenderer SDL_GDKSuspendRenderer_REAL
#define SDL_GDKResumeRenderer SDL_GDKResumeRenderer_REAL
#define SDL_IsPhone SDL_IsPhone_REAL
+#define SDL_LoadJPG_IO SDL_LoadJPG_IO_REAL
+#define SDL_LoadJPG SDL_LoadJPG_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index 0d2d4ce5c7723..24a5afad988e4 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1320,3 +1320,5 @@ SDL_DYNAPI_PROC(bool,SDL_SetGPURenderStateStorageBuffers,(SDL_GPURenderState *a,
SDL_DYNAPI_PROC(void,SDL_GDKSuspendRenderer,(SDL_Renderer *a),(a),)
SDL_DYNAPI_PROC(void,SDL_GDKResumeRenderer,(SDL_Renderer *a),(a),)
SDL_DYNAPI_PROC(bool,SDL_IsPhone,(void),(),return)
+SDL_DYNAPI_PROC(SDL_Surface*,SDL_LoadJPG_IO,(SDL_IOStream *a,bool b),(a,b),return)
+SDL_DYNAPI_PROC(SDL_Surface*,SDL_LoadJPG,(const char *a),(a),return)
diff --git a/src/video/SDL_stb.c b/src/video/SDL_stb.c
index 732b4ef016135..fe5d0c70e97c0 100644
--- a/src/video/SDL_stb.c
+++ b/src/video/SDL_stb.c
@@ -363,6 +363,116 @@ static SDL_Surface *SDL_LoadSTB_IO(SDL_IOStream *src)
}
#endif // SDL_HAVE_STB
+/* FIXME: This is a copypaste from JPEGLIB! Pull that out of the ifdefs */
+/* Define this for quicker (but less perfect) JPEG identification */
+#define FAST_IS_JPEG
+
+/* See if an image is contained in a data source */
+bool SDL_IsJPG(SDL_IOStream *src)
+{
+ Sint64 start;
+ bool is_JPG;
+ bool in_scan;
+ Uint8 magic[4];
+
+ /* This detection code is by Steaphan Greene <stea@cs.binghamton.edu> */
+ /* Blame me, not Sam, if this doesn't work right. */
+ /* And don't forget to report the problem to the the sdl list too! */
+
+ if (!src) {
+ return false;
+ }
+
+ start = SDL_TellIO(src);
+ is_JPG = false;
+ in_scan = false;
+ if (SDL_ReadIO(src, magic, 2) == 2) {
+ if ( (magic[0] == 0xFF) && (magic[1] == 0xD8) ) {
+ is_JPG = true;
+ while (is_JPG) {
+ if (SDL_ReadIO(src, magic, 2) != 2) {
+ is_JPG = false;
+ } else if ( (magic[0] != 0xFF) && !in_scan ) {
+ is_JPG = false;
+ } else if ( (magic[0] != 0xFF) || (magic[1] == 0xFF) ) {
+ /* Extra padding in JPEG (legal) */
+ /* or this is data and we are scanning */
+ SDL_SeekIO(src, -1, SDL_IO_SEEK_CUR);
+ } else if (magic[1] == 0xD9) {
+ /* Got to end of good JPEG */
+ break;
+ } else if ( in_scan && (magic[1] == 0x00) ) {
+ /* This is an encoded 0xFF within the data */
+ } else if ( (magic[1] >= 0xD0) && (magic[1] < 0xD9) ) {
+ /* These have nothing else */
+ } else if (SDL_ReadIO(src, magic+2, 2) != 2) {
+ is_JPG = false;
+ } else {
+ /* Yes, it's big-endian */
+ Sint64 innerStart;
+ Uint32 size;
+ Sint64 end;
+ innerStart = SDL_TellIO(src);
+ size = (magic[2] << 8) + magic[3];
+ end = SDL_SeekIO(src, size-2, SDL_IO_SEEK_CUR);
+ if ( end != innerStart + size - 2 ) {
+ is_JPG = false;
+ }
+ if ( magic[1] == 0xDA ) {
+ /* Now comes the actual JPEG meat */
+#ifdef FAST_IS_JPEG
+ /* Ok, I'm convinced. It is a JPEG. */
+ break;
+#else
+ /* I'm not convinced. Prove it! */
+ in_scan = true;
+#endif
+ }
+ }
+ }
+ }
+ }
+ SDL_SeekIO(src, start, SDL_IO_SEEK_SET);
+ return is_JPG;
+}
+
+SDL_Surface *SDL_LoadJPG_IO(SDL_IOStream *src, bool closeio)
+{
+ SDL_Surface *surface = NULL;
+
+ CHECK_PARAM(!src) {
+ SDL_InvalidParamError("src");
+ goto done;
+ }
+
+ if (!SDL_IsJPG(src)) {
+ SDL_SetError("File is not a JPEG file");
+ goto done;
+ }
+
+#ifdef SDL_HAVE_STB
+ surface = SDL_LoadSTB_IO(src);
+#else
+ SDL_SetError("SDL not built with STB image support");
+#endif // SDL_HAVE_STB
+
+done:
+ if (src && closeio) {
+ SDL_CloseIO(src);
+ }
+ return surface;
+}
+
+SDL_Surface *SDL_LoadJPG(const char *file)
+{
+ SDL_IOStream *stream = SDL_IOFromFile(file, "rb");
+ if (!stream) {
+ return NULL;
+ }
+
+ return SDL_LoadJPG_IO(stream, true);
+}
+
bool SDL_IsPNG(SDL_IOStream *src)
{
Sint64 start;
diff --git a/src/video/SDL_surface.c b/src/video/SDL_surface.c
index 8164f76fbc65a..6a03fad7327db 100644
--- a/src/video/SDL_surface.c
+++ b/src/video/SDL_surface.c
@@ -3118,6 +3118,8 @@ SDL_Surface *SDL_LoadSurface_IO(SDL_IOStream *src, bool closeio)
return SDL_LoadBMP_IO(src, closeio);
} else if (SDL_IsPNG(src)) {
return SDL_LoadPNG_IO(src, closeio);
+ } else if (SDL_IsJPG(src)) {
+ return SDL_LoadJPG_IO(src, closeio);
} else {
if (closeio) {
SDL_CloseIO(src);
diff --git a/src/video/SDL_surface_c.h b/src/video/SDL_surface_c.h
index 168f05e515f96..231cc7430b69c 100644
--- a/src/video/SDL_surface_c.h
+++ b/src/video/SDL_surface_c.h
@@ -94,6 +94,7 @@ extern float SDL_GetSurfaceHDRHeadroom(SDL_Surface *surface, SDL_Colorspace colo
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_IsJPG(SDL_IOStream *src);
extern bool SDL_IsPNG(SDL_IOStream *src);
#endif // SDL_surface_c_h_