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]
