use crate::container::{fourcc, WebpChunk, WebpContainer};
pub const VP8_START_CODE_0: u8 = 0x9D;
pub const VP8_START_CODE_1: u8 = 0x01;
pub const VP8_START_CODE_2: u8 = 0x2A;
pub const VP8_KEYFRAME_HEADER_LEN: usize = 10;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WebpLossyError {
NotVp8Chunk {
got: [u8; 4],
},
PayloadTooShortForKeyframe {
got: usize,
},
NotAKeyframe,
BadStartCode {
got: [u8; 3],
},
}
impl core::fmt::Display for WebpLossyError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NotVp8Chunk { got } => write!(
f,
"§2.5 lossy-chunk handle wraps a non-'VP8 ' FourCC (got {:02x?})",
got
),
Self::PayloadTooShortForKeyframe { got } => write!(
f,
"RFC 6386 §9.1 keyframe header needs {} bytes; chunk payload has {got}",
VP8_KEYFRAME_HEADER_LEN
),
Self::NotAKeyframe => write!(
f,
"RFC 9649 §2.5 / RFC 6386 §9.1: frame_type=1 (interframe) \
is not permitted inside a 'VP8 ' WebP chunk"
),
Self::BadStartCode { got } => write!(
f,
"RFC 6386 §9.1 sync bytes mismatch — expected {:02x?}, got {:02x?}",
[VP8_START_CODE_0, VP8_START_CODE_1, VP8_START_CODE_2],
got
),
}
}
}
impl std::error::Error for WebpLossyError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WebpLossyChunk<'a> {
payload: &'a [u8],
version: u8,
show_frame: bool,
first_partition_size: u32,
width: u16,
height: u16,
horizontal_scale: u8,
vertical_scale: u8,
}
impl<'a> WebpLossyChunk<'a> {
pub fn from_chunk(buf: &'a [u8], chunk: &WebpChunk) -> Result<Self, WebpLossyError> {
if chunk.fourcc != fourcc::VP8 {
return Err(WebpLossyError::NotVp8Chunk { got: chunk.fourcc });
}
Self::from_payload(chunk.payload(buf))
}
pub fn from_payload(payload: &'a [u8]) -> Result<Self, WebpLossyError> {
if payload.len() < VP8_KEYFRAME_HEADER_LEN {
return Err(WebpLossyError::PayloadTooShortForKeyframe { got: payload.len() });
}
let tag = (payload[0] as u32) | ((payload[1] as u32) << 8) | ((payload[2] as u32) << 16);
let frame_type = (tag & 0x1) as u8;
if frame_type != 0 {
return Err(WebpLossyError::NotAKeyframe);
}
let version = ((tag >> 1) & 0x7) as u8;
let show_frame = ((tag >> 4) & 0x1) == 1;
let first_partition_size = (tag >> 5) & 0x7_FFFF;
let sc = [payload[3], payload[4], payload[5]];
if sc != [VP8_START_CODE_0, VP8_START_CODE_1, VP8_START_CODE_2] {
return Err(WebpLossyError::BadStartCode { got: sc });
}
let w_raw = (payload[6] as u16) | ((payload[7] as u16) << 8);
let h_raw = (payload[8] as u16) | ((payload[9] as u16) << 8);
let width = w_raw & 0x3FFF;
let height = h_raw & 0x3FFF;
let horizontal_scale = ((w_raw >> 14) & 0x3) as u8;
let vertical_scale = ((h_raw >> 14) & 0x3) as u8;
Ok(Self {
payload,
version,
show_frame,
first_partition_size,
width,
height,
horizontal_scale,
vertical_scale,
})
}
pub fn bitstream(&self) -> &'a [u8] {
self.payload
}
pub fn version(&self) -> u8 {
self.version
}
pub fn show_frame(&self) -> bool {
self.show_frame
}
pub fn first_partition_size(&self) -> u32 {
self.first_partition_size
}
pub fn width(&self) -> u16 {
self.width
}
pub fn height(&self) -> u16 {
self.height
}
pub fn horizontal_scale(&self) -> u8 {
self.horizontal_scale
}
pub fn vertical_scale(&self) -> u8 {
self.vertical_scale
}
}
pub fn extract_lossy<'a>(
buf: &'a [u8],
container: &WebpContainer,
) -> Result<Option<WebpLossyChunk<'a>>, WebpLossyError> {
match container.first_chunk_with_fourcc(fourcc::VP8) {
Some(chunk) => WebpLossyChunk::from_chunk(buf, chunk).map(Some),
None => Ok(None),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::container::parse;
#[allow(clippy::too_many_arguments)]
fn keyframe(
version: u8,
show_frame: bool,
first_partition_size: u32,
h_scale: u8,
width: u16,
v_scale: u8,
height: u16,
extra: &[u8],
) -> Vec<u8> {
let frame_type: u32 = 0; let mut tag: u32 = 0;
tag |= frame_type & 0x1;
tag |= ((version as u32) & 0x7) << 1;
tag |= (if show_frame { 1 } else { 0 } & 0x1) << 4;
tag |= (first_partition_size & 0x7_FFFF) << 5;
let w_word = ((h_scale as u16 & 0x3) << 14) | (width & 0x3FFF);
let h_word = ((v_scale as u16 & 0x3) << 14) | (height & 0x3FFF);
let mut out = Vec::with_capacity(VP8_KEYFRAME_HEADER_LEN + extra.len());
out.push((tag & 0xFF) as u8);
out.push(((tag >> 8) & 0xFF) as u8);
out.push(((tag >> 16) & 0xFF) as u8);
out.push(VP8_START_CODE_0);
out.push(VP8_START_CODE_1);
out.push(VP8_START_CODE_2);
out.push((w_word & 0xFF) as u8);
out.push(((w_word >> 8) & 0xFF) as u8);
out.push((h_word & 0xFF) as u8);
out.push(((h_word >> 8) & 0xFF) as u8);
out.extend_from_slice(extra);
out
}
#[test]
fn from_payload_decodes_minimal_1x1_keyframe_header() {
let payload = keyframe(0, true, 11, 0, 1, 0, 1, &[]);
let h = WebpLossyChunk::from_payload(&payload).expect("1x1 keyframe parses");
assert_eq!(h.width(), 1);
assert_eq!(h.height(), 1);
assert_eq!(h.version(), 0);
assert!(h.show_frame());
assert_eq!(h.first_partition_size(), 11);
assert_eq!(h.horizontal_scale(), 0);
assert_eq!(h.vertical_scale(), 0);
assert_eq!(h.bitstream(), payload.as_slice());
}
#[test]
fn from_payload_extracts_14bit_width_and_height_at_extremes() {
let payload = keyframe(2, true, 0x7_FFFE, 3, 0x3FFF, 1, 0x2222, &[]);
let h = WebpLossyChunk::from_payload(&payload).unwrap();
assert_eq!(h.width(), 0x3FFF);
assert_eq!(h.height(), 0x2222);
assert_eq!(h.horizontal_scale(), 3);
assert_eq!(h.vertical_scale(), 1);
assert_eq!(h.version(), 2);
assert_eq!(h.first_partition_size(), 0x7_FFFE);
}
#[test]
fn from_payload_refuses_short_payload() {
let payload = vec![0u8; 9];
match WebpLossyChunk::from_payload(&payload) {
Err(WebpLossyError::PayloadTooShortForKeyframe { got }) => assert_eq!(got, 9),
other => panic!("expected PayloadTooShortForKeyframe, got {other:?}"),
}
}
#[test]
fn from_payload_refuses_interframes_per_section_2_5() {
let mut payload = keyframe(0, true, 0, 0, 1, 0, 1, &[]);
payload[0] |= 0x01; assert_eq!(
WebpLossyChunk::from_payload(&payload).unwrap_err(),
WebpLossyError::NotAKeyframe,
);
}
#[test]
fn from_payload_refuses_bad_start_code() {
let mut payload = keyframe(0, true, 0, 0, 1, 0, 1, &[]);
payload[3] = 0xDE;
payload[4] = 0xAD;
payload[5] = 0xBE;
match WebpLossyChunk::from_payload(&payload) {
Err(WebpLossyError::BadStartCode { got }) => assert_eq!(got, [0xDE, 0xAD, 0xBE]),
other => panic!("expected BadStartCode, got {other:?}"),
}
}
#[test]
fn from_chunk_refuses_non_vp8_fourcc() {
let payload = keyframe(0, true, 0, 0, 1, 0, 1, &[]);
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);
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 WebpLossyChunk::from_chunk(&buf, chunk) {
Err(WebpLossyError::NotVp8Chunk { got }) => assert_eq!(&got, b"VP8L"),
other => panic!("expected NotVp8Chunk, got {other:?}"),
}
}
#[test]
fn from_chunk_round_trips_payload_bytes_through_walker() {
let header = keyframe(1, true, 7, 0, 12, 0, 8, &[]);
let mut payload = header.clone();
payload.extend_from_slice(&[0x11, 0x22, 0x33, 0x44]);
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("synthetic lossy file walks");
let handle = WebpLossyChunk::from_chunk(&buf, &c.chunks[0]).expect("keyframe handle peeks");
assert_eq!(handle.width(), 12);
assert_eq!(handle.height(), 8);
assert_eq!(handle.version(), 1);
assert_eq!(handle.first_partition_size(), 7);
assert_eq!(handle.bitstream(), payload.as_slice());
assert_eq!(
&handle.bitstream()[VP8_KEYFRAME_HEADER_LEN..],
&[0x11, 0x22, 0x33, 0x44]
);
}
#[test]
fn extract_lossy_returns_none_for_lossless_container() {
let mut body = Vec::new();
body.extend_from_slice(b"VP8L");
body.extend_from_slice(&4u32.to_le_bytes());
body.extend_from_slice(&[0x2F, 0x00, 0x00, 0x00]);
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_lossy(&buf, &c).unwrap().is_none());
}
#[test]
fn extract_lossy_returns_some_for_simple_lossy_container() {
let payload = keyframe(0, true, 13, 0, 1, 0, 1, &[]);
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);
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_lossy(&buf, &c).unwrap().expect("VP8 chunk present");
assert_eq!(handle.width(), 1);
assert_eq!(handle.height(), 1);
}
}