SDL_image: Add a simple automated test

From 59dff46f643fd2a11ddfba88c93704df9adda8f2 Mon Sep 17 00:00:00 2001
From: Simon McVittie <[EMAIL REDACTED]>
Date: Tue, 17 May 2022 20:58:20 +0100
Subject: [PATCH] Add a simple automated test

This loads and saves various supported formats, and asserts that the
same pixels (or close enough) end up in the output files.

For a distribution that knows it intends to support particular formats,
if an environment variable like SVG_IMAGE_TEST_REQUIRE_LOAD_JPG or
SVG_IMAGE_TEST_REQUIRE_SAVE_JPG is set, the test will fail if the given
format is not available.

Signed-off-by: Simon McVittie <smcv@debian.org>
---
 CMakeLists.txt        |   7 +
 Makefile.am           |   6 +
 configure.ac          |  10 +
 test/.gitignore       |   5 +
 test/CMakeLists.txt   |  75 ++++
 test/Makefile.am      |  69 +++
 test/main.c           | 976 ++++++++++++++++++++++++++++++++++++++++++
 test/palette.bmp      | Bin 0 -> 1242 bytes
 test/palette.gif      | Bin 0 -> 568 bytes
 test/sample.avif      | Bin 0 -> 322 bytes
 test/sample.bmp       | Bin 0 -> 3162 bytes
 test/sample.cur       | Bin 0 -> 3254 bytes
 test/sample.ico       | Bin 0 -> 3254 bytes
 test/sample.jpg       | Bin 0 -> 578 bytes
 test/sample.jxl       | Bin 0 -> 163 bytes
 test/sample.png       | Bin 0 -> 850 bytes
 test/sample.pnm       |  28 ++
 test/sample.qoi       | Bin 0 -> 1480 bytes
 test/sample.tif       | Bin 0 -> 3143 bytes
 test/sample.webp      | Bin 0 -> 668 bytes
 test/sample.xcf       | Bin 0 -> 3511 bytes
 test/sample.xpm       | 546 +++++++++++++++++++++++
 test/svg.bmp          | Bin 0 -> 4218 bytes
 test/svg.svg          |  24 ++
 test/svg64.bmp        | Bin 0 -> 16506 bytes
 test/template.test.in |   3 +
 26 files changed, 1749 insertions(+)
 create mode 100644 test/.gitignore
 create mode 100644 test/CMakeLists.txt
 create mode 100644 test/Makefile.am
 create mode 100644 test/main.c
 create mode 100644 test/palette.bmp
 create mode 100644 test/palette.gif
 create mode 100644 test/sample.avif
 create mode 100644 test/sample.bmp
 create mode 100644 test/sample.cur
 create mode 100644 test/sample.ico
 create mode 100644 test/sample.jpg
 create mode 100644 test/sample.jxl
 create mode 100644 test/sample.png
 create mode 100644 test/sample.pnm
 create mode 100644 test/sample.qoi
 create mode 100644 test/sample.tif
 create mode 100644 test/sample.webp
 create mode 100644 test/sample.xcf
 create mode 100644 test/sample.xpm
 create mode 100644 test/svg.bmp
 create mode 100644 test/svg.svg
 create mode 100644 test/svg64.bmp
 create mode 100644 test/template.test.in

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9f54397..24978ec 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -239,6 +239,8 @@ include(CMakeDependentOption)
 include(CMakePackageConfigHelpers)
 include(GNUInstallDirs)
 
+option(BUILD_TESTS "Build unit tests?" OFF)
+cmake_dependent_option(INSTALL_TESTS "Install unit tests?" OFF BUILD_TESTS OFF)
 option(VENDORED_DEFAULT "Default value for *_VENDORED options. Can be overridden for each library. Is only used in the first configure run." ON)
 option(SDL2_IMAGE_DISABLE_INSTALL "Disable installing SDL2_image" OFF)
 
@@ -767,3 +769,8 @@ if (BUILD_SAMPLES)
         endif()
     endforeach()
 endif()
+
+if (BUILD_TESTS)
+  include(CTest)
+  add_subdirectory(test)
+endif()
diff --git a/Makefile.am b/Makefile.am
index 8c864a2..b47d071 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -74,6 +74,12 @@ noinst_PROGRAMS = showimage showanim
 showimage_LDADD = libSDL2_image.la
 showanim_LDADD = libSDL2_image.la
 
+SUBDIRS = .
+
+if BUILD_TESTS
+SUBDIRS += test
+endif
+
 # Rule to build tar-gzipped distribution package
 $(PACKAGE)-$(VERSION).tar.gz: distcheck
 
