PR #22975 opened by Ramiro Polla (ramiro) URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22975 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22975.patch
This PR is based on the work by Josef Zlomek and Thilo Borgmann. A different approach has been used from [the previous patchset](https://ffmpeg.org/pipermail/ffmpeg-devel/2024-June/thread.html#330023), having a separate simpler demuxer dedicated to Animated WebP (which does not support still WebP like in Josef's demuxer), and a separate decoder dedicated to Animated WebP (that does not modify the still WebP decoder). This way, it's still possible to run `ffmpeg -i frame%03d.webp` with frame-based parallelism. Animated WebP files are based on a persistent canvas. Each frame will update (overwriting or blending) a region of the canvas using a VP8 (lossy or lossless) subframe, which may be disposed before the next frame. The pixel format may change between frames (`ARGB` for lossless or `YUV(A)` for lossy), but the canvas may still contain data from a different pixel format. To work around this discrepancy, when a new frame is lossless (`ARGB`) and the previous frame was lossy (`YUV(A)`), the canvas is upgraded to `ARGB`. But when a new frame is lossy over a lossless canvas, the subframe is converted to `ARGB` instead, to preserve the higher quality of the canvas. I don't know of any real-world files that would hit this condition, but since it is not forbidden by the spec, we support it. >From 0ab5cf146f937c9f9f3aae9b2bd0348800a770ff Mon Sep 17 00:00:00 2001 From: Thilo Borgmann <[email protected]> Date: Fri, 21 Jun 2024 12:43:23 +0200 Subject: [PATCH 1/5] avcodec/webp: export XMP metadata Signed-off-by: Ramiro Polla <[email protected]> --- libavcodec/webp.c | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/libavcodec/webp.c b/libavcodec/webp.c index 39fdd961a1..b34ed5e44d 100644 --- a/libavcodec/webp.c +++ b/libavcodec/webp.c @@ -35,9 +35,11 @@ * Exif metadata * ICC profile * + * @author Thilo Borgmann <thilo.borgmann _at_ mail.de> + * XMP metadata + * * Unimplemented: * - Animation - * - XMP metadata */ #include "libavutil/imgutils.h" @@ -205,6 +207,7 @@ typedef struct WebPContext { int alpha_data_size; /* alpha chunk data size */ int has_exif; /* set after an EXIF chunk has been processed */ int has_iccp; /* set after an ICCP chunk has been processed */ + int has_xmp; /* set after an XMP chunk has been processed */ int width; /* image width */ int height; /* image height */ @@ -1350,6 +1353,7 @@ static int webp_decode_frame(AVCodecContext *avctx, AVFrame *p, s->has_alpha = 0; s->has_exif = 0; s->has_iccp = 0; + s->has_xmp = 0; bytestream2_init(&gb, avpkt->data, avpkt->size); if (bytestream2_get_bytes_left(&gb) < 12) @@ -1509,11 +1513,34 @@ exif_end: } case MKTAG('A', 'N', 'I', 'M'): case MKTAG('A', 'N', 'M', 'F'): - case MKTAG('X', 'M', 'P', ' '): av_log(avctx, AV_LOG_WARNING, "skipping unsupported chunk: %s\n", av_fourcc2str(chunk_type)); bytestream2_skip(&gb, chunk_size); break; + case MKTAG('X', 'M', 'P', ' '): { + if (s->has_xmp) { + av_log(avctx, AV_LOG_VERBOSE, "Ignoring extra XMP chunk\n"); + bytestream2_skip(&gb, chunk_size); + break; + } + if (!(vp8x_flags & VP8X_FLAG_XMP_METADATA)) + av_log(avctx, AV_LOG_WARNING, + "XMP chunk present, but XMP bit not set in the " + "VP8X header\n"); + + s->has_xmp = 1; + + // there are at least chunk_size bytes left to read + uint8_t *buffer = av_malloc(chunk_size + 1); + if (!buffer) + return AVERROR(ENOMEM); + + bytestream2_get_buffer(&gb, buffer, chunk_size); + buffer[chunk_size] = '\0'; + + av_dict_set(&p->metadata, "xmp", buffer, AV_DICT_DONT_STRDUP_VAL); + break; + } default: av_log(avctx, AV_LOG_VERBOSE, "skipping unknown chunk: %s\n", av_fourcc2str(chunk_type)); -- 2.52.0 >From 8c0a6cd6fdc65b3f58c40e65313ed1e6882e2a79 Mon Sep 17 00:00:00 2001 From: Ramiro Polla <[email protected]> Date: Wed, 29 Apr 2026 22:20:38 +0200 Subject: [PATCH 2/5] avcodec/codec_id: add Animated WebP codec ID and descriptor Signed-off-by: Ramiro Polla <[email protected]> --- libavcodec/codec_desc.c | 8 ++++++++ libavcodec/codec_id.h | 1 + 2 files changed, 9 insertions(+) diff --git a/libavcodec/codec_desc.c b/libavcodec/codec_desc.c index a9f21f8152..99e7c0de3d 100644 --- a/libavcodec/codec_desc.c +++ b/libavcodec/codec_desc.c @@ -2009,6 +2009,14 @@ static const AVCodecDescriptor codec_descriptors[] = { AV_CODEC_PROP_LOSSLESS, .mime_types= MT("image/jxs"), }, + { + .id = AV_CODEC_ID_WEBP_ANIM, + .type = AVMEDIA_TYPE_VIDEO, + .name = "webp_anim", + .long_name = NULL_IF_CONFIG_SMALL("Animated WebP"), + .props = AV_CODEC_PROP_LOSSY | AV_CODEC_PROP_LOSSLESS, + .mime_types= MT("image/webp"), + }, /* various PCM "codecs" */ { diff --git a/libavcodec/codec_id.h b/libavcodec/codec_id.h index 6529f0a6bc..bb2c16a958 100644 --- a/libavcodec/codec_id.h +++ b/libavcodec/codec_id.h @@ -332,6 +332,7 @@ enum AVCodecID { AV_CODEC_ID_APV, AV_CODEC_ID_PRORES_RAW, AV_CODEC_ID_JPEGXS, + AV_CODEC_ID_WEBP_ANIM, /* various PCM "codecs" */ AV_CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs -- 2.52.0 >From bfd8c958c48e503e3a4db4274cb40fa13a2d59de Mon Sep 17 00:00:00 2001 From: Ramiro Polla <[email protected]> Date: Wed, 22 Apr 2026 18:09:26 +0200 Subject: [PATCH 3/5] avcodec/webp: add support for Animated WebP decoding Fixes: 4907 Adds Animated WebP feature according to spec: https://developers.google.com/speed/webp/docs/riff_container#animation Original work by Josef Zlomek <[email protected]> and Thilo Borgmann <[email protected]> Signed-off-by: Ramiro Polla <[email protected]> --- Changelog | 1 + configure | 1 + libavcodec/Makefile | 1 + libavcodec/allcodecs.c | 1 + libavcodec/version.h | 2 +- libavcodec/webp.c | 675 ++++++++++++++++++++++++++++++++++++++++- 6 files changed, 678 insertions(+), 3 deletions(-) diff --git a/Changelog b/Changelog index 3f5e38e0c7..4af1667d54 100644 --- a/Changelog +++ b/Changelog @@ -9,6 +9,7 @@ version <next>: - HE-AAC 960 decoding (DAB+) - transpose_cuda filter - Add AMF Frame Rate Converter (vf_frc_amf) filter +- Animated WebP decoder version 8.1: diff --git a/configure b/configure index 9bce658932..692bda984f 100755 --- a/configure +++ b/configure @@ -3364,6 +3364,7 @@ vp9_decoder_select="videodsp vp9_parser cbs_vp9 vp9_superframe_split_bsf" vvc_decoder_select="cabac cbs_h266 golomb videodsp vvc_sei" wcmv_decoder_select="inflate_wrapper" webp_decoder_select="vp8_decoder" +webp_anim_decoder_select="vp8_decoder" wmalossless_decoder_select="llauddsp" wmapro_decoder_select="sinewin wma_freqs" wmav1_decoder_select="sinewin wma_freqs" diff --git a/libavcodec/Makefile b/libavcodec/Makefile index 85d35913f3..56cf04d2c9 100644 --- a/libavcodec/Makefile +++ b/libavcodec/Makefile @@ -846,6 +846,7 @@ OBJS-$(CONFIG_WBMP_DECODER) += wbmpdec.o OBJS-$(CONFIG_WBMP_ENCODER) += wbmpenc.o OBJS-$(CONFIG_WCMV_DECODER) += wcmv.o OBJS-$(CONFIG_WEBP_DECODER) += webp.o +OBJS-$(CONFIG_WEBP_ANIM_DECODER) += webp.o OBJS-$(CONFIG_WEBVTT_DECODER) += webvttdec.o ass.o OBJS-$(CONFIG_WEBVTT_ENCODER) += webvttenc.o ass_split.o OBJS-$(CONFIG_WMALOSSLESS_DECODER) += wmalosslessdec.o wma_common.o diff --git a/libavcodec/allcodecs.c b/libavcodec/allcodecs.c index 5e798c1430..e29a3a4020 100644 --- a/libavcodec/allcodecs.c +++ b/libavcodec/allcodecs.c @@ -396,6 +396,7 @@ extern const FFCodec ff_vqc_decoder; extern const FFCodec ff_vvc_decoder; extern const FFCodec ff_wbmp_decoder; extern const FFCodec ff_wbmp_encoder; +extern const FFCodec ff_webp_anim_decoder; extern const FFCodec ff_webp_decoder; extern const FFCodec ff_wcmv_decoder; extern const FFCodec ff_wrapped_avframe_encoder; diff --git a/libavcodec/version.h b/libavcodec/version.h index aeb58e3fed..497389d3f3 100644 --- a/libavcodec/version.h +++ b/libavcodec/version.h @@ -29,7 +29,7 @@ #include "version_major.h" -#define LIBAVCODEC_VERSION_MINOR 30 +#define LIBAVCODEC_VERSION_MINOR 31 #define LIBAVCODEC_VERSION_MICRO 100 #define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \ diff --git a/libavcodec/webp.c b/libavcodec/webp.c index b34ed5e44d..c38f443f04 100644 --- a/libavcodec/webp.c +++ b/libavcodec/webp.c @@ -2,6 +2,7 @@ * WebP (.webp) image decoder * Copyright (c) 2013 Aneesh Dogra <[email protected]> * Copyright (c) 2013 Justin Ruggles <[email protected]> + * Copyright (c) 2020 Pexeso Inc. * * This file is part of FFmpeg. * @@ -38,10 +39,13 @@ * @author Thilo Borgmann <thilo.borgmann _at_ mail.de> * XMP metadata * - * Unimplemented: - * - Animation + * @author Josef Zlomek, Pexeso Inc. <[email protected]> + * Animation */ +#include "config_components.h" + +#include "libavutil/colorspace.h" #include "libavutil/imgutils.h" #include "libavutil/mem.h" @@ -1593,3 +1597,670 @@ const FFCodec ff_webp_decoder = { .caps_internal = FF_CODEC_CAP_ICC_PROFILES | FF_CODEC_CAP_USES_PROGRESSFRAMES, }; + +#if CONFIG_WEBP_ANIM_DECODER + +#define ANMF_DISPOSAL_METHOD 0x01 +#define ANMF_DISPOSAL_METHOD_UNCHANGED 0x00 +#define ANMF_DISPOSAL_METHOD_BACKGROUND 0x01 + +#define ANMF_BLENDING_METHOD 0x02 +#define ANMF_BLENDING_METHOD_ALPHA 0x00 +#define ANMF_BLENDING_METHOD_OVERWRITE 0x02 + +typedef struct AnimatedWebPContext { + WebPContext w; + + AVFrame *canvas; /* AVFrame for canvas */ + AVFrame *subframe; /* AVFrame for subframe */ + int canvas_width; /* canvas width */ + int canvas_height; /* canvas height */ + int anmf_flags; /* frame flags from ANMF chunk */ + int pos_x; /* frame position X */ + int pos_y; /* frame position Y */ + int duration; /* frame duration */ + int prev_anmf_flags; /* previous frame flags from ANMF chunk */ + int prev_width; /* previous frame width */ + int prev_height; /* previous frame height */ + int prev_pos_x; /* previous frame position X */ + int prev_pos_y; /* previous frame position Y */ + uint8_t background_argb[4]; /* background color in ARGB format */ + uint8_t background_yuva[4]; /* background color in YUVA format */ +} AnimatedWebPContext; + +/* + * Blend src1 (foreground) and src2 (background) into dest, in ARGB format. + * width, height are the dimensions of src1. + * pos_x, pos_y is the position in src2 and in dest. + */ +static void blend_alpha_argb(uint8_t *dest_data[4], int dest_linesize[4], + const uint8_t *src1_data[4], int src1_linesize[4], + const uint8_t *src2_data[4], int src2_linesize[4], + int width, int height, int pos_x, int pos_y) +{ + for (int y = 0; y < height; y++) { + const uint8_t *src1 = src1_data[0] + y * src1_linesize[0]; + const uint8_t *src2 = src2_data[0] + (pos_y + y) * src2_linesize[0] + pos_x * sizeof(uint32_t); + uint8_t *dest = dest_data[0] + (pos_y + y) * dest_linesize[0] + pos_x * sizeof(uint32_t); + for (int x = 0; x < width; x++) { + int src1_alpha = src1[0]; + int src2_alpha = src2[0]; + + if (src1_alpha == 255) { + memcpy(dest, src1, sizeof(uint32_t)); + } else if (src1_alpha == 0) { + memcpy(dest, src2, sizeof(uint32_t)); + } else { + int tmp_alpha = (src2_alpha * (256 - src1_alpha)) >> 8; + int blend_alpha = src1_alpha + tmp_alpha; + int scale = (1UL << 24) / blend_alpha; + + dest[0] = blend_alpha; + dest[1] = (((uint32_t) (src1[1] * src1_alpha + src2[1] * tmp_alpha)) * scale) >> 24; + dest[2] = (((uint32_t) (src1[2] * src1_alpha + src2[2] * tmp_alpha)) * scale) >> 24; + dest[3] = (((uint32_t) (src1[3] * src1_alpha + src2[3] * tmp_alpha)) * scale) >> 24; + } + src1 += sizeof(uint32_t); + src2 += sizeof(uint32_t); + dest += sizeof(uint32_t); + } + } +} + +/* + * Blend src1 (foreground) and src2 (background) into dest, in YUVA format. + * width, height are the dimensions of src1. + * pos_x, pos_y is the position in src2 and in dest. + */ +static void blend_alpha_yuva(uint8_t *dest_data[4], int dest_linesize[4], + const uint8_t *src1_data[4], int src1_linesize[4], + int src1_format, + const uint8_t *src2_data[4], int src2_linesize[4], + int width, int height, int pos_x, int pos_y) +{ + const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(src1_format); + + int plane_y = desc->comp[0].plane; + int plane_u = desc->comp[1].plane; + int plane_v = desc->comp[2].plane; + int plane_a = desc->comp[3].plane; + + // blend U & V planes first, because the later step may modify alpha plane + int w = AV_CEIL_RSHIFT(width, 1); + int h = AV_CEIL_RSHIFT(height, 1); + int px = AV_CEIL_RSHIFT(pos_x, 1); + int py = AV_CEIL_RSHIFT(pos_y, 1); + int tile_w = 2; + int tile_h = 2; + + for (int y = 0; y < h; y++) { + const uint8_t *src1_u = src1_data[plane_u] + y * src1_linesize[plane_u]; + const uint8_t *src1_v = src1_data[plane_v] + y * src1_linesize[plane_v]; + const uint8_t *src2_u = src2_data[plane_u] + (py + y) * src2_linesize[plane_u] + px; + const uint8_t *src2_v = src2_data[plane_v] + (py + y) * src2_linesize[plane_v] + px; + uint8_t *dest_u = dest_data[plane_u] + (py + y) * dest_linesize[plane_u] + px; + uint8_t *dest_v = dest_data[plane_v] + (py + y) * dest_linesize[plane_v] + px; + for (int x = 0; x < w; x++) { + // calculate the average alpha of the tile + int src1_alpha = 0; + int src2_alpha = 0; + for (int yy = 0; yy < tile_h; yy++) { + for (int xx = 0; xx < tile_w; xx++) { + src1_alpha += src1_data[plane_a][(y * tile_h + yy) * src1_linesize[plane_a] + + (x * tile_w + xx)]; + src2_alpha += src2_data[plane_a][((py + y) * tile_h + yy) * src2_linesize[plane_a] + + ((px + x) * tile_w + xx)]; + } + } + src1_alpha = AV_CEIL_RSHIFT(src1_alpha, 2); + src2_alpha = AV_CEIL_RSHIFT(src2_alpha, 2); + + if (src1_alpha == 255) { + *dest_u = *src1_u; + *dest_v = *src1_v; + } else if (src1_alpha == 0) { + *dest_u = *src2_u; + *dest_v = *src2_v; + } else { + int tmp_alpha = (src2_alpha * (256 - src1_alpha)) >> 8; + int blend_alpha = src1_alpha + tmp_alpha; + int scale = (1UL << 24) / blend_alpha; + *dest_u = (((uint32_t) (*src1_u * src1_alpha + *src2_u * tmp_alpha)) * scale) >> 24; + *dest_v = (((uint32_t) (*src1_v * src1_alpha + *src2_v * tmp_alpha)) * scale) >> 24; + } + src1_u++; + src1_v++; + src2_u++; + src2_v++; + dest_u++; + dest_v++; + } + } + + // blend Y & A planes + for (int y = 0; y < height; y++) { + const uint8_t *src1_y = src1_data[plane_y] + y * src1_linesize[plane_y]; + const uint8_t *src1_a = src1_data[plane_a] + y * src1_linesize[plane_a]; + const uint8_t *src2_y = src2_data[plane_y] + (pos_y + y) * src2_linesize[plane_y] + pos_x; + const uint8_t *src2_a = src2_data[plane_a] + (pos_y + y) * src2_linesize[plane_a] + pos_x; + uint8_t *dest_y = dest_data[plane_y] + (pos_y + y) * dest_linesize[plane_y] + pos_x; + uint8_t *dest_a = dest_data[plane_a] + (pos_y + y) * dest_linesize[plane_a] + pos_x; + for (int x = 0; x < width; x++) { + int src1_alpha = *src1_a; + int src2_alpha = *src2_a; + + if (src1_alpha == 255) { + *dest_y = *src1_y; + *dest_a = 255; + } else if (src1_alpha == 0) { + *dest_y = *src2_y; + *dest_a = src2_alpha; + } else { + int tmp_alpha = (src2_alpha * (256 - src1_alpha)) >> 8; + int blend_alpha = src1_alpha + tmp_alpha; + int scale = (1UL << 24) / blend_alpha; + *dest_y = (((uint32_t) (*src1_y * src1_alpha + *src2_y * tmp_alpha)) * scale) >> 24; + *dest_a = blend_alpha; + } + src1_y++; + src1_a++; + src2_y++; + src2_a++; + dest_y++; + dest_a++; + } + } +} + +static av_always_inline void webp_yuva2argb(uint8_t *out, int Y, int U, int V, int A) +{ + // variables used in macros + const uint8_t *cm = ff_crop_tab + MAX_NEG_CROP; + uint8_t r, g, b; + int y, cb, cr; + int r_add, g_add, b_add; + + YUV_TO_RGB1_CCIR(U, V); + YUV_TO_RGB2_CCIR(r, g, b, Y); + + out[0] = av_clip_uint8(A); + out[1] = av_clip_uint8(r); + out[2] = av_clip_uint8(g); + out[3] = av_clip_uint8(b); +} + +static void copy_yuva2argb(AVFrame *dst, AVFrame *src, int pos_x, int pos_y) +{ + const AVPixFmtDescriptor *src_desc = av_pix_fmt_desc_get(src->format); + + int alpha = src_desc->nb_components > 3; + int plane_y = src_desc->comp[0].plane; + int plane_u = src_desc->comp[1].plane; + int plane_v = src_desc->comp[2].plane; + int plane_a = src_desc->comp[3].plane; + + for (int y = 0; y < src->height; y++) { + uint8_t *dest = dst->data[0] + (pos_y + y) * dst->linesize[0] + pos_x * 4; + uint8_t *src_y = src->data[plane_y] + y * src->linesize[plane_y]; + uint8_t *src_u = src->data[plane_u] + (y >> 1) * src->linesize[plane_u]; + uint8_t *src_v = src->data[plane_v] + (y >> 1) * src->linesize[plane_v]; + uint8_t *src_a = NULL; + if (alpha) + src_a = src->data[plane_a] + y * src->linesize[plane_a]; + + for (int x = 0; x < src->width; x++) { + webp_yuva2argb(dest, *src_y, *src_u, *src_v, (alpha ? *src_a : 255)); + src_y += 1; + src_u += x & 1; + src_v += x & 1; + if (alpha) + src_a += 1; + dest += 4; + } + } +} + +static void blend_yuva2argb(AVFrame *dst, AVFrame *src, int pos_x, int pos_y) +{ + const AVPixFmtDescriptor *src_desc = av_pix_fmt_desc_get(src->format); + + int plane_y = src_desc->comp[0].plane; + int plane_u = src_desc->comp[1].plane; + int plane_v = src_desc->comp[2].plane; + int plane_a = src_desc->comp[3].plane; + + for (int y = 0; y < src->height; y++) { + uint8_t *dest = dst->data[0] + (pos_y + y) * dst->linesize[0] + pos_x * 4; + uint8_t *src_y = src->data[plane_y] + y * src->linesize[plane_y]; + uint8_t *src_u = src->data[plane_u] + (y >> 1) * src->linesize[plane_u]; + uint8_t *src_v = src->data[plane_v] + (y >> 1) * src->linesize[plane_v]; + uint8_t *src_a = src->data[plane_a] + y * src->linesize[plane_a]; + + for (int x = 0; x < src->width; x++) { + int dst_alpha = dest[0]; + int src_alpha = *src_a; + + if (src_alpha == 255) { + webp_yuva2argb(dest, *src_y, *src_u, *src_v, src_alpha); + } else if (src_alpha == 0) { + // no-op + } else { + uint8_t tmp[4]; + int tmp_alpha = (dst_alpha * (256 - src_alpha)) >> 8; + int blend_alpha = src_alpha + tmp_alpha; + int scale = (1UL << 24) / blend_alpha; + + webp_yuva2argb(tmp, *src_y, *src_u, *src_v, src_alpha); + + dest[0] = blend_alpha; + dest[1] = (((uint32_t) (tmp[1] * src_alpha + dest[1] * tmp_alpha)) * scale) >> 24; + dest[2] = (((uint32_t) (tmp[2] * src_alpha + dest[2] * tmp_alpha)) * scale) >> 24; + dest[3] = (((uint32_t) (tmp[3] * src_alpha + dest[3] * tmp_alpha)) * scale) >> 24; + } + + src_y += 1; + src_u += x & 1; + src_v += x & 1; + src_a += 1; + dest += 4; + } + } +} + +static int blend_subframe_into_canvas(AnimatedWebPContext *s) +{ + AVFrame *canvas = s->canvas; + AVFrame *frame = s->subframe; + + if ((s->anmf_flags & ANMF_BLENDING_METHOD) == ANMF_BLENDING_METHOD_OVERWRITE + || frame->format == AV_PIX_FMT_YUV420P) { + // do not blend, overwrite + + if (canvas->format == AV_PIX_FMT_ARGB) { + if (canvas->format == frame->format) { + for (int y = 0; y < s->w.height; y++) { + const uint32_t *src = (uint32_t *) (frame->data[0] + y * frame->linesize[0]); + uint32_t *dst = (uint32_t *) (canvas->data[0] + (s->pos_y + y) * canvas->linesize[0]) + s->pos_x; + memcpy(dst, src, s->w.width * sizeof(uint32_t)); + } + } else { + copy_yuva2argb(canvas, frame, s->pos_x, s->pos_y); + } + } else /* if (canvas->format == AV_PIX_FMT_YUVA420P) */ { + const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(frame->format); + + for (int comp = 0; comp < desc->nb_components; comp++) { + int plane = desc->comp[comp].plane; + int width = s->w.width; + int height = s->w.height; + int pos_x = s->pos_x; + int pos_y = s->pos_y; + if (comp == 1 || comp == 2) { + width = AV_CEIL_RSHIFT(width, 1); + height = AV_CEIL_RSHIFT(height, 1); + pos_x = AV_CEIL_RSHIFT(pos_x, 1); + pos_y = AV_CEIL_RSHIFT(pos_y, 1); + } + + for (int y = 0; y < height; y++) { + const uint8_t *src = frame->data[plane] + y * frame->linesize[plane]; + uint8_t *dst = canvas->data[plane] + (pos_y + y) * canvas->linesize[plane] + pos_x; + memcpy(dst, src, width); + } + } + + if (canvas->format == AV_PIX_FMT_YUVA420P && desc->nb_components < 4) { + // frame does not have alpha, set alpha to 255 + const AVPixFmtDescriptor *canvas_desc = av_pix_fmt_desc_get(canvas->format); + int plane = canvas_desc->comp[3].plane; + int width = s->w.width; + int height = s->w.height; + int pos_x = s->pos_x; + int pos_y = s->pos_y; + + for (int y = 0; y < height; y++) { + uint8_t *dst = canvas->data[plane] + (pos_y + y) * canvas->linesize[plane] + pos_x; + memset(dst, 255, width); + } + } + } + } else { + // alpha blending + + if (canvas->format == AV_PIX_FMT_ARGB) { + if (canvas->format == frame->format) { + blend_alpha_argb(canvas->data, canvas->linesize, + (const uint8_t **) frame->data, frame->linesize, + (const uint8_t **) canvas->data, canvas->linesize, + s->w.width, s->w.height, s->pos_x, s->pos_y); + } else { + blend_yuva2argb(canvas, frame, s->pos_x, s->pos_y); + } + } else /* if (canvas->format == AV_PIX_FMT_YUVA420P) */ { + blend_alpha_yuva(canvas->data, canvas->linesize, + (const uint8_t **) frame->data, frame->linesize, + frame->format, + (const uint8_t **) canvas->data, canvas->linesize, + s->w.width, s->w.height, s->pos_x, s->pos_y); + } + } + + return 0; +} + +/** + * Fill a rectangle on the canvas with the background color (transparent black + * by default, or the color from the ANIM chunk if provided by the demuxer). + */ +static void fill_canvas_rect(AnimatedWebPContext *s, int pos_x, int pos_y, int width, int height) +{ + AVFrame *canvas = s->canvas; + + if (canvas->format == AV_PIX_FMT_ARGB) { + uint32_t bg_color = AV_RN32(s->background_argb); + int is_repeatable = (bg_color == ((bg_color & 0xff) * 0x01010101)); + for (int y = 0; y < height; y++) { + uint32_t *dst = (uint32_t *) (canvas->data[0] + (pos_y + y) * canvas->linesize[0]) + pos_x; + if (is_repeatable) { + memset(dst, bg_color, width * sizeof(uint32_t)); + } else { + for (int x = 0; x < width; x++) + dst[x] = bg_color; + } + } + } else /* if (canvas->format == AV_PIX_FMT_YUVA420P) */ { + const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(canvas->format); + for (int comp = 0; comp < desc->nb_components; comp++) { + int shift = (comp == 1 || comp == 2) ? 1 : 0; + int plane = desc->comp[comp].plane; + uint8_t *dst = canvas->data[plane] + + AV_CEIL_RSHIFT(pos_y, shift) * canvas->linesize[plane] + + AV_CEIL_RSHIFT(pos_x, shift); + for (int y = 0; y < AV_CEIL_RSHIFT(height, shift); y++) { + memset(dst, s->background_yuva[plane], AV_CEIL_RSHIFT(width, shift)); + dst += canvas->linesize[plane]; + } + } + } +} + +static int allocate_canvas(AnimatedWebPContext *s, int format) +{ + s->w.avctx->pix_fmt = format; + int ret = ff_set_dimensions(s->w.avctx, s->canvas_width, s->canvas_height); + if (ret < 0) + return ret; + return ff_reget_buffer(s->w.avctx, s->canvas, 0); +} + +static int prepare_canvas(AnimatedWebPContext *s, int key_frame, int format) +{ + int ret; + + /** + * Clear the canvas on keyframes and frames that overwrite the entire + * canvas. + */ + if (key_frame || + ((s->anmf_flags & ANMF_BLENDING_METHOD) == ANMF_BLENDING_METHOD_OVERWRITE && + (s->pos_x == 0) && (s->pos_x + s->w.width == s->canvas_width) && + (s->pos_y == 0) && (s->pos_y + s->w.height == s->canvas_height))) + av_frame_unref(s->canvas); + + if (!s->canvas->buf[0]) { + /* Allocate new canvas frame */ + ret = allocate_canvas(s, format); + if (ret < 0) + return ret; + /* ... and initialize it. */ + fill_canvas_rect(s, 0, 0, s->canvas->width, s->canvas->height); + } else { + if (format == AV_PIX_FMT_ARGB && s->canvas->format == AV_PIX_FMT_YUVA420P) { + /** + * If we have a lossless frame following a lossy frame, we upgrade + * the canvas to ARGB, but we don't convert the canvas back to YUVA + * if there is a lossy frame following a lossless frame. + */ + AVFrame *yuva_canvas = av_frame_clone(s->canvas); + if (!yuva_canvas) + return AVERROR(ENOMEM); + av_frame_unref(s->canvas); + ret = allocate_canvas(s, AV_PIX_FMT_ARGB); + if (ret < 0) { + av_frame_free(&yuva_canvas); + return ret; + } + copy_yuva2argb(s->canvas, yuva_canvas, 0, 0); + av_frame_free(&yuva_canvas); + } else { + /** + * The decode frame function returns a reference to the canvas, + * therefore we have to ensure it is writable before using it + * for a new frame. + */ + ret = av_frame_make_writable(s->canvas); + if (ret < 0) + return ret; + } + /* Dispose of previous frame if needed. */ + if ((s->prev_anmf_flags & ANMF_DISPOSAL_METHOD) == ANMF_DISPOSAL_METHOD_BACKGROUND) + fill_canvas_rect(s, s->prev_pos_x, s->prev_pos_y, s->prev_width, s->prev_height); + } + + return 0; +} + +static int webp_anim_decode_frame(AVCodecContext *avctx, AVFrame *p, + int *got_frame, AVPacket *avpkt) +{ + AnimatedWebPContext *s = avctx->priv_data; + int key_frame = (avpkt->flags & AV_PKT_FLAG_KEY); + int ret; + + GetByteContext gb; + bytestream2_init(&gb, avpkt->data, avpkt->size); + + /* Parse ANMF header. */ + s->pos_x = bytestream2_get_le24(&gb) * 2; + s->pos_y = bytestream2_get_le24(&gb) * 2; + s->w.width = bytestream2_get_le24(&gb) + 1; + s->w.height = bytestream2_get_le24(&gb) + 1; + s->duration = bytestream2_get_le24(&gb); + s->anmf_flags = bytestream2_get_byte(&gb); + + av_log(avctx, AV_LOG_DEBUG, + "ANMF frame pos: %dx%d size: %dx%d duration: %d\n", + s->pos_x, s->pos_y, s->w.width, s->w.height, s->duration); + + if (s->pos_x + s->w.width > s->canvas_width || + s->pos_y + s->w.height > s->canvas_height) { + av_log(avctx, AV_LOG_ERROR, + "Frame (%dx%d at pos %dx%d) does not fit into canvas (%dx%d)\n", + s->w.width, s->w.height, s->pos_x, s->pos_y, + s->canvas_width, s->canvas_height); + ret = AVERROR_INVALIDDATA; + goto end; + } + + /* Reset alpha field from previous frame. */ + s->w.has_alpha = 0; + + /* Parse ANMF subchunks. */ + while (bytestream2_get_bytes_left(&gb) > 8) { + uint32_t chunk_type = bytestream2_get_le32(&gb); + uint32_t chunk_size = bytestream2_get_le32(&gb); + + if (chunk_size == UINT32_MAX) { + ret = AVERROR_INVALIDDATA; + goto end; + } + chunk_size += chunk_size & 1; + + 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; + } + + switch (chunk_type) { + case MKTAG('A', 'L', 'P', 'H'): { + if (chunk_size == 0) { + av_log(avctx, AV_LOG_ERROR, "invalid ALPHA chunk size\n"); + ret = AVERROR_INVALIDDATA; + goto end; + } + int alpha_header = bytestream2_get_byte(&gb); + s->w.alpha_data = avpkt->data + bytestream2_tell(&gb); + s->w.alpha_data_size = chunk_size - 1; + bytestream2_skip(&gb, s->w.alpha_data_size); + + int filter_m = (alpha_header >> 2) & 0x03; + int compression = alpha_header & 0x03; + + if (compression > ALPHA_COMPRESSION_VP8L) { + av_log(avctx, AV_LOG_VERBOSE, + "skipping unsupported ALPHA chunk\n"); + } else { + s->w.has_alpha = 1; + s->w.alpha_compression = compression; + s->w.alpha_filter = filter_m; + } + + break; + } + case MKTAG('V', 'P', '8', ' '): + if (*got_frame) { + av_log(avctx, AV_LOG_VERBOSE, "Ignoring extra VP8 chunk\n"); + bytestream2_skip(&gb, chunk_size); + break; + } + ret = prepare_canvas(s, key_frame, AV_PIX_FMT_YUVA420P); + if (ret < 0) + goto end; + ret = vp8_lossy_decode_frame(avctx, s->subframe, got_frame, + avpkt->data + bytestream2_tell(&gb), + chunk_size); + if (ret < 0) + goto end; + bytestream2_skip(&gb, chunk_size); + break; + case MKTAG('V', 'P', '8', 'L'): + if (*got_frame) { + av_log(avctx, AV_LOG_VERBOSE, "Ignoring extra VP8L chunk\n"); + bytestream2_skip(&gb, chunk_size); + break; + } + ret = prepare_canvas(s, key_frame, AV_PIX_FMT_ARGB); + if (ret < 0) + goto end; + ret = vp8_lossless_decode_frame(avctx, s->subframe, got_frame, + avpkt->data + bytestream2_tell(&gb), + chunk_size, 0); + if (ret < 0) + goto end; +#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: + av_log(avctx, AV_LOG_VERBOSE, "skipping unknown chunk: %s\n", + av_fourcc2str(chunk_type)); + bytestream2_skip(&gb, chunk_size); + break; + } + } + + if (!*got_frame) { + av_log(avctx, AV_LOG_ERROR, "image data not found\n"); + ret = AVERROR_INVALIDDATA; + goto end; + } + + ret = blend_subframe_into_canvas(s); + if (ret < 0) + goto end; + + ret = av_frame_ref(p, s->canvas); + if (ret < 0) + goto end; + + p->pict_type = key_frame ? AV_PICTURE_TYPE_I : AV_PICTURE_TYPE_P; + p->pts = avpkt->pts; + p->duration = s->duration; + + s->prev_anmf_flags = s->anmf_flags; + s->prev_width = s->w.width; + s->prev_height = s->w.height; + s->prev_pos_x = s->pos_x; + s->prev_pos_y = s->pos_y; + + ret = avpkt->size; + +end: + av_frame_unref(s->subframe); + return ret; +} + +static av_cold int webp_anim_decode_init(AVCodecContext *avctx) +{ + AnimatedWebPContext *s = avctx->priv_data; + + s->w.avctx = avctx; + s->canvas_width = avctx->width; + s->canvas_height = avctx->height; + + s->canvas = av_frame_alloc(); + if (!s->canvas) + return AVERROR(ENOMEM); + + s->subframe = av_frame_alloc(); + if (!s->subframe) + return AVERROR(ENOMEM); + + /** + * Use background color if it was provided by the demuxer. Otherwise, the + * background color will be 0x00000000 (transparent black). + */ + if (avctx->extradata_size >= 4) { + s->background_argb[0] = avctx->extradata[3]; + s->background_argb[1] = avctx->extradata[2]; + s->background_argb[2] = avctx->extradata[1]; + s->background_argb[3] = avctx->extradata[0]; + } + + /* Convert background color to YUVA. */ + const uint8_t *argb = s->background_argb; + s->background_yuva[0] = RGB_TO_Y_CCIR(argb[1], argb[2], argb[3]); + s->background_yuva[1] = RGB_TO_U_CCIR(argb[1], argb[2], argb[3], 0); + s->background_yuva[2] = RGB_TO_V_CCIR(argb[1], argb[2], argb[3], 0); + s->background_yuva[3] = argb[0]; + + return webp_decode_init(avctx); +} + +static av_cold int webp_anim_decode_close(AVCodecContext *avctx) +{ + AnimatedWebPContext *s = avctx->priv_data; + + av_frame_free(&s->canvas); + av_frame_free(&s->subframe); + + return webp_decode_close(avctx); +} + +const FFCodec ff_webp_anim_decoder = { + .p.name = "webp_anim", + CODEC_LONG_NAME("Animated WebP image"), + .p.type = AVMEDIA_TYPE_VIDEO, + .p.id = AV_CODEC_ID_WEBP_ANIM, + .priv_data_size = sizeof(AnimatedWebPContext), + .init = webp_anim_decode_init, + FF_CODEC_DECODE_CB(webp_anim_decode_frame), + .close = webp_anim_decode_close, + .p.capabilities = AV_CODEC_CAP_DR1 | AV_CODEC_CAP_SLICE_THREADS, + .caps_internal = FF_CODEC_CAP_USES_PROGRESSFRAMES, +}; +#endif /* CONFIG_WEBP_ANIM_DECODER */ -- 2.52.0 >From 952cdc1ad46ffca81d4b514c3ee9497e8cae5a62 Mon Sep 17 00:00:00 2001 From: Ramiro Polla <[email protected]> Date: Wed, 22 Apr 2026 18:09:53 +0200 Subject: [PATCH 4/5] avformat/webp: add Animated WebP demuxer Original work by Josef Zlomek <[email protected]> Signed-off-by: Ramiro Polla <[email protected]> --- Changelog | 1 + doc/demuxers.texi | 35 ++++ libavformat/Makefile | 1 + libavformat/allformats.c | 1 + libavformat/version.h | 4 +- libavformat/webp_anim_dec.c | 371 ++++++++++++++++++++++++++++++++++++ 6 files changed, 411 insertions(+), 2 deletions(-) create mode 100644 libavformat/webp_anim_dec.c diff --git a/Changelog b/Changelog index 4af1667d54..bdbdcdc58b 100644 --- a/Changelog +++ b/Changelog @@ -10,6 +10,7 @@ version <next>: - transpose_cuda filter - Add AMF Frame Rate Converter (vf_frc_amf) filter - Animated WebP decoder +- Animated WebP demuxer version 8.1: diff --git a/doc/demuxers.texi b/doc/demuxers.texi index a49d3406f6..170fafb4bb 100644 --- a/doc/demuxers.texi +++ b/doc/demuxers.texi @@ -1187,4 +1187,39 @@ this is set to 0, which means that a sensible value is chosen based on the input format. @end table +@section webp + +Animated WebP demuxer. + +It accepts the following options: + +@table @option +@item -min_delay @var{int} +Set the minimum valid delay between frames in milliseconds. +Range is 0 to 60000. Default value is 10. + +@item -max_webp_delay @var{int} +Set the maximum valid delay between frames in milliseconds. +Range is 0 to 16777215. Default value is 16777215 (over four hours), +the maximum value allowed by the specification. + +@item -default_delay @var{int} +Set the default delay between frames in milliseconds. +Range is 0 to 60000. Default value is 100. + +@item -ignore_loop @var{bool} +WebP files can contain information to loop a certain number of times +(or infinitely). If @option{ignore_loop} is set to true, then the loop +setting from the input will be ignored and looping will not occur. +If set to false, then looping will occur and will cycle the number +of times according to the WebP. Default value is true. + +@item usebgcolor @var{bool} +WebP files contain a background color hint in the ANIM chunk, but the +WebP specification says that viewer applications are not required to +use it. If @option{usebgcolor} is set to true, then the background +color hint will be used, otherwise transparent black will be used for +the background. Default value is false. +@end table + @c man end DEMUXERS diff --git a/libavformat/Makefile b/libavformat/Makefile index 7c2fcad93e..8f2ed1c749 100644 --- a/libavformat/Makefile +++ b/libavformat/Makefile @@ -644,6 +644,7 @@ OBJS-$(CONFIG_WEBM_MUXER) += matroskaenc.o matroska.o \ avlanguage.o OBJS-$(CONFIG_WEBM_DASH_MANIFEST_MUXER) += webmdashenc.o OBJS-$(CONFIG_WEBM_CHUNK_MUXER) += webm_chunk.o +OBJS-$(CONFIG_WEBP_ANIM_DEMUXER) += webp_anim_dec.o OBJS-$(CONFIG_WEBP_MUXER) += webpenc.o OBJS-$(CONFIG_WEBVTT_DEMUXER) += webvttdec.o subtitles.o OBJS-$(CONFIG_WEBVTT_MUXER) += webvttenc.o diff --git a/libavformat/allformats.c b/libavformat/allformats.c index a491e3a7e8..af7eea5e5c 100644 --- a/libavformat/allformats.c +++ b/libavformat/allformats.c @@ -516,6 +516,7 @@ extern const FFInputFormat ff_webm_dash_manifest_demuxer; extern const FFOutputFormat ff_webm_dash_manifest_muxer; extern const FFOutputFormat ff_webm_chunk_muxer; extern const FFOutputFormat ff_webp_muxer; +extern const FFInputFormat ff_webp_anim_demuxer; extern const FFInputFormat ff_webvtt_demuxer; extern const FFOutputFormat ff_webvtt_muxer; extern const FFInputFormat ff_wsaud_demuxer; diff --git a/libavformat/version.h b/libavformat/version.h index ff831498b3..14bf700274 100644 --- a/libavformat/version.h +++ b/libavformat/version.h @@ -31,8 +31,8 @@ #include "version_major.h" -#define LIBAVFORMAT_VERSION_MINOR 13 -#define LIBAVFORMAT_VERSION_MICRO 102 +#define LIBAVFORMAT_VERSION_MINOR 14 +#define LIBAVFORMAT_VERSION_MICRO 100 #define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \ LIBAVFORMAT_VERSION_MINOR, \ diff --git a/libavformat/webp_anim_dec.c b/libavformat/webp_anim_dec.c new file mode 100644 index 0000000000..f54544ba5a --- /dev/null +++ b/libavformat/webp_anim_dec.c @@ -0,0 +1,371 @@ +/* + * Animated WebP demuxer + * Copyright (c) 2020 Pexeso Inc. + * Copyright (c) 2026 Ramiro Polla + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * @file + * Animated WebP demuxer. + */ + +#include "avio_internal.h" +#include "demux.h" +#include "avformat.h" +#include "internal.h" +#include "libavutil/intreadwrite.h" +#include "libavutil/mem.h" +#include "libavutil/opt.h" + +#define VP8X_FLAG_ANIMATION 0x02 +#define VP8X_FLAG_XMP_METADATA 0x04 +#define VP8X_FLAG_EXIF_METADATA 0x08 +#define VP8X_FLAG_ALPHA 0x10 +#define VP8X_FLAG_ICC 0x20 + +typedef struct WebPAnimDemuxContext { + const AVClass *class; + + /** + * Minimum allowed delay between frames in milliseconds. + * Values below this threshold are considered to be invalid + * and set to value of default_delay. + */ + int min_delay; + int max_delay; + int default_delay; + + /* + * loop options + */ + int ignore_loop; ///< ignore loop setting + int loop_count; ///< number of times to loop the animation + int cur_loop; ///< current loop counter + + /* + * variables for the key frame detection + */ + int cur_frame; ///< number of frames of the current animation file + int vp8x_flags; + + int has_iccp; + int has_exif; + int has_anim; + int has_xmp; + + int usebgcolor; + + int64_t first_anmf_offset; +} WebPAnimDemuxContext; + +/** + * Major web browsers display WebPs at ~10-15fps when rate is not + * explicitly set or have too low values. We assume default rate to be 10. + * Default delay = 1000 microseconds / 10fps = 100 milliseconds per frame. + */ +#define WEBP_DEFAULT_DELAY 100 +/** + * By default delay values less than this threshold considered to be invalid. + */ +#define WEBP_MIN_DELAY 10 + +static int webp_anim_probe(const AVProbeData *p) +{ + const uint8_t *b = p->buf; + + if (AV_RL32(b) == MKTAG('R', 'I', 'F', 'F') && + AV_RL32(b + 8) == MKTAG('W', 'E', 'B', 'P') && + AV_RL32(b + 12) == MKTAG('V', 'P', '8', 'X') && + AV_RL32(b + 16) == 10 && + (b[20] & VP8X_FLAG_ANIMATION)) + return AVPROBE_SCORE_MAX; + + return 0; +} + +static int webp_anim_read_header(AVFormatContext *s) +{ + WebPAnimDemuxContext *ctx = s->priv_data; + AVIOContext *pb = s->pb; + int ret; + + /* Check for signature. */ + if (avio_rl32(pb) != MKTAG('R', 'I', 'F', 'F')) + return AVERROR_INVALIDDATA; + avio_skip(pb, 4); /* file size */ + if (avio_rl32(pb) != MKTAG('W', 'E', 'B', 'P')) + return AVERROR_INVALIDDATA; + + /* VP8X must be first chunk */ + if (avio_rl32(pb) != MKTAG('V', 'P', '8', 'X') || + avio_rl32(pb) != 10 /* chunk size */) + return AVERROR_INVALIDDATA; + ctx->vp8x_flags = avio_r8(pb); + if (!(ctx->vp8x_flags & VP8X_FLAG_ANIMATION)) + return AVERROR_INVALIDDATA; + avio_skip(pb, 3); + + AVStream *st = avformat_new_stream(s, NULL); + if (!st) + return AVERROR(ENOMEM); + + avpriv_set_pts_info(st, 64, 1, 1000); + st->codecpar->format = AV_PIX_FMT_ARGB; + st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; + st->codecpar->codec_id = AV_CODEC_ID_WEBP_ANIM; + st->codecpar->width = avio_rl24(pb) + 1; + st->codecpar->height = avio_rl24(pb) + 1; + st->start_time = 0; + + int explode = (s->error_recognition & AV_EF_EXPLODE); + int loglevel = explode ? AV_LOG_ERROR : AV_LOG_WARNING; + while (1) { + int64_t offset = avio_tell(pb); + uint32_t fourcc = avio_rl32(pb); + uint32_t size = avio_rl32(pb); + + av_log(s, AV_LOG_DEBUG, "Chunk %s of size %u at offset %" PRId64 "\n", + av_fourcc2str(fourcc), size, offset); + + if (size == UINT32_MAX) + return AVERROR_INVALIDDATA; + size += size & 1; + + if (avio_feof(pb)) + break; + + switch (fourcc) { + case MKTAG('I', 'C', 'C', 'P'): + if (ctx->has_iccp) { + av_log(s, loglevel, "Extra ICCP chunk found\n"); + if (explode) + return AVERROR_INVALIDDATA; + avio_skip(pb, size); + } else { + if (!(ctx->vp8x_flags & VP8X_FLAG_ICC)) { + av_log(s, loglevel, + "ICCP chunk present, but ICC Profile bit not set in the VP8X header\n"); + if (explode) + return AVERROR_INVALIDDATA; + } + + AVPacketSideData *sd = av_packet_side_data_new(&st->codecpar->coded_side_data, + &st->codecpar->nb_coded_side_data, + AV_PKT_DATA_ICC_PROFILE, size, 0); + if (!sd) + return AVERROR(ENOMEM); + ret = avio_read(pb, sd->data, size); + if (ret < 0) + return ret; + ctx->has_iccp = 1; + } + break; + case MKTAG('A', 'N', 'I', 'M'): + if (ctx->has_anim) { + av_log(s, loglevel, "Extra ANIM chunk found\n"); + if (explode) + return AVERROR_INVALIDDATA; + avio_skip(pb, size); + } else { + if (size != 6) + return AVERROR_INVALIDDATA; + uint32_t bg_color = avio_rb32(pb); + ctx->loop_count = avio_rl16(pb); + if (ctx->usebgcolor) { + st->codecpar->extradata = av_mallocz(4 + AV_INPUT_BUFFER_PADDING_SIZE); + if (!st->codecpar->extradata) + return AVERROR(ENOMEM); + AV_WB32(st->codecpar->extradata, bg_color); + st->codecpar->extradata_size = 4; + } + av_log(s, AV_LOG_DEBUG, + "ANIM: background BGRA 0x%08x loop count %d\n", + bg_color, ctx->loop_count); + ctx->has_anim = 1; + } + break; + case MKTAG('A', 'N', 'M', 'F'): + if (!ctx->has_anim) { + av_log(s, loglevel, + "ANMF chunk present, but no previous ANIM chunk found\n"); + if (explode) + return AVERROR_INVALIDDATA; + ctx->loop_count = 1; + } else if (!ctx->ignore_loop && ctx->loop_count != 1) { + int64_t file_size = avio_size(pb); + if (file_size < 0 || offset < 0 || + (ret = ffio_ensure_seekback(pb, file_size - offset)) < 0) { + av_log(s, AV_LOG_WARNING, + "Could not ensure seekback, will not loop\n"); + ctx->loop_count = 1; + } + } + ctx->first_anmf_offset = offset; + ret = avio_seek(pb, -8, SEEK_CUR); + if (ret < 0) + return ret; + return 0; + default: + av_log(s, AV_LOG_WARNING, "Skipping chunk: %s\n", av_fourcc2str(fourcc)); + avio_skip(pb, size); + break; + } + } + + return AVERROR_INVALIDDATA; +} + +static int webp_anim_read_packet(AVFormatContext *s, AVPacket *pkt) +{ + WebPAnimDemuxContext *ctx = s->priv_data; + AVIOContext *pb = s->pb; + int ret; + + int explode = (s->error_recognition & AV_EF_EXPLODE); + int loglevel = explode ? AV_LOG_ERROR : AV_LOG_WARNING; + while (1) { + int64_t offset = avio_tell(pb); + uint32_t fourcc = avio_rl32(pb); + uint32_t size = avio_rl32(pb); + + if (size == UINT32_MAX) + return AVERROR_INVALIDDATA; + size += size & 1; + + if (avio_feof(pb)) { + if (!ctx->ignore_loop && + (ctx->loop_count == 0 || ++ctx->cur_loop < ctx->loop_count)) { + ctx->cur_frame = 0; + ret = avio_seek(pb, ctx->first_anmf_offset, SEEK_SET); + if (ret < 0) + return ret; + continue; + } + break; + } + + av_log(s, AV_LOG_DEBUG, "Chunk %s of size %u at offset %" PRId64 "\n", + av_fourcc2str(fourcc), size, offset); + + switch (fourcc) { + case MKTAG('A', 'N', 'M', 'F'): + if (size < 16) + return AVERROR_INVALIDDATA; + ret = av_get_packet(pb, pkt, size); + if (ret < 0) + return ret; + if (!ctx->cur_frame++) + pkt->flags |= AV_PKT_FLAG_KEY; + pkt->pts = AV_NOPTS_VALUE; + pkt->dts = AV_NOPTS_VALUE; + uint32_t duration = AV_RL24(pkt->data + 12); + if (duration < ctx->min_delay) + duration = ctx->default_delay; + pkt->duration = FFMIN(duration, ctx->max_delay); + return ret; + case MKTAG('E', 'X', 'I', 'F'): + if (ctx->has_exif) { + av_log(s, loglevel, "Extra EXIF chunk found\n"); + if (explode) + return AVERROR_INVALIDDATA; + avio_skip(pb, size); + } else { + if (!(ctx->vp8x_flags & VP8X_FLAG_EXIF_METADATA)) { + av_log(s, loglevel, + "EXIF chunk present, but EXIF bit not set in the VP8X header\n"); + if (explode) + return AVERROR_INVALIDDATA; + } + + AVStream *st = s->streams[0]; + AVPacketSideData *sd = av_packet_side_data_new(&st->codecpar->coded_side_data, + &st->codecpar->nb_coded_side_data, + AV_PKT_DATA_EXIF, size, 0); + if (!sd) + return AVERROR(ENOMEM); + ret = avio_read(pb, sd->data, size); + if (ret < 0) + return ret; + ctx->has_exif = 1; + } + break; + case MKTAG('X', 'M', 'P', ' '): + if (ctx->has_xmp) { + av_log(s, loglevel, "Extra XMP chunk found\n"); + if (explode) + return AVERROR_INVALIDDATA; + avio_skip(pb, size); + } else { + if (!(ctx->vp8x_flags & VP8X_FLAG_XMP_METADATA)) { + av_log(s, loglevel, + "XMP chunk present, but XMP bit not set in the VP8X header\n"); + if (explode) + return AVERROR_INVALIDDATA; + } + + uint8_t *xmp = av_malloc(size + 1); + if (!xmp) + return AVERROR(ENOMEM); + ret = ffio_read_size(pb, xmp, size); + if (ret < 0) { + av_free(xmp); + return ret; + } + xmp[size] = '\0'; + av_dict_set(&s->metadata, "xmp", xmp, AV_DICT_DONT_STRDUP_VAL); + ctx->has_xmp = 1; + } + break; + default: + av_log(s, AV_LOG_WARNING, "Skipping chunk: %s\n", av_fourcc2str(fourcc)); + avio_skip(pb, size); + break; + } + } + + return AVERROR_EOF; +} + +static const AVOption options[] = { + { "min_delay", "minimum valid delay between frames (in milliseconds)", offsetof(WebPAnimDemuxContext, min_delay), AV_OPT_TYPE_INT, {.i64 = WEBP_MIN_DELAY}, 0, 1000 * 60, AV_OPT_FLAG_DECODING_PARAM }, + { "max_webp_delay", "maximum valid delay between frames (in milliseconds)", offsetof(WebPAnimDemuxContext, max_delay), AV_OPT_TYPE_INT, {.i64 = 0xffffff}, 0, 0xffffff, AV_OPT_FLAG_DECODING_PARAM }, + { "default_delay", "default delay between frames (in milliseconds)", offsetof(WebPAnimDemuxContext, default_delay), AV_OPT_TYPE_INT, {.i64 = WEBP_DEFAULT_DELAY}, 0, 1000 * 60, AV_OPT_FLAG_DECODING_PARAM }, + { "ignore_loop", "ignore loop setting", offsetof(WebPAnimDemuxContext, ignore_loop), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, AV_OPT_FLAG_DECODING_PARAM }, + { "usebgcolor", "use background color from ANIM chunk", offsetof(WebPAnimDemuxContext, usebgcolor), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, AV_OPT_FLAG_DECODING_PARAM }, + { NULL }, +}; + +static const AVClass demuxer_class = { + .class_name = "Animated WebP demuxer", + .item_name = av_default_item_name, + .option = options, + .version = LIBAVUTIL_VERSION_INT, + .category = AV_CLASS_CATEGORY_DEMUXER, +}; + +const FFInputFormat ff_webp_anim_demuxer = { + .p.name = "webp_anim", + .p.long_name = NULL_IF_CONFIG_SMALL("Animated WebP"), + .p.flags = AVFMT_GENERIC_INDEX, + .p.priv_class = &demuxer_class, + .priv_data_size = sizeof(WebPAnimDemuxContext), + .read_probe = webp_anim_probe, + .read_header = webp_anim_read_header, + .read_packet = webp_anim_read_packet, +}; -- 2.52.0 >From 69a0297df2127b5529f46c2e4b6fc1dc676dd4eb Mon Sep 17 00:00:00 2001 From: Thilo Borgmann via ffmpeg-devel <ffmpeg-devel at ffmpeg.org> Date: Fri, 21 Jun 2024 12:43:22 +0200 Subject: [PATCH 5/5] fate: add test for animated WebP Signed-off-by: Ramiro Polla <[email protected]> --- tests/fate/image.mak | 13 +++++ tests/ref/fate/webp-anim | 22 ++++++++ tests/ref/fate/webp-chfmt1 | 23 ++++++++ tests/ref/fate/webp-chfmt2 | 106 +++++++++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+) create mode 100644 tests/ref/fate/webp-anim create mode 100644 tests/ref/fate/webp-chfmt1 create mode 100644 tests/ref/fate/webp-chfmt2 diff --git a/tests/fate/image.mak b/tests/fate/image.mak index 334c421140..d4b5ec5437 100644 --- a/tests/fate/image.mak +++ b/tests/fate/image.mak @@ -589,6 +589,19 @@ FATE_WEBP-$(call DEMDEC, IMAGE2, WEBP) += $(FATE_WEBP) FATE_IMAGE_FRAMECRC += $(FATE_WEBP-yes) fate-webp: $(FATE_WEBP-yes) +FATE_WEBP_ANIM += fate-webp-anim +fate-webp-anim: CMD = framecrc -i $(TARGET_SAMPLES)/webp/anim.webp + +FATE_WEBP_ANIM += fate-webp-chfmt1 +fate-webp-chfmt1: CMD = framecrc -i $(TARGET_SAMPLES)/webp/anim_rgb_yuv.webp + +FATE_WEBP_ANIM += fate-webp-chfmt2 +fate-webp-chfmt2: CMD = framecrc -i $(TARGET_SAMPLES)/webp/anim_yuv_rgb.webp + +FATE_WEBP_ANIM-$(call DEMDEC, WEBP_ANIM, WEBP_ANIM) += $(FATE_WEBP_ANIM) +FATE_IMAGE_FRAMECRC += $(FATE_WEBP_ANIM-yes) +fate-webp: $(FATE_WEBP_ANIM-yes) + FATE_IMAGE_FRAMECRC-$(call DEMDEC, IMAGE2, XFACE) += fate-xface fate-xface: CMD = framecrc -i $(TARGET_SAMPLES)/xface/lena.xface diff --git a/tests/ref/fate/webp-anim b/tests/ref/fate/webp-anim new file mode 100644 index 0000000000..1c9ec18b18 --- /dev/null +++ b/tests/ref/fate/webp-anim @@ -0,0 +1,22 @@ +#tb 0: 1/25 +#media_type 0: video +#codec_id 0: rawvideo +#dimensions 0: 100x70 +#sar 0: 0/1 +0, 0, 0, 2, 28000, 0x2023ba6e +0, 2, 2, 2, 28000, 0x4292b778 +0, 4, 4, 2, 28000, 0x1c972ef1 +0, 6, 6, 2, 28000, 0xa98d8d04 +0, 8, 8, 2, 28000, 0xd323b6af +0, 10, 10, 2, 28000, 0x508aba99 +0, 12, 12, 2, 28000, 0x5c672dda +0, 14, 14, 2, 28000, 0xc8961ebb +0, 16, 16, 25, 28000, 0x82460e1b +0, 41, 41, 2, 28000, 0x3debbfc9 +0, 43, 43, 2, 28000, 0x427ab31f +0, 45, 45, 2, 28000, 0x6bbdec2e +0, 47, 47, 2, 28000, 0x5690b56b +0, 49, 49, 2, 28000, 0xb62963f3 +0, 51, 51, 2, 28000, 0x68dd37b2 +0, 53, 53, 2, 28000, 0x465c47d2 +0, 55, 55, 250, 28000, 0xa92033df diff --git a/tests/ref/fate/webp-chfmt1 b/tests/ref/fate/webp-chfmt1 new file mode 100644 index 0000000000..bd0de28718 --- /dev/null +++ b/tests/ref/fate/webp-chfmt1 @@ -0,0 +1,23 @@ +#tb 0: 2/25 +#media_type 0: video +#codec_id 0: rawvideo +#dimensions 0: 488x488 +#sar 0: 0/1 +0, 0, 0, 1, 952576, 0x5f74d3ae +0, 1, 1, 1, 952576, 0x8b0f6cfe +0, 2, 2, 1, 952576, 0x3e47771e +0, 3, 3, 1, 952576, 0x63816e85 +0, 4, 4, 1, 952576, 0xf7a5d79a +0, 5, 5, 6, 952576, 0x50c75e53 +0, 11, 11, 1, 952576, 0x54d4cdd3 +0, 12, 12, 1, 952576, 0x00648e70 +0, 13, 13, 2, 952576, 0xd9f7f652 +0, 15, 15, 2, 952576, 0x34c15d97 +0, 17, 17, 2, 952576, 0x94b9ab85 +0, 19, 19, 7, 952576, 0xa702612f +0, 26, 26, 1, 952576, 0xd248d21e +0, 27, 27, 1, 952576, 0xc0f7ffcf +0, 28, 28, 9, 952576, 0x4e281a20 +0, 37, 37, 1, 952576, 0x114617ec +0, 38, 38, 1, 952576, 0x79ba99a5 +0, 39, 39, 9, 952576, 0x79cb003d diff --git a/tests/ref/fate/webp-chfmt2 b/tests/ref/fate/webp-chfmt2 new file mode 100644 index 0000000000..cb4d48cb5a --- /dev/null +++ b/tests/ref/fate/webp-chfmt2 @@ -0,0 +1,106 @@ +#tb 0: 3/100 +#media_type 0: video +#codec_id 0: rawvideo +#dimensions 0: 320x240 +#sar 0: 0/1 +0, 0, 0, 1, 192000, 0x41a50269 +0, 1, 1, 1, 192000, 0x963823c8 +0, 2, 2, 1, 192000, 0x43bf206f +0, 3, 3, 1, 192000, 0x4ce51d59 +0, 4, 4, 1, 192000, 0x528d1a81 +0, 5, 5, 1, 192000, 0x1e11fb1d +0, 6, 6, 1, 192000, 0x37672368 +0, 7, 7, 1, 192000, 0x7a6f20f9 +0, 8, 8, 1, 192000, 0xe6aa1dfc +0, 9, 9, 1, 192000, 0x295b10e6 +0, 10, 10, 1, 192000, 0xd263ff87 +0, 11, 11, 1, 192000, 0x3cf105ee +0, 12, 12, 1, 192000, 0x0e26f9c3 +0, 13, 13, 1, 192000, 0x62fa2869 +0, 14, 14, 1, 192000, 0xed44fcca +0, 15, 15, 1, 192000, 0x0874fd8d +0, 16, 16, 1, 192000, 0x87412a3c +0, 17, 17, 1, 192000, 0x3d522eb5 +0, 18, 18, 1, 192000, 0xda660879 +0, 19, 19, 1, 192000, 0x78211c16 +0, 20, 20, 1, 192000, 0xe19e0f5d +0, 21, 21, 1, 192000, 0xbf95f295 +0, 22, 22, 1, 192000, 0x2986e59e +0, 23, 23, 1, 192000, 0x7087e1f0 +0, 24, 24, 1, 192000, 0xee15e30e +0, 25, 25, 1, 192000, 0xfd10df47 +0, 26, 26, 1, 192000, 0xa5c6d5df +0, 27, 27, 1, 192000, 0x3ee9d40b +0, 28, 28, 1, 192000, 0xd3bed274 +0, 29, 29, 1, 192000, 0x3374cfab +0, 30, 30, 1, 192000, 0x993bdae7 +0, 31, 31, 1, 192000, 0xed14d83a +0, 32, 32, 1, 192000, 0x0703dada +0, 33, 33, 1, 192000, 0xbfd5df7c +0, 34, 34, 1, 192000, 0x4796f2ee +0, 35, 35, 1, 192000, 0xce73fa45 +0, 36, 36, 1, 192000, 0x5c8928fc +0, 37, 37, 1, 192000, 0x5e84fc03 +0, 38, 38, 1, 192000, 0xe14c3368 +0, 39, 39, 1, 192000, 0x4436fc7a +0, 40, 40, 1, 192000, 0x53510a70 +0, 41, 41, 1, 192000, 0xf228071c +0, 42, 42, 1, 192000, 0xb9c00645 +0, 43, 43, 1, 192000, 0xe82606f9 +0, 44, 44, 1, 192000, 0x40a7f673 +0, 45, 45, 1, 192000, 0xb1c11d82 +0, 46, 46, 1, 192000, 0xc6991a22 +0, 47, 47, 1, 192000, 0x653a1837 +0, 48, 48, 1, 192000, 0x240516f4 +0, 49, 49, 1, 192000, 0x12a8f922 +0, 50, 50, 1, 192000, 0xe1921ebe +0, 51, 51, 1, 192000, 0x80a01c6d +0, 52, 52, 1, 192000, 0xbdc21afb +0, 53, 53, 1, 192000, 0x25be1c54 +0, 54, 54, 1, 192000, 0x582bff4e +0, 55, 55, 1, 192000, 0x54772168 +0, 56, 56, 1, 192000, 0x6ad31e78 +0, 57, 57, 1, 192000, 0xcf001d7e +0, 58, 58, 1, 192000, 0xea6f1d81 +0, 59, 59, 1, 192000, 0xc769ff48 +0, 60, 60, 1, 192000, 0xea192087 +0, 61, 61, 1, 192000, 0x30dd1e4e +0, 62, 62, 1, 192000, 0x941f1f9b +0, 63, 63, 1, 192000, 0xfc0f1d1b +0, 64, 64, 1, 192000, 0xbd961a3e +0, 65, 65, 1, 192000, 0x4eb3fd83 +0, 66, 66, 1, 192000, 0xcdd11c59 +0, 67, 67, 1, 192000, 0xbfec1a23 +0, 68, 68, 1, 192000, 0x10cf19b0 +0, 69, 69, 1, 192000, 0xb7d61910 +0, 70, 70, 1, 192000, 0xc95d04f3 +0, 71, 71, 1, 192000, 0xdc6725a6 +0, 72, 72, 1, 192000, 0xb3c32418 +0, 73, 73, 1, 192000, 0x2c87240d +0, 74, 74, 1, 192000, 0x502123b5 +0, 75, 75, 1, 192000, 0x2549049c +0, 76, 76, 1, 192000, 0x900627cd +0, 77, 77, 1, 192000, 0x16f6256f +0, 78, 78, 1, 192000, 0x112c245c +0, 79, 79, 1, 192000, 0xb9d308b0 +0, 80, 80, 1, 192000, 0x86c92bb8 +0, 81, 81, 1, 192000, 0x59832991 +0, 82, 82, 1, 192000, 0x224528e1 +0, 83, 83, 1, 192000, 0x23c9282c +0, 84, 84, 1, 192000, 0x9c7a0a6a +0, 85, 85, 1, 192000, 0x1cef27c4 +0, 86, 86, 1, 192000, 0x822124b4 +0, 87, 87, 1, 192000, 0xd1a22470 +0, 88, 88, 1, 192000, 0x695f22f6 +0, 89, 89, 1, 192000, 0x84191ff2 +0, 90, 90, 1, 192000, 0x022701d8 +0, 91, 91, 1, 192000, 0xa8622719 +0, 92, 92, 1, 192000, 0xdb412440 +0, 93, 93, 1, 192000, 0xb9a222ee +0, 94, 94, 1, 192000, 0x904f0405 +0, 95, 95, 1, 192000, 0x8d142732 +0, 96, 96, 1, 192000, 0xb0f92390 +0, 97, 97, 1, 192000, 0x75ab21bd +0, 98, 98, 1, 192000, 0x6e9d2047 +0, 99, 99, 1, 192000, 0x586a1e6b +0, 100, 100, 1, 192000, 0x7c6bfe5e -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
