use crate::lossless::bit_io::reader::BitReader;
use crate::lossless::prelude::*;
use crate::lossless::transform::{cross_color, palette, predictor, subtract_green};
use crate::lossless::vp8l::decode::{
ParsedStream, PixelCore, Transform, decode_one, parse_top_level,
};
use crate::lossless::{Error, Result};
pub(crate) struct Vp8lStream {
state: Phase,
}
enum Phase {
Parsing,
Streaming(Box<StreamState>),
Done(Vec<u32>),
}
struct StreamState {
core: PixelCore,
resume_bits: u64,
coded_rows_done: u32,
inverse: InverseChain,
reduced_width: usize,
}
struct InverseChain {
stages: Vec<Stage>,
height_done: u32,
ready: Vec<u32>,
}
enum Stage {
SubtractGreen,
CrossColor {
bits: u32,
width: u32,
data: Vec<u32>,
},
Predictor {
bits: u32,
width: u32,
data: Vec<u32>,
prev_out: Vec<u32>,
},
ColorIndexing {
bits: u32,
dst_width: u32,
palette: Vec<u32>,
},
}
impl InverseChain {
fn new(transforms: &[Transform]) -> Self {
let stages = transforms
.iter()
.rev()
.map(|transform| match transform {
Transform::SubtractGreen => Stage::SubtractGreen,
Transform::CrossColor { bits, width, data } => Stage::CrossColor {
bits: *bits,
width: *width,
data: data.clone(),
},
Transform::Predictor { bits, width, data } => Stage::Predictor {
bits: *bits,
width: *width,
data: data.clone(),
prev_out: Vec::new(),
},
Transform::ColorIndexing {
bits,
dst_width,
palette,
} => Stage::ColorIndexing {
bits: *bits,
dst_width: *dst_width,
palette: palette.clone(),
},
})
.collect();
Self {
stages,
height_done: 0,
ready: Vec::new(),
}
}
fn push_coded_row(&mut self, coded_row: &[u32]) {
let mut buf = coded_row.to_vec();
let y = self.height_done as usize;
for stage in &mut self.stages {
match stage {
Stage::SubtractGreen => subtract_green::inverse_row(&mut buf),
Stage::CrossColor { bits, width, data } => {
debug_assert_eq!(buf.len(), *width as usize, "cross-color coded-row width");
cross_color::inverse_row(&mut buf, y, *bits, data);
},
Stage::Predictor {
bits,
width,
data,
prev_out,
} => {
let w = *width as usize;
debug_assert_eq!(buf.len(), w, "predictor coded-row width");
let mut out = vec![0u32; w];
predictor::reconstruct_row_into(&mut out, &buf, prev_out, y, *bits, data);
prev_out.clone_from(&out);
buf = out;
},
Stage::ColorIndexing {
bits,
dst_width,
palette,
} => {
buf = palette::inverse_row(&buf, *dst_width, *bits, palette);
},
}
}
self.ready.extend_from_slice(&buf);
self.height_done += 1;
}
}
#[derive(Debug)]
pub(crate) enum Step {
NeedMore,
Header(u32, u32, bool),
Rows { first_row: u32, count: u32 },
Done,
}
impl Vp8lStream {
pub(crate) const fn new() -> Self {
Self {
state: Phase::Parsing,
}
}
pub(crate) fn advance(&mut self, payload: &[u8], final_input: bool) -> Result<Step> {
match core::mem::replace(&mut self.state, Phase::Parsing) {
Phase::Parsing => self.advance_parsing(payload, final_input),
Phase::Streaming(ss) => self.advance_streaming(ss, payload, final_input),
Phase::Done(pixels) => {
self.state = Phase::Done(pixels);
Ok(Step::Done)
},
}
}
#[cfg_attr(
not(any(test, feature = "oracle")),
expect(
dead_code,
reason = "into_pixels yields the whole decoded buffer at once, which \
only the test/oracle split driver (`stream_over_splits`) \
wants; the public IncrementalDecoder consumes rows \
incrementally through `ready` and never needs it"
)
)]
pub(crate) fn into_pixels(self) -> Option<Vec<u32>> {
match self.state {
Phase::Done(pixels) => Some(pixels),
Phase::Parsing | Phase::Streaming(_) => None,
}
}
pub(crate) fn ready(&self) -> &[u32] {
match &self.state {
Phase::Parsing => &[],
Phase::Streaming(ss) => &ss.inverse.ready,
Phase::Done(pixels) => pixels,
}
}
fn advance_parsing(&mut self, payload: &[u8], final_input: bool) -> Result<Step> {
let mut br = BitReader::new(payload);
match parse_top_level(&mut br) {
Ok((header, stream)) => {
if br.is_eos() {
return if final_input {
Err(Error::Truncated)
} else {
Ok(Step::NeedMore)
};
}
let resume_bits = br.bit_position();
let ParsedStream {
transforms,
working_width,
total,
cache_bits,
groups,
entropy,
} = stream;
let inverse = InverseChain::new(&transforms);
let core = PixelCore::new(working_width, total, cache_bits, groups, entropy);
self.state = Phase::Streaming(Box::new(StreamState {
core,
resume_bits,
coded_rows_done: 0,
inverse,
reduced_width: working_width as usize,
}));
Ok(Step::Header(header.0, header.1, header.2))
},
Err(err) => {
if br.is_eos() {
if final_input {
Err(err)
} else {
Ok(Step::NeedMore)
}
} else {
Err(err)
}
},
}
}
fn advance_streaming(
&mut self,
mut ss: Box<StreamState>,
payload: &[u8],
final_input: bool,
) -> Result<Step> {
let mut br = BitReader::new_at(payload, ss.resume_bits);
let rows_before = ss.inverse.height_done;
loop {
if ss.core.pos == ss.core.total {
let new_rows = ss.inverse.height_done - rows_before;
if new_rows > 0 {
self.state = Phase::Streaming(ss);
return Ok(Step::Rows {
first_row: rows_before,
count: new_rows,
});
}
let pixels = core::mem::take(&mut ss.inverse.ready);
self.state = Phase::Done(pixels);
return Ok(Step::Done);
}
match decode_one(&mut br, &mut ss.core) {
Ok(true) => {
ss.resume_bits = br.bit_position();
while (ss.coded_rows_done as usize) < ss.core.pos / ss.reduced_width {
let start = ss.coded_rows_done as usize * ss.reduced_width;
let end = start + ss.reduced_width;
ss.inverse.push_coded_row(&ss.core.argb[start..end]);
ss.coded_rows_done += 1;
}
},
Ok(false) => {
let new_rows = ss.inverse.height_done - rows_before;
self.state = Phase::Streaming(ss);
if final_input {
return Err(Error::Truncated);
}
return Ok(if new_rows > 0 {
Step::Rows {
first_row: rows_before,
count: new_rows,
}
} else {
Step::NeedMore
});
},
Err(err) => {
self.state = Phase::Streaming(ss);
return Err(err);
},
}
}
}
}
#[cfg(any(test, feature = "oracle"))]
pub(crate) fn stream_over_splits(
payload: &[u8],
splits: &[usize],
) -> Result<(u32, u32, bool, Vec<u32>)> {
let len = payload.len();
let mut stream = Vp8lStream::new();
let mut header: Option<(u32, u32, bool)> = None;
let mut rows_reported = 0u32;
let cuts = splits.iter().copied().chain(core::iter::once(len));
'outer: for cut in cuts {
let cut = cut.min(len);
let final_input = cut == len;
loop {
match stream.advance(&payload[..cut], final_input)? {
Step::Header(w, h, a) => header = Some((w, h, a)),
Step::Rows { first_row, count } => {
assert_eq!(first_row, rows_reported, "Rows payout is not contiguous");
rows_reported += count;
},
Step::NeedMore => break,
Step::Done => break 'outer,
}
}
}
let (w, h, a) = header.ok_or(Error::InvalidBitstream {
codec: crate::lossless::Codec::Lossless,
})?;
assert_eq!(
rows_reported, h,
"reported rows did not sum to the image height"
);
let pixels = stream.into_pixels().ok_or(Error::Truncated)?;
Ok((w, h, a, pixels))
}
#[cfg(any(test, feature = "oracle"))]
pub(crate) fn split_patterns(len: usize) -> Vec<Vec<usize>> {
let mut patterns = vec![
Vec::new(),
(1..len).collect(),
];
if len >= 2 {
patterns.push(vec![len / 2]);
}
if len >= 3 {
patterns.push(vec![len / 3, 2 * len / 3]);
}
patterns
}
#[cfg(test)]
mod tests {
use super::{InverseChain, Step, Vp8lStream, split_patterns, stream_over_splits};
use crate::Codec;
use crate::lossless::Error;
use crate::lossless::bit_io::reader::BitReader;
use crate::lossless::vp8l::decode::{
ParsedStream, PixelCore, decode, decode_one, parse_top_level,
};
use crate::lossless::vp8l::encode::encode as vp8l_encode;
use proptest::prelude::*;
type DecodeResult = Result<(u32, u32, Vec<u32>), Error>;
fn one_shot(payload: &[u8]) -> DecodeResult {
decode(payload).map(|d| (d.width, d.height, d.argb))
}
fn streamed(payload: &[u8], splits: &[usize]) -> DecodeResult {
stream_over_splits(payload, splits).map(|(w, h, _a, px)| (w, h, px))
}
fn assert_stream_equivalence(payload: &[u8]) {
let expected = one_shot(payload);
for splits in split_patterns(payload.len()) {
assert_eq!(
streamed(payload, &splits),
expected,
"stream != one-shot for split {splits:?} on a {}-byte payload",
payload.len()
);
}
}
fn encode_image(width: u32, height: u32, argb: &[u32]) -> Vec<u8> {
vp8l_encode(width, height, argb)
}
fn extract_vp8l(webp: &[u8]) -> Option<&[u8]> {
if webp.len() < 12 || &webp[0..4] != b"RIFF" || &webp[8..12] != b"WEBP" {
return None;
}
let mut off = 12usize;
while off + 8 <= webp.len() {
let size =
u32::from_le_bytes([webp[off + 4], webp[off + 5], webp[off + 6], webp[off + 7]])
as usize;
let start = off + 8;
let end = start.checked_add(size)?;
if end > webp.len() {
return None;
}
if &webp[off..off + 4] == b"VP8L" {
return Some(&webp[start..end]);
}
off = end + (size & 1);
}
None
}
fn arbitrary_argb_image() -> impl Strategy<Value = (u32, u32, Vec<u32>)> {
(1u32..=32, 1u32..=32).prop_flat_map(|(w, h)| {
let n = (w * h) as usize;
(Just(w), Just(h), prop::collection::vec(any::<u32>(), n..=n))
})
}
proptest! {
#[test]
fn stream_equals_one_shot_over_arbitrary_splits(
(w, h, argb) in arbitrary_argb_image(),
raw_splits in prop::collection::vec(0usize..2048, 0..16),
) {
let payload = encode_image(w, h, &argb);
let expected = one_shot(&payload);
for splits in split_patterns(payload.len()) {
prop_assert_eq!(
streamed(&payload, &splits),
expected.clone(),
"canonical split {:?}",
splits
);
}
let mut splits: Vec<usize> =
raw_splits.iter().map(|&s| s % (payload.len() + 1)).collect();
splits.sort_unstable();
prop_assert_eq!(streamed(&payload, &splits), expected, "random split {:?}", splits);
}
#[test]
fn stream_equals_one_shot_on_arbitrary_bytes(
bytes in prop::collection::vec(any::<u8>(), 0..256),
) {
assert_stream_equivalence(&bytes);
}
#[test]
fn stream_equals_one_shot_on_signed_arbitrary_bytes(
tail in prop::collection::vec(any::<u8>(), 0..64),
) {
let mut bytes = vec![0x2fu8];
bytes.extend_from_slice(&tail);
assert_stream_equivalence(&bytes);
}
}
#[test]
fn stream_equals_one_shot_on_conformance_fixtures() {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../webpkit-lossless-conformance/fixtures/decode");
let mut checked = 0usize;
for entry in std::fs::read_dir(&dir).expect("read conformance fixtures dir") {
let input = entry.expect("dir entry").path().join("input.webp");
if !input.exists() {
continue;
}
let webp = std::fs::read(&input).expect("read fixture input.webp");
let Some(payload) = extract_vp8l(&webp) else {
continue; };
assert!(
one_shot(payload).is_ok(),
"{input:?} did not decode one-shot"
);
assert_stream_equivalence(payload);
checked += 1;
}
assert!(
checked >= 10,
"expected the streaming equivalence to cover many transform families, \
only reached {checked} fixtures"
);
}
fn conformance_vp8l_payloads() -> Vec<Vec<u8>> {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../webpkit-lossless-conformance/fixtures/decode");
let mut payloads = Vec::new();
for entry in std::fs::read_dir(&dir).expect("read conformance fixtures dir") {
let input = entry.expect("dir entry").path().join("input.webp");
if !input.exists() {
continue;
}
let webp = std::fs::read(&input).expect("read fixture input.webp");
if let Some(payload) = extract_vp8l(&webp) {
payloads.push(payload.to_vec());
}
}
payloads
}
#[test]
fn streamed_rows_equal_batch() {
let payloads = conformance_vp8l_payloads();
assert!(
payloads.len() >= 10,
"expected many transform-family fixtures, only found {}",
payloads.len()
);
for payload in &payloads {
let batch = decode(payload).expect("fixture decodes one-shot").argb;
let (_w, _h, _a, streamed) =
stream_over_splits(payload, &[]).expect("fixture streams to completion");
assert_eq!(streamed, batch, "incremental streamed rows != batch decode");
}
}
#[test]
fn inverse_chain_matches_batch_and_leaves_coded_unmodified() {
for payload in &conformance_vp8l_payloads() {
let mut br = BitReader::new(payload);
let (_header, stream) = parse_top_level(&mut br).expect("parse top-level");
let ParsedStream {
transforms,
working_width,
total,
cache_bits,
groups,
entropy,
} = stream;
let mut core = PixelCore::new(working_width, total, cache_bits, groups, entropy);
while core.pos < core.total {
assert!(
decode_one(&mut br, &mut core).expect("decode_one"),
"unexpected suspend on a whole fixture"
);
}
let reduced_width = working_width as usize;
let snapshot = core.argb.clone();
let mut chain = InverseChain::new(&transforms);
let mut y = 0usize;
while y * reduced_width < core.argb.len() {
chain.push_coded_row(&core.argb[y * reduced_width..(y + 1) * reduced_width]);
y += 1;
}
assert_eq!(core.argb, snapshot, "InverseChain mutated the coded buffer");
let batch = decode(payload).expect("fixture decodes one-shot").argb;
assert_eq!(chain.ready, batch, "InverseChain != batch inverse");
}
}
#[test]
fn short_prefix_needs_more() {
let payload = encode_image(4, 4, &[0xff00_0000u32; 16]);
let mut stream = Vp8lStream::new();
assert!(matches!(
stream.advance(&payload[..4], false),
Ok(Step::NeedMore)
));
}
#[test]
fn definite_error_on_non_final_prefix_surfaces_now() {
let bad = [0x00u8, 0x11, 0x22, 0x33, 0x44, 0x55];
let mut stream = Vp8lStream::new();
assert_eq!(
stream.advance(&bad, false).unwrap_err(),
Error::InvalidBitstream {
codec: Codec::Lossless
}
);
}
#[test]
fn final_truncated_buffer_is_truncated() {
let argb: Vec<u32> = (0..64u32).map(|i| 0xff00_0000 | (i * 4)).collect();
let full = encode_image(8, 8, &argb);
let cut = (1..full.len())
.find(|&k| one_shot(&full[..k]) == Err(Error::Truncated))
.expect("some prefix of a real image truncates mid-stream");
assert_eq!(streamed(&full[..cut], &[]), Err(Error::Truncated));
}
#[test]
fn split_patterns_are_the_canonical_set() {
assert_eq!(
split_patterns(6),
vec![
vec![], vec![1, 2, 3, 4, 5], vec![3], vec![2, 4], ],
);
assert_eq!(
split_patterns(1),
vec![Vec::<usize>::new(), Vec::<usize>::new()],
);
assert_eq!(split_patterns(2), vec![vec![], vec![1], vec![1]]);
}
#[test]
fn completion_reports_done_not_empty_rows() {
let payload = encode_image(4, 4, &[0xff00_0000u32; 16]);
let mut stream = Vp8lStream::new();
let mut rows_total = 0u32;
let mut saw_done = false;
for _ in 0..64 {
match stream
.advance(&payload, true)
.expect("full payload advances")
{
Step::Header(..) => {},
Step::Rows { count, .. } => {
assert!(count > 0, "a Rows step must report at least one row");
rows_total += count;
},
Step::NeedMore => panic!("a final whole-payload feed must not NeedMore"),
Step::Done => {
saw_done = true;
break;
},
}
}
assert!(saw_done, "streaming never reported Done");
assert_eq!(rows_total, 4, "every output row reported exactly once");
}
#[test]
fn streaming_suspend_with_no_new_rows_reports_need_more() {
let argb: Vec<u32> = (0..24u32).map(|i| 0xff00_0000 | (i * 9)).collect();
let payload = encode_image(24, 1, &argb);
let need_more = (1..payload.len()).find(|&len| {
let mut stream = Vp8lStream::new();
if !matches!(stream.advance(&payload[..len], false), Ok(Step::Header(..))) {
return false;
}
matches!(stream.advance(&payload[..len], false), Ok(Step::NeedMore))
});
assert!(
need_more.is_some(),
"expected a prefix that parses the header then suspends mid-row as NeedMore"
);
}
}