use crate::decoder::{canvas_filled, composite, decode_lazy_frame_to_rgba};
use crate::demux::{bgra_to_rgba, parse_webp_body_lazy, LazyParsedContainer, WebpFileMetadata};
use crate::error::{Result, WebpError as Error};
struct PerFrameMeta {
pts_ms: u64,
duration_ms: u32,
is_keyframe: bool,
blend_with_previous: bool,
dispose_to_background: bool,
frame_x: u32,
frame_y: u32,
frame_width: u32,
frame_height: u32,
}
#[derive(Debug)]
pub struct WebpAnimFrameRef<'a> {
pub pts_ms: u64,
pub duration_ms: u32,
pub rgba: &'a [u8],
pub canvas_width: u32,
pub canvas_height: u32,
pub is_keyframe: bool,
pub blend_with_previous: bool,
pub dispose_to_background: bool,
pub frame_x: u32,
pub frame_y: u32,
pub frame_width: u32,
pub frame_height: u32,
}
impl<'a> WebpAnimFrameRef<'a> {
pub fn to_owned(&self) -> WebpAnimFrame {
WebpAnimFrame {
pts_ms: self.pts_ms,
duration_ms: self.duration_ms,
rgba: self.rgba.to_vec(),
canvas_width: self.canvas_width,
canvas_height: self.canvas_height,
is_keyframe: self.is_keyframe,
blend_with_previous: self.blend_with_previous,
dispose_to_background: self.dispose_to_background,
frame_x: self.frame_x,
frame_y: self.frame_y,
frame_width: self.frame_width,
frame_height: self.frame_height,
}
}
}
#[derive(Debug, Clone)]
pub struct WebpAnimFrame {
pub pts_ms: u64,
pub duration_ms: u32,
pub rgba: Vec<u8>,
pub canvas_width: u32,
pub canvas_height: u32,
pub is_keyframe: bool,
pub blend_with_previous: bool,
pub dispose_to_background: bool,
pub frame_x: u32,
pub frame_y: u32,
pub frame_width: u32,
pub frame_height: u32,
}
#[derive(Debug, Clone)]
pub struct WebpAnimInfo {
pub canvas_width: u32,
pub canvas_height: u32,
pub frame_count: usize,
pub loop_count: Option<u16>,
pub background_rgba: Option<[u8; 4]>,
pub metadata: WebpFileMetadata,
}
pub struct WebpAnimDecoder {
info: WebpAnimInfo,
body: Vec<u8>,
parsed: LazyParsedContainer,
canvas: Vec<u8>,
bg_rgba: [u8; 4],
next_index: usize,
pts_ms: u64,
pending_dispose_bbox: Option<(u32, u32, u32, u32)>,
}
impl WebpAnimDecoder {
pub fn new(bytes: &[u8]) -> Result<Self> {
if bytes.len() < 12 || &bytes[0..4] != b"RIFF" || &bytes[8..12] != b"WEBP" {
return Err(Error::invalid("WebP: bad RIFF/WEBP magic"));
}
let riff_size = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as usize;
let end = (8 + riff_size).min(bytes.len());
let body: Vec<u8> = bytes[12..end].to_vec();
let parsed = parse_webp_body_lazy(&body)?;
let (canvas_w, canvas_h) = parsed.canvas;
let bg_rgba = parsed
.anim_background_bgra
.map(bgra_to_rgba)
.unwrap_or([0, 0, 0, 0]);
let canvas = canvas_filled(canvas_w as usize, canvas_h as usize, bg_rgba);
let info = WebpAnimInfo {
canvas_width: canvas_w,
canvas_height: canvas_h,
frame_count: parsed.frames.len(),
loop_count: parsed.anim_loop_count,
background_rgba: parsed.anim_background_bgra.map(bgra_to_rgba),
metadata: parsed.metadata.clone(),
};
Ok(Self {
info,
body,
parsed,
canvas,
bg_rgba,
next_index: 0,
pts_ms: 0,
pending_dispose_bbox: None,
})
}
pub fn info(&self) -> &WebpAnimInfo {
&self.info
}
pub fn done(&self) -> bool {
self.next_index >= self.parsed.frames.len()
}
pub fn reset(&mut self) {
self.canvas = canvas_filled(
self.info.canvas_width as usize,
self.info.canvas_height as usize,
self.bg_rgba,
);
self.next_index = 0;
self.pts_ms = 0;
self.pending_dispose_bbox = None;
}
pub fn next_frame(&mut self) -> Result<Option<WebpAnimFrame>> {
let meta = match self.advance_one_frame()? {
Some(meta) => meta,
None => return Ok(None),
};
let frame_rgba = self.canvas.clone();
Ok(Some(WebpAnimFrame {
pts_ms: meta.pts_ms,
duration_ms: meta.duration_ms,
rgba: frame_rgba,
canvas_width: self.info.canvas_width,
canvas_height: self.info.canvas_height,
is_keyframe: meta.is_keyframe,
blend_with_previous: meta.blend_with_previous,
dispose_to_background: meta.dispose_to_background,
frame_x: meta.frame_x,
frame_y: meta.frame_y,
frame_width: meta.frame_width,
frame_height: meta.frame_height,
}))
}
pub fn next_frame_borrowed(&mut self) -> Result<Option<WebpAnimFrameRef<'_>>> {
let meta = match self.advance_one_frame()? {
Some(meta) => meta,
None => return Ok(None),
};
Ok(Some(WebpAnimFrameRef {
pts_ms: meta.pts_ms,
duration_ms: meta.duration_ms,
rgba: &self.canvas,
canvas_width: self.info.canvas_width,
canvas_height: self.info.canvas_height,
is_keyframe: meta.is_keyframe,
blend_with_previous: meta.blend_with_previous,
dispose_to_background: meta.dispose_to_background,
frame_x: meta.frame_x,
frame_y: meta.frame_y,
frame_width: meta.frame_width,
frame_height: meta.frame_height,
}))
}
fn advance_one_frame(&mut self) -> Result<Option<PerFrameMeta>> {
if let Some((x, y, w, h)) = self.pending_dispose_bbox.take() {
self.fill_bbox_with_bg(x, y, w, h);
}
if self.next_index >= self.parsed.frames.len() {
return Ok(None);
}
let frame_index = self.next_index;
let (
duration_ms,
blend_with_previous,
dispose_to_background,
frame_x,
frame_y,
frame_w,
frame_h,
) = {
let f = &self.parsed.frames[frame_index];
(
f.duration_ms,
f.blend_with_previous,
f.dispose_to_background,
f.x_offset,
f.y_offset,
f.width,
f.height,
)
};
let f = &self.parsed.frames[frame_index];
let tile_rgba = decode_lazy_frame_to_rgba(f, &self.body)?;
composite(
&mut self.canvas,
self.info.canvas_width,
self.info.canvas_height,
&tile_rgba,
frame_x,
frame_y,
frame_w,
frame_h,
blend_with_previous,
);
let pts_for_this_frame = self.pts_ms;
self.pts_ms = self.pts_ms.saturating_add(duration_ms.max(1) as u64);
self.pending_dispose_bbox = if dispose_to_background {
Some((frame_x, frame_y, frame_w, frame_h))
} else {
None
};
self.next_index += 1;
Ok(Some(PerFrameMeta {
pts_ms: pts_for_this_frame,
duration_ms,
is_keyframe: frame_index == 0,
blend_with_previous,
dispose_to_background,
frame_x,
frame_y,
frame_width: frame_w,
frame_height: frame_h,
}))
}
pub fn next_frame_index(&self) -> usize {
self.next_index
}
pub fn seek_to_frame(&mut self, target: usize) -> Result<()> {
if target > self.parsed.frames.len() {
return Err(Error::invalid("WebP: seek_to_frame past end"));
}
if target == self.next_index {
return Ok(());
}
if target < self.next_index {
self.reset();
}
while self.next_index < target {
self.advance_one_frame_no_snapshot()?;
}
Ok(())
}
fn advance_one_frame_no_snapshot(&mut self) -> Result<()> {
debug_assert!(self.next_index < self.parsed.frames.len());
self.advance_one_frame()?;
Ok(())
}
fn fill_bbox_with_bg(&mut self, x: u32, y: u32, w: u32, h: u32) {
let cw = self.info.canvas_width as usize;
let ch = self.info.canvas_height as usize;
let x0 = (x as usize).min(cw);
let y0 = (y as usize).min(ch);
let x1 = (x as usize + w as usize).min(cw);
let y1 = (y as usize + h as usize).min(ch);
for yy in y0..y1 {
for xx in x0..x1 {
let i = (yy * cw + xx) * 4;
self.canvas[i] = self.bg_rgba[0];
self.canvas[i + 1] = self.bg_rgba[1];
self.canvas[i + 2] = self.bg_rgba[2];
self.canvas[i + 3] = self.bg_rgba[3];
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::encoder_anim::{build_animated_webp, AnimFrame};
const W: u32 = 8;
const H: u32 = 8;
fn solid(width: u32, height: u32, rgba: [u8; 4]) -> Vec<u8> {
let n = (width as usize) * (height as usize);
let mut v = Vec::with_capacity(n * 4);
for _ in 0..n {
v.extend_from_slice(&rgba);
}
v
}
fn three_frame_anim() -> Vec<u8> {
let red = solid(W, H, [0xff, 0, 0, 0xff]);
let green = solid(W, H, [0, 0xff, 0, 0xff]);
let blue = solid(W, H, [0, 0, 0xff, 0xff]);
let frames = [
AnimFrame {
width: W,
height: H,
x_offset: 0,
y_offset: 0,
duration_ms: 30,
blend: false,
dispose_to_background: false,
rgba: &red,
},
AnimFrame {
width: W,
height: H,
x_offset: 0,
y_offset: 0,
duration_ms: 40,
blend: false,
dispose_to_background: false,
rgba: &green,
},
AnimFrame {
width: W,
height: H,
x_offset: 0,
y_offset: 0,
duration_ms: 50,
blend: false,
dispose_to_background: false,
rgba: &blue,
},
];
build_animated_webp(W, H, [0, 0, 0, 0], 0, &frames).expect("encode")
}
#[test]
fn streams_three_frames_in_order_with_pts() {
let blob = three_frame_anim();
let mut dec = WebpAnimDecoder::new(&blob).expect("new");
assert_eq!(dec.info().frame_count, 3);
assert!(!dec.done());
let f0 = dec.next_frame().expect("ok").expect("Some");
assert_eq!(f0.pts_ms, 0);
assert_eq!(f0.duration_ms, 30);
assert!(f0.is_keyframe);
assert_eq!(&f0.rgba[0..4], &[0xff, 0, 0, 0xff], "F0 should be red");
let f1 = dec.next_frame().expect("ok").expect("Some");
assert_eq!(f1.pts_ms, 30);
assert!(!f1.is_keyframe);
assert_eq!(&f1.rgba[0..4], &[0, 0xff, 0, 0xff], "F1 should be green");
let f2 = dec.next_frame().expect("ok").expect("Some");
assert_eq!(f2.pts_ms, 70);
assert_eq!(&f2.rgba[0..4], &[0, 0, 0xff, 0xff], "F2 should be blue");
assert!(dec.done());
assert!(dec.next_frame().expect("ok").is_none());
}
#[test]
fn early_stop_does_not_decode_remaining_frames() {
let blob = three_frame_anim();
let mut dec = WebpAnimDecoder::new(&blob).expect("new");
let _ = dec.next_frame().expect("ok").expect("Some");
assert_eq!(dec.next_frame_index(), 1);
assert!(!dec.done());
}
#[test]
fn reset_rewinds_to_frame_zero() {
let blob = three_frame_anim();
let mut dec = WebpAnimDecoder::new(&blob).expect("new");
while dec.next_frame().expect("ok").is_some() {}
assert!(dec.done());
dec.reset();
assert!(!dec.done());
let f0 = dec.next_frame().expect("ok").expect("Some");
assert_eq!(f0.pts_ms, 0);
assert!(f0.is_keyframe);
assert_eq!(&f0.rgba[0..4], &[0xff, 0, 0, 0xff]);
}
#[test]
fn info_exposes_anim_metadata() {
let bg_bgra: [u8; 4] = [0x10, 0x20, 0x30, 0xff];
let red_tile = solid(4, 4, [0xff, 0, 0, 0xff]);
let frames = [AnimFrame {
width: 4,
height: 4,
x_offset: 0,
y_offset: 0,
duration_ms: 50,
blend: false,
dispose_to_background: false,
rgba: &red_tile,
}];
let blob = build_animated_webp(W, H, bg_bgra, 7, &frames).expect("encode");
let dec = WebpAnimDecoder::new(&blob).expect("new");
let info = dec.info();
assert_eq!(info.canvas_width, W);
assert_eq!(info.canvas_height, H);
assert_eq!(info.frame_count, 1);
assert_eq!(info.loop_count, Some(7));
assert_eq!(info.background_rgba, Some([0x30, 0x20, 0x10, 0xff]));
}
#[test]
fn dispose_to_background_applies_between_streamed_frames() {
let bg_bgra: [u8; 4] = [0x40, 0x50, 0x60, 0xff];
let bg_rgba_expected = [0x60, 0x50, 0x40, 0xff];
let f0_tile = solid(W, H, [0xff, 0, 0, 0xff]);
let f1_tile = solid(2, 2, [0, 0xff, 0, 0xff]);
let frames = [
AnimFrame {
width: W,
height: H,
x_offset: 0,
y_offset: 0,
duration_ms: 40,
blend: false,
dispose_to_background: true,
rgba: &f0_tile,
},
AnimFrame {
width: 2,
height: 2,
x_offset: 0,
y_offset: 0,
duration_ms: 40,
blend: false,
dispose_to_background: false,
rgba: &f1_tile,
},
];
let blob = build_animated_webp(W, H, bg_bgra, 0, &frames).expect("encode");
let mut dec = WebpAnimDecoder::new(&blob).expect("new");
let _f0 = dec.next_frame().expect("ok").expect("Some");
let f1 = dec.next_frame().expect("ok").expect("Some");
let stride = (W as usize) * 4;
for y in 0..2 {
for x in 0..2 {
let i = y * stride + x * 4;
assert_eq!(&f1.rgba[i..i + 4], &[0, 0xff, 0, 0xff]);
}
}
for y in 0..H as usize {
for x in 0..W as usize {
if x < 2 && y < 2 {
continue;
}
let i = y * stride + x * 4;
assert_eq!(&f1.rgba[i..i + 4], &bg_rgba_expected);
}
}
}
#[test]
fn still_webp_is_streamed_as_one_frame() {
let argb = vec![0xff_80_40_20u32; (W * H) as usize];
let blob = crate::encode_vp8l_argb_with_metadata(
W,
H,
&argb,
false,
&crate::WebpMetadata::default(),
)
.expect("encode");
let mut dec = WebpAnimDecoder::new(&blob).expect("new");
assert_eq!(dec.info().frame_count, 1);
assert_eq!(dec.info().loop_count, None);
let f0 = dec.next_frame().expect("ok").expect("Some");
assert!(f0.is_keyframe);
assert!(dec.next_frame().expect("ok").is_none());
}
#[test]
fn rejects_malformed_magic() {
let bad = b"junk junk junk junk".to_vec();
assert!(WebpAnimDecoder::new(&bad).is_err());
}
#[test]
fn seek_to_frame_zero_equals_reset() {
let blob = three_frame_anim();
let mut dec = WebpAnimDecoder::new(&blob).expect("new");
let _ = dec.next_frame().expect("ok").expect("Some");
let _ = dec.next_frame().expect("ok").expect("Some");
dec.seek_to_frame(0).expect("seek");
assert_eq!(dec.next_frame_index(), 0);
let f0 = dec.next_frame().expect("ok").expect("Some");
assert!(f0.is_keyframe);
assert_eq!(f0.pts_ms, 0);
assert_eq!(&f0.rgba[0..4], &[0xff, 0, 0, 0xff], "back to red");
}
#[test]
fn seek_to_frame_jumps_forward_and_lands_correctly() {
let blob = three_frame_anim();
let mut dec = WebpAnimDecoder::new(&blob).expect("new");
dec.seek_to_frame(2).expect("seek");
assert_eq!(dec.next_frame_index(), 2);
let f2 = dec.next_frame().expect("ok").expect("Some");
assert_eq!(f2.pts_ms, 70);
assert_eq!(&f2.rgba[0..4], &[0, 0, 0xff, 0xff], "F2 should be blue");
assert!(dec.done());
}
#[test]
fn seek_to_frame_backward_resets_then_replays() {
let blob = three_frame_anim();
let mut dec = WebpAnimDecoder::new(&blob).expect("new");
while dec.next_frame().expect("ok").is_some() {}
assert!(dec.done());
dec.seek_to_frame(1).expect("seek");
assert_eq!(dec.next_frame_index(), 1);
let f1 = dec.next_frame().expect("ok").expect("Some");
assert_eq!(f1.pts_ms, 30);
assert_eq!(&f1.rgba[0..4], &[0, 0xff, 0, 0xff], "F1 should be green");
}
#[test]
fn seek_to_frame_at_end_is_done() {
let blob = three_frame_anim();
let mut dec = WebpAnimDecoder::new(&blob).expect("new");
dec.seek_to_frame(3).expect("seek to end");
assert!(dec.done());
assert!(dec.next_frame().expect("ok").is_none());
}
#[test]
fn seek_to_frame_past_end_errors() {
let blob = three_frame_anim();
let mut dec = WebpAnimDecoder::new(&blob).expect("new");
assert!(dec.seek_to_frame(4).is_err());
assert_eq!(dec.next_frame_index(), 0);
}
#[test]
fn seek_to_frame_idempotent_at_current_position() {
let blob = three_frame_anim();
let mut dec = WebpAnimDecoder::new(&blob).expect("new");
let _ = dec.next_frame().expect("ok").expect("Some");
dec.seek_to_frame(1).expect("seek");
assert_eq!(dec.next_frame_index(), 1);
let f1 = dec.next_frame().expect("ok").expect("Some");
assert_eq!(f1.pts_ms, 30);
}
#[test]
fn seek_preserves_dispose_to_background_canvas_state() {
let bg_bgra: [u8; 4] = [0x40, 0x50, 0x60, 0xff];
let bg_rgba_expected = [0x60, 0x50, 0x40, 0xff];
let f0_tile = solid(W, H, [0xff, 0, 0, 0xff]);
let f1_tile = solid(2, 2, [0, 0xff, 0, 0xff]);
let frames = [
AnimFrame {
width: W,
height: H,
x_offset: 0,
y_offset: 0,
duration_ms: 40,
blend: false,
dispose_to_background: true,
rgba: &f0_tile,
},
AnimFrame {
width: 2,
height: 2,
x_offset: 0,
y_offset: 0,
duration_ms: 40,
blend: false,
dispose_to_background: false,
rgba: &f1_tile,
},
];
let blob = build_animated_webp(W, H, bg_bgra, 0, &frames).expect("encode");
let mut dec = WebpAnimDecoder::new(&blob).expect("new");
dec.seek_to_frame(1).expect("seek");
let f1 = dec.next_frame().expect("ok").expect("Some");
let stride = (W as usize) * 4;
let i = (3 * stride) + (5 * 4);
assert_eq!(&f1.rgba[i..i + 4], &bg_rgba_expected);
}
#[test]
fn borrowed_view_matches_owned_clone_byte_for_byte() {
let blob = three_frame_anim();
let mut owned_dec = WebpAnimDecoder::new(&blob).expect("new");
let mut borrowed_dec = WebpAnimDecoder::new(&blob).expect("new");
for i in 0..3 {
let owned = owned_dec.next_frame().expect("ok").expect("Some");
let borrowed = borrowed_dec
.next_frame_borrowed()
.expect("ok")
.expect("Some");
assert_eq!(owned.rgba.as_slice(), borrowed.rgba, "frame {i} pixels");
assert_eq!(owned.pts_ms, borrowed.pts_ms, "frame {i} pts");
assert_eq!(owned.duration_ms, borrowed.duration_ms, "frame {i} dur");
assert_eq!(owned.is_keyframe, borrowed.is_keyframe);
assert_eq!(owned.frame_x, borrowed.frame_x);
assert_eq!(owned.frame_y, borrowed.frame_y);
assert_eq!(owned.frame_width, borrowed.frame_width);
assert_eq!(owned.frame_height, borrowed.frame_height);
}
assert!(owned_dec.next_frame().expect("ok").is_none());
assert!(borrowed_dec.next_frame_borrowed().expect("ok").is_none());
}
#[test]
fn borrowed_view_returns_none_after_drain() {
let blob = three_frame_anim();
let mut dec = WebpAnimDecoder::new(&blob).expect("new");
for _ in 0..3 {
assert!(dec.next_frame_borrowed().expect("ok").is_some());
}
assert!(dec.next_frame_borrowed().expect("ok").is_none());
assert!(dec.done());
}
#[test]
fn borrowed_view_dispose_to_background_deferred_then_applied() {
let bg_bgra: [u8; 4] = [0x40, 0x50, 0x60, 0xff];
let bg_rgba_expected = [0x60, 0x50, 0x40, 0xff];
let f0_tile = solid(W, H, [0xff, 0, 0, 0xff]);
let f1_tile = solid(2, 2, [0, 0xff, 0, 0xff]);
let frames = [
AnimFrame {
width: W,
height: H,
x_offset: 0,
y_offset: 0,
duration_ms: 40,
blend: false,
dispose_to_background: true,
rgba: &f0_tile,
},
AnimFrame {
width: 2,
height: 2,
x_offset: 0,
y_offset: 0,
duration_ms: 40,
blend: false,
dispose_to_background: false,
rgba: &f1_tile,
},
];
let blob = build_animated_webp(W, H, bg_bgra, 0, &frames).expect("encode");
let mut dec = WebpAnimDecoder::new(&blob).expect("new");
{
let f0 = dec.next_frame_borrowed().expect("ok").expect("Some");
assert_eq!(&f0.rgba[0..4], &[0xff, 0, 0, 0xff], "F0 rendered");
assert!(f0.dispose_to_background, "flag exposes the deferral");
}
let f1 = dec.next_frame_borrowed().expect("ok").expect("Some");
let stride = (W as usize) * 4;
let i = (3 * stride) + (5 * 4);
assert_eq!(&f1.rgba[i..i + 4], &bg_rgba_expected);
assert_eq!(&f1.rgba[0..4], &[0, 0xff, 0, 0xff]);
}
#[test]
fn borrowed_view_to_owned_round_trips() {
let blob = three_frame_anim();
let mut owned_dec = WebpAnimDecoder::new(&blob).expect("new");
let mut borrowed_dec = WebpAnimDecoder::new(&blob).expect("new");
let owned = owned_dec.next_frame().expect("ok").expect("Some");
let borrowed_ref = borrowed_dec
.next_frame_borrowed()
.expect("ok")
.expect("Some");
let borrowed_owned = borrowed_ref.to_owned();
assert_eq!(owned.rgba, borrowed_owned.rgba);
assert_eq!(owned.pts_ms, borrowed_owned.pts_ms);
assert_eq!(owned.duration_ms, borrowed_owned.duration_ms);
assert_eq!(owned.is_keyframe, borrowed_owned.is_keyframe);
}
#[test]
fn mixing_owned_and_borrowed_pulls_advances_consistently() {
let blob = three_frame_anim();
let mut dec = WebpAnimDecoder::new(&blob).expect("new");
let f0 = dec.next_frame().expect("ok").expect("Some");
assert_eq!(&f0.rgba[0..4], &[0xff, 0, 0, 0xff]);
assert_eq!(f0.pts_ms, 0);
{
let f1 = dec.next_frame_borrowed().expect("ok").expect("Some");
assert_eq!(&f1.rgba[0..4], &[0, 0xff, 0, 0xff]);
assert_eq!(f1.pts_ms, 30);
}
let f2 = dec.next_frame().expect("ok").expect("Some");
assert_eq!(&f2.rgba[0..4], &[0, 0, 0xff, 0xff]);
assert_eq!(f2.pts_ms, 70);
}
}