// Animation Decoder/Encoder Test Suite written by Xen #include #include #include static bool apply_dummy_metadata; #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 #define MAX_METADATA 6 // Add a minimum of two frames so we don't conflict with specific formats (like WEBP). #define MIN_FRAMES 2 typedef struct { int width; int height; Uint64 duration; } FrameData; typedef struct { const char *metadata; const void *value; SDL_PropertyType type; } MetadataInfo; typedef struct { const char* format; MetadataInfo availableMetadata[MAX_METADATA]; } FormatInfo; static const Sint64 default_loop_count = 1; #define DEFAULT_TITLE "Lorem ipsum dolor sit amet" #define DEFAULT_AUTHOR "consectetur adipiscing elit" #define DEFAULT_DESCRIPTION "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua" #define DEFAULT_CREATION_TIME "Ut enim ad minim veniam" #define DEFAULT_COPYRIGHT "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat" static const FormatInfo formatInfo[] = { { "ANI", { { IMG_PROP_METADATA_TITLE_STRING, (const void*)DEFAULT_TITLE, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_AUTHOR_STRING, (const void*)DEFAULT_AUTHOR, SDL_PROPERTY_TYPE_STRING } } }, { "APNG", { { IMG_PROP_METADATA_TITLE_STRING, (const void*)DEFAULT_TITLE, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_AUTHOR_STRING, (const void*)DEFAULT_AUTHOR, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_DESCRIPTION_STRING, (const void*)DEFAULT_DESCRIPTION, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_LOOP_COUNT_NUMBER, (const void *)&default_loop_count, SDL_PROPERTY_TYPE_NUMBER }, { IMG_PROP_METADATA_CREATION_TIME_STRING, (const void*)DEFAULT_CREATION_TIME, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_COPYRIGHT_STRING, (const void*)DEFAULT_COPYRIGHT, SDL_PROPERTY_TYPE_STRING } } }, { "AVIFS", { { IMG_PROP_METADATA_TITLE_STRING, (const void*)DEFAULT_TITLE, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_AUTHOR_STRING, (const void*)DEFAULT_AUTHOR, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_DESCRIPTION_STRING, (const void*)DEFAULT_DESCRIPTION, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_LOOP_COUNT_NUMBER, (const void *)&default_loop_count, SDL_PROPERTY_TYPE_NUMBER }, { IMG_PROP_METADATA_CREATION_TIME_STRING, (const void*)DEFAULT_CREATION_TIME, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_COPYRIGHT_STRING, (const void*)DEFAULT_COPYRIGHT, SDL_PROPERTY_TYPE_STRING } } }, { "GIF", { { IMG_PROP_METADATA_DESCRIPTION_STRING, (const void*)DEFAULT_DESCRIPTION, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_LOOP_COUNT_NUMBER, (const void *)&default_loop_count, SDL_PROPERTY_TYPE_NUMBER } } }, { "WEBP", { { IMG_PROP_METADATA_TITLE_STRING, (const void*)DEFAULT_TITLE, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_AUTHOR_STRING, (const void*)DEFAULT_AUTHOR, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_DESCRIPTION_STRING, (const void*)DEFAULT_DESCRIPTION, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_LOOP_COUNT_NUMBER, (const void *)&default_loop_count, SDL_PROPERTY_TYPE_NUMBER }, { IMG_PROP_METADATA_CREATION_TIME_STRING, (const void*)DEFAULT_CREATION_TIME, SDL_PROPERTY_TYPE_STRING }, { IMG_PROP_METADATA_COPYRIGHT_STRING, (const void*)DEFAULT_COPYRIGHT, SDL_PROPERTY_TYPE_STRING } } }, }; static const struct { const char *format; const char *filename; } inputImages[] = { { "ANI", "rgbrgb.ani" }, { "APNG", "rgbrgb.png" }, { "AVIFS", "rgbrgb.avifs" }, { "GIF", "rgbrgb.gif" }, { "WEBP", "rgbrgb.webp" } }; static const char *outputImageFormats[] = { "ANI", "APNG", "AVIFS", "GIF", "WEBP" }; static const char *GetAnimationDecoderStatusString(IMG_AnimationDecoderStatus status) { switch (status) { case IMG_DECODER_STATUS_OK: return "IMG_DECODER_STATUS_OK"; case IMG_DECODER_STATUS_COMPLETE: return "IMG_DECODER_STATUS_COMPLETE"; case IMG_DECODER_STATUS_FAILED: return "IMG_DECODER_STATUS_FAILED"; case IMG_DECODER_STATUS_INVALID: default: return "IMG_DECODER_STATUS_INVALID"; } } static bool FormatAnimationEnabled(const char *format) { char env_name[64]; SDL_snprintf(env_name, sizeof(env_name), "SDL_IMAGE_ANIM_%s", format); SDL_strupr(env_name); const char *env_value = SDL_getenv(env_name); if (!env_value) { return true; } if (env_value[0] == '\0' || SDL_strcmp(env_value, "0") == 0 || SDL_strcasecmp(env_value, "no") == 0 || SDL_strcasecmp(env_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. * * Fails and returns NULL if out of memory. */ static char * GetTestFilename(const char *file) { const char *base; char *path = NULL; bool needPathSep = true; base = SDL_getenv("SDL_TEST_SRCDIR"); 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 int SDLCALL testDecodeEncode(void *args) { (void) args; SDLTest_Log("Starting Animation Decoder/Encoder Tests -- Decode, encode decoded frames then decode again to compare..."); SDLTest_Log("--TESTS--"); for (size_t cim = 0; cim < SDL_arraysize(inputImages); ++cim) { for (size_t com = 0; com < SDL_arraysize(outputImageFormats); ++com) { const char *inputImage = inputImages[cim].filename; const char *outputImageFormat = outputImageFormats[com]; SDLTest_Log("%s >> %s == %s", inputImage, outputImageFormat, inputImage); if (com == 0) { SDLTest_Log("--- Rewind Tests for %s ---", inputImage); SDLTest_Log("--- Metadata Tests for %s ---", inputImage); } } } for (size_t cim = 0; cim < SDL_arraysize(inputImages); ++cim) { for (size_t com = 0; com < SDL_arraysize(outputImageFormats); ++com) { const char *inputImage = inputImages[cim].filename; const char *outputImageFormat = outputImageFormats[com]; if (!FormatAnimationEnabled(inputImages[cim].format)) { SDLTest_Log("Animation format %s disabled (input)", inputImages[cim].format); continue; } if (!FormatAnimationEnabled(outputImageFormats[com])) { SDLTest_Log("animation format %s disabled (output)", outputImageFormats[com]); continue; } char *inputImagePath = GetTestFilename(inputImage); if (!inputImagePath) { SDLTest_LogError("Failed to convert '%s' to absolute path", inputImage); return TEST_ABORTED; } SDLTest_Log("Input Image: %s (%s)", inputImage, inputImagePath); SDLTest_Log("Output Format: %s", outputImageFormat); IMG_AnimationDecoder *decoder = IMG_CreateAnimationDecoder(inputImagePath); SDL_free(inputImagePath); SDLTest_AssertCheck(decoder != NULL, "IMG_CreateAnimationDecoder: Failed to create animation decoder for input image: %s", SDL_GetError()); if (!decoder) { continue; } SDL_IOStream *encoderIO = SDL_IOFromDynamicMem(); SDLTest_AssertCheck(encoderIO != NULL, "SDL_IOFromDynamicMem"); if (!encoderIO) { SDLTest_LogError("Failed to create IO stream for output image: %s", SDL_GetError()); IMG_CloseAnimationDecoder(decoder); return TEST_ABORTED; } SDL_PropertiesID encoderProps = SDL_CreateProperties(); SDLTest_AssertCheck(encoderProps, "SDL_CreateProperties"); if (!encoderProps) { SDLTest_LogError("Failed to create properties for encoder: %s", SDL_GetError()); IMG_CloseAnimationDecoder(decoder); SDL_CloseIO(encoderIO); return TEST_ABORTED; } SDL_SetPointerProperty(encoderProps, IMG_PROP_ANIMATION_ENCODER_CREATE_IOSTREAM_POINTER, encoderIO); SDL_SetBooleanProperty(encoderProps, IMG_PROP_ANIMATION_ENCODER_CREATE_IOSTREAM_AUTOCLOSE_BOOLEAN, false); SDL_SetStringProperty(encoderProps, IMG_PROP_ANIMATION_ENCODER_CREATE_TYPE_STRING, outputImageFormat); if (apply_dummy_metadata) { SDLTest_Log("Applying dummy metadata..."); // Most formats hold metadata in the beginning of the file (except WEBP and AVIF since those are EXIF and XMP), // when that is the case, the changes to metadata parser can destroy the reading of the frames as well since they // might do modifications to the stream. For that reason we add dummy metadata to the encoder properties to make // sure that the metadata code does not interfere with the frame reading process. for (size_t k = 0; k < SDL_arraysize(formatInfo); ++k) { const char *format = formatInfo[k].format; for (int mi = 0; mi < MAX_METADATA; ++mi) { const MetadataInfo *metadata = &formatInfo[k].availableMetadata[mi]; if (!metadata || !metadata->metadata || !metadata->value) { continue; } switch (metadata->type) { case SDL_PROPERTY_TYPE_BOOLEAN: SDL_SetBooleanProperty(encoderProps, metadata->metadata, *(bool *) metadata->value); break; case SDL_PROPERTY_TYPE_NUMBER: SDL_SetNumberProperty(encoderProps, metadata->metadata, *(Sint64 *) metadata->value); break; case SDL_PROPERTY_TYPE_STRING: SDL_SetStringProperty(encoderProps, metadata->metadata, (const char *)metadata->value); break; default: SDLTest_AssertCheck(false, "ERROR: Unsupported metadata type for %s: %d", format, metadata->type); SDL_DestroyProperties(encoderProps); IMG_CloseAnimationDecoder(decoder); SDL_CloseIO(encoderIO); return TEST_ABORTED; } } } } IMG_AnimationEncoder *encoder = IMG_CreateAnimationEncoderWithProperties(encoderProps); SDLTest_AssertCheck(encoder != NULL, "IMG_CreateAnimationEncoderWithProperties"); SDL_DestroyProperties(encoderProps); int arrayCapacity = 32; int arraySize = 0; FrameData **decodedFrameData = (FrameData **) SDL_calloc(arrayCapacity, sizeof(*decodedFrameData)); if (encoder) { Uint64 duration; SDL_Surface *frame; int i = 0, ii = 0; while (IMG_GetAnimationDecoderFrame(decoder, &frame, &duration)) { if (frame) { SDLTest_Log("Frame Duration (%i): %" SDL_PRIu64 " ms", i, duration); SDLTest_Log("Frame Format (%i): %s", i, SDL_GetPixelFormatName(frame->format)); SDLTest_Log("Frame Size (%i): %ix%i", i, frame->w, frame->h); bool result = IMG_AddAnimationEncoderFrame(encoder, frame, duration); SDLTest_AssertCheck(result, "IMG_AddAnimationEncoderFrame"); if (result) { SDLTest_Log("Frame (%i) added to encoder.", i); ii++; } else { SDLTest_Log("ERROR: Failed to add frame (%i) to encoder: %s", i, SDL_GetError()); for (int ai = 0; ai < arraySize; ++ai) { SDL_free(decodedFrameData[ai]); } SDL_free(decodedFrameData); IMG_CloseAnimationEncoder(encoder); IMG_CloseAnimationDecoder(decoder); return TEST_ABORTED; } if (arraySize >= arrayCapacity) { arrayCapacity *= 2; FrameData **temp = (FrameData **) SDL_realloc(decodedFrameData, arrayCapacity * sizeof(*decodedFrameData)); SDLTest_AssertCheck(temp != NULL, "SDL_realloc"); if (!temp) { SDLTest_LogError("ERROR: Failed to reallocate memory for decoded frames."); for (int ai = 0; ai < arraySize; ++ai) { SDL_free(decodedFrameData[ai]); } SDL_free(decodedFrameData); IMG_CloseAnimationEncoder(encoder); IMG_CloseAnimationDecoder(decoder); return TEST_ABORTED; } decodedFrameData = temp; } decodedFrameData[arraySize] = (FrameData *) SDL_calloc(1, sizeof(FrameData)); SDLTest_AssertCheck(decodedFrameData[arraySize] != NULL, "decodedFrameData[arraySize]"); if (!decodedFrameData[arraySize]) { SDLTest_LogError("ERROR: Failed to allocate memory for frame data."); for (int ai = 0; ai < arraySize; ++ai) { SDL_free(decodedFrameData[ai]); } SDL_free(decodedFrameData); IMG_CloseAnimationEncoder(encoder); IMG_CloseAnimationDecoder(decoder); return TEST_ABORTED; } decodedFrameData[arraySize]->width = frame->w; decodedFrameData[arraySize]->height = frame->h; decodedFrameData[arraySize]->duration = duration; arraySize++; } else { SDLTest_AssertCheck(false, "IMG_GetAnimationDecoderFrame"); SDLTest_LogError("Failed to get frame: %s", SDL_GetError()); for (int ai = 0; ai < arraySize; ++ai) { SDL_free(decodedFrameData[ai]); } SDL_free(decodedFrameData); IMG_CloseAnimationEncoder(encoder); IMG_CloseAnimationDecoder(decoder); return TEST_ABORTED; } i++; } if (!IMG_CloseAnimationEncoder(encoder)) { SDLTest_AssertCheck(false, "IMG_CloseAnimationEncoder"); SDLTest_LogError("Failed to close animation encoder: %s", SDL_GetError()); for (int ai = 0; ai < arraySize; ++ai) { SDL_free(decodedFrameData[ai]); } SDL_free(decodedFrameData); IMG_CloseAnimationDecoder(decoder); SDL_CloseIO(encoderIO); return TEST_COMPLETED; } else { SDLTest_AssertPass("Encoder closed successfully after adding %i frames.", ii); } SDLTest_AssertCheck(ii == i, "All frames were added to the encoder. Added %i, Total: %i", ii, i); if (ii != i) { for (int ai = 0; ai < arraySize; ++ai) { SDL_free(decodedFrameData[ai]); } SDL_free(decodedFrameData); IMG_CloseAnimationDecoder(decoder); SDL_CloseIO(encoderIO); return TEST_COMPLETED; } else { SDLTest_Log("All frames added to the encoder successfully."); SDLTest_Log("Total frames processed: %i", i); SDLTest_Log( "Loading memory stream for %s back to IMG_AnimationDecoder to see if it results the same as %s", outputImageFormat, inputImage); Sint64 newpos = SDL_SeekIO(encoderIO, 0, SDL_IO_SEEK_SET); SDLTest_AssertCheck(newpos == 0, "Failed to seek to the beginning of the IO stream: %s", SDL_GetError()); if (newpos != 0) { for (int ai = 0; ai < arraySize; ++ai) { SDL_free(decodedFrameData[ai]); } SDL_free(decodedFrameData); IMG_CloseAnimationDecoder(decoder); SDL_CloseIO(encoderIO); return TEST_ABORTED; } IMG_AnimationDecoder *decoder2 = IMG_CreateAnimationDecoder_IO(encoderIO, false, outputImageFormat); SDLTest_AssertCheck(decoder2 != NULL, "IMG_CreateAnimationDecoder_IO"); if (decoder2) { bool check_duration = true; Uint64 duration2; SDL_Surface *frame2; int j = 0; if (SDL_strcmp(inputImages[cim].format, "ANI") == 0 || SDL_strcmp(outputImageFormat, "ANI") == 0) { /* ANI uses 1/60 time units which can't represent our 20 ms sample frame durations. * If we switched the sample data to be 100 FPS, that would work for both, but as-is... */ check_duration = false; } while (IMG_GetAnimationDecoderFrame(decoder2, &frame2, &duration2)) { SDLTest_Log("Reloaded Frame Duration (%i): %" SDL_PRIu64 " ms", j, duration2); SDLTest_Log("Reloaded Frame Format (%i): %s", j, SDL_GetPixelFormatName(frame2->format)); SDLTest_Log("Reloaded Frame Size (%i): %ix%i", j, frame2->w, frame2->h); if (arraySize < j) { SDLTest_LogError("Reloaded frame index %i exceeds decoded frame data size %d", j, arraySize); for (int ai = 0; ai < arraySize; ++ai) { SDL_free(decodedFrameData[ai]); } SDL_free(decodedFrameData); IMG_CloseAnimationDecoder(decoder2); IMG_CloseAnimationDecoder(decoder); SDL_CloseIO(encoderIO); return TEST_ABORTED; } if (decodedFrameData[j]->width != frame2->w || decodedFrameData[j]->height != frame2->h || (check_duration && decodedFrameData[j]->duration != duration2)) { SDLTest_LogError("Frame data mismatch at index %i. Expected (%i, %i, %" SDL_PRIu64 "), Got (%i, %i, %" SDL_PRIu64 ")", j, decodedFrameData[j]->width, decodedFrameData[j]->height, decodedFrameData[j]->duration, frame2->w, frame2->h, duration2); for (int ai = 0; ai < arraySize; ++ai) { SDL_free(decodedFrameData[ai]); } SDL_free(decodedFrameData); IMG_CloseAnimationDecoder(decoder2); IMG_CloseAnimationDecoder(decoder); SDL_CloseIO(encoderIO); return TEST_ABORTED; } else { SDLTest_Log("Reloaded frame matches original frame data at index %i.", j); } j++; } if (j != i) { SDLTest_LogError("Frame count mismatch after reloading output. Expected %i, Got %i", i, j); for (int ai = 0; ai < arraySize; ++ai) { SDL_free(decodedFrameData[ai]); } SDL_free(decodedFrameData); IMG_CloseAnimationDecoder(decoder2); IMG_CloseAnimationDecoder(decoder); SDL_CloseIO(encoderIO); return TEST_ABORTED; } else { SDLTest_Log("All frames reloaded successfully from memory stream for %s with frame count %i.", outputImageFormat, j); } IMG_CloseAnimationDecoder(decoder2); } else { SDLTest_LogError("Failed to create animation decoder for output image: %s", SDL_GetError()); for (int ai = 0; ai < arraySize; ++ai) { SDL_free(decodedFrameData[ai]); } SDL_free(decodedFrameData); IMG_CloseAnimationDecoder(decoder); SDL_CloseIO(encoderIO); return TEST_ABORTED; } } SDL_CloseIO(encoderIO); } else { SDLTest_LogError("Failed to create animation encoder for output format %s: %s", outputImageFormat, SDL_GetError()); for (int ai = 0; ai < arraySize; ++ai) { SDL_free(decodedFrameData[ai]); } SDL_free(decodedFrameData); IMG_CloseAnimationDecoder(decoder); return TEST_ABORTED; } for (int ai = 0; ai < arraySize; ++ai) { SDL_free(decodedFrameData[ai]); } SDL_free(decodedFrameData); IMG_CloseAnimationDecoder(decoder); } } SDLTest_Log("Finished test 'Starting Animation Decoder/Encoder Tests'."); return TEST_COMPLETED; } static int SDLCALL testDecoderRewind(void *args) { (void)args; SDLTest_Log("Starting test 'Decoder Rewind Test'"); for (size_t cim = 0; cim < SDL_arraysize(outputImageFormats); ++cim) { const char *outputImageFormat = outputImageFormats[cim]; if (!FormatAnimationEnabled(outputImageFormats[cim])) { SDLTest_Log("animation format %s disabled (output)", outputImageFormats[cim]); continue; } SDL_IOStream *rewindEncoderIO = SDL_IOFromDynamicMem(); SDLTest_AssertCheck(rewindEncoderIO != NULL, "SDL_IOFromDynamicMem"); if (!rewindEncoderIO) { SDLTest_LogError("Failed to create IO stream for frame encoder: %s", SDL_GetError()); return TEST_ABORTED; } IMG_AnimationEncoder *encoder = IMG_CreateAnimationEncoder_IO(rewindEncoderIO, false, outputImageFormat); if (!encoder) { SDLTest_LogError("Failed to create animation encoder for output format %s: %s", outputImageFormat, SDL_GetError()); SDL_CloseIO(rewindEncoderIO); return TEST_ABORTED; } // We have to create different colors for each frame to ensure they are distinguishable. Otherwise, some formats like WEBP truncates identical repeating frames. for (int fi = 0; fi < MIN_FRAMES; ++fi) { SDL_Surface *frame = SDL_CreateSurface(256, 256, SDL_PIXELFORMAT_RGBA32); SDLTest_AssertCheck(frame != NULL, "SDL_CreateSurface"); if (!frame) { SDLTest_LogError("Failed to create surface for frame: %s", SDL_GetError()); IMG_CloseAnimationEncoder(encoder); SDL_CloseIO(rewindEncoderIO); return TEST_ABORTED; } const SDL_PixelFormatDetails *pixelFormatDetails = SDL_GetPixelFormatDetails(frame->format); bool result = SDL_FillSurfaceRect(frame, NULL, SDL_MapRGBA(pixelFormatDetails, NULL, (Uint8)SDL_rand(256), (Uint8)SDL_rand(256), (Uint8)SDL_rand(256), 255)); SDLTest_AssertCheck(result, "SDL_FillSurfaceRect: Failed to fill surface with red color: %s", SDL_GetError()); result = IMG_AddAnimationEncoderFrame(encoder, frame, 1000); SDLTest_AssertCheck(result, "IMG_AddAnimationEncoderFrame"); if (result) { SDLTest_Log("Frame %i added to encoder successfully.", fi); SDL_DestroySurface(frame); } else { SDLTest_LogError("Failed to add frame to encoder: %sn", SDL_GetError()); SDL_DestroySurface(frame); IMG_CloseAnimationEncoder(encoder); SDL_CloseIO(rewindEncoderIO); return TEST_ABORTED; } } if (!IMG_CloseAnimationEncoder(encoder)) { SDLTest_LogError("Failed to close animation encoder for frame: %s", SDL_GetError()); SDL_CloseIO(rewindEncoderIO); return TEST_ABORTED; } SDLTest_Log("Frames encoded successfully for output format %s.", outputImageFormat); if (SDL_SeekIO(rewindEncoderIO, 0, SDL_IO_SEEK_SET) != 0) { SDLTest_AssertCheck(false, "SDL_SeekIO"); SDLTest_LogError("Failed to seek to the beginning of the IO stream for frame encoder: %s", SDL_GetError()); SDL_CloseIO(rewindEncoderIO); return TEST_ABORTED; } IMG_AnimationDecoder *rewindDecoder = IMG_CreateAnimationDecoder_IO(rewindEncoderIO, false, outputImageFormat); if (rewindDecoder) { SDLTest_Log("Rewind decoder created successfully for output format %s.", outputImageFormat); SDLTest_Log("Decoding %i frames first.", MIN_FRAMES); for (int ri = 0; ri < MIN_FRAMES; ++ri) { if (!IMG_GetAnimationDecoderFrame(rewindDecoder, NULL, NULL)) { SDLTest_AssertCheck(false, "IMG_GetAnimationDecoderFrame"); SDLTest_LogError("Failed to get frame %i from rewind decoder. status: %s --- SDL Error: %s", ri, GetAnimationDecoderStatusString(IMG_GetAnimationDecoderStatus(rewindDecoder)), SDL_GetError()); IMG_CloseAnimationDecoder(rewindDecoder); SDL_CloseIO(rewindEncoderIO); return TEST_ABORTED; } } SDLTest_Log("Now resetting the animation decoder..."); if (!IMG_ResetAnimationDecoder(rewindDecoder)) { SDLTest_AssertCheck(false, "IMG_ResetAnimationDecoder"); SDLTest_LogError("Failed to reset rewind decoder: %s", SDL_GetError()); IMG_CloseAnimationDecoder(rewindDecoder); SDL_CloseIO(rewindEncoderIO); return TEST_ABORTED; } SDLTest_Log("Decoding %i frames again after reset.", MIN_FRAMES); for (int ri = 0; ri < MIN_FRAMES; ++ri) { if (!IMG_GetAnimationDecoderFrame(rewindDecoder, NULL, NULL)) { SDLTest_AssertCheck(false, "IMG_GetAnimationDecoderFrame"); SDLTest_LogError("Failed to get frame %i from rewind decoder. status: %s --- SDL Error: %s", ri, GetAnimationDecoderStatusString(IMG_GetAnimationDecoderStatus(rewindDecoder)), SDL_GetError()); IMG_CloseAnimationDecoder(rewindDecoder); SDL_CloseIO(rewindEncoderIO); return TEST_ABORTED; } } } else { SDLTest_AssertCheck(false, "IMG_CreateAnimationDecoder_IO"); SDLTest_LogError("Failed to create rewind decoder for input image: %s", SDL_GetError()); SDL_CloseIO(rewindEncoderIO); return TEST_ABORTED; } SDL_CloseIO(rewindEncoderIO); SDLTest_Log("Finished rewind test for output format %s.", outputImageFormat); } SDLTest_Log("Finished test 'Decoder Rewind Test'."); return TEST_COMPLETED; } static int SDLCALL testEncodeDecodeMetadata(void *args) { (void)args; SDLTest_Log("========================================================="); SDLTest_Log("========================================================="); SDLTest_Log("Starting test 'Encode Metadata and Decode Metadata Test'"); for (size_t k = 0; k < SDL_arraysize(formatInfo); ++k) { const char *format = formatInfo[k].format; if (!FormatAnimationEnabled(format)) { SDLTest_Log("animation format %s disabled", format); continue; } SDLTest_Log("Testing format: %s", format); SDL_IOStream *metadataEncoderIO = SDL_IOFromDynamicMem(); SDLTest_AssertCheck(metadataEncoderIO != NULL, "SDL_IOFromDynamicMem"); if (!metadataEncoderIO) { SDLTest_LogError("Failed to create IO stream for metadata encoder: %s", SDL_GetError()); return TEST_ABORTED; } SDL_PropertiesID metadataProps = SDL_CreateProperties(); SDLTest_AssertCheck(metadataProps != 0, "SDL_CreateProperties"); if (!metadataProps) { SDLTest_LogError("Failed to create properties for metadata encoder: %s", SDL_GetError()); SDL_CloseIO(metadataEncoderIO); return TEST_ABORTED; } for (int mi = 0; mi < MAX_METADATA; ++mi) { const MetadataInfo *metadata = &formatInfo[k].availableMetadata[mi]; if (!metadata || !metadata->metadata || !metadata->value) { continue; } switch (metadata->type) { case SDL_PROPERTY_TYPE_BOOLEAN: SDL_SetBooleanProperty(metadataProps, metadata->metadata, *(bool *) metadata->value); break; case SDL_PROPERTY_TYPE_NUMBER: SDL_SetNumberProperty(metadataProps, metadata->metadata, *(Sint64 *) metadata->value); break; case SDL_PROPERTY_TYPE_STRING: SDL_SetStringProperty(metadataProps, metadata->metadata, (const char *) metadata->value); break; default: SDLTest_AssertCheck(false, "Unsupported metadata type for %s: %d", metadata->metadata, metadata->type); SDL_DestroyProperties(metadataProps); SDL_CloseIO(metadataEncoderIO); return TEST_ABORTED; } } SDL_SetStringProperty(metadataProps, IMG_PROP_ANIMATION_ENCODER_CREATE_TYPE_STRING, format); SDL_SetPointerProperty(metadataProps, IMG_PROP_ANIMATION_ENCODER_CREATE_IOSTREAM_POINTER, metadataEncoderIO); SDL_SetBooleanProperty(metadataProps, IMG_PROP_ANIMATION_ENCODER_CREATE_IOSTREAM_AUTOCLOSE_BOOLEAN, false); IMG_AnimationEncoder *metadataEncoder = IMG_CreateAnimationEncoderWithProperties(metadataProps); SDLTest_AssertCheck(metadataEncoder != NULL, "IMG_CreateAnimationEncoderWithProperties"); if (!metadataEncoder) { SDLTest_LogError("Failed to create animation encoder for format %s: %s", format, SDL_GetError()); SDL_DestroyProperties(metadataProps); SDL_CloseIO(metadataEncoderIO); return TEST_ABORTED; } SDL_DestroyProperties(metadataProps); for (int fi = 0; fi < MIN_FRAMES; ++fi) { SDL_Surface *metadataFrame = SDL_CreateSurface(256, 256, SDL_PIXELFORMAT_RGBA32); SDLTest_AssertCheck(metadataFrame != NULL, "SDL_CreateSurface"); if (!metadataFrame) { SDLTest_LogError("Failed to create surface for metadata frame: %s", SDL_GetError()); IMG_CloseAnimationEncoder(metadataEncoder); SDL_CloseIO(metadataEncoderIO); return TEST_ABORTED; } const SDL_PixelFormatDetails *metadataPixelFormatDetails = SDL_GetPixelFormatDetails(metadataFrame->format); bool result = SDL_FillSurfaceRect(metadataFrame, NULL, SDL_MapRGBA(metadataPixelFormatDetails, NULL, (Uint8) SDL_rand(256), (Uint8) SDL_rand(256), (Uint8) SDL_rand(256), 255)); SDLTest_AssertCheck(result, "SDL_FillSurfaceRect"); if (!result) { SDLTest_LogError("Failed to fill surface with green color for metadata frame: %s", SDL_GetError()); SDL_DestroySurface(metadataFrame); IMG_CloseAnimationEncoder(metadataEncoder); SDL_CloseIO(metadataEncoderIO); return TEST_ABORTED; } if (IMG_AddAnimationEncoderFrame(metadataEncoder, metadataFrame, 1000)) { SDLTest_Log("Metadata frame added to encoder successfully."); SDL_DestroySurface(metadataFrame); } else { SDLTest_LogError("Failed to add metadata frame to encoder: %s", SDL_GetError()); SDL_DestroySurface(metadataFrame); IMG_CloseAnimationEncoder(metadataEncoder); SDL_CloseIO(metadataEncoderIO); return TEST_ABORTED; } } bool result = IMG_CloseAnimationEncoder(metadataEncoder); SDLTest_AssertCheck(result, "IMG_CloseAnimationEncoder"); if (!result) { SDLTest_LogError("Failed to close animation encoder for format %s: %s", format, SDL_GetError()); SDL_CloseIO(metadataEncoderIO); return TEST_ABORTED; } SDLTest_Log("Metadata frame encoded successfully for format %s.", format); if (SDL_SeekIO(metadataEncoderIO, 0, SDL_IO_SEEK_SET) != 0) { SDLTest_AssertCheck(false, "SDL_SeekIO"); SDLTest_LogError("Failed to seek to the beginning of the IO stream for metadata encoder: %s", SDL_GetError()); SDL_CloseIO(metadataEncoderIO); return TEST_ABORTED; } IMG_AnimationDecoder *metadataDecoder = IMG_CreateAnimationDecoder_IO(metadataEncoderIO, false, format); SDLTest_AssertCheck(metadataDecoder != NULL, "IMG_CreateAnimationDecoder_IO"); if (metadataDecoder) { SDL_PropertiesID decodedProps = IMG_GetAnimationDecoderProperties(metadataDecoder); SDLTest_AssertCheck(decodedProps != 0, "IMG_GetAnimationDecoderProperties"); if (!decodedProps) { SDLTest_LogError("Failed to get properties from metadata decoder: %s", SDL_GetError()); IMG_CloseAnimationDecoder(metadataDecoder); SDL_CloseIO(metadataEncoderIO); return TEST_ABORTED; } for (int i = 0; i < MAX_METADATA; ++i) { const MetadataInfo *metadata = &formatInfo[k].availableMetadata[i]; if (!metadata || !metadata->metadata || !metadata->value) { continue; } const char *propName = metadata->metadata; if (metadata->type == SDL_PROPERTY_TYPE_BOOLEAN) { bool propValue = SDL_GetBooleanProperty(decodedProps, propName, false); SDLTest_Log("Decoded Property %s: %s", propName, propValue ? "true" : "false"); const bool expectedValue = *(const bool *) metadata->value; SDLTest_AssertCheck(propValue == expectedValue, "Decoded boolean property %s should match expected value. Expected: %s, Got: %s", propName, expectedValue ? "true" : "false", propValue ? "true" : "false"); if (propValue != expectedValue) { IMG_CloseAnimationDecoder(metadataDecoder); SDL_CloseIO(metadataEncoderIO); return TEST_ABORTED; } } else if (metadata->type == SDL_PROPERTY_TYPE_NUMBER) { Sint64 propValue = SDL_GetNumberProperty(decodedProps, propName, 0); SDLTest_Log("Decoded Property %s: %" SDL_PRIs64 "", propName, propValue); const Sint64 expectedValue = *(const Sint64 *) metadata->value; SDLTest_AssertCheck(propValue == expectedValue, "Decoded number property %s should match expected value. Expected: %" SDL_PRIs64 ", Got: %" SDL_PRIs64 "", propName, expectedValue, propValue); if (propValue != expectedValue) { IMG_CloseAnimationDecoder(metadataDecoder); SDL_CloseIO(metadataEncoderIO); return TEST_ABORTED; } } else if (metadata->type == SDL_PROPERTY_TYPE_STRING) { const char *propValue = SDL_GetStringProperty(decodedProps, propName, NULL); SDLTest_Log("Decoded Property %s: %s", propName, propValue ? propValue : "NULL"); const char *expectedValue = (const char *) metadata->value; if (propValue == NULL && expectedValue == NULL) { SDLTest_Log("Both decoded and expected string properties are NULL for %s.", propName); } else if (propValue == NULL || expectedValue == NULL || SDL_strcmp(propValue, expectedValue) != 0) { SDLTest_AssertCheck(false, "Decoded string property %s does not match expected value. Expected: %s, Got: %s", propName, expectedValue ? expectedValue : "NULL", propValue ? propValue : "NULL"); IMG_CloseAnimationDecoder(metadataDecoder); SDL_CloseIO(metadataEncoderIO); return TEST_ABORTED; } } else { SDLTest_LogError("Unsupported metadata type for %s: %d", propName, metadata->type); IMG_CloseAnimationDecoder(metadataDecoder); SDL_CloseIO(metadataEncoderIO); return TEST_ABORTED; } } IMG_CloseAnimationDecoder(metadataDecoder); } else { SDLTest_AssertCheck(false, "Failed to create animation decoder for format %s: %s", format, SDL_GetError()); SDL_CloseIO(metadataEncoderIO); SDL_Quit(); return TEST_ABORTED; } SDL_CloseIO(metadataEncoderIO); SDLTest_Log("Finished metadata encoding and decoding test for format %s.", format); } SDLTest_Log("Finished test 'Encode Metadata and Decode Metadata Test'."); return TEST_COMPLETED; } static int SDLCALL testDecodeThirdPartyMetadata(void *args) { (void)args; SDLTest_Log("Starting test 'Decode Third Party Metadata Test'"); if (!FormatAnimationEnabled("webp")) { SDLTest_Log("Animation format webp disabled, skipping test"); return TEST_SKIPPED; } const char *inputImage = "rgbrgb_thirdpartymetadata.webp"; char *inputImagePath = GetTestFilename(inputImage); if (!inputImagePath) { SDLTest_LogError("Failed to convert '%s' to absolute path", inputImage); return TEST_ABORTED; } IMG_AnimationDecoder *thirdPartyDecoder = IMG_CreateAnimationDecoder(inputImagePath); SDLTest_AssertCheck(thirdPartyDecoder != NULL, "IMG_CreateAnimationDecoder"); SDL_free(inputImagePath); if (thirdPartyDecoder) { SDL_PropertiesID thirdPartyMetadata = IMG_GetAnimationDecoderProperties(thirdPartyDecoder); SDLTest_AssertCheck(thirdPartyMetadata != 0, "IMG_GetAnimationDecoderProperties"); if (!thirdPartyMetadata) { SDLTest_LogError("Failed to get properties from third party metadata decoder: %s", SDL_GetError()); IMG_CloseAnimationDecoder(thirdPartyDecoder); return TEST_ABORTED; } const char *xmpDesc = SDL_GetStringProperty(thirdPartyMetadata, IMG_PROP_METADATA_DESCRIPTION_STRING, NULL); const char *xmpRights = SDL_GetStringProperty(thirdPartyMetadata, IMG_PROP_METADATA_COPYRIGHT_STRING, NULL); // Both of them have to be valid. SDLTest_AssertCheck(xmpDesc != NULL && xmpRights != NULL, "Failed to get XMP Description or Rights from third party metadata decoder. Description: %s, Rights: %s", xmpDesc ? xmpDesc : "NULL", xmpRights ? xmpRights : "NULL"); if (!xmpDesc || !xmpRights) { IMG_CloseAnimationDecoder(thirdPartyDecoder); return TEST_ABORTED; } size_t descLen = SDL_strnlen(xmpDesc, 256); size_t rightsLen = SDL_strnlen(xmpRights, 256); SDLTest_AssertCheck(descLen == 18, "XMP Description length mismatch. Expected: 18, Got: %" SDL_PRIu64, (Uint64)descLen); if (descLen != 18) { IMG_CloseAnimationDecoder(thirdPartyDecoder); return TEST_ABORTED; } SDLTest_AssertCheck(rightsLen == 16, "XMP Rights length mismatch. Expected: 16, Got: %" SDL_PRIu64, (Uint64)rightsLen); if (rightsLen != 16) { IMG_CloseAnimationDecoder(thirdPartyDecoder); return TEST_ABORTED; } SDLTest_AssertCheck(SDL_strncmp(xmpDesc, "Test ", 18) == 0, "XMP Description content mismatch. Expected: 'Test ', Got: '%s'", xmpDesc); if (SDL_strncmp(xmpDesc, "Test ", 18) != 0) { IMG_CloseAnimationDecoder(thirdPartyDecoder); return TEST_ABORTED; } SDLTest_AssertCheck(SDL_strncmp(xmpRights, "Test ", 16) == 0, "XMP Rights content mismatch. Expected: 'Test ', Got: '%s'", xmpRights); if (SDL_strncmp(xmpRights, "Test ", 16) != 0) { IMG_CloseAnimationDecoder(thirdPartyDecoder); return TEST_ABORTED; } SDLTest_Log("Third party metadata decoded successfully. Description: '%s', Rights: '%s'", xmpDesc, xmpRights); IMG_CloseAnimationDecoder(thirdPartyDecoder); } else { SDLTest_LogError("Failed to create animation decoder for third party metadata test image: %s", SDL_GetError()); return TEST_ABORTED; } SDLTest_Log("Finished test 'Decode Third Party Metadata Test'."); return TEST_COMPLETED; } static const SDLTest_TestCaseReference decodeEncodeAnimations = { testDecodeEncode, "decode_encode_animation", "Animation Decoder/Encoder Tests -- Decode, encode decoded frames then decode again to compare...", TEST_ENABLED }; static const SDLTest_TestCaseReference decoderRewindAnimations = { testDecoderRewind, "decoder_rewind", "Rewind Animatino decoder", TEST_ENABLED }; static const SDLTest_TestCaseReference animationMetadata = { testEncodeDecodeMetadata, "animation_metadata", "Encode Metadata and Decode Metadata", TEST_ENABLED }; static const SDLTest_TestCaseReference decodeThirdPartyMetadata = { testDecodeThirdPartyMetadata, "animation_decodeThirdPartyMetadata", "Decode Third Party Metadata", TEST_ENABLED }; static const SDLTest_TestCaseReference *animationTests[] = { &decodeEncodeAnimations, &decoderRewindAnimations, &animationMetadata, &decodeThirdPartyMetadata, NULL }; static SDLTest_TestSuiteReference animationTestSuite = { "animation", NULL, animationTests, NULL }; static SDLTest_TestSuiteReference *testSuites[] = { &animationTestSuite, NULL }; int main(int argc, char *argv[]) { int result; SDLTest_CommonState *state; SDLTest_TestSuiteRunner *runner; /* Initialize test framework */ state = SDLTest_CommonCreateState(argv, 0); if (!state) { return 1; } runner = SDLTest_CreateTestSuiteRunner(state, testSuites); /* Parse commandline */ for (int i = 1; i < argc;) { int consumed; consumed = SDLTest_CommonArg(state, i); if (!consumed) { if (SDL_strcmp(argv[i], "--no-dummy-metadata") == 0) { apply_dummy_metadata = false; consumed = 1; } } if (consumed <= 0) { const char *options[] = { "[--no-dummy-metadata]", NULL }; SDLTest_CommonLogUsage(state, argv[0], options); return 1; } i += consumed; } SDL_Log(" SDL version: %i.%i.%i", SDL_VERSIONNUM_MAJOR(SDL_GetVersion()), SDL_VERSIONNUM_MINOR(SDL_GetVersion()), SDL_VERSIONNUM_MICRO(SDL_GetVersion())); SDL_Log("SDL_image version: %i.%i.%i", SDL_VERSIONNUM_MAJOR(IMG_Version()), SDL_VERSIONNUM_MINOR(IMG_Version()), SDL_VERSIONNUM_MICRO(IMG_Version())); result = SDLTest_ExecuteTestSuiteRunner(runner); SDL_Quit(); SDLTest_DestroyTestSuiteRunner(runner); SDLTest_CommonDestroyState(state); return result; }