/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 | | }; |