FFmpeg coverage


Directory: ../../../ffmpeg/
File: src/libavformat/webp_anim_dec.c
Date: 2026-08-28 18:35:09
Exec Total Coverage
Lines: 91 195 46.7%
Functions: 3 3 100.0%
Branches: 37 120 30.8%

Line Branch Exec Source
1 /*
2 * Animated WebP demuxer
3 * Copyright (c) 2020 Pexeso Inc.
4 * Copyright (c) 2026 Ramiro Polla
5 *
6 * This file is part of FFmpeg.
7 *
8 * FFmpeg is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU Lesser General Public
10 * License as published by the Free Software Foundation; either
11 * version 2.1 of the License, or (at your option) any later version.
12 *
13 * FFmpeg is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * Lesser General Public License for more details.
17 *
18 * You should have received a copy of the GNU Lesser General Public
19 * License along with FFmpeg; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 */
22
23 /**
24 * @file
25 * Animated WebP demuxer.
26 */
27
28 #include "avio_internal.h"
29 #include "demux.h"
30 #include "avformat.h"
31 #include "internal.h"
32 #include "libavutil/intreadwrite.h"
33 #include "libavutil/mem.h"
34 #include "libavutil/opt.h"
35
36 #define VP8X_FLAG_ANIMATION 0x02
37 #define VP8X_FLAG_XMP_METADATA 0x04
38 #define VP8X_FLAG_EXIF_METADATA 0x08
39 #define VP8X_FLAG_ALPHA 0x10
40 #define VP8X_FLAG_ICC 0x20
41
42 typedef struct WebPAnimDemuxContext {
43 const AVClass *class;
44
45 /**
46 * Minimum allowed delay between frames in milliseconds.
47 * Values below this threshold are considered to be invalid
48 * and set to value of default_delay.
49 */
50 int min_delay;
51 int max_delay;
52 int default_delay;
53
54 /*
55 * loop options
56 */
57 int ignore_loop; ///< ignore loop setting
58 int loop_count; ///< number of times to loop the animation
59 int cur_loop; ///< current loop counter
60
61 /*
62 * variables for the key frame detection
63 */
64 int cur_frame; ///< number of frames of the current animation file
65 int vp8x_flags;
66
67 int has_iccp;
68 int has_exif;
69 int has_anim;
70 int has_xmp;
71
72 int usebgcolor;
73
74 int64_t first_anmf_offset;
75 } WebPAnimDemuxContext;
76
77 /**
78 * Major web browsers display WebPs at ~10-15fps when rate is not
79 * explicitly set or have too low values. We assume default rate to be 10.
80 * Default delay = 1000 microseconds / 10fps = 100 milliseconds per frame.
81 */
82 #define WEBP_DEFAULT_DELAY 100
83 /**
84 * By default delay values less than this threshold considered to be invalid.
85 */
86 #define WEBP_MIN_DELAY 10
87
88 7955 static int webp_anim_probe(const AVProbeData *p)
89 {
90 7955 const uint8_t *b = p->buf;
91
92
2/2
✓ Branch 0 taken 1036 times.
✓ Branch 1 taken 6919 times.
7955 if (AV_RL32(b) == MKTAG('R', 'I', 'F', 'F') &&
93
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 1025 times.
1036 AV_RL32(b + 8) == MKTAG('W', 'E', 'B', 'P') &&
94
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 6 times.
11 AV_RL32(b + 12) == MKTAG('V', 'P', '8', 'X') &&
95
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 AV_RL32(b + 16) == 10 &&
96
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2 times.
5 (b[20] & VP8X_FLAG_ANIMATION))
97 3 return AVPROBE_SCORE_MAX;
98
99 7952 return 0;
100 }
101
102 3 static int webp_anim_read_header(AVFormatContext *s)
103 {
104 3 WebPAnimDemuxContext *ctx = s->priv_data;
105 3 AVIOContext *pb = s->pb;
106 int ret;
107
108 /* Check for signature. */
109
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
3 if (avio_rl32(pb) != MKTAG('R', 'I', 'F', 'F'))
110 return AVERROR_INVALIDDATA;
111 3 avio_skip(pb, 4); /* file size */
112
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
3 if (avio_rl32(pb) != MKTAG('W', 'E', 'B', 'P'))
113 return AVERROR_INVALIDDATA;
114
115 /* VP8X must be first chunk */
116
2/4
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
6 if (avio_rl32(pb) != MKTAG('V', 'P', '8', 'X') ||
117 3 avio_rl32(pb) != 10 /* chunk size */)
118 return AVERROR_INVALIDDATA;
119 3 ctx->vp8x_flags = avio_r8(pb);
120
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (!(ctx->vp8x_flags & VP8X_FLAG_ANIMATION))
121 return AVERROR_INVALIDDATA;
122 3 avio_skip(pb, 3);
123
124 3 AVStream *st = avformat_new_stream(s, NULL);
125
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (!st)
126 return AVERROR(ENOMEM);
127
128 3 avpriv_set_pts_info(st, 64, 1, 1000);
129 3 st->codecpar->format = AV_PIX_FMT_ARGB;
130 3 st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
131 3 st->codecpar->codec_id = AV_CODEC_ID_WEBP_ANIM;
132 3 st->codecpar->width = avio_rl24(pb) + 1;
133 3 st->codecpar->height = avio_rl24(pb) + 1;
134 3 st->start_time = 0;
135
136 3 int explode = (s->error_recognition & AV_EF_EXPLODE);
137
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 int loglevel = explode ? AV_LOG_ERROR : AV_LOG_WARNING;
138 3 while (1) {
139 6 int64_t offset = avio_tell(pb);
140 6 uint32_t fourcc = avio_rl32(pb);
141 6 uint32_t size = avio_rl32(pb);
142
143 6 av_log(s, AV_LOG_DEBUG, "Chunk %s of size %u at offset %" PRId64 "\n",
144 6 av_fourcc2str(fourcc), size, offset);
145
146
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (size == UINT32_MAX)
147 3 return AVERROR_INVALIDDATA;
148 6 size += size & 1;
149
150
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
6 if (avio_feof(pb))
151 break;
152
153
2/4
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
6 switch (fourcc) {
154 case MKTAG('I', 'C', 'C', 'P'):
155 if (ctx->has_iccp) {
156 av_log(s, loglevel, "Extra ICCP chunk found\n");
157 if (explode)
158 3 return AVERROR_INVALIDDATA;
159 avio_skip(pb, size);
160 } else {
161 if (!(ctx->vp8x_flags & VP8X_FLAG_ICC)) {
162 av_log(s, loglevel,
163 "ICCP chunk present, but ICC Profile bit not set in the VP8X header\n");
164 if (explode)
165 return AVERROR_INVALIDDATA;
166 }
167
168 AVPacketSideData *sd = av_packet_side_data_new(&st->codecpar->coded_side_data,
169 &st->codecpar->nb_coded_side_data,
170 AV_PKT_DATA_ICC_PROFILE, size, 0);
171 if (!sd)
172 return AVERROR(ENOMEM);
173 ret = ffio_read_size(pb, sd->data, size);
174 if (ret < 0) {
175 av_packet_side_data_remove(st->codecpar->coded_side_data, &st->codecpar->nb_coded_side_data, AV_PKT_DATA_ICC_PROFILE);
176 return ret;
177 }
178 ctx->has_iccp = 1;
179 }
180 3 break;
181 3 case MKTAG('A', 'N', 'I', 'M'):
182
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (ctx->has_anim) {
183 av_log(s, loglevel, "Extra ANIM chunk found\n");
184 if (explode)
185 return AVERROR_INVALIDDATA;
186 avio_skip(pb, size);
187 } else {
188
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (size != 6)
189 return AVERROR_INVALIDDATA;
190 3 uint32_t bg_color = avio_rb32(pb);
191 3 ctx->loop_count = avio_rl16(pb);
192
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (ctx->usebgcolor) {
193 st->codecpar->extradata = av_mallocz(4 + AV_INPUT_BUFFER_PADDING_SIZE);
194 if (!st->codecpar->extradata)
195 return AVERROR(ENOMEM);
196 AV_WB32(st->codecpar->extradata, bg_color);
197 st->codecpar->extradata_size = 4;
198 }
199 3 av_log(s, AV_LOG_DEBUG,
200 "ANIM: background BGRA 0x%08x loop count %d\n",
201 bg_color, ctx->loop_count);
202 3 ctx->has_anim = 1;
203 }
204 3 break;
205 3 case MKTAG('A', 'N', 'M', 'F'):
206
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (!ctx->has_anim) {
207 av_log(s, loglevel,
208 "ANMF chunk present, but no previous ANIM chunk found\n");
209 if (explode)
210 return AVERROR_INVALIDDATA;
211 ctx->loop_count = 1;
212
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
3 } else if (!ctx->ignore_loop && ctx->loop_count != 1) {
213 int64_t file_size = avio_size(pb);
214 if (file_size < 0 || offset < 0 ||
215 (ret = ffio_ensure_seekback(pb, file_size - offset)) < 0) {
216 av_log(s, AV_LOG_WARNING,
217 "Could not ensure seekback, will not loop\n");
218 ctx->loop_count = 1;
219 }
220 }
221 3 ctx->first_anmf_offset = offset;
222 3 ret = avio_seek(pb, -8, SEEK_CUR);
223
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (ret < 0)
224 return ret;
225 3 return 0;
226 default:
227 av_log(s, AV_LOG_WARNING, "Skipping chunk: %s\n", av_fourcc2str(fourcc));
228 avio_skip(pb, size);
229 break;
230 }
231 }
232
233 return AVERROR_INVALIDDATA;
234 }
235
236 141 static int webp_anim_read_packet(AVFormatContext *s, AVPacket *pkt)
237 {
238 141 WebPAnimDemuxContext *ctx = s->priv_data;
239 141 AVIOContext *pb = s->pb;
240 int ret;
241
242 141 int explode = (s->error_recognition & AV_EF_EXPLODE);
243
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 141 times.
141 int loglevel = explode ? AV_LOG_ERROR : AV_LOG_WARNING;
244 while (1) {
245 141 int64_t offset = avio_tell(pb);
246 141 uint32_t fourcc = avio_rl32(pb);
247 141 uint32_t size = avio_rl32(pb);
248
249
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 141 times.
141 if (size == UINT32_MAX)
250 136 return AVERROR_INVALIDDATA;
251 141 size += size & 1;
252
253
2/2
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 136 times.
141 if (avio_feof(pb)) {
254
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (!ctx->ignore_loop &&
255 (ctx->loop_count == 0 || ++ctx->cur_loop < ctx->loop_count)) {
256 ctx->cur_frame = 0;
257 ret = avio_seek(pb, ctx->first_anmf_offset, SEEK_SET);
258 if (ret < 0)
259 return ret;
260 continue;
261 }
262 5 break;
263 }
264
265 136 av_log(s, AV_LOG_DEBUG, "Chunk %s of size %u at offset %" PRId64 "\n",
266 136 av_fourcc2str(fourcc), size, offset);
267
268
1/4
✓ Branch 0 taken 136 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
136 switch (fourcc) {
269 136 case MKTAG('A', 'N', 'M', 'F'):
270
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 136 times.
136 if (size < 16)
271 136 return AVERROR_INVALIDDATA;
272 136 ret = av_get_packet(pb, pkt, size);
273
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 136 times.
136 if (ret < 0)
274 return ret;
275
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 133 times.
136 if (!ctx->cur_frame++)
276 3 pkt->flags |= AV_PKT_FLAG_KEY;
277 136 pkt->pts = AV_NOPTS_VALUE;
278 136 pkt->dts = AV_NOPTS_VALUE;
279 136 uint32_t duration = AV_RL24(pkt->data + 12);
280
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 136 times.
136 if (duration <= ctx->min_delay)
281 duration = ctx->default_delay;
282 136 pkt->duration = FFMIN(duration, ctx->max_delay);
283 136 return ret;
284 case MKTAG('E', 'X', 'I', 'F'):
285 if (ctx->has_exif) {
286 av_log(s, loglevel, "Extra EXIF chunk found\n");
287 if (explode)
288 return AVERROR_INVALIDDATA;
289 avio_skip(pb, size);
290 } else {
291 if (!(ctx->vp8x_flags & VP8X_FLAG_EXIF_METADATA)) {
292 av_log(s, loglevel,
293 "EXIF chunk present, but EXIF bit not set in the VP8X header\n");
294 if (explode)
295 return AVERROR_INVALIDDATA;
296 }
297
298 AVStream *st = s->streams[0];
299 AVPacketSideData *sd = av_packet_side_data_new(&st->codecpar->coded_side_data,
300 &st->codecpar->nb_coded_side_data,
301 AV_PKT_DATA_EXIF, size, 0);
302 if (!sd)
303 return AVERROR(ENOMEM);
304 ret = ffio_read_size(pb, sd->data, size);
305 if (ret < 0) {
306 av_packet_side_data_remove(st->codecpar->coded_side_data, &st->codecpar->nb_coded_side_data, AV_PKT_DATA_EXIF);
307 return ret;
308 }
309 ctx->has_exif = 1;
310 }
311 break;
312 case MKTAG('X', 'M', 'P', ' '):
313 if (ctx->has_xmp) {
314 av_log(s, loglevel, "Extra XMP chunk found\n");
315 if (explode)
316 return AVERROR_INVALIDDATA;
317 avio_skip(pb, size);
318 } else {
319 if (!(ctx->vp8x_flags & VP8X_FLAG_XMP_METADATA)) {
320 av_log(s, loglevel,
321 "XMP chunk present, but XMP bit not set in the VP8X header\n");
322 if (explode)
323 return AVERROR_INVALIDDATA;
324 }
325
326 uint8_t *xmp = av_malloc(size + 1);
327 if (!xmp)
328 return AVERROR(ENOMEM);
329 ret = ffio_read_size(pb, xmp, size);
330 if (ret < 0) {
331 av_free(xmp);
332 return ret;
333 }
334 xmp[size] = '\0';
335 av_dict_set(&s->metadata, "xmp", xmp, AV_DICT_DONT_STRDUP_VAL);
336 ctx->has_xmp = 1;
337 }
338 break;
339 default:
340 av_log(s, AV_LOG_WARNING, "Skipping chunk: %s\n", av_fourcc2str(fourcc));
341 avio_skip(pb, size);
342 break;
343 }
344 }
345
346 5 return AVERROR_EOF;
347 }
348
349 static const AVOption options[] = {
350 { "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 },
351 { "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 },
352 { "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 },
353 { "ignore_loop", "ignore loop setting", offsetof(WebPAnimDemuxContext, ignore_loop), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, AV_OPT_FLAG_DECODING_PARAM },
354 { "usebgcolor", "use background color from ANIM chunk", offsetof(WebPAnimDemuxContext, usebgcolor), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, AV_OPT_FLAG_DECODING_PARAM },
355 { NULL },
356 };
357
358 static const AVClass demuxer_class = {
359 .class_name = "Animated WebP demuxer",
360 .item_name = av_default_item_name,
361 .option = options,
362 .version = LIBAVUTIL_VERSION_INT,
363 .category = AV_CLASS_CATEGORY_DEMUXER,
364 };
365
366 const FFInputFormat ff_webp_anim_demuxer = {
367 .p.name = "webp_anim",
368 .p.long_name = NULL_IF_CONFIG_SMALL("Animated WebP"),
369 .p.flags = AVFMT_GENERIC_INDEX,
370 .p.priv_class = &demuxer_class,
371 .priv_data_size = sizeof(WebPAnimDemuxContext),
372 .read_probe = webp_anim_probe,
373 .read_header = webp_anim_read_header,
374 .read_packet = webp_anim_read_packet,
375 };
376