use crate::container::{fourcc, WebpChunk, WebpContainer};
pub const VP8L_SIGNATURE: u8 = 0x2F;
pub const VP8L_IMAGE_HEADER_LEN: usize = 5;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WebpLosslessError {
NotVp8lChunk {
got: [u8; 4],
},
PayloadTooShortForHeader {
got: usize,
},
BadSignature {
got: u8,
},
}
impl core::fmt::Display for WebpLosslessError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NotVp8lChunk { got } => write!(
f,
"§2.6 lossless-chunk handle wraps a non-'VP8L' FourCC (got {:02x?})",
got
),
Self::PayloadTooShortForHeader { got } => write!(
f,
"§3.4 / §7.1 VP8L image-header needs {} bytes; chunk payload has {got}",
VP8L_IMAGE_HEADER_LEN
),
Self::BadSignature { got } => write!(
f,
"§3.4 / §7.1 VP8L signature mismatch — expected 0x{:02x}, got 0x{:02x}",
VP8L_SIGNATURE, got
),
}
}
}
impl std::error::Error for WebpLosslessError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WebpLosslessChunk<'a> {
payload: &'a [u8],
width: u32,
height: u32,
alpha_is_used: bool,
version: u8,
}
impl<'a> WebpLosslessChunk<'a> {
pub fn from_chunk(buf: &'a [u8], chunk: &WebpChunk) -> Result<Self, WebpLosslessError> {
if chunk.fourcc != fourcc::VP8L {
return Err(WebpLosslessError::NotVp8lChunk { got: chunk.fourcc });
}
Self::from_payload(chunk.payload(buf))
}
pub fn from_payload(payload: &'a [u8]) -> Result<Self, WebpLosslessError> {
if payload.len() < VP8L_IMAGE_HEADER_LEN {
return Err(WebpLosslessError::PayloadTooShortForHeader { got: payload.len() });
}
if payload[0] != VP8L_SIGNATURE {
return Err(WebpLosslessError::BadSignature { got: payload[0] });
}
let packed = (payload[1] as u32)
| ((payload[2] as u32) << 8)
| ((payload[3] as u32) << 16)
| ((payload[4] as u32) << 24);
let width_minus_one = packed & 0x3FFF; let height_minus_one = (packed >> 14) & 0x3FFF; let alpha_is_used = ((packed >> 28) & 0x1) == 1;
let version = ((packed >> 29) & 0x7) as u8;
Ok(Self {
payload,
width: width_minus_one + 1,
height: height_minus_one + 1,
alpha_is_used,
version,
})
}
pub fn bitstream(&self) -> &'a [u8] {
self.payload
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn alpha_is_used(&self) -> bool {
self.alpha_is_used
}
pub fn version(&self) -> u8 {
self.version
}
}
pub fn extract_lossless<'a>(
buf: &'a [u8],
container: &WebpContainer,
) -> Result<Option<WebpLosslessChunk<'a>>, WebpLosslessError> {
match container.first_chunk_with_fourcc(fourcc::VP8L) {
Some(chunk) => WebpLosslessChunk::from_chunk(buf, chunk).map(Some),
None => Ok(None),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::container::parse;
fn vp8l_header(
width: u32,
height: u32,
alpha_is_used: bool,
version: u8,
extra: &[u8],
) -> Vec<u8> {
assert!((1..=0x4000).contains(&width));
assert!((1..=0x4000).contains(&height));
assert!(version <= 0x7);
let width_minus_one = width - 1;
let height_minus_one = height - 1;
let mut packed: u32 = 0;
packed |= width_minus_one & 0x3FFF;
packed |= (height_minus_one & 0x3FFF) << 14;
packed |= (if alpha_is_used { 1 } else { 0 }) << 28;
packed |= ((version as u32) & 0x7) << 29;
let mut out = Vec::with_capacity(VP8L_IMAGE_HEADER_LEN + extra.len());
out.push(VP8L_SIGNATURE);
out.push((packed & 0xFF) as u8);
out.push(((packed >> 8) & 0xFF) as u8);
out.push(((packed >> 16) & 0xFF) as u8);
out.push(((packed >> 24) & 0xFF) as u8);
out.extend_from_slice(extra);
out
}
#[test]
fn from_payload_decodes_minimal_1x1_header() {
let payload = vp8l_header(1, 1, false, 0, &[]);
let h = WebpLosslessChunk::from_payload(&payload).expect("1x1 VP8L parses");
assert_eq!(h.width(), 1);
assert_eq!(h.height(), 1);
assert!(!h.alpha_is_used());
assert_eq!(h.version(), 0);
assert_eq!(h.bitstream(), payload.as_slice());
}
#[test]
fn from_payload_decodes_max_dims_with_alpha_and_version_zero() {
let payload = vp8l_header(0x4000, 0x4000, true, 0, &[]);
let h = WebpLosslessChunk::from_payload(&payload).unwrap();
assert_eq!(h.width(), 0x4000);
assert_eq!(h.height(), 0x4000);
assert!(h.alpha_is_used());
assert_eq!(h.version(), 0);
}
#[test]
fn from_payload_surfaces_nonstandard_version_without_refusing() {
let payload = vp8l_header(2, 3, false, 5, &[]);
let h = WebpLosslessChunk::from_payload(&payload).unwrap();
assert_eq!(h.width(), 2);
assert_eq!(h.height(), 3);
assert_eq!(h.version(), 5);
}
#[test]
fn from_payload_refuses_short_payload() {
let payload = [VP8L_SIGNATURE, 0, 0, 0];
match WebpLosslessChunk::from_payload(&payload) {
Err(WebpLosslessError::PayloadTooShortForHeader { got }) => assert_eq!(got, 4),
other => panic!("expected PayloadTooShortForHeader, got {other:?}"),
}
}
#[test]
fn from_payload_refuses_bad_signature() {
let mut payload = vp8l_header(1, 1, false, 0, &[]);
payload[0] = 0x55;
match WebpLosslessChunk::from_payload(&payload) {
Err(WebpLosslessError::BadSignature { got }) => assert_eq!(got, 0x55),
other => panic!("expected BadSignature, got {other:?}"),
}
}
#[test]
fn from_payload_borrows_trailing_image_stream_verbatim() {
let extra = [0xAA, 0xBB, 0xCC];
let payload = vp8l_header(7, 5, true, 0, &extra);
let h = WebpLosslessChunk::from_payload(&payload).unwrap();
assert_eq!(h.width(), 7);
assert_eq!(h.height(), 5);
assert!(h.alpha_is_used());
assert_eq!(h.bitstream(), payload.as_slice());
assert_eq!(&h.bitstream()[VP8L_IMAGE_HEADER_LEN..], &extra);
}
#[test]
fn from_chunk_refuses_non_vp8l_fourcc() {
let payload = vp8l_header(1, 1, false, 0, &[]);
let mut body = Vec::new();
body.extend_from_slice(b"VP8 ");
body.extend_from_slice(&(payload.len() as u32).to_le_bytes());
body.extend_from_slice(&payload);
if payload.len() % 2 == 1 {
body.push(0);
}
let file_size = (body.len() as u32) + 4;
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&file_size.to_le_bytes());
buf.extend_from_slice(b"WEBP");
buf.extend_from_slice(&body);
let c = parse(&buf).expect("valid RIFF parses");
let chunk = &c.chunks[0];
match WebpLosslessChunk::from_chunk(&buf, chunk) {
Err(WebpLosslessError::NotVp8lChunk { got }) => assert_eq!(&got, b"VP8 "),
other => panic!("expected NotVp8lChunk, got {other:?}"),
}
}
#[test]
fn from_chunk_round_trips_payload_bytes_through_walker() {
let mut payload = vp8l_header(12, 8, true, 0, &[]);
payload.extend_from_slice(&[0x11, 0x22, 0x33, 0x44]);
let mut body = Vec::new();
body.extend_from_slice(b"VP8L");
body.extend_from_slice(&(payload.len() as u32).to_le_bytes());
body.extend_from_slice(&payload);
if payload.len() % 2 == 1 {
body.push(0);
}
let file_size = (body.len() as u32) + 4;
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&file_size.to_le_bytes());
buf.extend_from_slice(b"WEBP");
buf.extend_from_slice(&body);
let c = parse(&buf).expect("synthetic lossless file walks");
let handle = WebpLosslessChunk::from_chunk(&buf, &c.chunks[0]).expect("VP8L handle peeks");
assert_eq!(handle.width(), 12);
assert_eq!(handle.height(), 8);
assert!(handle.alpha_is_used());
assert_eq!(handle.version(), 0);
assert_eq!(handle.bitstream(), payload.as_slice());
assert_eq!(
&handle.bitstream()[VP8L_IMAGE_HEADER_LEN..],
&[0x11, 0x22, 0x33, 0x44]
);
}
#[test]
fn extract_lossless_returns_none_for_lossy_container() {
let mut body = Vec::new();
body.extend_from_slice(b"VP8 ");
body.extend_from_slice(&10u32.to_le_bytes());
body.extend_from_slice(&[0u8; 10]);
let file_size = (body.len() as u32) + 4;
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&file_size.to_le_bytes());
buf.extend_from_slice(b"WEBP");
buf.extend_from_slice(&body);
let c = parse(&buf).unwrap();
assert!(extract_lossless(&buf, &c).unwrap().is_none());
}
#[test]
fn extract_lossless_returns_some_for_simple_lossless_container() {
let payload = vp8l_header(1, 1, false, 0, &[]);
let mut body = Vec::new();
body.extend_from_slice(b"VP8L");
body.extend_from_slice(&(payload.len() as u32).to_le_bytes());
body.extend_from_slice(&payload);
if payload.len() % 2 == 1 {
body.push(0);
}
let file_size = (body.len() as u32) + 4;
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&file_size.to_le_bytes());
buf.extend_from_slice(b"WEBP");
buf.extend_from_slice(&body);
let c = parse(&buf).unwrap();
let handle = extract_lossless(&buf, &c)
.unwrap()
.expect("VP8L chunk present");
assert_eq!(handle.width(), 1);
assert_eq!(handle.height(), 1);
}
}