Adds support for extended file format specified in
https://developers.google.com/speed/webp/docs/riff_container
which animated webp uses.

Fixes: #4907

Signed-off-by: Peter Xia <[email protected]>
---
 libavcodec/webp.c | 764 +++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 692 insertions(+), 72 deletions(-)

diff --git a/libavcodec/webp.c b/libavcodec/webp.c
index 62967a394..fe1e14ced 100644
--- a/libavcodec/webp.c
+++ b/libavcodec/webp.c
@@ -34,9 +34,11 @@
  * @author James Almer <[email protected]>
  * Exif metadata
  * ICC profile
+ * 
+ * @author Peter Xia <[email protected]>
+ * Animation
  *
  * Unimplemented:
- *   - Animation
  *   - XMP metadata
  */
 
@@ -216,6 +218,22 @@ typedef struct WebPContext {
     int reduced_width;
     int nb_huffman_groups;              /* number of huffman groups in the 
primary image */
     ImageContext image[IMAGE_ROLE_NB];  /* image context for each role */
+
+    /* Animation support */
+    AVPacket *anim_pkt;                 /* current input packet for animation 
*/
+    GetByteContext anim_gb;             /* chunk-level byte reader into 
anim_pkt */
+    int is_anim;                        /* non-zero when decoding animated 
WebP */
+    int anim_header_parsed;             /* RIFF/VP8X/ANIM header has been 
parsed */
+    int vp8x_flags;                     /* flags from VP8X chunk */
+    uint32_t anim_bg_color;             /* background color in BGRA (not used) 
*/
+    int anim_loop_count;                /* loop count (0 = infinite), ignored 
*/
+    int64_t anim_pts;                   /* accumulated frame start time in 
milliseconds */
+    AVFrame *canvas;                    /* ARGB compositing canvas */
+    int prev_disposal;                  /* disposal method applied after 
previous frame */
+    int prev_frame_x;                   /* previous frame x position */
+    int prev_frame_y;                   /* previous frame y position */
+    int prev_frame_w;                   /* previous frame width */
+    int prev_frame_h;                   /* previous frame height */
 } WebPContext;
 
 #define GET_PIXEL(frame, x, y) \
