/* Copyright 1997-2026 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 #include #include #if defined(SDL_PLATFORM_OS2) || defined(SDL_PLATFORM_WIN32) static const char pathsep[] = "\\"; #elif defined(SDL_PLATFORM_RISCOS) static const char pathsep[] = "."; #else static const char pathsep[] = "/"; #endif #if defined(__APPLE__) && !defined(SDL_IMAGE_USE_COMMON_BACKEND) # define USING_IMAGEIO 1 #else # define USING_IMAGEIO 0 #endif typedef enum { TEST_FILE_DIST, TEST_FILE_BUILT } TestFileType; static bool GetStringBoolean(const char *value, bool default_value) { if (!value || !*value) { return default_value; } if (*value == '0' || SDL_strcasecmp(value, "false") == 0) { return false; } return 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 *base; char *path = NULL; bool needPathSep = true; if (type == TEST_FILE_DIST) { base = SDL_getenv("SDL_TEST_SRCDIR"); } else { base = SDL_getenv("SDL_TEST_BUILDDIR"); } if (base == NULL) { base = SDL_GetBasePath(); /* SDL_GetBasePath() guarantees a trailing path separator */ needPathSep = false; } if (base != NULL) { size_t len = SDL_strlen(base) + SDL_strlen(pathsep) + SDL_strlen(file) + 1; path = SDL_malloc(len); if (path == NULL) { return NULL; } if (needPathSep) { SDL_snprintf(path, len, "%s%s%s", base, pathsep, file); } else { SDL_snprintf(path, len, "%s%s", base, file); } } else { path = SDL_strdup(file); if (path == NULL) { 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; bool canLoad; bool canSave; bool (SDLCALL * checkFunction)(SDL_IOStream *src); SDL_Surface *(SDLCALL * loadFunction)(SDL_IOStream *src); } Format; static const Format formats[] = { { "AVIF", "sample.avif", "sample.bmp", 23, 42, 300, #ifdef LOAD_AVIF true, #else false, #endif #if defined(SAVE_AVIF) && SAVE_AVIF true, #else false, #endif IMG_isAVIF, IMG_LoadAVIF_IO, }, { "BMP", "sample.bmp", "sample.png", 23, 42, 0, /* lossless */ #ifdef LOAD_BMP true, #else false, #endif #if defined(SAVE_BMP) && SAVE_BMP true, #else false, #endif IMG_isBMP, IMG_LoadBMP_IO, }, { "CUR", "sample.cur", "sample.bmp", 23, 42, 0, /* lossless */ #ifdef LOAD_BMP true, #else false, #endif true, /* can save */ IMG_isCUR, IMG_LoadCUR_IO, }, { "GIF", "palette.gif", "palette.bmp", 23, 42, 10, /* lossless indexed, dithered rgba */ #if USING_IMAGEIO || defined(LOAD_GIF) true, #else false, #endif #if defined(SAVE_GIF) && SAVE_GIF true, #else false, #endif IMG_isGIF, IMG_LoadGIF_IO, }, { "ICO", "sample.ico", "sample.bmp", 23, 42, 0, /* lossless */ #ifdef LOAD_BMP true, #else false, #endif true, /* can save */ IMG_isICO, IMG_LoadICO_IO, }, { "JPG", "sample.jpg", "sample.bmp", 23, 42, 100, #if (USING_IMAGEIO && defined(JPG_USES_IMAGEIO)) || defined(SDL_IMAGE_USE_WIC_BACKEND) || defined(LOAD_JPG) true, #else false, #endif #if defined(SAVE_JPG) && SAVE_JPG true, #else false, #endif IMG_isJPG, IMG_LoadJPG_IO, }, #if 0 /* Different versions of JXL yield different output images */ { "JXL", "sample.jxl", "sample.bmp", 23, 42, 300, #ifdef LOAD_JXL true, #else false, #endif false, /* can save */ IMG_isJXL, IMG_LoadJXL_IO, }, #endif #if 0 { "LBM", "sample.lbm", "sample.bmp", 23, 42, 0, /* lossless? */ #ifdef LOAD_LBM true, #else false, #endif false, /* can save */ IMG_isLBM, IMG_LoadLBM_IO, }, #endif { "PCX", "sample.pcx", "sample.bmp", 23, 42, 0, /* lossless? */ #ifdef LOAD_PCX true, #else false, #endif false, /* can save */ IMG_isPCX, IMG_LoadPCX_IO, }, { "PNG", "sample.png", "sample.bmp", 23, 42, 0, /* lossless */ #if (USING_IMAGEIO && defined(PNG_USES_IMAGEIO)) || defined(SDL_IMAGE_USE_WIC_BACKEND) || defined(LOAD_PNG) true, #else false, #endif #if defined(SAVE_PNG) && SAVE_PNG true, #else false, #endif IMG_isPNG, IMG_LoadPNG_IO, }, { "PNM", "sample.pnm", "sample.bmp", 23, 42, 0, /* lossless */ #ifdef LOAD_PNM true, #else false, #endif false, /* can save */ IMG_isPNM, IMG_LoadPNM_IO, }, { "QOI", "sample.qoi", "sample.bmp", 23, 42, 0, /* lossless */ #ifdef LOAD_QOI true, #else false, #endif false, /* can save */ IMG_isQOI, IMG_LoadQOI_IO, }, { "SVG", "svg.svg", "svg.bmp", 32, 32, 100, #ifdef LOAD_SVG true, #else false, #endif false, /* can save */ IMG_isSVG, IMG_LoadSVG_IO, }, { "SVG-sized", "svg.svg", "svg64.bmp", 64, 64, 100, #ifdef LOAD_SVG true, #else false, #endif false, /* can save */ IMG_isSVG, IMG_LoadSVG_IO, }, { "SVG-class", "svg-class.svg", "svg-class.bmp", 82, 82, 0, /* lossless? */ #ifdef LOAD_SVG true, #else false, #endif false, /* can save */ IMG_isSVG, IMG_LoadSVG_IO, }, { "TGA", "sample.tga", "sample.bmp", 23, 42, 0, /* lossless? */ #if USING_IMAGEIO || defined(LOAD_TGA) true, #else false, #endif #if defined(SAVE_TGA) && SAVE_TGA true, #else false, #endif NULL, IMG_LoadTGA_IO, }, { "TIF", "sample.tif", "sample.bmp", 23, 42, 0, /* lossless */ #if USING_IMAGEIO || defined(SDL_IMAGE_USE_WIC_BACKEND) || defined(LOAD_TIF) true, #else false, #endif false, /* can save */ IMG_isTIF, IMG_LoadTIF_IO, }, { "WEBP", "sample.webp", "sample.bmp", 23, 42, 300, #ifdef LOAD_WEBP true, #else false, #endif #if defined(SAVE_WEBP) && SAVE_WEBP true, #else false, #endif IMG_isWEBP, IMG_LoadWEBP_IO, }, { "XCF", "sample.xcf", "sample.bmp", 23, 42, 0, /* lossless */ #ifdef LOAD_XCF true, #else false, #endif false, /* can save */ IMG_isXCF, IMG_LoadXCF_IO, }, { "XPM", "sample.xpm", "sample.bmp", 23, 42, 0, /* lossless */ #ifdef LOAD_XPM true, #else false, #endif false, /* can save */ IMG_isXPM, IMG_LoadXPM_IO, }, #if 0 { "XV", "sample.xv", "sample.bmp", 23, 42, 0, /* lossless? */ #ifdef LOAD_XV true, #else false, #endif false, /* can save */ IMG_isXV, IMG_LoadXV_IO, }, #endif }; static bool StrHasSuffix(const char *str, const char *suffix) { size_t str_len = SDL_strlen(str); size_t suffix_len = SDL_strlen(suffix); return (str_len >= suffix_len && SDL_strcmp(str + (str_len - suffix_len), suffix) == 0); } typedef enum { LOAD_CONVENIENCE = 0, LOAD_IO, LOAD_TYPED_IO, LOAD_FORMAT_SPECIFIC, LOAD_SIZED } LoadMode; /* Convert to RGBA for comparison, if necessary */ static bool ConvertToRgba32(SDL_Surface **surface_p) { if ((*surface_p)->format != SDL_PIXELFORMAT_RGBA32) { SDL_Surface *temp; SDL_ClearError(); temp = SDL_ConvertSurface(*surface_p, SDL_PIXELFORMAT_RGBA32); SDLTest_AssertCheck(temp != NULL, "Converting to RGBA should succeed (%s)", SDL_GetError()); if (temp == NULL) { return false; } SDL_DestroySurface(*surface_p); *surface_p = temp; } return true; } static void DumpPixels(const char *filename, SDL_Surface *surface) { const SDL_Palette *palette = SDL_GetSurfacePalette(surface); const unsigned char *pixels = surface->pixels; const unsigned char *p; size_t w, h, pitch; size_t i, j; SDL_Log("%s:\n", filename); if (palette) { size_t n = 0; if (palette->ncolors >= 0) { n = (size_t) palette->ncolors; } SDL_Log(" Palette:\n"); for (i = 0; i < n; i++) { SDL_Log(" RGBA[0x%02x] = %02x%02x%02x%02x\n", (unsigned) i, palette->colors[i].r, palette->colors[i].g, palette->colors[i].b, palette->colors[i].a); } } if (surface->w < 0) { SDL_Log(" Invalid width %d\n", surface->w); return; } if (surface->h < 0) { SDL_Log(" Invalid height %d\n", surface->h); return; } if (surface->pitch < 0) { SDL_Log(" Invalid pitch %d\n", surface->pitch); return; } w = (size_t) surface->w; h = (size_t) surface->h; pitch = (size_t) surface->pitch; SDL_Log(" Pixels:\n"); for (j = 0; j < h; j++) { SDL_Log(" "); for (i = 0; i < w; i++) { p = pixels + (j * pitch) + (i * SDL_BYTESPERPIXEL(surface->format)); switch (SDL_BITSPERPIXEL(surface->format)) { case 1: case 4: case 8: SDL_Log("%02x ", *p); break; case 12: case 15: case 16: SDL_Log("%02x", *p++); SDL_Log("%02x ", *p); break; case 24: SDL_Log("%02x", *p++); SDL_Log("%02x", *p++); SDL_Log("%02x ", *p); break; case 32: SDL_Log("%02x", *p++); SDL_Log("%02x", *p++); SDL_Log("%02x", *p++); SDL_Log("%02x ", *p); break; } } SDL_Log("\n"); } } static void FormatLoadTest(const Format *format, LoadMode mode) { SDL_Surface *reference = NULL; SDL_Surface *surface = NULL; SDL_IOStream *src = NULL; char *filename = NULL; char *refFilename = NULL; int diff; SDL_ClearError(); filename = GetTestFilename(TEST_FILE_DIST, format->sample); if (!SDLTest_AssertCheck(filename != NULL, "Building filename should succeed (%s)", SDL_GetError())) { goto out; } SDL_ClearError(); refFilename = GetTestFilename(TEST_FILE_DIST, format->reference); if (!SDLTest_AssertCheck(refFilename != NULL, "Building ref filename should succeed (%s)", SDL_GetError())) { goto out; } if (StrHasSuffix(format->reference, ".bmp")) { SDL_ClearError(); SDLTest_AssertPass("About to call SDL_LoadBMP(\"%s\")", refFilename); 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 SDL_ClearError(); SDLTest_AssertPass("About to call IMG_Load(\"%s\")", refFilename); reference = IMG_Load(refFilename); if (!SDLTest_AssertCheck(reference != NULL, "Loading reference should succeed (%s)", SDL_GetError())) { goto out; } #endif } if (mode != LOAD_CONVENIENCE) { SDL_ClearError(); SDLTest_AssertPass("About to call SDL_IOFromFile(\"%s\", \"rb\")", refFilename); src = SDL_IOFromFile(filename, "rb"); SDLTest_AssertCheck(src != NULL, "Opening %s should succeed (%s)", filename, SDL_GetError()); if (src == NULL) goto out; } SDL_ClearError(); switch (mode) { case LOAD_CONVENIENCE: SDLTest_AssertPass("About to call IMG_Load(\"%s\")", filename); surface = IMG_Load(filename); break; case LOAD_IO: if (format->checkFunction != NULL) { SDL_IOStream *ref_src; int check; SDLTest_AssertPass("About to call SDL_IOFromFile(\"%s\", \"rb\")", refFilename); ref_src = SDL_IOFromFile(refFilename, "rb"); SDLTest_AssertCheck(ref_src != NULL, "Opening %s should succeed (%s)", refFilename, SDL_GetError()); if (ref_src != NULL) { SDLTest_AssertPass("About to call IMG_Is%s()", format->name); check = format->checkFunction(ref_src); SDLTest_AssertCheck(!check, "Should not detect %s as %s -> %d", refFilename, format->name, check); SDL_CloseIO(ref_src); } } if (format->checkFunction != NULL) { SDLTest_AssertPass("About to call IMG_Is%s()", format->name); int check = format->checkFunction(src); SDLTest_AssertCheck(check, "Should detect %s as %s -> %d", filename, format->name, check); } SDL_ClearError(); SDLTest_AssertPass("About to call IMG_Load_IO(, true)"); surface = IMG_Load_IO(src, true); src = NULL; /* ownership taken */ break; case LOAD_TYPED_IO: SDLTest_AssertPass("About to call IMG_LoadTyped_IO(, true, \"%s\")", format->name); surface = IMG_LoadTyped_IO(src, true, format->name); src = NULL; /* ownership taken */ break; case LOAD_FORMAT_SPECIFIC: SDLTest_AssertPass("About to call IMG_Load%s_IO()", format->name); surface = format->loadFunction(src); break; case LOAD_SIZED: if (SDL_strcmp(format->name, "SVG-sized") == 0) { surface = IMG_LoadSizedSVG_IO(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 (GetStringBoolean(SDL_getenv("SDL_IMAGE_TEST_DEBUG"), false)) { DumpPixels(filename, surface); } if (reference != NULL) { ConvertToRgba32(&reference); ConvertToRgba32(&surface); diff = SDLTest_CompareSurfaces(surface, reference, format->tolerance); SDLTest_AssertCheck(diff == 0, "Surface differed from reference by at most %d in %d pixels", format->tolerance, diff); if (diff != 0 || GetStringBoolean(SDL_getenv("SDL_IMAGE_TEST_DEBUG"), false)) { DumpPixels(filename, surface); DumpPixels(refFilename, reference); } } out: if (surface != NULL) { SDL_DestroySurface(surface); } if (reference != NULL) { SDL_DestroySurface(reference); } if (src != NULL) { SDL_CloseIO(src); } if (refFilename != NULL) { SDL_free(refFilename); } if (filename != NULL) { SDL_free(filename); } } static void FormatSaveTest(const Format *format, bool rw) { char *refFilename = NULL; char filename[64] = { 0 }; SDL_Surface *reference = NULL; SDL_Surface *surface = NULL; SDL_Surface *indexed_surface = NULL; SDL_IOStream *dest = NULL; int diff; bool result; SDL_snprintf(filename, sizeof(filename), "save%s.%s", rw ? "Rwops" : "", format->name); SDL_ClearError(); refFilename = GetTestFilename(TEST_FILE_DIST, "sample.bmp"); if (!SDLTest_AssertCheck(refFilename != NULL, "Building ref filename should succeed (%s)", SDL_GetError())) { goto out; } SDL_ClearError(); reference = SDL_LoadBMP(refFilename); if (!SDLTest_AssertCheck(reference != NULL, "Loading reference should succeed (%s)", SDL_GetError())) { goto out; } SDL_ClearError(); if (rw) { dest = SDL_IOFromFile(filename, "wb"); result = IMG_SaveTyped_IO(reference, dest, false, format->name); SDL_CloseIO(dest); } else { result = IMG_Save(reference, filename); } SDLTest_AssertCheck(result, "Save %s (%s)", filename, SDL_GetError()); if (format->canLoad) { SDL_ClearError(); SDLTest_AssertPass("About to call IMG_Load(\"%s\")", filename); surface = IMG_Load(filename); if (!SDLTest_AssertCheck(surface != NULL, "Load %s (%s)", "saved file", SDL_GetError())) { goto out; } ConvertToRgba32(&reference); ConvertToRgba32(&surface); 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 most %d in %d pixels", format->tolerance, diff); if (diff != 0 || GetStringBoolean(SDL_getenv("SDL_IMAGE_TEST_DEBUG"), false)) { DumpPixels(filename, surface); DumpPixels(refFilename, reference); } } SDLTest_Log("Saving an indexed surface without palette MUST fail"); indexed_surface = SDL_CreateSurface(32, 32, SDL_PIXELFORMAT_INDEX8); SDLTest_AssertCheck(indexed_surface != NULL, "SDL_CreateSurface(SDL_PIXELFORMAT_INDEX8) must succeed"); if (indexed_surface) { if (rw) { Sint64 position; SDL_IOStream *dyn_io = SDL_IOFromDynamicMem(); SDLTest_AssertPass("About to call IMG_SaveTyped_IO(\"%s\")", format->name); result = IMG_SaveTyped_IO(indexed_surface, dyn_io, false, format->name); SDLTest_Assert(!result, "Calling IMG_SaveTyped_IO of an indexed surface without palette should return false (actual=%d)", result); position = SDL_TellIO(dyn_io) ; SDLTest_AssertCheck(position == 0, "Nothing has been written to IO stream (position=%" SDL_PRIs64 ")", position); SDL_CloseIO(dyn_io); } else { char indexed_filename[64] = { 0 }; SDL_snprintf(indexed_filename, sizeof(indexed_filename), "save%s_index.%s", rw ? "Rwops" : "", format->name); SDL_RemovePath(indexed_filename); SDLTest_AssertCheck(!SDL_GetPathInfo(indexed_filename, NULL), "\"%s\" should not exist BEFORE test", indexed_filename); SDLTest_AssertPass("About to call IMG_Save(\"%s\")", indexed_filename); result = IMG_Save(indexed_surface, indexed_filename); SDLTest_Assert(!result, "Calling IMG_Save of an indexed surface without palette should return false (actual=%d)", result); SDLTest_AssertCheck(!SDL_GetPathInfo(indexed_filename, NULL), "\"%s\" should not exist AFTER test", indexed_filename); } } out: if (surface != NULL) { SDL_DestroySurface(surface); } if (indexed_surface != NULL) { SDL_DestroySurface(indexed_surface); } if (reference != NULL) { SDL_DestroySurface(reference); } if (refFilename != NULL) { SDL_free(refFilename); } } static void FormatTest(const Format *format) { bool forced; char envVar[64] = { 0 }; SDL_snprintf(envVar, sizeof(envVar), "SDL_IMAGE_TEST_REQUIRE_LOAD_%s", format->name); forced = GetStringBoolean(SDL_getenv(envVar), 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 (SDL_strcmp(format->name, "SVG-sized") == 0) { FormatLoadTest(format, LOAD_SIZED); } else { FormatLoadTest(format, LOAD_CONVENIENCE); if (SDL_strcmp(format->name, "TGA") == 0) { SDLTest_Log("SKIP: Recognising %s by magic number is not supported", format->name); } else { FormatLoadTest(format, LOAD_IO); } FormatLoadTest(format, LOAD_TYPED_IO); 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), 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, false); FormatSaveTest(format, true); } else { SDLTest_Log("Saving format %s is not supported", format->name); } } static int SDLCALL TestFormats(void *arg) { size_t i; (void)arg; 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 }; int main(int argc, char *argv[]) { int result; int i, done; SDL_Event event; SDLTest_TestSuiteRunner *runner; /* Initialize test framework */ state = SDLTest_CommonCreateState(argv, SDL_INIT_VIDEO); if (!state) { return 1; } runner = SDLTest_CreateTestSuiteRunner(state, testSuites); if (!SDLTest_CommonDefaultArgs(state, argc, argv)) { return 1; } /* Initialize common state */ if (!SDLTest_CommonInit(state)) { goto failure; } /* Create the windows, initialize the renderers */ for (i = 0; i < state->num_windows; ++i) { SDL_Renderer *renderer = state->renderers[i]; SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF); SDL_RenderClear(renderer); } /* Call Harness */ result = SDLTest_ExecuteTestSuiteRunner(runner); /* Empty event queue */ done = 0; for (i=0; i<100; i++) { while (SDL_PollEvent(&event)) { SDLTest_CommonEvent(state, &event, &done); } SDL_Delay(10); } /* Shutdown everything */ SDLTest_CommonQuit(state); return result; failure: SDLTest_CommonQuit(state); return 1; }