Coverage Report

Created: 2026-10-28 08:13

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/ffmpeg/libavformat/webp_anim_dec.c
Line
Count
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
2.31k
#define VP8X_FLAG_ANIMATION             0x02
37
28
#define VP8X_FLAG_XMP_METADATA          0x04
38
35
#define VP8X_FLAG_EXIF_METADATA         0x08
39
#define VP8X_FLAG_ALPHA                 0x10
40
161
#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
static int webp_anim_probe(const AVProbeData *p)
89
963k
{
90
963k
    const uint8_t *b = p->buf;
91
92
963k
    if (AV_RL32(b)      == MKTAG('R', 'I', 'F', 'F') &&
93
6.72k
        AV_RL32(b +  8) == MKTAG('W', 'E', 'B', 'P') &&
94
236
        AV_RL32(b + 12) == MKTAG('V', 'P', '8', 'X') &&
95
91
        AV_RL32(b + 16) == 10 &&
96
88
        (b[20] & VP8X_FLAG_ANIMATION))
97
68
        return AVPROBE_SCORE_MAX;
98
99
963k
    return 0;
100
963k
}
101
102
static int webp_anim_read_header(AVFormatContext *s)
103
2.59k
{
104
2.59k
    WebPAnimDemuxContext *ctx = s->priv_data;
105
2.59k
    AVIOContext *pb = s->pb;
106
2.59k
    int ret;
107
108
    /* Check for signature. */
109
2.59k
    if (avio_rl32(pb) != MKTAG('R', 'I', 'F', 'F'))
110
199
        return AVERROR_INVALIDDATA;
111
2.39k
    avio_skip(pb, 4); /* file size */
112
2.39k
    if (avio_rl32(pb) != MKTAG('W', 'E', 'B', 'P'))
113
68
        return AVERROR_INVALIDDATA;
114
115
    /* VP8X must be first chunk */
116
2.33k
    if (avio_rl32(pb) != MKTAG('V', 'P', '8', 'X') ||
117
2.27k
        avio_rl32(pb) != 10 /* chunk size */)
118
107
        return AVERROR_INVALIDDATA;
119
2.22k
    ctx->vp8x_flags = avio_r8(pb);
120
2.22k
    if (!(ctx->vp8x_flags & VP8X_FLAG_ANIMATION))
121
4
        return AVERROR_INVALIDDATA;
122
2.21k
    avio_skip(pb, 3);
123
124
2.21k
    AVStream *st = avformat_new_stream(s, NULL);
125
2.21k
    if (!st)
126
0
        return AVERROR(ENOMEM);
127
128
2.21k
    avpriv_set_pts_info(st, 64, 1, 1000);
129
2.21k
    st->codecpar->format     = AV_PIX_FMT_ARGB;
130
2.21k
    st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
131
2.21k
    st->codecpar->codec_id   = AV_CODEC_ID_WEBP_ANIM;
132
2.21k
    st->codecpar->width      = avio_rl24(pb) + 1;
133
2.21k
    st->codecpar->height     = avio_rl24(pb) + 1;
134
2.21k
    st->start_time           = 0;
135
136
2.21k
    int explode = (s->error_recognition & AV_EF_EXPLODE);
137
2.21k
    int loglevel = explode ? AV_LOG_ERROR : AV_LOG_WARNING;
138
192k
    while (1) {
139
192k
        int64_t offset = avio_tell(pb);
140
192k
        uint32_t fourcc = avio_rl32(pb);
141
192k
        uint32_t size = avio_rl32(pb);
142
143
192k
        av_log(s, AV_LOG_DEBUG, "Chunk %s of size %u at offset %" PRId64 "\n",
144
192k
               av_fourcc2str(fourcc), size, offset);
145
146
192k
        if (size == UINT32_MAX)
147
3
            return AVERROR_INVALIDDATA;
148
192k
        size += size & 1;
149
150
192k
        if (avio_feof(pb))
151
454
            break;
152
153
192k
        switch (fourcc) {
154
513
        case MKTAG('I', 'C', 'C', 'P'):
155
513
            if (ctx->has_iccp) {
156
352
                av_log(s, loglevel, "Extra ICCP chunk found\n");
157
352
                if (explode)
158
0
                    return AVERROR_INVALIDDATA;
159
352
                avio_skip(pb, size);
160
352
            } else {
161
161
                if (!(ctx->vp8x_flags & VP8X_FLAG_ICC)) {
162
101
                    av_log(s, loglevel,
163
101
                           "ICCP chunk present, but ICC Profile bit not set in the VP8X header\n");
164
101
                    if (explode)
165
0
                        return AVERROR_INVALIDDATA;
166
101
                }
167
168
161
                AVPacketSideData *sd = av_packet_side_data_new(&st->codecpar->coded_side_data,
169
161
                                                               &st->codecpar->nb_coded_side_data,
170
161
                                                               AV_PKT_DATA_ICC_PROFILE, size, 0);
171
161
                if (!sd)
172
33
                    return AVERROR(ENOMEM);
173
128
                ret = ffio_read_size(pb, sd->data, size);
174
128
                if (ret < 0) {
175
51
                    av_packet_side_data_remove(st->codecpar->coded_side_data, &st->codecpar->nb_coded_side_data, AV_PKT_DATA_ICC_PROFILE);
176
51
                    return ret;
177
51
                }
178
77
                ctx->has_iccp = 1;
179
77
            }
180
429
            break;
181
429
        case MKTAG('A', 'N', 'I', 'M'):
182
368
            if (ctx->has_anim) {
183
241
                av_log(s, loglevel, "Extra ANIM chunk found\n");
184
241
                if (explode)
185
0
                    return AVERROR_INVALIDDATA;
186
241
                avio_skip(pb, size);
187
241
            } else {
188
127
                if (size != 6)
189
40
                    return AVERROR_INVALIDDATA;
190
87
                uint32_t bg_color = avio_rb32(pb);
191
87
                ctx->loop_count   = avio_rl16(pb);
192
87
                if (ctx->usebgcolor) {
193
0
                    st->codecpar->extradata = av_mallocz(4 + AV_INPUT_BUFFER_PADDING_SIZE);
194
0
                    if (!st->codecpar->extradata)
195
0
                        return AVERROR(ENOMEM);
196
0
                    AV_WB32(st->codecpar->extradata, bg_color);
197
0
                    st->codecpar->extradata_size = 4;
198
0
                }
199
87
                av_log(s, AV_LOG_DEBUG,
200
87
                       "ANIM: background BGRA 0x%08x loop count %d\n",
201
87
                       bg_color, ctx->loop_count);
202
87
                ctx->has_anim = 1;
203
87
            }
204
328
            break;
205
1.63k
        case MKTAG('A', 'N', 'M', 'F'):
206
1.63k
            if (!ctx->has_anim) {
207
1.60k
                av_log(s, loglevel,
208
1.60k
                       "ANMF chunk present, but no previous ANIM chunk found\n");
209
1.60k
                if (explode)
210
0
                    return AVERROR_INVALIDDATA;
211
1.60k
                ctx->loop_count = 1;
212
1.60k
            } else if (!ctx->ignore_loop && ctx->loop_count != 1) {
213
0
                int64_t file_size = avio_size(pb);
214
0
                if (file_size < 0 || offset < 0 ||
215
0
                    (ret = ffio_ensure_seekback(pb, file_size - offset)) < 0) {
216
0
                    av_log(s, AV_LOG_WARNING,
217
0
                           "Could not ensure seekback, will not loop\n");
218
0
                    ctx->loop_count = 1;
219
0
                }
220
0
            }
221
1.63k
            ctx->first_anmf_offset = offset;
222
1.63k
            ret = avio_seek(pb, -8, SEEK_CUR);
223
1.63k
            if (ret < 0)
224
87
                return ret;
225
1.55k
            return 0;
226
189k
        default:
227
189k
            av_log(s, AV_LOG_WARNING, "Skipping chunk: %s\n", av_fourcc2str(fourcc));
228
189k
            avio_skip(pb, size);
229
189k
            break;
230
192k
        }
231
192k
    }
232
233
454
    return AVERROR_INVALIDDATA;
234
2.21k
}
235
236
static int webp_anim_read_packet(AVFormatContext *s, AVPacket *pkt)
237
51.4k
{
238
51.4k
    WebPAnimDemuxContext *ctx = s->priv_data;
239
51.4k
    AVIOContext *pb = s->pb;
240
51.4k
    int ret;
241
242
51.4k
    int explode = (s->error_recognition & AV_EF_EXPLODE);
243
51.4k
    int loglevel = explode ? AV_LOG_ERROR : AV_LOG_WARNING;
244
154k
    while (1) {
245
154k
        int64_t offset = avio_tell(pb);
246
154k
        uint32_t fourcc = avio_rl32(pb);
247
154k
        uint32_t size = avio_rl32(pb);
248
249
154k
        if (size == UINT32_MAX)
250
27
            return AVERROR_INVALIDDATA;
251
154k
        size += size & 1;
252
253
154k
        if (avio_feof(pb)) {
254
2.22k
            if (!ctx->ignore_loop &&
255
0
                (ctx->loop_count == 0 || ++ctx->cur_loop < ctx->loop_count)) {
256
0
                ctx->cur_frame = 0;
257
0
                ret = avio_seek(pb, ctx->first_anmf_offset, SEEK_SET);
258
0
                if (ret < 0)
259
0
                    return ret;
260
0
                continue;
261
0
            }
262
2.22k
            break;
263
2.22k
        }
264
265
152k
        av_log(s, AV_LOG_DEBUG, "Chunk %s of size %u at offset %" PRId64 "\n",
266
152k
               av_fourcc2str(fourcc), size, offset);
267
268
152k
        switch (fourcc) {
269
49.2k
        case MKTAG('A', 'N', 'M', 'F'):
270
49.2k
            if (size < 16)
271
30
                return AVERROR_INVALIDDATA;
272
49.1k
            ret = av_get_packet(pb, pkt, size);
273
49.1k
            if (ret < 0)
274
534
                return ret;
275
48.6k
            if (!ctx->cur_frame++)
276
1.05k
                pkt->flags |= AV_PKT_FLAG_KEY;
277
48.6k
            pkt->pts = AV_NOPTS_VALUE;
278
48.6k
            pkt->dts = AV_NOPTS_VALUE;
279
48.6k
            uint32_t duration = AV_RL24(pkt->data + 12);
280
48.6k
            if (duration <= ctx->min_delay)
281
4.73k
                duration = ctx->default_delay;
282
48.6k
            pkt->duration = FFMIN(duration, ctx->max_delay);
283
48.6k
            return ret;
284
242
        case MKTAG('E', 'X', 'I', 'F'):
285
242
            if (ctx->has_exif) {
286
207
                av_log(s, loglevel, "Extra EXIF chunk found\n");
287
207
                if (explode)
288
0
                    return AVERROR_INVALIDDATA;
289
207
                avio_skip(pb, size);
290
207
            } else {
291
35
                if (!(ctx->vp8x_flags & VP8X_FLAG_EXIF_METADATA)) {
292
24
                    av_log(s, loglevel,
293
24
                           "EXIF chunk present, but EXIF bit not set in the VP8X header\n");
294
24
                    if (explode)
295
0
                        return AVERROR_INVALIDDATA;
296
24
                }
297
298
35
                AVStream *st = s->streams[0];
299
35
                AVPacketSideData *sd = av_packet_side_data_new(&st->codecpar->coded_side_data,
300
35
                                                               &st->codecpar->nb_coded_side_data,
301
35
                                                               AV_PKT_DATA_EXIF, size, 0);
302
35
                if (!sd)
303
7
                    return AVERROR(ENOMEM);
304
28
                ret = ffio_read_size(pb, sd->data, size);
305
28
                if (ret < 0) {
306
5
                    av_packet_side_data_remove(st->codecpar->coded_side_data, &st->codecpar->nb_coded_side_data, AV_PKT_DATA_EXIF);
307
5
                    return ret;
308
5
                }
309
23
                ctx->has_exif = 1;
310
23
            }
311
230
            break;
312
233
        case MKTAG('X', 'M', 'P', ' '):
313
233
            if (ctx->has_xmp) {
314
205
                av_log(s, loglevel, "Extra XMP chunk found\n");
315
205
                if (explode)
316
0
                    return AVERROR_INVALIDDATA;
317
205
                avio_skip(pb, size);
318
205
            } else {
319
28
                if (!(ctx->vp8x_flags & VP8X_FLAG_XMP_METADATA)) {
320
25
                    av_log(s, loglevel,
321
25
                           "XMP chunk present, but XMP bit not set in the VP8X header\n");
322
25
                    if (explode)
323
0
                        return AVERROR_INVALIDDATA;
324
25
                }
325
326
28
                uint8_t *xmp = av_malloc(size + 1);
327
28
                if (!xmp)
328
6
                    return AVERROR(ENOMEM);
329
22
                ret = ffio_read_size(pb, xmp, size);
330
22
                if (ret < 0) {
331
3
                    av_free(xmp);
332
3
                    return ret;
333
3
                }
334
19
                xmp[size] = '\0';
335
19
                av_dict_set(&s->metadata, "xmp", xmp, AV_DICT_DONT_STRDUP_VAL);
336
19
                ctx->has_xmp = 1;
337
19
            }
338
224
            break;
339
102k
        default:
340
102k
            av_log(s, AV_LOG_WARNING, "Skipping chunk: %s\n", av_fourcc2str(fourcc));
341
102k
            avio_skip(pb, size);
342
102k
            break;
343
152k
        }
344
152k
    }
345
346
2.22k
    return AVERROR_EOF;
347
51.4k
}
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
};