@@ -1074,6 +1092,10 @@ static int apply_color_indexing_transform(WebPContext *s)
 static void update_canvas_size(AVCodecContext *avctx, int w, int h)
 {
     WebPContext *s = avctx->priv_data;
+    /* When decoding an ANMF sub-frame, the canvas dimensions are fixed;
+     * do not overwrite them with the sub-frame dimensions. */
+    if (s->is_anim)
+        return;
     if (s->width && s->width != w) {
         av_log(avctx, AV_LOG_WARNING, "Width mismatch. %d != %d\n",
                s->width, w);
@@ -1132,7 +1154,7 @@ static int vp8_lossless_decode_frame(AVCodecContext 
*avctx, AVFrame *p,
 
     /* parse transformations */
     s->nb_transforms = 0;
-    s->reduced_width = s->width;
+    s->reduced_width = w;
     used = 0;
     while (get_bits1(&s->gb)) {
         enum TransformType transform = get_bits(&s->gb, 2);
@@ -1254,10 +1276,12 @@ static int vp8_lossy_decode_alpha(AVCodecContext 
*avctx, AVFrame *p,
         GetByteContext gb;
 
         bytestream2_init(&gb, data_start, data_size);
-        for (y = 0; y < s->height; y++)
+        for (y = 0; y < p->height; y++)
             bytestream2_get_buffer(&gb, p->data[3] + p->linesize[3] * y,
-                                   s->width);
+                                   p->width);
     } else if (s->alpha_compression == ALPHA_COMPRESSION_VP8L) {
+        int saved_width  = s->width;
+        int saved_height = s->height;
         uint8_t *ap, *pp;
         int alpha_got_frame = 0;
 
@@ -1265,8 +1289,18 @@ static int vp8_lossy_decode_alpha(AVCodecContext *avctx, 
AVFrame *p,
         if (!s->alpha_frame)
             return AVERROR(ENOMEM);
 
+        /* Temporarily set s->width/height to the sub-frame size so 
+         * that all VP8L internal functions use the correct image
+         * dimensions. */
+        s->width  = p->width;
+        s->height = p->height;
+
         ret = vp8_lossless_decode_frame(avctx, s->alpha_frame, 
&alpha_got_frame,
                                         data_start, data_size, 1);
+
+        s->width  = saved_width;
+        s->height = saved_height;
+
         if (ret < 0) {
             av_frame_free(&s->alpha_frame);
             return ret;
@@ -1277,10 +1311,10 @@ static int vp8_lossy_decode_alpha(AVCodecContext 
*avctx, AVFrame *p,
         }
 
         /* copy green component of alpha image to alpha plane of primary image 
*/
-        for (y = 0; y < s->height; y++) {
+        for (y = 0; y < p->height; y++) {
             ap = GET_PIXEL(s->alpha_frame, 0, y) + 2;
             pp = p->data[3] + p->linesize[3] * y;
-            for (x = 0; x < s->width; x++) {
+            for (x = 0; x < p->width; x++) {
                 *pp = *ap;
                 pp++;
                 ap += 4;
@@ -1338,51 +1372,29 @@ static int vp8_lossy_decode_frame(AVCodecContext 
*avctx, AVFrame *p,
     return ret;
 }
 
-static int webp_decode_frame(AVCodecContext *avctx, AVFrame *p,
-                             int *got_frame, AVPacket *avpkt)
+/*
+ * Decode a non-animated WebP image from the given packet.
+ * gb must be positioned just after the 12-byte "RIFF|SIZE|WEBP" header.
+ * The function iterates over all top-level chunks and decodes the first
+ * VP8 / VP8L image it finds.
+ */
+static int webp_decode_static(AVCodecContext *avctx, AVFrame *p,
+                               GetByteContext *gb, AVPacket *avpkt)
 {
     WebPContext *s = avctx->priv_data;
-    GetByteContext gb;
-    int ret;
+    int ret, got_frame = 0;
     uint32_t chunk_type, chunk_size;
-    int vp8x_flags = 0;
-
-    s->avctx     = avctx;
-    s->width     = 0;
-    s->height    = 0;
-    *got_frame   = 0;
-    s->has_alpha = 0;
-    s->has_exif  = 0;
-    s->has_iccp  = 0;
-    bytestream2_init(&gb, avpkt->data, avpkt->size);
-
-    if (bytestream2_get_bytes_left(&gb) < 12)
-        return AVERROR_INVALIDDATA;
 
-    if (bytestream2_get_le32(&gb) != MKTAG('R', 'I', 'F', 'F')) {
-        av_log(avctx, AV_LOG_ERROR, "missing RIFF tag\n");
-        return AVERROR_INVALIDDATA;
-    }
-
-    chunk_size = bytestream2_get_le32(&gb);
-    if (bytestream2_get_bytes_left(&gb) < chunk_size)
-        return AVERROR_INVALIDDATA;
-
-    if (bytestream2_get_le32(&gb) != MKTAG('W', 'E', 'B', 'P')) {
-        av_log(avctx, AV_LOG_ERROR, "missing WEBP tag\n");
-        return AVERROR_INVALIDDATA;
-    }
-
-    while (bytestream2_get_bytes_left(&gb) > 8) {
+    while (bytestream2_get_bytes_left(gb) > 8) {
         char chunk_str[5] = { 0 };
 
-        chunk_type = bytestream2_get_le32(&gb);
-        chunk_size = bytestream2_get_le32(&gb);
+        chunk_type = bytestream2_get_le32(gb);
+        chunk_size = bytestream2_get_le32(gb);
         if (chunk_size == UINT32_MAX)
             return AVERROR_INVALIDDATA;
         chunk_size += chunk_size & 1;
 
-        if (bytestream2_get_bytes_left(&gb) < chunk_size) {
+        if (bytestream2_get_bytes_left(gb) < chunk_size) {
            /* we seem to be running out of data, but it could also be that the
               bitstream has trailing junk leading to bogus chunk_size. */
             break;
@@ -1390,19 +1402,19 @@ static int webp_decode_frame(AVCodecContext *avctx, 
AVFrame *p,
 
         switch (chunk_type) {
         case MKTAG('V', 'P', '8', ' '):
-            if (!*got_frame) {
-                ret = vp8_lossy_decode_frame(avctx, p, got_frame,
-                                             avpkt->data + 
bytestream2_tell(&gb),
+            if (!got_frame) {
+                ret = vp8_lossy_decode_frame(avctx, p, &got_frame,
+                                             avpkt->data + 
bytestream2_tell(gb),
                                              chunk_size);
                 if (ret < 0)
                     return ret;
             }
-            bytestream2_skip(&gb, chunk_size);
+            bytestream2_skip(gb, chunk_size);
             break;
         case MKTAG('V', 'P', '8', 'L'):
-            if (!*got_frame) {
-                ret = vp8_lossless_decode_frame(avctx, p, got_frame,
-                                                avpkt->data + 
bytestream2_tell(&gb),
+            if (!got_frame) {
+                ret = vp8_lossless_decode_frame(avctx, p, &got_frame,
+                                                avpkt->data + 
bytestream2_tell(gb),
                                                 chunk_size, 0);
                 if (ret < 0)
                     return ret;
@@ -1412,17 +1424,17 @@ FF_DISABLE_DEPRECATION_WARNINGS
 FF_ENABLE_DEPRECATION_WARNINGS
 #endif
             }
-            bytestream2_skip(&gb, chunk_size);
+            bytestream2_skip(gb, chunk_size);
             break;
         case MKTAG('V', 'P', '8', 'X'):
-            if (s->width || s->height || *got_frame) {
+            if (s->width || s->height || got_frame) {
                 av_log(avctx, AV_LOG_ERROR, "Canvas dimensions are already 
set\n");
                 return AVERROR_INVALIDDATA;
             }
-            vp8x_flags = bytestream2_get_byte(&gb);
-            bytestream2_skip(&gb, 3);
-            s->width  = bytestream2_get_le24(&gb) + 1;
-            s->height = bytestream2_get_le24(&gb) + 1;
+            s->vp8x_flags = bytestream2_get_byte(gb);
+            bytestream2_skip(gb, 3);
+            s->width  = bytestream2_get_le24(gb) + 1;
+            s->height = bytestream2_get_le24(gb) + 1;
             ret = av_image_check_size(s->width, s->height, 0, avctx);
             if (ret < 0)
                 return ret;
@@ -1430,7 +1442,7 @@ FF_ENABLE_DEPRECATION_WARNINGS
         case MKTAG('A', 'L', 'P', 'H'): {
             int alpha_header, filter_m, compression;
 
-            if (!(vp8x_flags & VP8X_FLAG_ALPHA)) {
+            if (!(s->vp8x_flags & VP8X_FLAG_ALPHA)) {
                 av_log(avctx, AV_LOG_WARNING,
                        "ALPHA chunk present, but alpha bit not set in the "
                        "VP8X header\n");
@@ -1439,10 +1451,10 @@ FF_ENABLE_DEPRECATION_WARNINGS
                 av_log(avctx, AV_LOG_ERROR, "invalid ALPHA chunk size\n");
                 return AVERROR_INVALIDDATA;
             }
-            alpha_header       = bytestream2_get_byte(&gb);
-            s->alpha_data      = avpkt->data + bytestream2_tell(&gb);
+            alpha_header       = bytestream2_get_byte(gb);
+            s->alpha_data      = avpkt->data + bytestream2_tell(gb);
             s->alpha_data_size = chunk_size - 1;
-            bytestream2_skip(&gb, s->alpha_data_size);
+            bytestream2_skip(gb, s->alpha_data_size);
 
             filter_m    = (alpha_header >> 2) & 0x03;
             compression =  alpha_header       & 0x03;
@@ -1466,7 +1478,7 @@ FF_ENABLE_DEPRECATION_WARNINGS
                 goto exif_end;
             }
 
-            if (!(vp8x_flags & VP8X_FLAG_EXIF_METADATA))
+            if (!(s->vp8x_flags & VP8X_FLAG_EXIF_METADATA))
                 av_log(avctx, AV_LOG_WARNING,
                        "EXIF chunk present, but Exif bit not set in the "
                        "VP8X header\n");
@@ -1477,14 +1489,14 @@ FF_ENABLE_DEPRECATION_WARNINGS
                 goto exif_end;
             }
             s->has_exif = 1;
-            memcpy(exif_buf->data, gb.buffer, chunk_size);
+            memcpy(exif_buf->data, gb->buffer, chunk_size);
 
             ret = ff_decode_exif_attach_buffer(avctx, p, &exif_buf, 
AV_EXIF_TIFF_HEADER);
             if (ret < 0)
                 av_log(avctx, AV_LOG_WARNING, "unable to attach EXIF 
buffer\n");
 
 exif_end:
-            bytestream2_skip(&gb, chunk_size);
+            bytestream2_skip(gb, chunk_size);
             break;
         }
         case MKTAG('I', 'C', 'C', 'P'): {
@@ -1492,10 +1504,10 @@ exif_end:
 
             if (s->has_iccp) {
                 av_log(avctx, AV_LOG_VERBOSE, "Ignoring extra ICCP chunk\n");
-                bytestream2_skip(&gb, chunk_size);
+                bytestream2_skip(gb, chunk_size);
                 break;
             }
-            if (!(vp8x_flags & VP8X_FLAG_ICC))
+            if (!(s->vp8x_flags & VP8X_FLAG_ICC))
                 av_log(avctx, AV_LOG_WARNING,
                        "ICCP chunk present, but ICC Profile bit not set in the 
"
                        "VP8X header\n");
@@ -1507,9 +1519,9 @@ exif_end:
                 return ret;
 
             if (sd) {
-                bytestream2_get_buffer(&gb, sd->data, chunk_size);
+                bytestream2_get_buffer(gb, sd->data, chunk_size);
             } else {
-                bytestream2_skip(&gb, chunk_size);
+                bytestream2_skip(gb, chunk_size);
             }
             break;
         }
@@ -1519,23 +1531,625 @@ exif_end:
             AV_WL32(chunk_str, chunk_type);
             av_log(avctx, AV_LOG_WARNING, "skipping unsupported chunk: %s\n",
                    chunk_str);
-            bytestream2_skip(&gb, chunk_size);
+            bytestream2_skip(gb, chunk_size);
             break;
         default:
             AV_WL32(chunk_str, chunk_type);
             av_log(avctx, AV_LOG_VERBOSE, "skipping unknown chunk: %s\n",
                    chunk_str);
-            bytestream2_skip(&gb, chunk_size);
+            bytestream2_skip(gb, chunk_size);
             break;
         }
     }
 
-    if (!*got_frame) {
+    if (!got_frame) {
         av_log(avctx, AV_LOG_ERROR, "image data not found\n");
         return AVERROR_INVALIDDATA;
     }
 
-    return avpkt->size;
+    return 0;
+}
+
+/* Fill the canvas with fully transparent pixels. */
+static void canvas_clear_background(AVFrame *canvas)
+{
+    int y;
+    for (y = 0; y < canvas->height; y++)
+        memset(canvas->data[0] + y * canvas->linesize[0], 0, canvas->width * 
4);
+}
+
+/* Apply disposal method 1 (background) to the given rectangle on the canvas.
+ * Clears the rectangle to fully transparent. */
+static void canvas_clear_rect(AVFrame *canvas,
+                               int x, int y, int w, int h)
+{
+    int cy;
+    for (cy = y; cy < y + h; cy++)
+        memset(canvas->data[0] + cy * canvas->linesize[0] + x * 4, 0, w * 4);
+}
+
+/* Composite an ARGB sub-frame onto the canvas at position (ox, oy) with 
blend. */
+static void composite_argb_onto_canvas(AVFrame *canvas, const AVFrame *frame,
+                                        int ox, int oy, int blend)
+{
+    int x, y;
+
+    for (y = 0; y < frame->height; y++) {
+        const uint8_t *src = frame->data[0] + y * frame->linesize[0];
+        uint8_t       *dst = canvas->data[0] + (oy + y) * canvas->linesize[0] 
+ ox * 4;
+
+        for (x = 0; x < frame->width; x++) {
+            uint8_t sa = src[0], sr = src[1], sg = src[2], sb = src[3];
+
+            if (!blend) {
+                /* No blending: overwrite canvas pixel directly, preserving 
alpha. */
+                dst[0] = sa;
+                dst[1] = sr;
+                dst[2] = sg;
+                dst[3] = sb;
+            } else if (sa == 255) {
+                /* Fully opaque: direct copy. */
+                dst[0] = sa;
+                dst[1] = sr;
+                dst[2] = sg;
+                dst[3] = sb;
+            } else if (sa == 0) {
+                /* Fully transparent: leave canvas pixel unchanged. */
+            } else {
+                /* Alpha-blend src over dst per the WebP spec. */
+                uint8_t da = dst[0];
+                int blend_a = sa + (int)da * (255 - sa) / 255;
+                dst[0] = blend_a;
+                if (blend_a > 0) {
+                    dst[1] = ((int)sr * sa + (int)dst[1] * da * (255 - sa) / 
255) / blend_a;
+                    dst[2] = ((int)sg * sa + (int)dst[2] * da * (255 - sa) / 
255) / blend_a;
+                    dst[3] = ((int)sb * sa + (int)dst[3] * da * (255 - sa) / 
255) / blend_a;
+                } else {
+                    dst[1] = dst[2] = dst[3] = 0;
+                }
+            }
+            src += 4;
+            dst += 4;
+        }
+    }
+}
+
+/* Convert a YUV420P or YUVA420P frame to ARGB and composite onto the canvas. 
*/
+static void composite_yuv_onto_canvas(AVFrame *canvas, const AVFrame *frame,
+                                       int ox, int oy, int blend)
+{
+    int has_alpha = frame->format == AV_PIX_FMT_YUVA420P;
+    int x, y;
+
+    for (y = 0; y < frame->height; y++) {
+        const uint8_t *yp  = frame->data[0] + y * frame->linesize[0];
+        const uint8_t *up  = frame->data[1] + (y >> 1) * frame->linesize[1];
+        const uint8_t *vp  = frame->data[2] + (y >> 1) * frame->linesize[2];
+        const uint8_t *ap  = has_alpha ? frame->data[3] + y * 
frame->linesize[3] : NULL;
+        uint8_t       *dst = canvas->data[0] + (oy + y) * canvas->linesize[0] 
+ ox * 4;
+
+        for (x = 0; x < frame->width; x++) {
+            /* BT.601 limited-range YUV -> RGB conversion */
+            int Y = yp[x] - 16;
+            int U = up[x >> 1] - 128;
+            int V = vp[x >> 1] - 128;
+            int R = av_clip_uint8((298 * Y + 409 * V + 128) >> 8);
+            int G = av_clip_uint8((298 * Y - 100 * U - 208 * V + 128) >> 8);
+            int B = av_clip_uint8((298 * Y + 516 * U + 128) >> 8);
+            int A = ap ? ap[x] : 255;
+
+            if (!blend) {
+                /* No blending: overwrite canvas pixel directly, preserving 
alpha. */
+                dst[0] = A;
+                dst[1] = R;
+                dst[2] = G;
+                dst[3] = B;
+            } else if (A == 255) {
+                dst[0] = A;
+                dst[1] = R;
+                dst[2] = G;
+                dst[3] = B;
+            } else if (A == 0) {
+                /* transparent: leave canvas unchanged */
+            } else {
+                uint8_t da = dst[0];
+                int blend_a = A + (int)da * (255 - A) / 255;
+                dst[0] = blend_a;
+                if (blend_a > 0) {
+                    dst[1] = ((int)R * A + (int)dst[1] * da * (255 - A) / 255) 
/ blend_a;
+                    dst[2] = ((int)G * A + (int)dst[2] * da * (255 - A) / 255) 
/ blend_a;
+                    dst[3] = ((int)B * A + (int)dst[3] * da * (255 - A) / 255) 
/ blend_a;
+                } else {
+                    dst[1] = dst[2] = dst[3] = 0;
+                }
+            }
+            dst += 4;
+        }
+    }
+}
+
+/*
+ * Decode the sub-chunks inside an ANMF chunk payload (single frame).
+ * data/data_size cover everything after the fixed 16-byte ANMF header fields
+ * (i.e. from the first subchunk tag onwards).
+ * On success the decoded frame is stored in *frame.
+ */
+static int decode_anmf_subchunks(AVCodecContext *avctx, AVFrame *frame,
+                                  const uint8_t *data, uint32_t data_size)
+{
+    WebPContext *s = avctx->priv_data;
+    GetByteContext gb;
+    uint32_t chunk_type, chunk_size;
+    int got_frame = 0, ret = 0;
+
+    /* Reset per-frame alpha state. */
+    s->has_alpha         = 0;
+    s->alpha_data        = NULL;
+    s->alpha_data_size   = 0;
+
+    bytestream2_init(&gb, data, data_size);
+
+    while (bytestream2_get_bytes_left(&gb) > 8) {
+        chunk_type = bytestream2_get_le32(&gb);
+        chunk_size = bytestream2_get_le32(&gb);
+        if (chunk_size == UINT32_MAX)
+            return AVERROR_INVALIDDATA;
+        chunk_size += chunk_size & 1;
+        if (bytestream2_get_bytes_left(&gb) < chunk_size)
+            break;
+
+        switch (chunk_type) {
+        case MKTAG('A', 'L', 'P', 'H'): {
+            int alpha_header, filter_m, compression;
+            if (chunk_size == 0) {
+                av_log(avctx, AV_LOG_ERROR, "invalid ALPHA chunk size\n");
+                return AVERROR_INVALIDDATA;
+            }
+            alpha_header       = bytestream2_get_byte(&gb);
+            s->alpha_data      = gb.buffer;
+            s->alpha_data_size = chunk_size - 1;
+            bytestream2_skip(&gb, s->alpha_data_size);
+
+            filter_m    = (alpha_header >> 2) & 0x03;
+            compression =  alpha_header       & 0x03;
+            if (compression <= ALPHA_COMPRESSION_VP8L) {
+                s->has_alpha         = 1;
+                s->alpha_compression = compression;
+                s->alpha_filter      = filter_m;
+            } else {
+                av_log(avctx, AV_LOG_VERBOSE, "skipping unsupported ALPHA 
chunk\n");
+            }
+            break;
+        }
+        case MKTAG('V', 'P', '8', ' '):
+            if (!got_frame) {
+                ret = vp8_lossy_decode_frame(avctx, frame, &got_frame,
+                                             (uint8_t *)gb.buffer, chunk_size);
+                if (ret < 0)
+                    return ret;
+            }
+            bytestream2_skip(&gb, chunk_size);
+            break;
+        case MKTAG('V', 'P', '8', 'L'):
+            if (!got_frame) {
+                ret = vp8_lossless_decode_frame(avctx, frame, &got_frame,
+                                                gb.buffer, chunk_size, 0);
+                if (ret < 0)
+                    return ret;
+#if FF_API_CODEC_PROPS
+FF_DISABLE_DEPRECATION_WARNINGS
+                avctx->properties |= FF_CODEC_PROPERTY_LOSSLESS;
+FF_ENABLE_DEPRECATION_WARNINGS
+#endif
+            }
+            bytestream2_skip(&gb, chunk_size);
+            break;
+        default:
+            bytestream2_skip(&gb, chunk_size);
+            break;
+        }
+    }
+
+    if (!got_frame)
+        return AVERROR_INVALIDDATA;
+    return 0;
+}
+
+/*
+ * Decode a ANMF chunk (single frame).
+ * anim_gb must be positioned after the 'ANMF' chunk header.
+ */
+static int decode_anmf_chunk(AVCodecContext *avctx, AVFrame *frame,
+                              uint32_t chunk_size)
+{
+    WebPContext *s = avctx->priv_data;
+    GetByteContext *gb = &s->anim_gb;
+    AVFrame *sub = NULL;
+    int frame_x, frame_y, frame_w, frame_h, duration, flags;
+    int blend, disposal, ret;
+    uint32_t payload_size;
+
+    if (chunk_size < 16) {
+        av_log(avctx, AV_LOG_ERROR, "ANMF chunk too small\n");
+        return AVERROR_INVALIDDATA;
+    }
+
+    frame_x  = bytestream2_get_le24(gb) * 2;
+    frame_y  = bytestream2_get_le24(gb) * 2;
+    frame_w  = bytestream2_get_le24(gb) + 1;
+    frame_h  = bytestream2_get_le24(gb) + 1;
+    duration = bytestream2_get_le24(gb);   /* in milliseconds */
+    flags    = bytestream2_get_byte(gb);
+    blend    = (flags >> 1) & 1;           /* 0=alpha blend, 1=no blend */
+    disposal = (flags >> 0) & 1;           /* 0=none, 1=background */
+
+    /* Validate frame rectangle against canvas. */
+    if (frame_x + frame_w > s->width || frame_y + frame_h > s->height) {
+        av_log(avctx, AV_LOG_ERROR,
+               "ANMF frame (%d,%d)+%dx%d out of canvas %dx%d\n",
+               frame_x, frame_y, frame_w, frame_h, s->width, s->height);
+        return AVERROR_INVALIDDATA;
+    }
+
+    /* Apply the previous frame's disposal method before compositing. */
+    if (s->prev_disposal == 1) {
+        canvas_clear_rect(s->canvas,
+                          s->prev_frame_x, s->prev_frame_y,
+                          s->prev_frame_w, s->prev_frame_h);
+    }
+
+    /* Payload = chunk_size minus the 16 bytes of fixed ANMF header fields
+     * (6+6+3+1 = 16 bytes: x,y,w,h,duration,flags).
+     * We already consumed those 16 bytes above. */
+    payload_size = chunk_size - 16;
+    if (bytestream2_get_bytes_left(gb) < payload_size) {
+        av_log(avctx, AV_LOG_ERROR, "truncated ANMF payload\n");
+        return AVERROR_INVALIDDATA;
+    }
+
+    /* Decode the sub-frame image. */
+    sub = av_frame_alloc();
+    if (!sub)
+        return AVERROR(ENOMEM);
+
+    ret = decode_anmf_subchunks(avctx, sub, gb->buffer, payload_size);
+    bytestream2_skip(gb, payload_size);
+    if (ret < 0) {
+        av_frame_free(&sub);
+        return ret;
+    }
+
+    /* Sanity check decoded dimensions against the ANMF header. */
+    if (sub->width != frame_w || sub->height != frame_h) {
+        av_log(avctx, AV_LOG_WARNING,
+               "ANMF sub-frame size %dx%d differs from header %dx%d\n",
+               sub->width, sub->height, frame_w, frame_h);
+    }
+
+    /* Composite the sub-frame onto the canvas.
+     * blend==0 means alpha-blending; blend==1 means no blending. */
+    if (sub->format == AV_PIX_FMT_ARGB) {
+        composite_argb_onto_canvas(s->canvas, sub, frame_x, frame_y, !blend);
+    } else {
+        /* Lossy frame: YUV420P or YUVA420P */
+        composite_yuv_onto_canvas(s->canvas, sub, frame_x, frame_y, !blend);
+    }
+    av_frame_free(&sub);
+
+    /* Lossy VP8 sub-frame decoding sets avctx->pix_fmt to YUV(A)420P.
+     * The frame pool in ff_get_buffer is keyed on avctx->pix_fmt, so if it
+     * does not match the format we request (ARGB), extra plane buffers get
+     * allocated and the validation check emits a "unused plane pointers"
+     * error.  Restore ARGB here before allocating the output frame. */
+    avctx->pix_fmt = AV_PIX_FMT_ARGB;
+
+    /* Allocate output frame and copy the canvas into it. */
+    frame->format = AV_PIX_FMT_ARGB;
+    frame->width  = s->width;
+    frame->height = s->height;
+    ret = ff_get_buffer(avctx, frame, 0);
+    if (ret < 0)
+        return ret;
+
+    /* Copy canvas rows into the output frame. */
+    for (int y = 0; y < s->height; y++) {
+        memcpy(frame->data[0] + y * frame->linesize[0],
+               s->canvas->data[0] + y * s->canvas->linesize[0],
+               s->width * 4);
+    }
+
+    /* Set timestamps. */
+    frame->pts = av_rescale_q(s->anim_pts, (AVRational){1, 1000},
+                              avctx->pkt_timebase);
+    frame->duration = av_rescale_q(duration, (AVRational){1, 1000},
+                                   avctx->pkt_timebase);
+    s->anim_pts += duration;
+
+    frame->pict_type = AV_PICTURE_TYPE_I;
+    frame->flags    |= AV_FRAME_FLAG_KEY;
+
+    /* Remember this frame's region and disposal method for next iteration. */
+    s->prev_disposal = disposal;
+    s->prev_frame_x  = frame_x;
+    s->prev_frame_y  = frame_y;
+    s->prev_frame_w  = frame_w;
+    s->prev_frame_h  = frame_h;
+
+    return 0;
+}
+
+/*
+ * Main decode entry point using the receive_frame callback model.
+ * Handles both static and animated WebP.
+ */
+static int webp_receive_frame(AVCodecContext *avctx, AVFrame *frame)
+{
+    WebPContext *s = avctx->priv_data;
+    int ret;
+
+    s->avctx = avctx;
+
+    /* --- Animation: return the next frame from the current packet --- */
+    if (s->anim_header_parsed) {
+        GetByteContext *gb = &s->anim_gb;
+        uint32_t chunk_type, chunk_size;
+
+        while (bytestream2_get_bytes_left(gb) > 8) {
+            chunk_type = bytestream2_get_le32(gb);
+            chunk_size = bytestream2_get_le32(gb);
+            if (chunk_size == UINT32_MAX) {
+                ret = AVERROR_INVALIDDATA;
+                goto anim_done;
+            }
+            chunk_size += chunk_size & 1;
+
+            if (bytestream2_get_bytes_left(gb) < chunk_size) {
+                /* Trailing junk or truncated; treat as end of animation. */
+                break;
+            }
+
+            if (chunk_type == MKTAG('A', 'N', 'M', 'F')) {
+                ret = decode_anmf_chunk(avctx, frame, chunk_size);
+                if (ret < 0)
+                    goto anim_done;
+                return 0;
+            } else {
+                /* Skip non-ANMF chunks after the animation header
+                 * (e.g. EXIF, XMP that may appear at the end). */
+                bytestream2_skip(gb, chunk_size);
+            }
+        }
+
+        /* No more ANMF chunks in this packet. */
+        ret = AVERROR(EAGAIN);
+
+anim_done:
+        av_packet_unref(s->anim_pkt);
+        s->anim_header_parsed = 0;
+        s->is_anim            = 0;
+        s->anim_pts           = 0;
+        s->prev_disposal      = 0;
+        av_frame_free(&s->canvas);
+        return ret;
+    }
+
+    /* --- Get a new packet --- */
+    ret = ff_decode_get_packet(avctx, s->anim_pkt);
+    if (ret < 0)
+        return ret;
+
+    /* --- Parse the RIFF/WEBP header --- */
+    s->width     = 0;
+    s->height    = 0;
+    s->has_alpha = 0;
+    s->has_exif  = 0;
+    s->has_iccp  = 0;
+    s->vp8x_flags = 0;
+
+    bytestream2_init(&s->anim_gb, s->anim_pkt->data, s->anim_pkt->size);
+
+    if (bytestream2_get_bytes_left(&s->anim_gb) < 12) {
+        ret = AVERROR_INVALIDDATA;
+        goto fail;
+    }
+    if (bytestream2_get_le32(&s->anim_gb) != MKTAG('R', 'I', 'F', 'F')) {
+        av_log(avctx, AV_LOG_ERROR, "missing RIFF tag\n");
+        ret = AVERROR_INVALIDDATA;
+        goto fail;
+    }
+    bytestream2_skip(&s->anim_gb, 4); /* RIFF chunk size, ignored */
+    if (bytestream2_get_le32(&s->anim_gb) != MKTAG('W', 'E', 'B', 'P')) {
+        av_log(avctx, AV_LOG_ERROR, "missing WEBP tag\n");
+        ret = AVERROR_INVALIDDATA;
+        goto fail;
+    }
+
+    /* --- Peek at the first chunk to decide static vs. animated --- */
+    {
+        GetByteContext peek = s->anim_gb;
+        uint32_t first_tag;
+        if (bytestream2_get_bytes_left(&peek) < 4) {
+            ret = AVERROR_INVALIDDATA;
+            goto fail;
+        }
+        first_tag = bytestream2_get_le32(&peek);
+        if (first_tag != MKTAG('V', 'P', '8', 'X')) {
+            /* Simple lossy (VP8 ) or lossless (VP8L): not animated. */
+            ret = webp_decode_static(avctx, frame, &s->anim_gb, s->anim_pkt);
+            av_packet_unref(s->anim_pkt);
+            if (ret < 0)
+                return ret;
+            return 0;
+        }
+    }
+
+    /* --- Extended file format: scan the header chunks --- */
+    {
+        GetByteContext *gb = &s->anim_gb;
+        uint32_t chunk_type, chunk_size;
+        int found_anim = 0;
+
+        while (bytestream2_get_bytes_left(gb) > 8) {
+            chunk_type = bytestream2_get_le32(gb);
+            chunk_size = bytestream2_get_le32(gb);
+            if (chunk_size == UINT32_MAX) {
+                ret = AVERROR_INVALIDDATA;
+                goto fail;
+            }
+            chunk_size += chunk_size & 1;
+            if (bytestream2_get_bytes_left(gb) < chunk_size) {
+                ret = AVERROR_INVALIDDATA;
+                goto fail;
+            }
+
+            switch (chunk_type) {
+            case MKTAG('V', 'P', '8', 'X'):
+                if (s->width || s->height) {
+                    av_log(avctx, AV_LOG_ERROR, "duplicate VP8X chunk\n");
+                    ret = AVERROR_INVALIDDATA;
+                    goto fail;
+                }
+                s->vp8x_flags = bytestream2_get_byte(gb);
+                bytestream2_skip(gb, 3);
+                s->width  = bytestream2_get_le24(gb) + 1;
+                s->height = bytestream2_get_le24(gb) + 1;
+                ret = av_image_check_size(s->width, s->height, 0, avctx);
+                if (ret < 0)
+                    goto fail;
+                if (!(s->vp8x_flags & VP8X_FLAG_ANIMATION)) {
+                    /* Extended but not animated: fall back to static decode. 
*/
+                    /* Reset gb to start of chunks and re-decode statically. */
+                    bytestream2_init(gb, s->anim_pkt->data + 12,
+                                     s->anim_pkt->size - 12);
+                    s->width  = 0;
+                    s->height = 0;
+                    s->vp8x_flags = 0;
+                    ret = webp_decode_static(avctx, frame, gb, s->anim_pkt);
+                    av_packet_unref(s->anim_pkt);
+                    if (ret < 0)
+                        return ret;
+                    return 0;
+                }
+                break;
+            case MKTAG('I', 'C', 'C', 'P'):
+                /* Skip ICC profile in animation mode (no output frame yet). */
+                bytestream2_skip(gb, chunk_size);
+                break;
+            case MKTAG('A', 'N', 'I', 'M'):
+                if (chunk_size < 6) {
+                    av_log(avctx, AV_LOG_ERROR, "ANIM chunk too small\n");
+                    ret = AVERROR_INVALIDDATA;
+                    goto fail;
+                }
+                /* Background color and loop count are not used for decoding.
+                 * Specification is not clear on how and when to use bg color.
+                 * Most existing decoders seem ignoring it and we want our
+                 * decoder to generate the same result.
+                 * Loop count is also not used and we always output one loop. 
+                 * User can always use filters to loop if they want.
+                 */
+                s->anim_bg_color  = bytestream2_get_le32(gb);  // BGRA
+                s->anim_loop_count = bytestream2_get_le16(gb);
+                bytestream2_skip(gb, chunk_size - 6);
+                found_anim = 1;
+                break;
+            case MKTAG('A', 'N', 'M', 'F'):
+                /* First frame: stop header parsing, back up to re-read 
tag+size. */
+                bytestream2_seek(gb,
+                    bytestream2_tell(gb) - 8, /* rewind past tag+size */
+                    SEEK_SET);
+                goto header_done;
+            default:
+                bytestream2_skip(gb, chunk_size);
+                break;
+            }
+        }
+
+header_done:
+        if (!found_anim) {
+            /* VP8X with animation flag but no ANIM chunk – try static. */
+            bytestream2_init(gb, s->anim_pkt->data + 12,
+                             s->anim_pkt->size - 12);
+            s->width     = 0;
+            s->height    = 0;
+            s->vp8x_flags = 0;
+            ret = webp_decode_static(avctx, frame, gb, s->anim_pkt);
+            av_packet_unref(s->anim_pkt);
+            if (ret < 0)
+                return ret;
+            return 0;
+        }
+    }
+
+    /* --- Set up for animation --- */
+    ret = ff_set_dimensions(avctx, s->width, s->height);
+    if (ret < 0)
+        goto fail;
+    avctx->pix_fmt = AV_PIX_FMT_ARGB;
+
+    /* Allocate the compositing canvas. */
+    av_frame_free(&s->canvas);
+    s->canvas = av_frame_alloc();
+    if (!s->canvas) {
+        ret = AVERROR(ENOMEM);
+        goto fail;
+    }
+    s->canvas->format = AV_PIX_FMT_ARGB;
+    s->canvas->width  = s->width;
+    s->canvas->height = s->height;
+    ret = av_frame_get_buffer(s->canvas, 0);
+    if (ret < 0) {
+        av_frame_free(&s->canvas);
+        goto fail;
+    }
+    canvas_clear_background(s->canvas);
+
+    s->is_anim           = 1;
+    s->anim_header_parsed = 1;
+    s->anim_pts           = 0;
+    s->prev_disposal      = 0;
+    s->prev_frame_x       = 0;
+    s->prev_frame_y       = 0;
+    s->prev_frame_w       = 0;
+    s->prev_frame_h       = 0;
+
+    /* Decode and return the first ANMF frame via recursive tail. */
+    {
+        GetByteContext *gb = &s->anim_gb;
+        uint32_t chunk_type, chunk_size;
+
+        while (bytestream2_get_bytes_left(gb) > 8) {
+            chunk_type = bytestream2_get_le32(gb);
+            chunk_size = bytestream2_get_le32(gb);
+            if (chunk_size == UINT32_MAX) {
+                ret = AVERROR_INVALIDDATA;
+                goto anim_fail;
+            }
+            chunk_size += chunk_size & 1;
+            if (bytestream2_get_bytes_left(gb) < chunk_size) {
+                ret = AVERROR_INVALIDDATA;
+                goto anim_fail;
+            }
+            if (chunk_type == MKTAG('A', 'N', 'M', 'F')) {
+                ret = decode_anmf_chunk(avctx, frame, chunk_size);
+                if (ret < 0)
+                    goto anim_fail;
+                return 0;
+            }
+            bytestream2_skip(gb, chunk_size);
+        }
+        /* No ANMF found at all. */
+        av_log(avctx, AV_LOG_ERROR, "animated WebP has no frames\n");
+        ret = AVERROR_INVALIDDATA;
+    }
+
+anim_fail:
+    av_frame_free(&s->canvas);
+    s->anim_header_parsed = 0;
+    s->is_anim            = 0;
+fail:
+    av_packet_unref(s->anim_pkt);
+    return ret;
 }
 
 static av_cold int webp_decode_init(AVCodecContext *avctx)
@@ -1546,6 +2160,10 @@ static av_cold int webp_decode_init(AVCodecContext 
*avctx)
     if (!s->pkt)
         return AVERROR(ENOMEM);
 
+    s->anim_pkt = av_packet_alloc();
+    if (!s->anim_pkt)
+        return AVERROR(ENOMEM);
+
     return 0;
 }
 
@@ -1554,6 +2172,8 @@ static av_cold int webp_decode_close(AVCodecContext 
*avctx)
     WebPContext *s = avctx->priv_data;
 
     av_packet_free(&s->pkt);
+    av_packet_free(&s->anim_pkt);
+    av_frame_free(&s->canvas);
 
     if (s->initialized)
         return ff_vp8_decode_free(avctx);
@@ -1563,14 +2183,14 @@ static av_cold int webp_decode_close(AVCodecContext 
*avctx)
 
 const FFCodec ff_webp_decoder = {
     .p.name         = "webp",
-    CODEC_LONG_NAME("WebP image"),
+    CODEC_LONG_NAME("WebP image and animation"),
     .p.type         = AVMEDIA_TYPE_VIDEO,
     .p.id           = AV_CODEC_ID_WEBP,
     .priv_data_size = sizeof(WebPContext),
     .init           = webp_decode_init,
-    FF_CODEC_DECODE_CB(webp_decode_frame),
+    FF_CODEC_RECEIVE_FRAME_CB(webp_receive_frame),
     .close          = webp_decode_close,
-    .p.capabilities = AV_CODEC_CAP_DR1 | AV_CODEC_CAP_FRAME_THREADS,
+    .p.capabilities = AV_CODEC_CAP_DR1,
     .caps_internal  = FF_CODEC_CAP_ICC_PROFILES |
                       FF_CODEC_CAP_USES_PROGRESSFRAMES,
 };
-- 
2.44.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to