Skip to main content

webp_rust/decoder/
header.rs

1//! WebP RIFF container and chunk parsing helpers.
2
3use crate::decoder::alpha::{parse_alpha_header, AlphaHeader};
4use crate::decoder::vp8::{get_info, get_lossless_info};
5use crate::decoder::vp8i::{
6    WebpFormat, ALPHA_FLAG, ANIMATION_FLAG, CHUNK_HEADER_SIZE, MAX_CHUNK_PAYLOAD, MAX_IMAGE_AREA,
7    RIFF_HEADER_SIZE, TAG_SIZE, VP8X_CHUNK_SIZE,
8};
9use crate::decoder::DecoderError;
10
11/// Common metadata for a RIFF chunk.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct ChunkHeader {
14    /// FourCC tag.
15    pub fourcc: [u8; 4],
16    /// Chunk start offset in the source buffer.
17    pub offset: usize,
18    /// Unpadded payload size.
19    pub size: usize,
20    /// Payload size including RIFF padding.
21    pub padded_size: usize,
22    /// Start offset of the chunk payload.
23    pub data_offset: usize,
24}
25
26/// Parsed `VP8X` extended header.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct Vp8xHeader {
29    /// Raw feature flags from the header.
30    pub flags: u32,
31    /// Canvas width in pixels.
32    pub canvas_width: usize,
33    /// Canvas height in pixels.
34    pub canvas_height: usize,
35}
36
37/// High-level image features derived from the container and bitstream.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct WebpFeatures {
40    /// Image or canvas width in pixels.
41    pub width: usize,
42    /// Image or canvas height in pixels.
43    pub height: usize,
44    /// Whether alpha is present.
45    pub has_alpha: bool,
46    /// Whether the container is animated.
47    pub has_animation: bool,
48    /// Underlying still-image codec kind.
49    pub format: WebpFormat,
50    /// Optional extended header.
51    pub vp8x: Option<Vp8xHeader>,
52}
53
54/// Parsed still-image WebP container.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct ParsedWebp<'a> {
57    /// High-level image features.
58    pub features: WebpFeatures,
59    /// RIFF size field when the input is RIFF-wrapped.
60    pub riff_size: Option<usize>,
61    /// Primary image chunk header.
62    pub image_chunk: ChunkHeader,
63    /// Primary image payload (`VP8 ` or `VP8L`).
64    pub image_data: &'a [u8],
65    /// Optional `ALPH` chunk header.
66    pub alpha_chunk: Option<ChunkHeader>,
67    /// Optional `ALPH` payload.
68    pub alpha_data: Option<&'a [u8]>,
69    /// Optional parsed `ALPH` header byte.
70    pub alpha_header: Option<AlphaHeader>,
71}
72
73/// Parsed `ANIM` chunk.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub struct AnimationHeader {
76    /// Canvas background color in little-endian ARGB order.
77    pub background_color: u32,
78    /// Loop count from the container. `0` means infinite loop.
79    pub loop_count: u16,
80}
81
82/// Parsed animation frame entry.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct ParsedAnimationFrame<'a> {
85    /// Enclosing `ANMF` chunk header.
86    pub frame_chunk: ChunkHeader,
87    /// X offset on the canvas in pixels.
88    pub x_offset: usize,
89    /// Y offset on the canvas in pixels.
90    pub y_offset: usize,
91    /// Frame width in pixels.
92    pub width: usize,
93    /// Frame height in pixels.
94    pub height: usize,
95    /// Display duration in milliseconds.
96    pub duration: usize,
97    /// Whether the frame should be alpha-blended.
98    pub blend: bool,
99    /// Whether the frame should be disposed to background.
100    pub dispose_to_background: bool,
101    /// Embedded `VP8 ` or `VP8L` image chunk.
102    pub image_chunk: ChunkHeader,
103    /// Embedded image payload.
104    pub image_data: &'a [u8],
105    /// Optional embedded `ALPH` chunk header.
106    pub alpha_chunk: Option<ChunkHeader>,
107    /// Optional embedded `ALPH` payload.
108    pub alpha_data: Option<&'a [u8]>,
109    /// Optional parsed `ALPH` header byte.
110    pub alpha_header: Option<AlphaHeader>,
111}
112
113/// Parsed animated WebP container.
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct ParsedAnimationWebp<'a> {
116    /// High-level canvas features.
117    pub features: WebpFeatures,
118    /// RIFF size field.
119    pub riff_size: Option<usize>,
120    /// Global animation settings.
121    pub animation: AnimationHeader,
122    /// Parsed animation frames in display order.
123    pub frames: Vec<ParsedAnimationFrame<'a>>,
124}
125
126fn read_le24(bytes: &[u8]) -> usize {
127    bytes[0] as usize | ((bytes[1] as usize) << 8) | ((bytes[2] as usize) << 16)
128}
129
130fn read_le32(bytes: &[u8]) -> usize {
131    u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize
132}
133
134fn read_le16(bytes: &[u8]) -> u16 {
135    u16::from_le_bytes([bytes[0], bytes[1]])
136}
137
138fn padded_payload_size(size: usize) -> usize {
139    size + (size & 1)
140}
141
142fn parse_chunk(
143    data: &[u8],
144    offset: usize,
145    riff_limit: Option<usize>,
146) -> Result<ChunkHeader, DecoderError> {
147    if data.len() < offset + CHUNK_HEADER_SIZE {
148        return Err(DecoderError::NotEnoughData("chunk header"));
149    }
150    let size = read_le32(&data[offset + TAG_SIZE..offset + CHUNK_HEADER_SIZE]);
151    if size > MAX_CHUNK_PAYLOAD {
152        return Err(DecoderError::Bitstream("invalid chunk size"));
153    }
154
155    let padded_size = padded_payload_size(size);
156    let total_size = CHUNK_HEADER_SIZE + padded_size;
157    let end = offset + total_size;
158    if let Some(limit) = riff_limit {
159        if end > limit {
160            return Err(DecoderError::Bitstream("chunk exceeds RIFF payload"));
161        }
162    }
163    if data.len() < end {
164        return Err(DecoderError::NotEnoughData("chunk payload"));
165    }
166
167    Ok(ChunkHeader {
168        fourcc: data[offset..offset + TAG_SIZE].try_into().unwrap(),
169        offset,
170        size,
171        padded_size,
172        data_offset: offset + CHUNK_HEADER_SIZE,
173    })
174}
175
176fn parse_riff(data: &[u8]) -> Result<(Option<usize>, usize), DecoderError> {
177    if data.len() < RIFF_HEADER_SIZE {
178        return Err(DecoderError::NotEnoughData("RIFF header"));
179    }
180    if &data[..4] != b"RIFF" {
181        return Ok((None, 0));
182    }
183    if &data[8..12] != b"WEBP" {
184        return Err(DecoderError::Bitstream("wrong RIFF WEBP signature"));
185    }
186
187    let riff_size = read_le32(&data[4..8]);
188    if riff_size < TAG_SIZE + CHUNK_HEADER_SIZE {
189        return Err(DecoderError::Bitstream("RIFF payload is too small"));
190    }
191    if riff_size > MAX_CHUNK_PAYLOAD {
192        return Err(DecoderError::Bitstream("RIFF payload is too large"));
193    }
194    if riff_size > data.len() - CHUNK_HEADER_SIZE {
195        return Err(DecoderError::NotEnoughData("truncated RIFF payload"));
196    }
197
198    Ok((Some(riff_size), RIFF_HEADER_SIZE))
199}
200
201fn parse_vp8x(data: &[u8], offset: usize) -> Result<(Option<Vp8xHeader>, usize), DecoderError> {
202    if data.len() < offset + CHUNK_HEADER_SIZE {
203        return Ok((None, offset));
204    }
205    if &data[offset..offset + TAG_SIZE] != b"VP8X" {
206        return Ok((None, offset));
207    }
208
209    let chunk = parse_chunk(data, offset, None)?;
210    if chunk.size != VP8X_CHUNK_SIZE {
211        return Err(DecoderError::Bitstream("wrong VP8X chunk size"));
212    }
213
214    let flags = read_le32(&data[offset + 8..offset + 12]) as u32;
215    let canvas_width = read_le24(&data[offset + 12..offset + 15]) + 1;
216    let canvas_height = read_le24(&data[offset + 15..offset + 18]) + 1;
217    if (canvas_width as u64) * (canvas_height as u64) >= MAX_IMAGE_AREA {
218        return Err(DecoderError::Bitstream("canvas is too large"));
219    }
220
221    Ok((
222        Some(Vp8xHeader {
223            flags,
224            canvas_width,
225            canvas_height,
226        }),
227        offset + CHUNK_HEADER_SIZE + chunk.padded_size,
228    ))
229}
230
231/// Returns high-level WebP features without fully decoding the image.
232pub fn get_features(data: &[u8]) -> Result<WebpFeatures, DecoderError> {
233    let (riff_size, mut offset) = parse_riff(data)?;
234    let riff_limit = riff_size.map(|size| size + CHUNK_HEADER_SIZE);
235
236    let (vp8x, next_offset) = parse_vp8x(data, offset)?;
237    offset = next_offset;
238    if riff_size.is_none() && vp8x.is_some() {
239        return Err(DecoderError::Bitstream("VP8X chunk requires RIFF"));
240    }
241
242    let mut has_alpha = vp8x
243        .map(|chunk| (chunk.flags & ALPHA_FLAG) != 0)
244        .unwrap_or(false);
245    let has_animation = vp8x
246        .map(|chunk| (chunk.flags & ANIMATION_FLAG) != 0)
247        .unwrap_or(false);
248
249    if let Some(vp8x) = vp8x {
250        if has_animation {
251            return Ok(WebpFeatures {
252                width: vp8x.canvas_width,
253                height: vp8x.canvas_height,
254                has_alpha,
255                has_animation,
256                format: WebpFormat::Undefined,
257                vp8x: Some(vp8x),
258            });
259        }
260    }
261
262    if data.len() < offset + TAG_SIZE {
263        return Err(DecoderError::NotEnoughData("chunk tag"));
264    }
265
266    if (riff_size.is_some() && vp8x.is_some())
267        || (riff_size.is_none() && vp8x.is_none() && &data[offset..offset + TAG_SIZE] == b"ALPH")
268    {
269        loop {
270            let chunk = parse_chunk(data, offset, riff_limit)?;
271            if &chunk.fourcc == b"VP8 " || &chunk.fourcc == b"VP8L" {
272                break;
273            }
274            if &chunk.fourcc == b"ALPH" {
275                has_alpha = true;
276            }
277            offset += CHUNK_HEADER_SIZE + chunk.padded_size;
278        }
279    }
280
281    let chunk = parse_chunk(data, offset, riff_limit)?;
282    let payload = &data[chunk.data_offset..chunk.data_offset + chunk.size];
283    let (format, width, height) = if &chunk.fourcc == b"VP8 " {
284        let (width, height) = get_info(payload, chunk.size)?;
285        (WebpFormat::Lossy, width, height)
286    } else if &chunk.fourcc == b"VP8L" {
287        let info = get_lossless_info(payload)?;
288        has_alpha |= info.has_alpha;
289        (WebpFormat::Lossless, info.width, info.height)
290    } else {
291        return Err(DecoderError::Bitstream("missing VP8/VP8L image chunk"));
292    };
293
294    if let Some(vp8x) = vp8x {
295        if vp8x.canvas_width != width || vp8x.canvas_height != height {
296            return Err(DecoderError::Bitstream(
297                "VP8X canvas does not match image size",
298            ));
299        }
300    }
301
302    Ok(WebpFeatures {
303        width,
304        height,
305        has_alpha,
306        has_animation,
307        format,
308        vp8x,
309    })
310}
311
312/// Parses a still-image WebP container and returns raw chunk slices.
313pub fn parse_still_webp(data: &[u8]) -> Result<ParsedWebp<'_>, DecoderError> {
314    let (riff_size, mut offset) = parse_riff(data)?;
315    let riff_limit = riff_size.map(|size| size + CHUNK_HEADER_SIZE);
316
317    let (vp8x, next_offset) = parse_vp8x(data, offset)?;
318    offset = next_offset;
319    if riff_size.is_none() && vp8x.is_some() {
320        return Err(DecoderError::Bitstream("VP8X chunk requires RIFF"));
321    }
322    if vp8x
323        .map(|chunk| (chunk.flags & ANIMATION_FLAG) != 0)
324        .unwrap_or(false)
325    {
326        return Err(DecoderError::Unsupported(
327            "animated WebP is not implemented",
328        ));
329    }
330
331    let mut alpha_chunk = None;
332    if data.len() < offset + TAG_SIZE {
333        return Err(DecoderError::NotEnoughData("chunk tag"));
334    }
335    if (riff_size.is_some() && vp8x.is_some())
336        || (riff_size.is_none() && vp8x.is_none() && &data[offset..offset + TAG_SIZE] == b"ALPH")
337    {
338        loop {
339            let chunk = parse_chunk(data, offset, riff_limit)?;
340            if &chunk.fourcc == b"VP8 " || &chunk.fourcc == b"VP8L" {
341                break;
342            }
343            if &chunk.fourcc == b"ALPH" {
344                alpha_chunk = Some(chunk);
345            }
346            offset += CHUNK_HEADER_SIZE + chunk.padded_size;
347        }
348    }
349
350    let image_chunk = parse_chunk(data, offset, riff_limit)?;
351    if &image_chunk.fourcc != b"VP8 " && &image_chunk.fourcc != b"VP8L" {
352        return Err(DecoderError::Bitstream("missing VP8/VP8L image chunk"));
353    }
354    let image_data = &data[image_chunk.data_offset..image_chunk.data_offset + image_chunk.size];
355    let mut features = get_features(data)?;
356    let alpha_data =
357        alpha_chunk.map(|chunk| &data[chunk.data_offset..chunk.data_offset + chunk.size]);
358    let alpha_header = alpha_data.map(parse_alpha_header).transpose()?;
359    if alpha_chunk.is_some() {
360        features.has_alpha = true;
361    }
362
363    Ok(ParsedWebp {
364        features,
365        riff_size,
366        image_chunk,
367        image_data,
368        alpha_chunk,
369        alpha_data,
370        alpha_header,
371    })
372}
373
374fn parse_animation_frame<'a>(
375    data: &'a [u8],
376    features: WebpFeatures,
377    chunk: ChunkHeader,
378    riff_limit: Option<usize>,
379) -> Result<ParsedAnimationFrame<'a>, DecoderError> {
380    if chunk.size < 16 {
381        return Err(DecoderError::Bitstream("ANMF chunk is too small"));
382    }
383
384    let header = &data[chunk.data_offset..chunk.data_offset + 16];
385    let x_offset = read_le24(&header[0..3]) * 2;
386    let y_offset = read_le24(&header[3..6]) * 2;
387    let width = read_le24(&header[6..9]) + 1;
388    let height = read_le24(&header[9..12]) + 1;
389    let duration = read_le24(&header[12..15]);
390    let flags = header[15];
391    if flags >> 2 != 0 {
392        return Err(DecoderError::Bitstream("ANMF reserved bits must be zero"));
393    }
394    if x_offset + width > features.width || y_offset + height > features.height {
395        return Err(DecoderError::Bitstream(
396            "ANMF frame exceeds animation canvas",
397        ));
398    }
399
400    let mut offset = chunk.data_offset + 16;
401    let frame_limit = Some(chunk.data_offset + chunk.size);
402    let mut alpha_chunk = None;
403    let image_chunk;
404    loop {
405        let subchunk = parse_chunk(data, offset, frame_limit)?;
406        if &subchunk.fourcc == b"VP8 " || &subchunk.fourcc == b"VP8L" {
407            image_chunk = subchunk;
408            break;
409        }
410        if &subchunk.fourcc == b"ALPH" {
411            alpha_chunk = Some(subchunk);
412        }
413        offset += CHUNK_HEADER_SIZE + subchunk.padded_size;
414        if let Some(limit) = riff_limit {
415            if offset > limit {
416                return Err(DecoderError::Bitstream(
417                    "ANMF frame data exceeds RIFF payload",
418                ));
419            }
420        }
421    }
422
423    let image_data = &data[image_chunk.data_offset..image_chunk.data_offset + image_chunk.size];
424    let alpha_data = alpha_chunk
425        .map(|subchunk| &data[subchunk.data_offset..subchunk.data_offset + subchunk.size]);
426    let alpha_header = alpha_data.map(parse_alpha_header).transpose()?;
427
428    Ok(ParsedAnimationFrame {
429        frame_chunk: chunk,
430        x_offset,
431        y_offset,
432        width,
433        height,
434        duration,
435        blend: (flags & 0x02) == 0,
436        dispose_to_background: (flags & 0x01) != 0,
437        image_chunk,
438        image_data,
439        alpha_chunk,
440        alpha_data,
441        alpha_header,
442    })
443}
444
445/// Parses an animated WebP container and returns frame-level chunk slices.
446pub fn parse_animation_webp(data: &[u8]) -> Result<ParsedAnimationWebp<'_>, DecoderError> {
447    let (riff_size, mut offset) = parse_riff(data)?;
448    let riff_limit = riff_size.map(|size| size + CHUNK_HEADER_SIZE);
449
450    let (vp8x, next_offset) = parse_vp8x(data, offset)?;
451    offset = next_offset;
452    let vp8x = vp8x.ok_or(DecoderError::Bitstream("animated WebP requires VP8X"))?;
453    if (vp8x.flags & ANIMATION_FLAG) == 0 {
454        return Err(DecoderError::Unsupported("animated WebP flag is not set"));
455    }
456
457    let anim_chunk = parse_chunk(data, offset, riff_limit)?;
458    if &anim_chunk.fourcc != b"ANIM" {
459        return Err(DecoderError::Bitstream("missing ANIM chunk"));
460    }
461    if anim_chunk.size != 6 {
462        return Err(DecoderError::Bitstream("wrong ANIM chunk size"));
463    }
464    let animation = AnimationHeader {
465        background_color: u32::from_le_bytes(
466            data[anim_chunk.data_offset..anim_chunk.data_offset + 4]
467                .try_into()
468                .unwrap(),
469        ),
470        loop_count: read_le16(&data[anim_chunk.data_offset + 4..anim_chunk.data_offset + 6]),
471    };
472    offset += CHUNK_HEADER_SIZE + anim_chunk.padded_size;
473
474    let features = WebpFeatures {
475        width: vp8x.canvas_width,
476        height: vp8x.canvas_height,
477        has_alpha: (vp8x.flags & ALPHA_FLAG) != 0,
478        has_animation: true,
479        format: WebpFormat::Undefined,
480        vp8x: Some(vp8x),
481    };
482
483    let limit = riff_limit.unwrap_or(data.len());
484    let mut frames = Vec::new();
485    while offset + CHUNK_HEADER_SIZE <= limit {
486        let chunk = parse_chunk(data, offset, riff_limit)?;
487        if &chunk.fourcc != b"ANMF" {
488            break;
489        }
490        let frame = parse_animation_frame(data, features, chunk, riff_limit)?;
491        frames.push(frame);
492        offset += CHUNK_HEADER_SIZE + chunk.padded_size;
493    }
494
495    if frames.is_empty() {
496        return Err(DecoderError::Bitstream("animated WebP has no ANMF frames"));
497    }
498
499    Ok(ParsedAnimationWebp {
500        features,
501        riff_size,
502        animation,
503        frames,
504    })
505}