Skip to main content

webp_rust/decoder/
animation.rs

1//! Animated WebP decode and compositing helpers.
2
3use crate::decoder::header::{parse_animation_webp, ParsedAnimationFrame};
4use crate::decoder::lossless::decode_lossless_vp8l_to_rgba;
5use crate::decoder::lossy::{decode_lossy_vp8_frame_to_rgba, DecodedImage};
6use crate::decoder::DecoderError;
7
8/// One fully composited animation frame.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct DecodedAnimationFrame {
11    /// Display duration in milliseconds.
12    pub duration: usize,
13    /// Packed RGBA8 canvas pixels after compositing this frame.
14    pub rgba: Vec<u8>,
15}
16
17/// Decoded animated WebP sequence.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct DecodedAnimation {
20    /// Canvas width in pixels.
21    pub width: usize,
22    /// Canvas height in pixels.
23    pub height: usize,
24    /// Canvas background color in little-endian ARGB order.
25    pub background_color: u32,
26    /// Loop count from the container. `0` means infinite loop.
27    pub loop_count: u16,
28    /// Composited frames in display order.
29    pub frames: Vec<DecodedAnimationFrame>,
30}
31
32fn argb_to_rgba(argb: u32) -> [u8; 4] {
33    [
34        ((argb >> 16) & 0xff) as u8,
35        ((argb >> 8) & 0xff) as u8,
36        (argb & 0xff) as u8,
37        (argb >> 24) as u8,
38    ]
39}
40
41fn fill_rect(
42    canvas: &mut [u8],
43    canvas_width: usize,
44    x_offset: usize,
45    y_offset: usize,
46    width: usize,
47    height: usize,
48    rgba: [u8; 4],
49) {
50    for y in 0..height {
51        let row = ((y_offset + y) * canvas_width + x_offset) * 4;
52        let row = &mut canvas[row..row + width * 4];
53        for pixel in row.chunks_exact_mut(4) {
54            pixel.copy_from_slice(&rgba);
55        }
56    }
57}
58
59fn blend_channel(src: u8, src_alpha: u32, dst: u8, dst_factor_alpha: u32, scale: u32) -> u8 {
60    let blended = (src as u32 * src_alpha + dst as u32 * dst_factor_alpha) * scale;
61    (blended >> 24) as u8
62}
63
64fn blend_pixel_non_premult(src: [u8; 4], dst: [u8; 4]) -> [u8; 4] {
65    let src_alpha = src[3] as u32;
66    if src_alpha == 0 {
67        return dst;
68    }
69    if src_alpha == 255 {
70        return src;
71    }
72
73    let dst_alpha = dst[3] as u32;
74    let dst_factor_alpha = (dst_alpha * (256 - src_alpha)) >> 8;
75    let blend_alpha = src_alpha + dst_factor_alpha;
76    let scale = (1u32 << 24) / blend_alpha;
77
78    [
79        blend_channel(src[0], src_alpha, dst[0], dst_factor_alpha, scale),
80        blend_channel(src[1], src_alpha, dst[1], dst_factor_alpha, scale),
81        blend_channel(src[2], src_alpha, dst[2], dst_factor_alpha, scale),
82        blend_alpha as u8,
83    ]
84}
85
86fn composite_frame(
87    canvas: &mut [u8],
88    canvas_width: usize,
89    frame_rgba: &[u8],
90    frame: &ParsedAnimationFrame<'_>,
91) {
92    if !frame.blend {
93        for y in 0..frame.height {
94            let src = y * frame.width * 4;
95            let dst = ((frame.y_offset + y) * canvas_width + frame.x_offset) * 4;
96            let len = frame.width * 4;
97            canvas[dst..dst + len].copy_from_slice(&frame_rgba[src..src + len]);
98        }
99        return;
100    }
101
102    for y in 0..frame.height {
103        let src_row = y * frame.width * 4;
104        let dst_row = ((frame.y_offset + y) * canvas_width + frame.x_offset) * 4;
105        for x in 0..frame.width {
106            let src = src_row + x * 4;
107            let dst = dst_row + x * 4;
108            let src_alpha = frame_rgba[src + 3];
109            if src_alpha == 0 {
110                continue;
111            }
112            if src_alpha == 255 {
113                canvas[dst..dst + 4].copy_from_slice(&frame_rgba[src..src + 4]);
114                continue;
115            }
116            let src_pixel = [
117                frame_rgba[src],
118                frame_rgba[src + 1],
119                frame_rgba[src + 2],
120                src_alpha,
121            ];
122            let dst_pixel = [
123                canvas[dst],
124                canvas[dst + 1],
125                canvas[dst + 2],
126                canvas[dst + 3],
127            ];
128            let out = blend_pixel_non_premult(src_pixel, dst_pixel);
129            canvas[dst..dst + 4].copy_from_slice(&out);
130        }
131    }
132}
133
134fn decode_frame_image(frame: &ParsedAnimationFrame<'_>) -> Result<DecodedImage, DecoderError> {
135    let image = match &frame.image_chunk.fourcc {
136        b"VP8L" => {
137            if frame.alpha_chunk.is_some() {
138                return Err(DecoderError::Bitstream(
139                    "VP8L animation frame must not carry ALPH chunk",
140                ));
141            }
142            decode_lossless_vp8l_to_rgba(frame.image_data)?
143        }
144        b"VP8 " => decode_lossy_vp8_frame_to_rgba(frame.image_data, frame.alpha_data)?,
145        _ => return Err(DecoderError::Bitstream("unsupported animation frame chunk")),
146    };
147
148    if image.width != frame.width || image.height != frame.height {
149        return Err(DecoderError::Bitstream(
150            "animation frame dimensions do not match bitstream",
151        ));
152    }
153    Ok(image)
154}
155
156/// Decodes an animated WebP container to a sequence of composited RGBA frames.
157pub fn decode_animation_webp(data: &[u8]) -> Result<DecodedAnimation, DecoderError> {
158    let parsed = parse_animation_webp(data)?;
159    let background = argb_to_rgba(parsed.animation.background_color);
160    let mut canvas = vec![0u8; parsed.features.width * parsed.features.height * 4];
161    fill_rect(
162        &mut canvas,
163        parsed.features.width,
164        0,
165        0,
166        parsed.features.width,
167        parsed.features.height,
168        background,
169    );
170
171    let mut previous_rect = None;
172    let mut frames = Vec::with_capacity(parsed.frames.len());
173    for frame in &parsed.frames {
174        if let Some((x_offset, y_offset, width, height)) = previous_rect.take() {
175            fill_rect(
176                &mut canvas,
177                parsed.features.width,
178                x_offset,
179                y_offset,
180                width,
181                height,
182                background,
183            );
184        }
185
186        let decoded = decode_frame_image(frame)?;
187        composite_frame(&mut canvas, parsed.features.width, &decoded.rgba, frame);
188        frames.push(DecodedAnimationFrame {
189            duration: frame.duration,
190            rgba: canvas.clone(),
191        });
192
193        if frame.dispose_to_background {
194            previous_rect = Some((frame.x_offset, frame.y_offset, frame.width, frame.height));
195        }
196    }
197
198    Ok(DecodedAnimation {
199        width: parsed.features.width,
200        height: parsed.features.height,
201        background_color: parsed.animation.background_color,
202        loop_count: parsed.animation.loop_count,
203        frames,
204    })
205}