diff --git a/configure.ac b/configure.ac
index e5b76cb..d76cdfe 100644
--- a/configure.ac
+++ b/configure.ac
@@ -250,6 +250,12 @@ AC_ARG_ENABLE([webp-shared], [AS_HELP_STRING([--enable-webp-shared], [dynamicall
  [], [enable_webp_shared=yes])
 AC_ARG_ENABLE([qoi], [AS_HELP_STRING([--enable-qoi], [support loading QOI images [default=yes]])],
  [], [enable_qoi=yes])
+AC_ARG_ENABLE([tests],
+              [AS_HELP_STRING([--enable-tests], [build tests [default=no]])],
+              [], [enable_tests=no])
+AC_ARG_ENABLE([installed-tests],
+              [AS_HELP_STRING([--enable-installed-tests], [install tests [default=no]])],
+              [], [enable_installed_tests=no])
 
 dnl Check for SDL
 SDL_VERSION=2.0.8
@@ -665,6 +671,9 @@ AC_SUBST([IMG_LIBS])
 AC_SUBST([PC_LIBS])
 AC_SUBST([PC_REQUIRES])
 
+AM_CONDITIONAL([BUILD_TESTS], [test "x$enable_tests" = xyes])
+AM_CONDITIONAL([INSTALL_TESTS], [test "x$enable_installed_tests" = xyes])
+
 dnl check for GCC warning options
 CheckWarnAll
 
@@ -678,5 +687,6 @@ AC_CONFIG_FILES([
 Makefile
 SDL2_image.spec
 SDL2_image.pc
+test/Makefile
 ])
 AC_OUTPUT
diff --git a/test/.gitignore b/test/.gitignore
new file mode 100644
index 0000000..185ec1c
--- /dev/null
+++ b/test/.gitignore
@@ -0,0 +1,5 @@
+/*.test
+/CompareSurfaces*.bmp
+/save-*.bmp
+/save.*
+/testimage
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
new file mode 100644
index 0000000..1b6594f
--- /dev/null
+++ b/test/CMakeLists.txt
@@ -0,0 +1,75 @@
+add_executable(testimage main.c)
+
+set(ALL_TESTS
+    testimage
+)
+set(RESOURCE_FILES
+    palette.bmp
+    palette.gif
+    sample.avif
+    sample.bmp
+    sample.cur
+    sample.ico
+    sample.jpg
+    sample.jxl
+    sample.png
+    sample.pnm
+    sample.qoi
+    sample.tif
+    sample.webp
+    sample.xcf
+    sample.xpm
+    svg.bmp
+    svg.svg
+    svg64.bmp
+)
+
+set(TESTS_ENVIRONMENT
+    "SDL_TEST_SRCDIR=${CMAKE_CURRENT_SOURCE_DIR}"
+    "SDL_TEST_BUILDDIR=${CMAKE_CURRENT_BINARY_DIR}"
+    "SDL_VIDEODRIVER=dummy"
+)
+
+foreach(prog ${ALL_TESTS})
+    target_compile_definitions(${prog} PRIVATE $<TARGET_PROPERTY:SDL2_image,COMPILE_DEFINITIONS>)
+    target_link_libraries(${prog} PRIVATE SDL2_image::${sdl2_image_export_name})
+    if (TARGET SDL2::SDL2main)
+        target_link_libraries(${prog} PRIVATE SDL2::SDL2main)
+    endif()
+    target_link_libraries(${prog} PRIVATE SDL2_test)
+    if (BUILD_SHARED_LIBS)
+        target_link_libraries(${prog} PRIVATE SDL2::SDL2)
+    else()
+        target_link_libraries(${prog} PRIVATE SDL2::SDL2-static)
+    endif()
+
+    add_test(
+        NAME ${prog}
+        COMMAND ${prog}
+        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+    )
+    set_tests_properties(
+        ${prog}
+        PROPERTIES ENVIRONMENT "${TESTS_ENVIRONMENT}"
+    )
+    if(INSTALL_TESTS)
+        set(exe ${prog})
+        set(installedtestsdir "${CMAKE_INSTALL_FULL_LIBEXECDIR}/installed-tests/${CMAKE_PROJECT_NAME}")
+        configure_file(template.test.in "${exe}.test" @ONLY)
+        install(
+            FILES "${CMAKE_CURRENT_BINARY_DIR}/${exe}.test"
+            DESTINATION "${CMAKE_INSTALL_DATADIR}/installed-tests/${CMAKE_PROJECT_NAME}"
+        )
+    endif()
+endforeach()
+
+if(INSTALL_TESTS)
+    install(
+        TARGETS ${ALL_TESTS}
+        DESTINATION "${CMAKE_INSTALL_LIBEXECDIR}/installed-tests/${CMAKE_PROJECT_NAME}"
+    )
+    install(
+        FILES ${RESOURCE_FILES}
+        DESTINATION "${CMAKE_INSTALL_LIBEXECDIR}/installed-tests/${CMAKE_PROJECT_NAME}"
+    )
+endif()
diff --git a/test/Makefile.am b/test/Makefile.am
new file mode 100644
index 0000000..4ed70ed
--- /dev/null
+++ b/test/Makefile.am
@@ -0,0 +1,69 @@
+testmetadir = $(datadir)/installed-tests/$(PACKAGE_TARNAME)
+testexecdir = $(libexecdir)/installed-tests/$(PACKAGE_TARNAME)
+
+test_programs = \
+	testimage \
+	$(NULL)
+
+testimage_CPPFLAGS = -I$(top_srcdir)
+testimage_SOURCES = main.c
+testimage_LDADD = \
+	../libSDL2_image.la \
+	$(SDL_LIBS) \
+	-lSDL2_test \
+	$(NULL)
+
+AM_TESTS_ENVIRONMENT = \
+	SDL_TEST_SRCDIR=$(abs_srcdir) \
+	SDL_TEST_BUILDDIR=$(abs_builddir) \
+	SDL_VIDEODRIVER=dummy \
+	$(NULL)
+
+if INSTALL_TESTS
+testexec_PROGRAMS = $(test_programs)
+else
+noinst_PROGRAMS = $(test_programs)
+endif
+
+TESTS = $(test_programs)
+
+if INSTALL_TESTS
+dist_testexec_DATA = \
+	palette.bmp \
+	palette.gif \
+	sample.avif \
+	sample.bmp \
+	sample.cur \
+	sample.ico \
+	sample.jpg \
+	sample.jxl \
+	sample.png \
+	sample.pnm \
+	sample.qoi \
+	sample.tif \
+	sample.webp \
+	sample.xcf \
+	sample.xpm \
+	svg.bmp \
+	svg.svg \
+	svg64.bmp \
+	$(NULL)
+
+all-local: generatetestmeta
+generatetestmeta:
+	rm -f *.test
+	set -e; for exe in $(test_programs); do \
+		sed \
+			-e 's#@installedtestsdir@#$(testexecdir)#g' \
+			-e "s#@exe@#$$exe#g" \
+			< $(srcdir)/template.test.in > $$exe.test; \
+	done
+
+install-data-hook: installtestmeta
+installtestmeta: generatetestmeta
+	install -m644 -D -t $(DESTDIR)$(testmetadir) *.test
+
+clean-local:
+	rm -f *.test
+	rm -f save.jpg save.bmp CompareSurfaces*.bmp
+endif
diff --git a/test/main.c b/test/main.c
new file mode 100644
index 0000000..56a34ea
--- /dev/null
+++ b/test/main.c
@@ -0,0 +1,976 @@
+/*
+  Copyright 1997-2022 Sam Lantinga
+  Copyright 2022 Collabora Ltd.
+
+  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.
+*/
+
+#include "SDL_image.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <time.h>
+
+#include "SDL.h"
+#include "SDL_test.h"
+
+#if defined(SDL_FILESYSTEM_OS2) || defined(SDL_FILESYSTEM_WINDOWS)
+static const char pathsep[] = "\\";
+#elif defined(SDL_FILESYSTEM_RISCOS)
+static const char pathsep[] = ".";
+#else
+static const char pathsep[] = "/";
+#endif
+
+/* TODO: https://github.com/libsdl-org/SDL_image/issues/243 */
+#ifndef SDL_IMAGE_SAVE_JPG
+# define SDL_IMAGE_SAVE_JPG 0
+#endif
+#ifndef SDL_IMAGE_SAVE_PNG
+# define SDL_IMAGE_SAVE_PNG 0
+#endif
+
+typedef enum
+{
+    TEST_FILE_DIST,
+    TEST_FILE_BUILT
+} TestFileType;
+
+static SDL_bool
+GetStringBoolean(const char *value, SDL_bool default_value)
+{
+    if (!value || !*value) {
+        return default_value;
+    }
+    if (*value == '0' || SDL_strcasecmp(value, "false") == 0) {
+        return SDL_FALSE;
+    }
+    return SDL_TRUE;
+}
+
+/*
+ * Return the absolute path to a resource file, similar to GLib's
+ * g_test_build_filename().
+ *
+ * If type is TEST_FILE_DIST, look for it in $SDL_TEST_SRCDIR or next
+ * to the executable.
+ *
+ * If type is TEST_FILE_BUILT, look for it in $SDL_TEST_BUILDDIR or next
+ * to the executable.
+ *
+ * Fails and returns NULL if out of memory.
+ */
+static char *
+GetTestFilename(TestFileType type, const char *file)
+{
+    const char *env;
+    char *base = NULL;
+    char *path = NULL;
+    SDL_bool needPathSep = SDL_TRUE;
+
+    if (type == TEST_FILE_DIST) {
+        env = SDL_getenv("SDL_TEST_SRCDIR");
+    } else {
+        env = SDL_getenv("SDL_TEST_BUILDDIR");
+    }
+
+    if (env != NULL) {
+        base = SDL_strdup(env);
+        if (base == NULL) {
+            SDL_OutOfMemory();
+            return NULL;
+        }
+    }
+
+    if (base == NULL) {
+        base = SDL_GetBasePath();
+        /* SDL_GetBasePath() guarantees a trailing path separator */
+        needPathSep = SDL_FALSE;
+    }
+
+    if (base != NULL) {
+        size_t len = SDL_strlen(base) + SDL_strlen(pathsep) + SDL_strlen(file) + 1;
+
+        path = SDL_malloc(len);
+
+        if (path == NULL) {
+            SDL_OutOfMemory();
+            return NULL;
+        }
+
+        if (needPathSep) {
+            SDL_snprintf(path, len, "%s%s%s", base, pathsep, file);
+        } else {
+            SDL_snprintf(path, len, "%s%s", base, file);
+        }
+
+        SDL_free(base);
+    } else {
+        path = SDL_strdup(file);
+        if (path == NULL) {
+            SDL_OutOfMemory();
+            return NULL;
+        }
+    }
+
+    return path;
+}
+
+static SDLTest_CommonState *state;
+
+typedef struct
+{
+    const char *name;
+    const char *sample;
+    const char *reference;
+    int w;
+    int h;
+    int tolerance;
+    int initFlag;
+    SDL_bool canLoad;
+    SDL_bool canSave;
+    int (SDLCALL * checkFunction)(SDL_RWops *src);
+    SDL_Surface *(SDLCALL * loadFunction)(SDL_RWops *src);
+} Format;
+
+static const Format formats[] =
+{
+    {
+        "AVIF",
+        "sample.avif",
+        "sample.bmp",
+        23,
+        42,
+        0,              /* lossless */
+        IMG_INIT_AVIF,
+#ifdef LOAD_AVIF
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_FALSE,      /* can save */
+        IMG_isAVIF,
+        IMG_LoadAVIF_RW,
+    },
+    {
+        "BMP",
+        "sample.bmp",
+        "sample.png",
+        23,
+        42,
+        0,              /* lossless */
+        0,              /* no initialization */
+#ifdef LOAD_BMP
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_FALSE,      /* can save */
+        IMG_isBMP,
+        IMG_LoadBMP_RW,
+    },
+    {
+        "CUR",
+        "sample.cur",
+        "sample.bmp",
+        23,
+        42,
+        0,              /* lossless */
+        0,              /* no initialization */
+#ifdef LOAD_BMP
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_FALSE,      /* can save */
+        IMG_isCUR,
+        IMG_LoadCUR_RW,
+    },
+    {
+        "GIF",
+        "palette.gif",
+        "palette.bmp",
+        23,
+        42,
+        0,              /* lossless */
+        0,              /* no initialization */
+#ifdef LOAD_GIF
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_FALSE,      /* can save */
+        IMG_isGIF,
+        IMG_LoadGIF_RW,
+    },
+    {
+        "ICO",
+        "sample.ico",
+        "sample.bmp",
+        23,
+        42,
+        0,              /* lossless */
+        0,              /* no initialization */
+#ifdef LOAD_BMP
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_FALSE,      /* can save */
+        IMG_isICO,
+        IMG_LoadICO_RW,
+    },
+    {
+        "JPG",
+        "sample.jpg",
+        "sample.bmp",
+        23,
+        42,
+        100,
+        IMG_INIT_JPG,
+#ifdef LOAD_JPG
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_IMAGE_SAVE_JPG,
+        IMG_isJPG,
+        IMG_LoadJPG_RW,
+    },
+    {
+        "JXL",
+        "sample.jxl",
+        "sample.bmp",
+        23,
+        42,
+        100,
+        IMG_INIT_JXL,
+#ifdef LOAD_JXL
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_FALSE,      /* can save */
+        IMG_isJXL,
+        IMG_LoadJXL_RW,
+    },
+#if 0
+    {
+        "LBM",
+        "sample.lbm",
+        "sample.bmp",
+        23,
+        42,
+        0,              /* lossless? */
+        0,              /* no initialization */
+#ifdef LOAD_LBM
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_FALSE,      /* can save */
+        IMG_isLBM,
+        IMG_LoadLBM_RW,
+    },
+#endif
+#if 0
+    {
+        "PCX",
+        "sample.pcx",
+        "sample.bmp",
+        23,
+        42,
+        0,              /* lossless? */
+        0,              /* no initialization */
+#ifdef LOAD_PCX
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_FALSE,      /* can save */
+        IMG_isPCX,
+        IMG_LoadPCX_RW,
+    },
+#endif
+    {
+        "PNG",
+        "sample.png",
+        "sample.bmp",
+        23,
+        42,
+        0,              /* lossless */
+        IMG_INIT_PNG,
+#ifdef LOAD_PNM
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_IMAGE_SAVE_PNG,
+        IMG_isPNG,
+        IMG_LoadPNG_RW,
+    },
+    {
+        "PNM",
+        "sample.pnm",
+        "sample.bmp",
+        23,
+        42,
+        0,              /* lossless */
+        0,              /* no initialization */
+#ifdef LOAD_PNM
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_FALSE,      /* can save */
+        IMG_isPNM,
+        IMG_LoadPNM_RW,
+    },
+    {
+        "QOI",
+        "sample.qoi",
+        "sample.bmp",
+        23,
+        42,
+        0,              /* lossless */
+        0,              /* no initialization */
+#ifdef LOAD_QOI
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_FALSE,      /* can save */
+        IMG_isQOI,
+        IMG_LoadQOI_RW,
+    },
+    {
+        "SVG",
+        "svg.svg",
+        "svg.bmp",
+        32,
+        32,
+        100,
+        0,              /* no initialization */
+#ifdef LOAD_SVG
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_FALSE,      /* can save */
+        IMG_isSVG,
+        IMG_LoadSVG_RW,
+    },
+    {
+        "SVG-sized",
+        "svg.svg",
+        "svg64.bmp",
+        64,
+        64,
+        100,
+        0,              /* no initialization */
+#ifdef LOAD_SVG
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_FALSE,      /* can save */
+        IMG_isSVG,
+        IMG_LoadSVG_RW,
+    },
+#if 0
+    {
+        "TGA",
+        "sample.tga",
+        "sample.bmp",
+        23,
+        42,
+        0,              /* lossless? */
+        0,              /* no initialization */
+#ifdef LOAD_TGA
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_FALSE,      /* can save */
+        NULL,
+        IMG_LoadTGA_RW,
+    },
+#endif
+    {
+        "TIF",
+        "sample.tif",
+        "sample.bmp",
+        23,
+        42,
+        0,              /* lossless */
+        IMG_INIT_TIF,
+#ifdef LOAD_TIF
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_FALSE,      /* can save */
+        IMG_isTIF,
+        IMG_LoadTIF_RW,
+    },
+    {
+        "WEBP",
+        "sample.webp",
+        "sample.bmp",
+        23,
+        42,
+        0,              /* lossless */
+        IMG_INIT_WEBP,
+#ifdef LOAD_WEBP
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_FALSE,      /* can save */
+        IMG_isWEBP,
+        IMG_LoadWEBP_RW,
+    },
+    {
+        "XCF",
+        "sample.xcf",
+        "sample.bmp",
+        23,
+        42,
+        0,              /* lossless */
+        0,              /* no initialization */
+#ifdef LOAD_XCF
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_FALSE,      /* can save */
+        IMG_isXCF,
+        IMG_LoadXCF_RW,
+    },
+    {
+        "XPM",
+        "sample.xpm",
+        "sample.bmp",
+        23,
+        42,
+        0,              /* lossless */
+        0,              /* no initialization */
+#ifdef LOAD_XPM
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_FALSE,      /* can save */
+        IMG_isXPM,
+        IMG_LoadXPM_RW,
+    },
+#if 0
+    {
+        "XV",
+        "sample.xv",
+        "sample.bmp",
+        23,
+        42,
+        0,              /* lossless? */
+        0,              /* no initialization */
+#ifdef LOAD_XV
+        SDL_TRUE,
+#else
+        SDL_FALSE,
+#endif
+        SDL_FALSE,      /* can save */
+        IMG_isXV,
+        IMG_LoadXV_RW,
+    },
+#endif
+};
+
+static SDL_bool
+StrHasSuffix(const char *str, const char *suffix)
+{
+    size_t str_len = strlen(str);
+    size_t suffix_len = strlen(suffix);
+
+    return (str_len >= suffix_len
+            && strcmp(str + (str_len - suffix_len), suffix) == 0);
+}
+
+typedef enum
+{
+    LOAD_CONVENIENCE = 0,
+    LOAD_RW,
+    LOAD_TYPED_RW,
+    LOAD_FORMAT_SPECIFIC,
+    LOAD_SIZED
+} LoadMode;
+
+/* Convert to RGBA for comparison, if necessary */
+static SDL_bool
+ConvertToRgba32(SDL_Surface **surface_p)
+{
+    if ((*surface_p)->format->format != SDL_PIXELFORMAT_RGBA32) {
+        SDL_Surface *temp;
+
+        temp = SDL_ConvertSurfaceFormat(*surface_p, SDL_PIXELFORMAT_RGBA32, 0);
+        SDLTest_AssertCheck(temp != NULL,
+                            "Converting to RGBA should succeed (%s)",
+                            SDL_GetError());
+        if (temp != NULL) {
+            return SDL_FALSE;
+        }
+        SDL_FreeSurface(*surface_p);
+        *surface_p = temp;
+    }
+    return SDL_TRUE;
+}
+
+static void
+FormatLoadTest(const Format *format,
+               LoadMode mode)
+{
+    SDL_Surface *reference = NULL;
+    SDL_Surface *surface = NULL;
+    SDL_RWops *src = NULL;
+    char *filename = GetTestFilename(TEST_FILE_DIST, format->sample);
+    char *refFilename = GetTestFilename(TEST_FILE_DIST, format->reference);
+    int initResult = 0;
+    int diff;
+
+    if (!SDLTest_AssertCheck(filename != NULL,
+                             "Building filename should succeed (%s)",
+                             SDL_GetError())) {
+        goto out;
+    }
+    if (!SDLTest_AssertCheck(refFilename != NULL,
+                             "Building ref filename should succeed (%s)",
+                             SDL_GetError())) {
+        goto out;
+    }
+
+    if (StrHasSuffix(format->reference, ".bmp")) {
+        reference = SDL_LoadBMP(refFilename);
+        if (!SDLTest_AssertCheck(reference != NULL,
+                                 "Loading reference should succeed (%s)",
+                                 SDL_GetError())) {
+            goto out;
+        }
+    }
+    else if (StrHasSuffix (format->reference, ".png")) {
+#ifdef LOAD_PNG
+        reference = IMG_Load(refFilename);
+        if (!SDLTest_AssertCheck(reference != NULL,
+                                 "Loading reference should succeed (%s)",
+                                 SDL_GetError())) {
+            goto out;
+        }
+#endif
+    }
+
+    if (format->initFlag) {
+        initResult = IMG_Init(format->initFlag);
+        if (!SDLTest_AssertCheck(initResult != 0,
+                                 "Initialization should succeed (%s)",
+                                 SDL_GetError())) {
+            goto out;
+        }
+        SDLTest_AssertCheck(initResult & format->initFlag,
+                            "Expected at least bit 0x%x set, got 0x%x",
+                            format->initFlag, initResult);
+    }
+
+    if (mode != LOAD_CONVENIENCE) {
+        src = SDL_RWFromFile(filename, "rb");
+        SDLTest_AssertCheck(src != NULL,
+                            "Opening %s should succeed (%s)",
+                            filename, SDL_GetError());
+        if (src == NULL)
+            goto out;
+    }
+
+    switch (mode) {
+        case LOAD_CONVENIENCE:
+            surface = IMG_Load(filename);
+            break;
+
+        case LOAD_RW:
+            if (format->checkFunction != NULL) {
+                SDL_RWops *ref_src;
+                int check;
+
+                ref_src = SDL_RWFromFile(refFilename, "rb");
+                SDLTest_AssertCheck(ref_src != NULL,
+                                    "Opening %s should succeed (%s)",
+                                    refFilename, SDL_GetError());
+                if (ref_src != NULL) {
+                    check = format->checkFunction(ref_src);
+                    SDLTest_AssertCheck(!check,
+                                        "Should not detect %s as %s -> %d",
+                                        refFilename, format->name, check);
+                    SDL_RWclose(ref_src);
+                }
+            }
+
+            if (format->checkFunction != NULL) {
+                int check = format->checkFunction(src);
+
+                SDLTest_AssertCheck(check,
+                                    "Should detect %s as %s -> %d",
+                                    filename, format->name, check);
+            }
+
+            surface = IMG_Load_RW(src, SDL_TRUE);
+            src = NULL;      /* ownership taken */
+            break;
+
+        case LOAD_TYPED_RW:
+            surface = IMG_LoadTyped_RW(src, SDL_TRUE, format->name);
+            src = NULL;      /* ownership taken */
+            break;
+
+        case LOAD_FORMAT_SPECIFIC:
+            surface = format->loadFunction(src);
+            break;
+
+        case LOAD_SIZED:
+            if (strcmp(format->name, "SVG-sized") == 0) {
+                surface = IMG_LoadSizedSVG_RW(src, 64, 64);
+            }
+            break;
+    }
+
+    if (!SDLTest_AssertCheck(surface != NULL,
+                             "Load %s (%s)", filename, SDL_GetError())) {
+        goto out;
+    }
+
+    SDLTest_AssertCheck(surface->w == format->w,
+                        "Expected width %d px, got %d",
+                        format->w, surface->w);
+    SDLTest_AssertCheck(surface->h == format->h,
+                        "Expected height %d px, got %d",
+                        format->h, surface->h);
+
+    if (reference != NULL) {
+        ConvertToRgba32(&reference);
+        ConvertToRgba32(&surface);
+        diff = SDLTest_CompareSurfaces(surface, reference, format->tolerance);
+        SDLTest_AssertCheck(diff == 0,
+                            "Surface differed from reference by at least %d in %d pixels",
+                            format->tolerance, diff);
+    }
+
+out:
+    if (surface != NULL) {
+        SDL_FreeSurface(surface);
+    }
+    if (reference != NULL) {
+        SDL_FreeSurface(reference);
+    }
+    if (src != NULL) {
+        SDL_RWclose(src);
+    }
+    if (refFilename != NULL) {
+        SDL_free(refFilename);
+    }
+    if (filename != NULL) {
+        SDL_free(filename);
+    }
+    if (initResult) {
+        IMG_Quit();
+    }
+}
+
+static void
+FormatSaveTest(const Format *format,
+               SDL_bool rw)
+{
+    char *refFilename = GetTestFilename(TEST_FILE_DIST, "sample.bmp");
+    const char *filename;
+    SDL_Surface *reference = NULL;
+    SDL_Surface *surface = NULL;
+    SDL_RWops *dest = NULL;
+    int initResult = 0;
+    int diff;
+    int result;
+
+    if (!SDLTest_AssertCheck(refFilename != NULL,
+                             "Building ref filename should succeed (%s)",
+                             SDL_GetError())) {
+        goto out;
+    }
+
+    reference = SDL_LoadBMP(refFilename);
+    if (!SDLTest_AssertCheck(reference != NULL,
+                             "Loading reference should succeed (%s)",
+                             SDL_GetError())) {
+        goto out;
+    }
+
+    if (format->initFlag) {
+        initResult = IMG_Init(format->initFlag);
+        if (!SDLTest_AssertCheck(initResult != 0,
+                                 "Initialization should succeed (%s)",
+                                 SDL_GetError())) {
+            goto out;
+        }
+        SDLTest_AssertCheck(initResult & format->initFlag,
+                            "Expected at least bit 0x%x set, got 0x%x",
+                            format->initFlag, initResult);
+    }
+
+    if (strcmp (format->name, "PNG") == 0) {
+        filename = "save.png";
+
+        if (rw) {
+            dest = SDL_RWFromFile(filename, "wb");
+            result = IMG_SavePNG_RW(reference, dest, SDL_FALSE);
+            SDL_RWclose(dest);
+        } else {
+            result = IMG_SavePNG(reference, filename);
+        }
+    } else if (strcmp(format->name, "JPG") == 0) {
+        filename = "save.jpg";
+
+        if (rw) {
+            dest = SDL_RWFromFile(filename, "wb");
+            result = IMG_SaveJPG_RW(reference, dest, SDL_FALSE, 90);
+            SDL_RWclose(dest);
+        } else {
+            result = IMG_SaveJPG(reference, "save.jpg", 90);
+        }
+    } else {
+        SDLTest_AssertCheck(SDL_FALSE, "How do I save %s?", format->name);
+        goto out;
+    }
+
+    SDLTest_AssertCheck(result == 0, "Save %s (%s)", filename, SDL_GetError());
+
+    if (format->canLoad) {
+        if (strcmp (format->name, "PNG") == 0) {
+            surface = IMG_Load("save.png");
+        } else if (strcmp (format->name, "JPG") == 0) {
+            surface = IMG_Load("save.jpg");
+        }
+
+        if (!SDLTest_AssertCheck(surface != NULL,
+                                 "Load %s (%s)", "saved file", SDL_GetError())) {
+            goto out;
+        }
+
+        SDLTest_AssertCheck(surface->w == format->w,
+                            "Expected width %d px, got %d",
+                            format->w, surface->w);
+        SDLTest_AssertCheck(surface->h == format->h,
+                            "Expected height %d px, got %d",
+                            format->h, surface->h);
+
+        diff = SDLTest_CompareSurfaces(surface, reference, format->tolerance);
+        SDLTest_AssertCheck(diff == 0,
+                            "Surface differed from reference by at least %d in %d pixels",
+                            format->tolerance, diff);
+    }
+
+out:
+    if (surface != NULL) {
+        SDL_FreeSurface(surface);
+    }
+    if (reference != NULL) {
+        SDL_FreeSurface(reference);
+    }
+    if (refFilename != NULL) {
+        SDL_free(refFilename);
+    }
+    if (initResult) {
+        IMG_Quit();
+    }
+}
+
+static void
+FormatTest(const Format *format)
+{
+    SDL_bool forced;
+    char envVar[64] = { 0 };
+
+    SDL_snprintf(envVar, sizeof(envVar), "SDL_IMAGE_TEST_REQUIRE_LOAD_%s",
+                 format->name);
+
+    forced = GetStringBoolean(SDL_getenv(envVar), SDL_FALSE);
+    if (forced) {
+        SDLTest_AssertCheck(format->canLoad,
+                            "%s loading should be enabled", format->name);
+    }
+
+    if (format->canLoad || forced) {
+        SDLTest_Log("Testing ability to load format %s", format->name);
+
+        if (strcmp(format->name, "SVG-sized") == 0) {
+            FormatLoadTest(format, LOAD_SIZED);
+        } else {
+            FormatLoadTest(format, LOAD_CONVENIENCE);
+            FormatLoadTest(format, LOAD_RW);
+            FormatLoadTest(format, LOAD_TYPED_RW);
+
+            if (format->loadFunction != NULL) {
+                FormatLoadTest(format, LOAD_FORMAT_SPECIFIC);
+            }
+        }
+    } else {
+        SDLTest_Log("Format %s is not supported", format->name);
+    }
+
+    SDL_snprintf(envVar, sizeof(envVar), "SDL_IMAGE_TEST_REQUIRE_SAVE_%s",
+                 format->name);
+
+    forced = GetStringBoolean(SDL_getenv(envVar), SDL_FALSE);
+    if (forced) {
+        SDLTest_AssertCheck(format->canSave,
+                            "%s saving should be enabled", format->name);
+    }
+
+    if (format->canSave || forced) {
+        SDLTest_Log("Testing ability to save format %s", format->name);
+        FormatSaveTest(format, SDL_FALSE);
+        FormatSaveTest(format, SDL_TRUE);
+    } else {
+        SDLTest_Log("Saving format %s is not supported", format->name);
+    }
+}
+
+static int
+TestFormats(void *arg)
+{
+    size_t i;
+
+    for (i = 0; i < SDL_arraysize(formats); i++) {
+        FormatTest(&formats[i]);
+    }
+
+    return TEST_COMPLETED;
+}
+
+static const SDLTest_TestCaseReference formatsTestCase = {
+    TestFormats, "Images", "Load and save various image formats", TEST_ENABLED
+};
+
+static const SDLTest_TestCaseReference *testCases[] =  {
+    &formatsTestCase,
+    NULL
+};
+static SDLTest_TestSuiteReference testSuite = {
+    "img",
+    NULL,
+    testCases,
+    NULL
+};
+static SDLTest_TestSuiteReference *testSuites[] =  {
+    &testSuite,
+    NULL
+};
+
+/* Call this instead of exit(), so we can clean up SDL: atexit() is evil. */
+static void
+quit(int rc)
+{
+    SDLTest_CommonQuit(state);
+    exit(rc);
+}
+
+int
+main(int argc, char *argv[])
+{
+    int result;
+    int testIterations = 1;
+    Uint64 userExecKey = 0;
+    char *userRunSeed = NULL;
+    char *filter = NULL;
+    int i, done;
+    SDL_Event event;
+
+    /* Initialize test framework */
+    state = SDLTest_CommonCreateState(argv, SDL_INIT_VIDEO);
+    if (!state) {
+        return 1;
+    }
+
+    /* Parse commandline */
+    for (i = 1; i < argc;) {
+        int consumed;
+
+        consumed = SDLTest_CommonArg(state, i);
+        if (consumed == 0) {
+    

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