use crate::anim::{Compositor, decode_anmf, frame_meta};
use crate::container::anim::FrameMeta;
use crate::container::fourcc::FourCc;
use crate::container::reader::{body_range, parse_container, read_chunk_at};
use crate::container::scan::{is_complete, scan_chunks};
use crate::container::vp8x::{VP8X_PAYLOAD_LEN, Vp8xInfo};
use crate::error::{Codec, Error, Result};
use crate::image::{self, Dimensions, Image, Metadata, PixelLayout};
use crate::lossless::animation::{CompositedFrame, Frames, Vp8lFrameDecoder};
use crate::lossless::prelude::*;
use crate::lossless::vp8l;
use crate::lossless::vp8l::decode_incr::{Step, Vp8lStream};
pub use crate::stream::{
DecodeOptions, DecodedFrame, FrameDecoder, FramePayload, ImageInfo, Progress, RowDrain,
};
#[cfg(feature = "std")]
use std::io::Read;
pub(crate) fn decode_image(bytes: &[u8], options: &DecodeOptions) -> Result<Image> {
if is_animated_file(bytes) {
return decode_first_frame(bytes, options);
}
let parsed = parse_container(bytes, options.read_metadata)?;
let image = decode_vp8l(parsed.vp8l, options)?;
if parsed
.vp8x
.is_some_and(|vp8x| vp8x.canvas != image.dimensions())
{
return Err(Error::InvalidContainer);
}
Ok(image.with_metadata(parsed.metadata))
}
pub fn decode_vp8l(payload: &[u8], options: &DecodeOptions) -> Result<Image> {
let (width, height, _header_alpha) = vp8l::decode::peek_header(payload)?;
let pixels = u64::from(width) * u64::from(height);
if let Some(limit) = options.max_pixels.filter(|&limit| pixels > limit) {
return Err(Error::LimitExceeded { pixels, limit });
}
let decoded = vp8l::decode::decode(payload)?;
let dims =
Dimensions::new(decoded.width, decoded.height).map_err(|_| Error::InvalidBitstream {
codec: Codec::Lossless,
})?;
let has_alpha = image::argb_has_alpha(&decoded.argb);
let pixels = image::pack_pixels(options.layout, &decoded.argb);
Ok(Image::from_parts(
dims,
options.layout,
pixels,
has_alpha,
Metadata::none(),
))
}
fn is_animated_file(bytes: &[u8]) -> bool {
matches!(peek_info(bytes), Ok(Some(info)) if info.is_animated)
}
pub(crate) fn check_pixel_limit(dims: Dimensions, options: &DecodeOptions) -> Result<()> {
let pixels = dims.pixel_count();
if let Some(limit) = options.max_pixels.filter(|&limit| pixels > limit) {
return Err(Error::LimitExceeded { pixels, limit });
}
Ok(())
}
fn decode_first_frame(bytes: &[u8], options: &DecodeOptions) -> Result<Image> {
Frames::new(Cow::Borrowed(bytes), options.clone(), Vp8lFrameDecoder)?
.composited()
.next()
.ok_or(Error::MissingImage)?
.map(CompositedFrame::into_image)
}
struct StillState {
stream: Vp8lStream,
info: ImageInfo,
payload_start: usize,
payload_end: usize,
layout: PixelLayout,
width: u32,
height: u32,
has_alpha: bool,
done: bool,
packed: Vec<u8>,
packed_base: u32,
next_row: u32,
drained: u32,
}
impl core::fmt::Debug for StillState {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("StillState")
.field("info", &self.info)
.field("payload_start", &self.payload_start)
.field("payload_end", &self.payload_end)
.field("width", &self.width)
.field("height", &self.height)
.field("has_alpha", &self.has_alpha)
.field("done", &self.done)
.field("packed_len", &self.packed.len())
.field("packed_base", &self.packed_base)
.field("next_row", &self.next_row)
.field("drained", &self.drained)
.finish_non_exhaustive()
}
}
impl StillState {
const fn new(
info: ImageInfo,
payload_start: usize,
payload_end: usize,
layout: PixelLayout,
) -> Self {
Self {
stream: Vp8lStream::new(),
width: info.dimensions.width(),
height: info.dimensions.height(),
info,
payload_start,
payload_end,
layout,
has_alpha: false,
done: false,
packed: Vec::new(),
packed_base: 0,
next_row: 0,
drained: 0,
}
}
const fn row_bytes(&self) -> usize {
self.width as usize * 4
}
fn free_drained(&mut self) {
if self.drained > 0 {
let cut = self.drained as usize * self.row_bytes();
self.packed.drain(..cut);
self.packed_base += self.drained;
self.drained = 0;
}
}
fn take_drain(&mut self) -> Option<RowDrain<'_>> {
let pending = self.next_row - self.packed_base - self.drained;
if pending == 0 {
return None;
}
let offset = self.drained as usize * self.row_bytes();
let drain = RowDrain::new(
self.packed_base + self.drained,
pending,
self.width,
self.layout,
&self.packed[offset..],
);
self.drained += pending;
Some(drain)
}
fn assemble(self, buf: &[u8], options: &DecodeOptions) -> Result<Image> {
let dims =
Dimensions::new(self.width, self.height).map_err(|_| Error::InvalidBitstream {
codec: Codec::Lossless,
})?;
let parsed = parse_container(buf, options.read_metadata)?;
let vp8x_canvas = parsed.vp8x.map(|vp8x| vp8x.canvas);
if vp8x_canvas.is_some_and(|canvas| canvas != dims) {
return Err(Error::InvalidContainer);
}
Ok(Image::from_parts(
dims,
self.layout,
self.packed,
self.has_alpha,
parsed.metadata,
))
}
}
#[derive(Debug)]
struct AnimState<D> {
compositor: Compositor,
options: DecodeOptions,
cursor: usize,
latest: Option<Image>,
decoder: D,
}
impl<D: FrameDecoder> AnimState<D> {
fn new(info: ImageInfo, options: DecodeOptions, decoder: D) -> Self {
Self {
compositor: Compositor::new(info.dimensions, options.layout),
cursor: 12,
options,
latest: None,
decoder,
}
}
fn next_frame(&mut self, buf: &[u8], complete: bool) -> Result<Option<FrameMeta>> {
let (_start, body_end) = body_range(buf)?;
let body = &buf[..body_end];
loop {
if self.cursor >= body_end {
return Ok(None);
}
match read_chunk_at(body, self.cursor) {
Ok(None) => return Ok(None),
Ok(Some((chunk, next))) => {
if chunk.id == FourCc::ANMF {
let (header, alpha_used, argb) =
decode_anmf(chunk.data, &self.decoder, &self.options)?;
self.cursor = next;
let meta = frame_meta(header);
let image = self.compositor.paint(header, alpha_used, &argb)?;
self.latest = Some(image);
return Ok(Some(meta));
}
self.cursor = next;
},
Err(err) => {
return if complete { Err(err) } else { Ok(None) };
},
}
}
}
}
#[derive(Debug)]
enum Mode<D> {
Undecided,
Still(StillState),
Anim(AnimState<D>),
}
#[derive(Debug)]
pub struct IncrementalDecoder<D = Vp8lFrameDecoder> {
buf: Vec<u8>,
options: DecodeOptions,
reported_header: bool,
image: Option<Image>,
mode: Mode<D>,
decoder: D,
}
impl IncrementalDecoder<Vp8lFrameDecoder> {
#[must_use]
pub fn new() -> Self {
Self::with_options(DecodeOptions::default())
}
#[must_use]
pub const fn with_options(options: DecodeOptions) -> Self {
Self::with_options_and_decoder(options, Vp8lFrameDecoder)
}
}
impl<D: FrameDecoder + Clone> IncrementalDecoder<D> {
#[must_use]
pub const fn with_options_and_decoder(options: DecodeOptions, decoder: D) -> Self {
Self {
buf: Vec::new(),
options,
reported_header: false,
image: None,
mode: Mode::Undecided,
decoder,
}
}
pub fn push(&mut self, chunk: &[u8]) -> Result<Progress> {
if let Mode::Still(st) = &mut self.mode {
st.free_drained();
}
if self.image.is_some() {
return Ok(Progress::Finished);
}
self.buf.extend_from_slice(chunk);
if matches!(self.mode, Mode::Anim(_)) {
return self.drive_anim();
}
if matches!(self.mode, Mode::Undecided) {
let outcome = self.begin_or_buffer()?;
if let Some(progress) = outcome {
return Ok(progress);
}
}
if !self.reported_header {
self.reported_header = true;
if let Mode::Still(st) = &self.mode {
return Ok(Progress::HeaderReady(st.info));
}
}
self.drive_still()
}
fn begin_or_buffer(&mut self) -> Result<Option<Progress>> {
if let Ok(Some(info)) = peek_info(&self.buf)
&& info.is_animated
{
check_pixel_limit(info.dimensions, &self.options)?;
self.reported_header = true;
self.mode = Mode::Anim(AnimState::new(
info,
self.options.clone(),
self.decoder.clone(),
));
return Ok(Some(Progress::HeaderReady(info)));
}
if is_complete(&self.buf) {
self.image = Some(decode_image(&self.buf, &self.options)?);
return Ok(Some(Progress::Finished));
}
let Some(info) = peek_info(&self.buf)? else {
return Ok(Some(Progress::NeedMoreInput));
};
check_pixel_limit(info.dimensions, &self.options)?;
let Some((start, end)) = locate_vp8l(&self.buf) else {
return Ok(Some(Progress::NeedMoreInput));
};
self.mode = Mode::Still(StillState::new(info, start, end, self.options.layout));
Ok(None)
}
fn drive_still(&mut self) -> Result<Progress> {
let complete = is_complete(&self.buf);
let Mode::Still(st) = &mut self.mode else {
return Ok(Progress::NeedMoreInput);
};
let final_input = self.buf.len() >= st.payload_end || complete;
let end = self.buf.len().min(st.payload_end);
let payload = &self.buf[st.payload_start..end];
let width = st.width as usize;
let mut rows_first: Option<u32> = None;
let mut rows_count = 0u32;
loop {
match st.stream.advance(payload, final_input)? {
Step::Header(w, h, alpha) => {
st.width = w;
st.height = h;
if !self.reported_header {
self.reported_header = true;
let info = ImageInfo::new(
Dimensions::new(w, h).map_err(|_| Error::InvalidBitstream {
codec: Codec::Lossless,
})?,
alpha || st.info.has_alpha,
st.info.has_metadata,
false,
);
st.info = info;
return Ok(Progress::HeaderReady(info));
}
},
Step::Rows { first_row, count } => {
let start = first_row as usize * width;
let stop = (first_row + count) as usize * width;
let rows_argb = &st.stream.ready()[start..stop];
st.has_alpha = st.has_alpha || image::argb_has_alpha(rows_argb);
let bytes = image::pack_pixels(st.layout, rows_argb);
st.packed.extend_from_slice(&bytes);
st.next_row = first_row + count;
if rows_first.is_none() {
rows_first = Some(first_row);
}
rows_count += count;
},
Step::NeedMore => break,
Step::Done => {
st.done = true;
break;
},
}
}
if st.done && complete {
return Ok(Progress::Finished);
}
if let Some(first_row) = rows_first {
return Ok(Progress::RowsDecoded {
first_row,
count: rows_count,
});
}
Ok(Progress::NeedMoreInput)
}
fn drive_anim(&mut self) -> Result<Progress> {
let complete = is_complete(&self.buf);
let next = {
let buf = &self.buf;
let Mode::Anim(anim) = &mut self.mode else {
return Ok(Progress::NeedMoreInput);
};
anim.next_frame(buf, complete)?
};
if let Some(meta) = next {
return Ok(Progress::FrameComplete(meta));
}
if complete {
Ok(Progress::Finished)
} else {
Ok(Progress::NeedMoreInput)
}
}
#[must_use]
pub const fn frame_image(&self) -> Option<&Image> {
match &self.mode {
Mode::Anim(anim) => anim.latest.as_ref(),
_ => None,
}
}
pub fn drain_rows(&mut self) -> Option<RowDrain<'_>> {
match &mut self.mode {
Mode::Still(st) => st.take_drain(),
_ => None,
}
}
pub fn into_image(self) -> Result<Image> {
let Self {
buf,
options,
image,
mode,
..
} = self;
if let Some(image) = image {
return Ok(image);
}
match mode {
Mode::Still(st) if st.done && st.packed_base == 0 && is_complete(&buf) => {
st.assemble(&buf, &options)
},
_ => decode_image(&buf, &options),
}
}
}
impl Default for IncrementalDecoder<Vp8lFrameDecoder> {
fn default() -> Self {
Self::new()
}
}
fn locate_vp8l(bytes: &[u8]) -> Option<(usize, usize)> {
scan_chunks(bytes)
.find(|chunk| chunk.id == FourCc::VP8L)
.map(|chunk| (chunk.payload_start, chunk.payload_end))
}
fn peek_info(bytes: &[u8]) -> Result<Option<ImageInfo>> {
if bytes.len() < 12 {
return Ok(None);
}
if bytes[0..4] != FourCc::RIFF.0 || bytes[8..12] != FourCc::WEBP.0 {
return Err(Error::NotWebp);
}
let mut has_metadata = false;
let mut vp8x_alpha = false;
for chunk in scan_chunks(bytes) {
match chunk.id {
FourCc::VP8X => {
let Some(data) =
bytes.get(chunk.payload_start..chunk.payload_start + VP8X_PAYLOAD_LEN)
else {
return Ok(None);
};
let info = Vp8xInfo::parse(data)?;
has_metadata =
info.flags.has_icc() || info.flags.has_exif() || info.flags.has_xmp();
vp8x_alpha = info.flags.has_alpha();
if info.flags.is_animated() {
return Ok(Some(ImageInfo::new(
info.canvas,
vp8x_alpha,
has_metadata,
true,
)));
}
},
FourCc::VP8L => {
return peek_vp8l_info(bytes.get(chunk.payload_start..), has_metadata, vp8x_alpha);
},
FourCc::VP8 => return Err(Error::UnsupportedFeature),
FourCc::ANIM | FourCc::ANMF => return Err(Error::InvalidContainer),
_ => {},
}
}
Ok(None)
}
fn peek_vp8l_info(
payload: Option<&[u8]>,
has_metadata: bool,
vp8x_alpha: bool,
) -> Result<Option<ImageInfo>> {
let Some(payload) = payload else {
return Ok(None);
};
if payload.len() < 5 {
return Ok(None); }
let (width, height, header_alpha) = vp8l::decode::peek_header(payload)?;
let dimensions = Dimensions::new(width, height).map_err(|_| Error::InvalidBitstream {
codec: Codec::Lossless,
})?;
Ok(Some(ImageInfo::new(
dimensions,
header_alpha || vp8x_alpha,
has_metadata,
false,
)))
}
#[cfg(feature = "std")]
#[derive(Debug)]
pub struct Decoder<R> {
reader: R,
options: DecodeOptions,
buf: Option<Vec<u8>>,
}
#[cfg(feature = "std")]
impl<R: Read> Decoder<R> {
pub fn new(reader: R) -> Self {
Self::with_options(reader, DecodeOptions::default())
}
pub const fn with_options(reader: R, options: DecodeOptions) -> Self {
Self {
reader,
options,
buf: None,
}
}
pub fn read_info(&mut self) -> Result<ImageInfo> {
self.fill()?;
let bytes = self.buf.as_deref().unwrap_or(&[]);
peek_info(bytes)?.ok_or(Error::Truncated)
}
pub fn decode(mut self) -> Result<Image> {
let bytes = if let Some(bytes) = self.buf.take() {
bytes
} else {
let mut bytes = Vec::new();
self.reader.read_to_end(&mut bytes)?;
bytes
};
decode_image(&bytes, &self.options)
}
pub fn into_frames(mut self) -> Result<Frames<'static>> {
let bytes = if let Some(bytes) = self.buf.take() {
bytes
} else {
let mut bytes = Vec::new();
self.reader.read_to_end(&mut bytes)?;
bytes
};
Frames::new(Cow::Owned(bytes), self.options, Vp8lFrameDecoder)
}
fn fill(&mut self) -> Result<()> {
if self.buf.is_none() {
let mut bytes = Vec::new();
self.reader.read_to_end(&mut bytes)?;
self.buf = Some(bytes);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::cast_possible_truncation,
reason = "test fixtures build container byte lengths with casts that fit their targets \
by construction"
)]
use super::{DecodeOptions, IncrementalDecoder, Progress, decode_image};
use crate::container::fourcc::FourCc;
use crate::container::vp8x::{Vp8xFlags, Vp8xInfo};
use crate::container::writer::{push_chunk, riff_envelope, wrap, wrap_vp8l};
use crate::error::Error;
use crate::image::{Dimensions, Metadata, PixelLayout};
use crate::lossless::vp8l;
fn sample_webp(width: u32, height: u32) -> Vec<u8> {
let argb: Vec<u32> = (0..width * height).map(|i| 0xff00_0000 | i).collect();
wrap_vp8l(&vp8l::encode::encode(width, height, &argb))
}
#[test]
fn decode_image_matches_layout() {
let file = sample_webp(4, 4);
let rgba = decode_image(&file, &DecodeOptions::default()).unwrap();
let bgra =
decode_image(&file, &DecodeOptions::default().layout(PixelLayout::Bgra8)).unwrap();
assert_eq!(rgba.dimensions(), bgra.dimensions());
let r = rgba.as_bytes();
let b = bgra.as_bytes();
assert_eq!([r[0], r[1], r[2], r[3]], [b[2], b[1], b[0], b[3]]);
}
#[test]
fn max_pixels_rejects_before_decode() {
let file = sample_webp(8, 8);
let opts = DecodeOptions::default().max_pixels(10);
assert_eq!(
decode_image(&file, &opts).unwrap_err(),
Error::LimitExceeded {
pixels: 64,
limit: 10
}
);
}
#[test]
fn max_pixels_rejects_still_incremental_before_stream() {
let file = sample_webp(8, 8);
let mut dec = IncrementalDecoder::with_options(DecodeOptions::default().max_pixels(10));
let mut err = None;
for byte in &file {
if let Err(e) = dec.push(core::slice::from_ref(byte)) {
err = Some(e);
break;
}
}
assert_eq!(
err,
Some(Error::LimitExceeded {
pixels: 64,
limit: 10,
})
);
}
#[test]
fn max_pixels_rejects_animated_canvas_before_alloc() {
let canvas = Dimensions::new(16384, 16384).unwrap();
let flags = Vp8xFlags::for_output(&Metadata::none(), false).with_animation();
let mut body = Vec::new();
push_chunk(&mut body, FourCc::VP8X, &Vp8xInfo::build(flags, canvas));
let file = riff_envelope(&body);
let mut dec = IncrementalDecoder::with_options(DecodeOptions::default().max_pixels(4));
assert_eq!(
dec.push(&file).unwrap_err(),
Error::LimitExceeded {
pixels: 16384 * 16384,
limit: 4,
}
);
}
#[test]
fn incremental_equals_one_shot_over_splits() {
let file = sample_webp(7, 5);
let one_shot = decode_image(&file, &DecodeOptions::default()).unwrap();
let mut dec = IncrementalDecoder::new();
let mut finished = false;
for byte in &file {
if matches!(dec.push(&[*byte]).unwrap(), Progress::Finished) {
finished = true;
}
}
assert!(finished, "must finish once all bytes are pushed");
assert_eq!(dec.into_image().unwrap(), one_shot);
}
#[test]
fn incremental_reports_header_before_finish() {
let file = sample_webp(6, 3);
let mut dec = IncrementalDecoder::new();
let split = file.len() - 1;
let mut saw_header = false;
for byte in &file[..split] {
if let Progress::HeaderReady(info) = dec.push(&[*byte]).unwrap() {
saw_header = true;
assert_eq!((info.dimensions.width(), info.dimensions.height()), (6, 3));
}
}
assert!(
saw_header,
"a bare VP8L file exposes its header before completion"
);
}
#[test]
fn incremental_into_image_errors_when_incomplete() {
let file = sample_webp(2, 2);
let mut dec = IncrementalDecoder::new();
let _ = dec.push(&file[..8]); assert!(dec.into_image().is_err());
}
#[test]
fn tiny_riff_size_does_not_finish_early() {
let mut file = b"RIFF".to_vec();
file.extend_from_slice(&2u32.to_le_bytes());
file.extend_from_slice(b"WE");
let mut dec = IncrementalDecoder::new();
for byte in &file {
assert!(!matches!(dec.push(&[*byte]), Ok(Progress::Finished)));
}
assert_eq!(
dec.into_image().is_err(),
decode_image(&file, &DecodeOptions::default()).is_err()
);
}
#[test]
fn drained_rows_reassemble_to_one_shot() {
let file = sample_webp(9, 7);
let one_shot = decode_image(&file, &DecodeOptions::default()).unwrap();
let mut dec = IncrementalDecoder::new();
let mut collected: Vec<u8> = Vec::new();
let steps = [3usize, 1, 5, 2, 11, 4, 7];
let mut cursor = 0usize;
let mut i = 0usize;
while cursor < file.len() {
let end = (cursor + steps[i % steps.len()]).min(file.len());
dec.push(&file[cursor..end]).unwrap();
cursor = end;
let batch = if i.is_multiple_of(3) {
None
} else {
dec.drain_rows()
};
if let Some(drain) = batch {
collected.extend_from_slice(drain.as_bytes());
}
i += 1;
}
while let Some(drain) = dec.drain_rows() {
collected.extend_from_slice(drain.as_bytes());
}
assert_eq!(collected, one_shot.as_bytes());
}
#[test]
fn progress_sequence_is_monotonic() {
let (w, h) = (5u32, 8u32);
let argb: Vec<u32> = (0..w * h)
.map(|i| 0xff00_0000 | (i.wrapping_mul(7)))
.collect();
let payload = vp8l::encode::encode(w, h, &argb);
let dims = Dimensions::new(w, h).unwrap();
let metadata = Metadata {
exif: Some(vec![1, 2, 3, 4, 5, 6]),
..Metadata::none()
};
let file = wrap(&payload, dims, &metadata, false);
let one_shot = decode_image(&file, &DecodeOptions::default()).unwrap();
let mut dec = IncrementalDecoder::new();
let mut header_at: Option<usize> = None;
let mut first_rows_at: Option<usize> = None;
let mut rows: Vec<(u32, u32)> = Vec::new();
let mut finished = 0usize;
for (event, byte) in file.iter().enumerate() {
match dec.push(&[*byte]).unwrap() {
Progress::HeaderReady(info) => {
assert!(header_at.is_none(), "header reported more than once");
assert_eq!((info.dimensions.width(), info.dimensions.height()), (w, h));
header_at = Some(event);
},
Progress::RowsDecoded { first_row, count } => {
first_rows_at.get_or_insert(event);
rows.push((first_row, count));
},
Progress::FrameComplete(_) => panic!("a still image must not report FrameComplete"),
Progress::Finished => finished += 1,
_ => {},
}
}
let header_event = header_at.expect("the header must be reported");
if let Some(rows_event) = first_rows_at {
assert!(
header_event < rows_event,
"HeaderReady must precede every RowsDecoded"
);
}
let mut expected = 0u32;
for (first_row, count) in &rows {
assert_eq!(*first_row, expected, "row payout is not contiguous");
expected += count;
}
assert_eq!(expected, h, "reported rows must sum to the height");
assert_eq!(finished, 1, "exactly one Finished");
assert_eq!(dec.into_image().unwrap(), one_shot);
}
#[test]
fn frame_complete_fires_per_frame_over_splits() {
use super::{FrameMeta, Image};
let file: &[u8] = include_bytes!(
"../../../webpkit-lossless-conformance/fixtures/decode/animation_frames/input.webp"
);
let batch: Vec<_> = crate::lossless::decode_frames(file)
.unwrap()
.composited()
.map(Result::unwrap)
.collect();
let metas: Vec<FrameMeta> = crate::lossless::decode_frames(file)
.unwrap()
.map(|frame| frame.unwrap().meta())
.collect();
assert!(batch.len() >= 2, "fixture should have several frames");
assert_eq!(batch.len(), metas.len());
let steps = [7usize, 3, 13, 1, 5, 29, 2];
let mut dec = IncrementalDecoder::new();
let mut header_at: Option<usize> = None;
let mut first_frame_at: Option<usize> = None;
let mut got_metas: Vec<FrameMeta> = Vec::new();
let mut got_frames: Vec<Image> = Vec::new();
let (mut cursor, mut si, mut step) = (0usize, 0usize, 0usize);
loop {
step += 1;
assert!(step < file.len() + 1000, "driver failed to terminate");
let feed: &[u8] = if cursor < file.len() {
let end = (cursor + steps[si % steps.len()]).min(file.len());
si += 1;
let slice = &file[cursor..end];
cursor = end;
slice
} else {
&[]
};
match dec.push(feed).unwrap() {
Progress::RowsDecoded { .. } => panic!("an animation must not report RowsDecoded"),
Progress::HeaderReady(info) => {
assert!(header_at.is_none(), "header reported more than once");
assert!(info.is_animated, "the fixture is an animation");
assert_eq!(info.dimensions, batch[0].image().dimensions());
header_at = Some(step);
},
Progress::FrameComplete(meta) => {
first_frame_at.get_or_insert(step);
got_metas.push(meta);
got_frames.push(dec.frame_image().expect("canvas after a frame").clone());
},
Progress::Finished => break,
_ => {},
}
}
let header = header_at.expect("HeaderReady must be reported");
let first_frame = first_frame_at.expect("at least one FrameComplete");
assert!(header < first_frame, "HeaderReady must precede the frames");
assert_eq!(
got_metas.len(),
batch.len(),
"exactly one FrameComplete per frame"
);
for (i, (meta, frame)) in got_metas.iter().zip(&got_frames).enumerate() {
assert_eq!(*meta, metas[i], "frame {i} meta mismatch");
assert_eq!(
frame,
batch[i].image(),
"frame {i} incremental composited canvas must match the batch compositor"
);
}
assert_eq!(dec.into_image().unwrap(), *batch[0].image());
}
#[test]
fn into_image_returns_complete_image_after_draining() {
let file = sample_webp(6, 9);
let one_shot = decode_image(&file, &DecodeOptions::default()).unwrap();
let mut dec = IncrementalDecoder::new();
for byte in &file {
dec.push(&[*byte]).unwrap();
}
let mut drained: Vec<u8> = Vec::new();
while let Some(d) = dec.drain_rows() {
drained.extend_from_slice(d.as_bytes());
}
assert_eq!(
drained,
one_shot.as_bytes(),
"the drain view covers every row once"
);
assert_eq!(
dec.into_image().unwrap(),
one_shot,
"into_image is still complete after draining every row"
);
let mut dec = IncrementalDecoder::new();
let half = file.len() / 2;
dec.push(&file[..half]).unwrap();
let _ = dec.drain_rows();
dec.push(&file[half..]).unwrap();
assert_eq!(dec.into_image().unwrap(), one_shot);
}
#[test]
fn into_image_rejects_complete_but_invalid_container_when_streamed() {
let mut file = sample_webp(4, 4);
let extra: &[u8] = &[b'V', b'P', b'8', b' ', 4, 0, 0, 0, 0, 0, 0, 0];
let riff = u32::from_le_bytes(file[4..8].try_into().unwrap());
file[4..8].copy_from_slice(&(riff + u32::try_from(extra.len()).unwrap()).to_le_bytes());
file.extend_from_slice(extra);
let one_shot = decode_image(&file, &DecodeOptions::default());
assert!(
one_shot.is_err(),
"one-shot must reject the trailing lossy chunk"
);
let mut dec = IncrementalDecoder::new();
for byte in &file {
let _ = dec.push(&[*byte]);
}
assert_eq!(
dec.into_image().unwrap_err(),
one_shot.unwrap_err(),
"the streamed path must reject the same complete-but-invalid file"
);
}
#[test]
fn max_pixels_allows_exactly_the_limit() {
let file = sample_webp(8, 8); let opts = DecodeOptions::default().max_pixels(64);
let img = decode_image(&file, &opts).unwrap();
assert_eq!(
(img.dimensions().width(), img.dimensions().height()),
(8, 8)
);
}
#[test]
fn check_pixel_limit_allows_exactly_the_limit_incremental() {
let file = sample_webp(8, 8); let mut dec = IncrementalDecoder::with_options(DecodeOptions::default().max_pixels(64));
let mut err = None;
for byte in &file {
if let Err(e) = dec.push(core::slice::from_ref(byte)) {
err = Some(e);
break;
}
}
assert_eq!(err, None, "pixels == limit must be accepted, not rejected");
let img = dec.into_image().unwrap();
assert_eq!(
(img.dimensions().width(), img.dimensions().height()),
(8, 8)
);
}
#[test]
fn still_state_debug_reports_its_fields() {
let file = sample_webp(5, 5); let mut dec = IncrementalDecoder::new();
for byte in &file[..file.len() - 1] {
dec.push(core::slice::from_ref(byte)).unwrap();
}
let debug = format!("{dec:?}");
assert!(
debug.contains("StillState"),
"still-state name is rendered: {debug}"
);
assert!(
debug.contains("payload_start: 20"),
"payload_start field: {debug}"
);
assert!(debug.contains("width: 5"), "width field: {debug}");
}
#[test]
fn free_drained_resets_the_drained_counter_on_push() {
let file = sample_webp(4, 16);
let mut dec = IncrementalDecoder::new();
let mut i = 0usize;
let mut drained_rows = false;
while i < file.len() {
dec.push(core::slice::from_ref(&file[i])).unwrap();
i += 1;
if dec.drain_rows().is_some() {
drained_rows = true;
break;
}
}
assert!(
drained_rows,
"a row must drain mid-stream before completion"
);
assert!(
i < file.len(),
"bytes must remain to push after the first drain"
);
dec.push(core::slice::from_ref(&file[i])).unwrap();
let debug = format!("{dec:?}");
assert!(
debug.contains("drained: 0"),
"free_drained resets the drained counter: {debug}"
);
assert!(
!debug.contains("packed_base: 0,"),
"free_drained advances packed_base off zero: {debug}"
);
}
#[test]
fn drain_reports_absolute_first_row_after_freeing() {
let file = sample_webp(4, 16);
let mut dec = IncrementalDecoder::new();
let mut expected = 0u32;
let mut saw_nonzero_first = false;
for byte in &file {
dec.push(core::slice::from_ref(byte)).unwrap();
if let Some(drain) = dec.drain_rows() {
assert_eq!(
drain.first_row, expected,
"drain batches must be contiguous from 0"
);
saw_nonzero_first |= drain.first_row > 0;
expected += drain.rows;
}
}
while let Some(drain) = dec.drain_rows() {
assert_eq!(
drain.first_row, expected,
"trailing drain batches stay contiguous"
);
saw_nonzero_first |= drain.first_row > 0;
expected += drain.rows;
}
assert_eq!(expected, 16, "every row drained exactly once");
assert!(
saw_nonzero_first,
"a drain after freeing must report a non-zero absolute first_row"
);
}
#[test]
fn drive_still_treats_a_complete_riff_as_final_input() {
let (w, h) = (12u32, 12u32);
let argb: Vec<u32> = (0..w * h)
.map(|i| 0xff00_0000 | i.wrapping_mul(2_654_435_761))
.collect();
let full = vp8l::encode::encode(w, h, &argb);
assert!(
full.len() > 12,
"payload long enough to truncate meaningfully"
);
let chopped = &full[..full.len() - 8];
let mut file = Vec::new();
file.extend_from_slice(b"RIFF");
let riff_size = (4 + 8 + chopped.len()) as u32; file.extend_from_slice(&riff_size.to_le_bytes());
file.extend_from_slice(b"WEBP");
file.extend_from_slice(b"VP8L");
file.extend_from_slice(&(full.len() as u32).to_le_bytes()); file.extend_from_slice(chopped);
let mut dec = IncrementalDecoder::new();
let mut err = None;
for byte in &file {
if let Err(e) = dec.push(core::slice::from_ref(byte)) {
err = Some(e);
break;
}
}
assert!(
err.is_some(),
"a VP8L bitstream truncated within a complete RIFF must error"
);
}
#[test]
fn streamed_image_accumulates_alpha_across_rows() {
let (w, h) = (4u32, 6u32);
let argb: Vec<u32> = (0..w * h).map(|i| 0x8000_0000 | i).collect();
let file = wrap_vp8l(&vp8l::encode::encode(w, h, &argb));
let mut dec = IncrementalDecoder::new();
for byte in &file {
dec.push(core::slice::from_ref(byte)).unwrap();
}
let img = dec.into_image().unwrap();
assert!(
img.has_alpha(),
"a streamed non-opaque image reports has_alpha"
);
}
#[test]
fn into_image_redecodes_after_all_rows_freed() {
let (w, h) = (5u32, 8u32);
let argb: Vec<u32> = (0..w * h)
.map(|i| 0xff00_0000 | i.wrapping_mul(7))
.collect();
let payload = vp8l::encode::encode(w, h, &argb);
let dims = Dimensions::new(w, h).unwrap();
let metadata = Metadata {
exif: Some(vec![1, 2, 3, 4, 5, 6]),
..Metadata::none()
};
let file = wrap(&payload, dims, &metadata, false);
let one_shot = decode_image(&file, &DecodeOptions::default()).unwrap();
let mut dec = IncrementalDecoder::new();
for byte in &file {
dec.push(core::slice::from_ref(byte)).unwrap();
let _ = dec.drain_rows();
}
let debug = format!("{dec:?}");
assert!(
debug.contains(&format!("packed_base: {h}")),
"all rows freed: {debug}"
);
assert!(
debug.contains("packed_len: 0"),
"retained rows emptied: {debug}"
);
assert_eq!(dec.into_image().unwrap(), one_shot);
}
#[test]
fn locate_vp8l_reads_the_correct_payload_end() {
let file = sample_webp(6, 4);
let size = u32::from_le_bytes(file[16..20].try_into().unwrap()) as usize;
let expected_end = 20 + size;
let mut dec = IncrementalDecoder::new();
for byte in &file[..file.len() - 1] {
dec.push(core::slice::from_ref(byte)).unwrap();
}
let debug = format!("{dec:?}");
assert!(
debug.contains("payload_start: 20"),
"payload starts after the header: {debug}"
);
assert!(
debug.contains(&format!("payload_end: {expected_end}")),
"payload_end must be payload_start + declared size: {debug}"
);
}
#[test]
fn peek_info_validates_magic_at_exactly_twelve_bytes() {
let mut buf = vec![0u8; 12];
buf[4..8].copy_from_slice(&u32::MAX.to_le_bytes());
let mut dec = IncrementalDecoder::new();
assert_eq!(dec.push(&buf).unwrap_err(), Error::NotWebp);
}
#[test]
fn peek_info_rejects_wrong_webp_fourcc() {
let mut buf = b"RIFF".to_vec();
buf.extend_from_slice(&u32::MAX.to_le_bytes()); buf.extend_from_slice(b"XXXX"); let mut dec = IncrementalDecoder::new();
assert_eq!(dec.push(&buf).unwrap_err(), Error::NotWebp);
}
#[test]
fn peek_info_skips_unknown_chunk_by_its_true_size() {
let (w, h) = (6u32, 4u32);
let argb: Vec<u32> = (0..w * h).map(|i| 0xff00_0000 | i).collect();
let payload = vp8l::encode::encode(w, h, &argb);
let mut file = Vec::new();
file.extend_from_slice(b"RIFF");
file.extend_from_slice(&0x0101_0101u32.to_le_bytes()); file.extend_from_slice(b"WEBP");
file.extend_from_slice(b"TEST"); file.extend_from_slice(&4u32.to_le_bytes());
file.extend_from_slice(&[0u8, 0, 0, 0]);
file.extend_from_slice(b"VP8L");
file.extend_from_slice(&(payload.len() as u32).to_le_bytes());
file.extend_from_slice(&payload);
let mut dec = IncrementalDecoder::new();
let mut header_dims = None;
for byte in &file {
if let Progress::HeaderReady(info) = dec.push(core::slice::from_ref(byte)).unwrap() {
header_dims = Some((info.dimensions.width(), info.dimensions.height()));
}
}
assert_eq!(
header_dims,
Some((w, h)),
"peek_info must skip the unknown chunk by its true size and reach VP8L"
);
}
#[test]
fn peek_info_reports_metadata_from_any_single_vp8x_flag() {
let (w, h) = (4u32, 4u32);
let argb: Vec<u32> = (0..w * h).map(|i| 0xff00_0000 | i).collect();
let payload = vp8l::encode::encode(w, h, &argb);
let dims = Dimensions::new(w, h).unwrap();
let metadata = Metadata {
icc_profile: Some(vec![1, 2, 3, 4]),
..Metadata::none()
};
let file = wrap(&payload, dims, &metadata, false);
let mut dec = IncrementalDecoder::new();
let mut meta_flag = None;
for byte in &file[..file.len() - 1] {
if let Progress::HeaderReady(info) = dec.push(core::slice::from_ref(byte)).unwrap() {
meta_flag = Some(info.has_metadata);
}
}
assert_eq!(
meta_flag,
Some(true),
"an ICC-only VP8X still reports has_metadata"
);
}
#[test]
fn peek_info_reports_metadata_from_exif_only_vp8x_flag() {
let (w, h) = (4u32, 4u32);
let argb: Vec<u32> = (0..w * h).map(|i| 0xff00_0000 | i).collect();
let payload = vp8l::encode::encode(w, h, &argb);
let dims = Dimensions::new(w, h).unwrap();
let metadata = Metadata {
exif: Some(vec![9, 8, 7, 6]),
..Metadata::none()
};
let file = wrap(&payload, dims, &metadata, false);
let mut dec = IncrementalDecoder::new();
let mut meta_flag = None;
for byte in &file[..file.len() - 1] {
if let Progress::HeaderReady(info) = dec.push(core::slice::from_ref(byte)).unwrap() {
meta_flag = Some(info.has_metadata);
}
}
assert_eq!(
meta_flag,
Some(true),
"an Exif-only VP8X still reports has_metadata"
);
}
#[test]
fn peek_info_rejects_lossy_vp8_chunk() {
let mut file = Vec::new();
file.extend_from_slice(b"RIFF");
file.extend_from_slice(&0x0101_0101u32.to_le_bytes());
file.extend_from_slice(b"WEBP");
file.extend_from_slice(b"VP8 ");
file.extend_from_slice(&8u32.to_le_bytes());
file.extend_from_slice(&[0u8; 8]);
let mut dec = IncrementalDecoder::new();
assert_eq!(dec.push(&file).unwrap_err(), Error::UnsupportedFeature);
}
#[test]
fn peek_info_rejects_anim_chunk_without_vp8x() {
let mut file = Vec::new();
file.extend_from_slice(b"RIFF");
file.extend_from_slice(&0x0101_0101u32.to_le_bytes());
file.extend_from_slice(b"WEBP");
file.extend_from_slice(b"ANMF");
file.extend_from_slice(&8u32.to_le_bytes());
file.extend_from_slice(&[0u8; 8]);
let mut dec = IncrementalDecoder::new();
assert_eq!(dec.push(&file).unwrap_err(), Error::InvalidContainer);
}
#[test]
fn peek_vp8l_info_reports_header_at_exactly_five_payload_bytes() {
let file = sample_webp(6, 4); assert!(
file.len() >= 25,
"sample must have at least 5 payload bytes"
);
let mut dec = IncrementalDecoder::new();
let mut header = None;
for byte in &file[..25] {
if let Progress::HeaderReady(info) = dec.push(core::slice::from_ref(byte)).unwrap() {
header = Some((info.dimensions.width(), info.dimensions.height()));
}
}
assert_eq!(
header,
Some((6, 4)),
"the VP8L header is complete at 5 payload bytes"
);
}
#[test]
fn peek_vp8l_info_reports_header_alpha_advisory() {
let (w, h) = (4u32, 4u32);
let argb: Vec<u32> = (0..w * h).map(|i| 0x8000_0000 | i).collect();
let file = wrap_vp8l(&vp8l::encode::encode(w, h, &argb));
let mut dec = IncrementalDecoder::new();
let mut alpha = None;
for byte in &file[..file.len() - 1] {
if let Progress::HeaderReady(info) = dec.push(core::slice::from_ref(byte)).unwrap() {
alpha = Some(info.has_alpha);
}
}
assert_eq!(
alpha,
Some(true),
"a bare VP8L header alpha advisory reports has_alpha"
);
}
#[cfg(feature = "std")]
#[test]
fn decoder_fill_reads_the_source_for_read_info() {
use super::Decoder;
let file = sample_webp(4, 4);
let mut dec = Decoder::new(&file[..]); let info = dec.read_info().unwrap();
assert_eq!((info.dimensions.width(), info.dimensions.height()), (4, 4));
}
}