SDL_image: Handle webp animation composition

https://github.com/libsdl-org/SDL_image/commit/9f115a1e1dc935ddd8fd64abb0a59b588c8b2e2a

From 9f115a1e1dc935ddd8fd64abb0a59b588c8b2e2a Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Wed, 8 Jan 2025 11:27:51 -0800
Subject: [PATCH] Handle webp animation composition

Fixes https://github.com/libsdl-org/SDL_image/issues/500
---
 src/IMG_webp.c | 152 +++++++++++++++++++++++++++++++------------------
 1 file changed, 98 insertions(+), 54 deletions(-)

diff --git a/src/IMG_webp.c b/src/IMG_webp.c
index 0f1490e4..328d8375 100644
--- a/src/IMG_webp.c
+++ b/src/IMG_webp.c
@@ -50,7 +50,9 @@ static struct {
     uint8_t* (*WebPDecodeRGBInto) (const uint8_t* data, size_t data_size, uint8_t* output_buffer, size_t output_buffer_size, int output_stride);
     uint8_t* (*WebPDecodeRGBAInto) (const uint8_t* data, size_t data_size, uint8_t* output_buffer, size_t output_buffer_size, int output_stride);
     WebPDemuxer* (*WebPDemuxInternal)(const WebPData* data, int allow_partial, WebPDemuxState* state, int version);
-    int (*WebPDemuxGetFrame)(const WebPDemuxer* dmux, int frame_number, WebPIterator* iter);
+    int (*WebPDemuxGetFrame)(const WebPDemuxer *dmux, int frame_number, WebPIterator *iter);
+    int (*WebPDemuxNextFrame)(WebPIterator *iter);
+    void (*WebPDemuxReleaseIterator)(WebPIterator *iter);
     uint32_t (*WebPDemuxGetI)(const WebPDemuxer* dmux, WebPFormatFeature feature);
     void (*WebPDemuxDelete)(WebPDemuxer* dmux);
 } lib;
@@ -92,7 +94,9 @@ static bool IMG_InitWEBP(void)
         FUNCTION_LOADER_LIBWEBP(WebPDecodeRGBInto, uint8_t * (*) (const uint8_t* data, size_t data_size, uint8_t* output_buffer, size_t output_buffer_size, int output_stride))
         FUNCTION_LOADER_LIBWEBP(WebPDecodeRGBAInto, uint8_t * (*) (const uint8_t* data, size_t data_size, uint8_t* output_buffer, size_t output_buffer_size, int output_stride))
         FUNCTION_LOADER_LIBWEBPDEMUX(WebPDemuxInternal, WebPDemuxer* (*)(const WebPData*, int, WebPDemuxState*, int))
-        FUNCTION_LOADER_LIBWEBPDEMUX(WebPDemuxGetFrame, int (*)(const WebPDemuxer* dmux, int frame_number, WebPIterator* iter))
+        FUNCTION_LOADER_LIBWEBPDEMUX(WebPDemuxGetFrame, int (*)(const WebPDemuxer *dmux, int frame_number, WebPIterator *iter))
+        FUNCTION_LOADER_LIBWEBPDEMUX(WebPDemuxNextFrame, int (*)(WebPIterator *iter))
+        FUNCTION_LOADER_LIBWEBPDEMUX(WebPDemuxReleaseIterator, void (*)(WebPIterator *iter))
         FUNCTION_LOADER_LIBWEBPDEMUX(WebPDemuxGetI, uint32_t (*)(const WebPDemuxer* dmux, WebPFormatFeature feature))
         FUNCTION_LOADER_LIBWEBPDEMUX(WebPDemuxDelete, void (*)(WebPDemuxer* dmux))
     }
@@ -266,16 +270,16 @@ IMG_Animation *IMG_LoadWEBPAnimation_IO(SDL_IOStream *src)
 {
     Sint64 start;
     const char *error = NULL;
-    Uint32 format;
     WebPBitstreamFeatures features;
-    struct WebPDemuxer* dmuxer = NULL;
+    struct WebPDemuxer *demuxer = NULL;
     WebPIterator iter;
     IMG_Animation *anim = NULL;
     size_t raw_data_size;
     uint8_t *raw_data = NULL;
-    uint8_t *ret;
-    int frame_idx;
     WebPData wd;
+    uint32_t bgcolor;
+    SDL_Surface *canvas = NULL;
+    WebPMuxAnimDispose dispose_method = WEBP_MUX_DISPOSE_BACKGROUND;
 
     if (!src) {
         /* The error message has been set in SDL_IOFromFile */
@@ -296,79 +300,119 @@ IMG_Animation *IMG_LoadWEBPAnimation_IO(SDL_IOStream *src)
 
     raw_data = (uint8_t*) SDL_malloc(raw_data_size);
     if (raw_data == NULL) {
-        error = "Failed to allocate enough buffer for WEBP Animation";
         goto error;
     }
 
     if (SDL_ReadIO(src, raw_data, raw_data_size) != raw_data_size) {
-        error = "Failed to read WEBP Animation";
         goto error;
     }
 
     if (lib.WebPGetFeaturesInternal(raw_data, raw_data_size, &features, WEBP_DECODER_ABI_VERSION) != VP8_STATUS_OK) {
-        error = "WebPGetFeatures has failed";
+        error = "WebPGetFeatures() failed";
         goto error;
     }
 
-   if (features.has_alpha) {
-       format = SDL_PIXELFORMAT_RGBA32;
-    } else {
-       format = SDL_PIXELFORMAT_RGB24;
-    }
-
     wd.size = raw_data_size;
     wd.bytes = raw_data;
-    dmuxer = lib.WebPDemuxInternal(&wd, 0, NULL, WEBP_DEMUX_ABI_VERSION);
-    anim = (IMG_Animation *)SDL_malloc(sizeof(IMG_Animation));
+    demuxer = lib.WebPDemuxInternal(&wd, 0, NULL, WEBP_DEMUX_ABI_VERSION);
+    if (!demuxer) {
+        error = "WebPDemux() failed";
+        goto error;
+    }
+
+    anim = (IMG_Animation *)SDL_calloc(1, sizeof(*anim));
+    if (!anim) {
+        goto error;
+    }
     anim->w = features.width;
     anim->h = features.height;
-    anim->count = lib.WebPDemuxGetI(dmuxer, WEBP_FF_FRAME_COUNT);
+    anim->count = lib.WebPDemuxGetI(demuxer, WEBP_FF_FRAME_COUNT);
     anim->frames = (SDL_Surface **)SDL_calloc(anim->count, sizeof(*anim->frames));
     anim->delays = (int *)SDL_calloc(anim->count, sizeof(*anim->delays));
-    for (frame_idx = 0; frame_idx < (anim->count); frame_idx++) {
-        SDL_Surface* curr;
-        if (lib.WebPDemuxGetFrame(dmuxer, frame_idx, &iter) == 0) {
-            break;
-        }
-        curr = SDL_CreateSurface(features.width, features.height, format);
-        if (curr == NULL) {
-            error = "Failed to allocate SDL_Surface";
-            goto error;
-        }
-        anim->frames[frame_idx] = curr;
-        anim->delays[frame_idx] = iter.duration;
-        if (features.has_alpha) {
-            ret = lib.WebPDecodeRGBAInto(
-                iter.fragment.bytes,
-                iter.fragment.size,
-                (uint8_t *)curr->pixels,
-                curr->pitch * curr->h,
-                curr->pitch);
-        } else {
-            ret = lib.WebPDecodeRGBInto(
-                iter.fragment.bytes, iter.fragment.size,
-                (uint8_t *)curr->pixels,
-                curr->pitch * curr->h,
-                curr->pitch);
-        }
-        if (ret == NULL) {
-            break;
-        }
-    }
-    if (dmuxer) {
-        lib.WebPDemuxDelete(dmuxer);
+
+    canvas = SDL_CreateSurface(anim->w, anim->h, features.has_alpha ? SDL_PIXELFORMAT_RGBA32 : SDL_PIXELFORMAT_RGBX32);
+    if (!canvas) {
+        goto error;
     }
 
-    if (raw_data) {
-        SDL_free(raw_data);
+    /* Background color is BGRA byte order according to the spec */
+    bgcolor = lib.WebPDemuxGetI(demuxer, WEBP_FF_BACKGROUND_COLOR);
+#if SDL_BYTEORDER == SDL_BIG_ENDIAN
+    bgcolor = SDL_MapSurfaceRGBA(canvas,
+                                 (bgcolor >> 8) & 0xFF,
+                                 (bgcolor >> 16) & 0xFF,
+                                 (bgcolor >> 24) & 0xFF,
+                                 (bgcolor >> 0) & 0xFF);
+#else
+    bgcolor = SDL_MapSurfaceRGBA(canvas,
+                                 (bgcolor >> 16) & 0xFF,
+                                 (bgcolor >> 8) & 0xFF,
+                                 (bgcolor >> 0) & 0xFF,
+                                 (bgcolor >> 24) & 0xFF);
+#endif
+
+    SDL_zero(iter);
+    if (lib.WebPDemuxGetFrame(demuxer, 1, &iter)) {
+        do {
+            int frame_idx = (iter.frame_num - 1);
+            if (frame_idx < 0 || frame_idx >= anim->count) {
+                continue;
+            }
+
+            if (dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) {
+                SDL_FillSurfaceRect(canvas, NULL, bgcolor);
+            }
+
+            SDL_Surface *curr = SDL_CreateSurface(iter.width, iter.height, SDL_PIXELFORMAT_RGBA32);
+            if (!curr) {
+                goto error;
+            }
+
+            if (!lib.WebPDecodeRGBAInto(iter.fragment.bytes,
+                                        iter.fragment.size,
+                                        (uint8_t *)curr->pixels,
+                                        curr->pitch * curr->h,
+                                        curr->pitch)) {
+                error = "WebPDecodeRGBAInto() failed";
+                SDL_DestroySurface(curr);
+                goto error;
+            }
+
+            SDL_Rect dst = { iter.x_offset, iter.y_offset, iter.width, iter.height };
+            if (iter.blend_method == WEBP_MUX_BLEND) {
+                SDL_SetSurfaceBlendMode(curr, SDL_BLENDMODE_BLEND);
+            } else {
+                SDL_SetSurfaceBlendMode(curr, SDL_BLENDMODE_NONE);
+            }
+            SDL_BlitSurface(curr, NULL, canvas, &dst);
+            SDL_DestroySurface(curr);
+
+            anim->frames[frame_idx] = SDL_DuplicateSurface(canvas);
+            anim->delays[frame_idx] = iter.duration;
+            dispose_method = iter.dispose_method;
+
+        } while (lib.WebPDemuxNextFrame(&iter));
+
+        lib.WebPDemuxReleaseIterator(&iter);
     }
+
+    SDL_DestroySurface(canvas);
+
+    lib.WebPDemuxDelete(demuxer);
+
+    SDL_free(raw_data);
+
     return anim;
+
 error:
+    if (canvas) {
+        SDL_DestroySurface(canvas);
+    }
     if (anim) {
         IMG_FreeAnimation(anim);
     }
-    if (dmuxer) {
-        lib.WebPDemuxDelete(dmuxer);
+    if (demuxer) {
+        lib.WebPDemuxDelete(demuxer);
     }
     if (raw_data) {
         SDL_free(raw_data);