use crate::container::{fourcc, FourCc};
pub const MAX_VP8X_CANVAS_DIM: u32 = 0x0100_0000;
pub const MAX_CHUNK_PAYLOAD: u32 = u32::MAX - 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageKind {
Lossy,
Lossless,
ExtendedLossy,
ExtendedLossless,
}
impl ImageKind {
pub fn bitstream_fourcc(self) -> FourCc {
match self {
Self::Lossy | Self::ExtendedLossy => fourcc::VP8,
Self::Lossless | Self::ExtendedLossless => fourcc::VP8L,
}
}
pub fn is_extended(self) -> bool {
matches!(self, Self::ExtendedLossy | Self::ExtendedLossless)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BuildError {
CanvasDimZero {
which: &'static str,
},
CanvasDimTooLarge {
which: &'static str,
got: u32,
},
CanvasTooLarge {
canvas_width: u32,
canvas_height: u32,
},
PayloadTooLargeForChunk {
got: usize,
},
}
impl core::fmt::Display for BuildError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::CanvasDimZero { which } => {
write!(f, "§2.7.1 VP8X canvas {which} must be ≥ 1 (got 0)")
}
Self::CanvasDimTooLarge { which, got } => write!(
f,
"§2.7.1 VP8X canvas {which} {got} exceeds the 24-bit Minus-One field's 2^24 cap",
),
Self::CanvasTooLarge {
canvas_width,
canvas_height,
} => write!(
f,
"§2.7.1 VP8X canvas {canvas_width}x{canvas_height} \
exceeds the 2^32 - 1 product cap",
),
Self::PayloadTooLargeForChunk { got } => write!(
f,
"§2.3 chunk payload of {got} bytes exceeds the uint32 Size field's range",
),
}
}
}
impl std::error::Error for BuildError {}
pub fn build_chunk(fourcc: FourCc, payload: &[u8]) -> Result<Vec<u8>, BuildError> {
if payload.len() as u64 > MAX_CHUNK_PAYLOAD as u64 {
return Err(BuildError::PayloadTooLargeForChunk { got: payload.len() });
}
let size = payload.len() as u32;
let needs_pad = (size & 1) == 1;
let total = 8 + payload.len() + if needs_pad { 1 } else { 0 };
let mut out = Vec::with_capacity(total);
out.extend_from_slice(&fourcc);
out.extend_from_slice(&size.to_le_bytes());
out.extend_from_slice(payload);
if needs_pad {
out.push(0);
}
Ok(out)
}
pub fn build_vp8x_chunk(
canvas_width: u32,
canvas_height: u32,
flags: Vp8xFlags,
) -> Result<Vec<u8>, BuildError> {
if canvas_width == 0 {
return Err(BuildError::CanvasDimZero { which: "width" });
}
if canvas_height == 0 {
return Err(BuildError::CanvasDimZero { which: "height" });
}
if canvas_width > MAX_VP8X_CANVAS_DIM {
return Err(BuildError::CanvasDimTooLarge {
which: "width",
got: canvas_width,
});
}
if canvas_height > MAX_VP8X_CANVAS_DIM {
return Err(BuildError::CanvasDimTooLarge {
which: "height",
got: canvas_height,
});
}
if (canvas_width as u64) * (canvas_height as u64) > u64::from(u32::MAX) {
return Err(BuildError::CanvasTooLarge {
canvas_width,
canvas_height,
});
}
let cwm1 = canvas_width - 1;
let chm1 = canvas_height - 1;
let mut flag_byte: u8 = 0;
if flags.has_iccp {
flag_byte |= 1 << 5;
}
if flags.has_alpha {
flag_byte |= 1 << 4;
}
if flags.has_exif {
flag_byte |= 1 << 3;
}
if flags.has_xmp {
flag_byte |= 1 << 2;
}
if flags.has_animation {
flag_byte |= 1 << 1;
}
let mut payload = Vec::with_capacity(10);
payload.push(flag_byte);
payload.extend_from_slice(&[0u8, 0u8, 0u8]);
payload.push((cwm1 & 0xFF) as u8);
payload.push(((cwm1 >> 8) & 0xFF) as u8);
payload.push(((cwm1 >> 16) & 0xFF) as u8);
payload.push((chm1 & 0xFF) as u8);
payload.push(((chm1 >> 8) & 0xFF) as u8);
payload.push(((chm1 >> 16) & 0xFF) as u8);
Ok(payload)
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Vp8xFlags {
pub has_iccp: bool,
pub has_alpha: bool,
pub has_exif: bool,
pub has_xmp: bool,
pub has_animation: bool,
}
pub fn build_webp_file(
payload: &[u8],
image_kind: ImageKind,
canvas_width: u32,
canvas_height: u32,
) -> Result<Vec<u8>, BuildError> {
if payload.len() as u64 > MAX_CHUNK_PAYLOAD as u64 {
return Err(BuildError::PayloadTooLargeForChunk { got: payload.len() });
}
let bitstream_chunk = build_chunk(image_kind.bitstream_fourcc(), payload)?;
let body = if image_kind.is_extended() {
let vp8x_payload = build_vp8x_chunk(canvas_width, canvas_height, Vp8xFlags::default())?;
let vp8x_chunk = build_chunk(fourcc::VP8X, &vp8x_payload)?;
let mut b = Vec::with_capacity(vp8x_chunk.len() + bitstream_chunk.len());
b.extend_from_slice(&vp8x_chunk);
b.extend_from_slice(&bitstream_chunk);
b
} else {
bitstream_chunk
};
let file_size = (body.len() as u64) + 4;
if file_size > u64::from(u32::MAX) {
return Err(BuildError::PayloadTooLargeForChunk { got: payload.len() });
}
let file_size = file_size as u32;
let mut out = Vec::with_capacity(12 + body.len());
out.extend_from_slice(&fourcc::RIFF);
out.extend_from_slice(&file_size.to_le_bytes());
out.extend_from_slice(&fourcc::WEBP);
out.extend_from_slice(&body);
Ok(out)
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct FileMetadata<'a> {
pub iccp: Option<&'a [u8]>,
pub exif: Option<&'a [u8]>,
pub xmp: Option<&'a [u8]>,
}
impl FileMetadata<'_> {
pub fn is_empty(&self) -> bool {
self.iccp.is_none() && self.exif.is_none() && self.xmp.is_none()
}
}
pub fn build_webp_file_with_metadata(
payload: &[u8],
image_kind: ImageKind,
canvas_width: u32,
canvas_height: u32,
has_alpha: bool,
metadata: FileMetadata<'_>,
) -> Result<Vec<u8>, BuildError> {
if payload.len() as u64 > MAX_CHUNK_PAYLOAD as u64 {
return Err(BuildError::PayloadTooLargeForChunk { got: payload.len() });
}
let flags = Vp8xFlags {
has_iccp: metadata.iccp.is_some(),
has_alpha,
has_exif: metadata.exif.is_some(),
has_xmp: metadata.xmp.is_some(),
has_animation: false,
};
let vp8x_payload = build_vp8x_chunk(canvas_width, canvas_height, flags)?;
let vp8x_chunk = build_chunk(fourcc::VP8X, &vp8x_payload)?;
let bitstream_chunk = build_chunk(image_kind.bitstream_fourcc(), payload)?;
let mut body = Vec::with_capacity(
vp8x_chunk.len()
+ metadata.iccp.map_or(0, |b| 8 + b.len() + (b.len() & 1))
+ bitstream_chunk.len()
+ metadata.exif.map_or(0, |b| 8 + b.len() + (b.len() & 1))
+ metadata.xmp.map_or(0, |b| 8 + b.len() + (b.len() & 1)),
);
body.extend_from_slice(&vp8x_chunk);
if let Some(iccp) = metadata.iccp {
let c = build_chunk(fourcc::ICCP, iccp)?;
body.extend_from_slice(&c);
}
body.extend_from_slice(&bitstream_chunk);
if let Some(exif) = metadata.exif {
let c = build_chunk(fourcc::EXIF, exif)?;
body.extend_from_slice(&c);
}
if let Some(xmp) = metadata.xmp {
let c = build_chunk(fourcc::XMP, xmp)?;
body.extend_from_slice(&c);
}
let file_size = (body.len() as u64) + 4;
if file_size > u64::from(u32::MAX) {
return Err(BuildError::PayloadTooLargeForChunk { got: payload.len() });
}
let file_size = file_size as u32;
let mut out = Vec::with_capacity(12 + body.len());
out.extend_from_slice(&fourcc::RIFF);
out.extend_from_slice(&file_size.to_le_bytes());
out.extend_from_slice(&fourcc::WEBP);
out.extend_from_slice(&body);
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::container::{parse, ContainerError};
use crate::vp8x::Vp8xHeader;
#[test]
fn build_chunk_even_payload_has_no_pad_byte() {
let bytes = build_chunk(fourcc::VP8, &[0u8; 8]).unwrap();
assert_eq!(bytes.len(), 8 + 8);
assert_eq!(&bytes[0..4], b"VP8 ");
assert_eq!(&bytes[4..8], &8u32.to_le_bytes());
}
#[test]
fn build_chunk_odd_payload_appends_one_zero_pad_byte() {
let bytes = build_chunk(fourcc::ICCP, &[0xAA, 0xBB, 0xCC]).unwrap();
assert_eq!(bytes.len(), 8 + 3 + 1);
assert_eq!(&bytes[4..8], &3u32.to_le_bytes());
assert_eq!(bytes[bytes.len() - 1], 0u8);
assert_eq!(&bytes[8..8 + 3], &[0xAA, 0xBB, 0xCC]);
}
#[test]
fn build_vp8x_payload_layout_matches_figure_7_byte_for_byte() {
let payload = build_vp8x_chunk(
128,
64,
Vp8xFlags {
has_alpha: true,
..Default::default()
},
)
.unwrap();
assert_eq!(payload.len(), 10);
assert_eq!(payload[0], 0b0001_0000);
assert_eq!(&payload[1..4], &[0u8, 0u8, 0u8]);
assert_eq!(&payload[4..7], &[0x7F, 0x00, 0x00]);
assert_eq!(&payload[7..10], &[0x3F, 0x00, 0x00]);
}
#[test]
fn build_vp8x_flag_bits_match_parser_table() {
let cases: &[(Vp8xFlags, u8)] = &[
(
Vp8xFlags {
has_iccp: true,
..Default::default()
},
0b0010_0000,
),
(
Vp8xFlags {
has_alpha: true,
..Default::default()
},
0b0001_0000,
),
(
Vp8xFlags {
has_exif: true,
..Default::default()
},
0b0000_1000,
),
(
Vp8xFlags {
has_xmp: true,
..Default::default()
},
0b0000_0100,
),
(
Vp8xFlags {
has_animation: true,
..Default::default()
},
0b0000_0010,
),
];
for (flags, expected) in cases {
let payload = build_vp8x_chunk(1, 1, *flags).unwrap();
assert_eq!(payload[0], *expected, "flags={flags:?}");
}
let all = build_vp8x_chunk(
1,
1,
Vp8xFlags {
has_iccp: true,
has_alpha: true,
has_exif: true,
has_xmp: true,
has_animation: true,
},
)
.unwrap();
assert_eq!(all[0], 0b0011_1110);
}
#[test]
fn build_vp8x_canvas_dims_are_24bit_little_endian() {
let payload = build_vp8x_chunk(0x00ABCD, 0x000124, Vp8xFlags::default()).unwrap();
assert_eq!(&payload[4..7], &[0xCC, 0xAB, 0x00]);
assert_eq!(&payload[7..10], &[0x23, 0x01, 0x00]);
}
#[test]
fn build_vp8x_rejects_zero_canvas_dim() {
assert_eq!(
build_vp8x_chunk(0, 1, Vp8xFlags::default()).unwrap_err(),
BuildError::CanvasDimZero { which: "width" }
);
assert_eq!(
build_vp8x_chunk(1, 0, Vp8xFlags::default()).unwrap_err(),
BuildError::CanvasDimZero { which: "height" }
);
}
#[test]
fn build_vp8x_rejects_canvas_dim_above_2_pow_24() {
let too_big = MAX_VP8X_CANVAS_DIM + 1;
assert_eq!(
build_vp8x_chunk(too_big, 1, Vp8xFlags::default()).unwrap_err(),
BuildError::CanvasDimTooLarge {
which: "width",
got: too_big
}
);
assert_eq!(
build_vp8x_chunk(1, too_big, Vp8xFlags::default()).unwrap_err(),
BuildError::CanvasDimTooLarge {
which: "height",
got: too_big
}
);
let ok = build_vp8x_chunk(MAX_VP8X_CANVAS_DIM, 1, Vp8xFlags::default()).unwrap();
assert_eq!(&ok[4..7], &[0xFF, 0xFF, 0xFF]); }
#[test]
fn build_vp8x_rejects_canvas_above_product_cap() {
let err = build_vp8x_chunk(65_536, 65_536, Vp8xFlags::default()).unwrap_err();
assert_eq!(
err,
BuildError::CanvasTooLarge {
canvas_width: 65_536,
canvas_height: 65_536,
}
);
}
#[test]
fn build_webp_file_simple_lossy_round_trips_through_parser() {
let payload = b"\xDE\xAD\xBE\xEF\x01\x02\x03"; let bytes = build_webp_file(payload, ImageKind::Lossy, 0, 0).unwrap();
assert_eq!(bytes.len(), 12 + 8 + 7 + 1);
assert_eq!(&bytes[0..4], b"RIFF");
assert_eq!(&bytes[8..12], b"WEBP");
let c = parse(&bytes).expect("simple-lossy file built by builder parses");
assert_eq!(c.chunks.len(), 1);
assert_eq!(c.chunks[0].fourcc, fourcc::VP8);
assert_eq!(c.chunks[0].size, 7);
assert_eq!(c.chunks[0].payload(&bytes), payload);
assert!(!c.is_extended());
assert_eq!(c.riff_file_size, 20);
}
#[test]
fn build_webp_file_simple_lossless_uses_vp8l_chunk() {
let payload = vec![0x2F, 0x00, 0x00, 0x00]; let bytes = build_webp_file(&payload, ImageKind::Lossless, 0, 0).unwrap();
assert_eq!(bytes.len(), 12 + 8 + 4);
let c = parse(&bytes).unwrap();
assert_eq!(c.chunks.len(), 1);
assert_eq!(c.chunks[0].fourcc, fourcc::VP8L);
assert_eq!(c.chunks[0].payload(&bytes), payload.as_slice());
}
#[test]
fn build_webp_file_extended_lossy_emits_vp8x_then_vp8() {
let payload = vec![0u8; 6]; let bytes = build_webp_file(&payload, ImageKind::ExtendedLossy, 320, 240).unwrap();
let c = parse(&bytes).unwrap();
assert_eq!(c.chunks.len(), 2);
assert_eq!(c.chunks[0].fourcc, fourcc::VP8X);
assert_eq!(c.chunks[1].fourcc, fourcc::VP8);
assert!(c.is_extended());
let vp8x = Vp8xHeader::parse(c.chunks[0].payload(&bytes)).unwrap();
assert_eq!(vp8x.canvas_width, 320);
assert_eq!(vp8x.canvas_height, 240);
assert!(!vp8x.has_iccp);
assert!(!vp8x.has_alpha);
assert!(!vp8x.has_exif);
assert!(!vp8x.has_xmp);
assert!(!vp8x.has_animation);
assert!(!vp8x.has_unknown);
}
#[test]
fn build_webp_file_extended_lossless_emits_vp8x_then_vp8l() {
let payload = vec![0u8; 5]; let bytes = build_webp_file(&payload, ImageKind::ExtendedLossless, 1, 1).unwrap();
let c = parse(&bytes).unwrap();
assert_eq!(c.chunks.len(), 2);
assert_eq!(c.chunks[0].fourcc, fourcc::VP8X);
assert_eq!(c.chunks[1].fourcc, fourcc::VP8L);
let vp8x = Vp8xHeader::parse(c.chunks[0].payload(&bytes)).unwrap();
assert_eq!(vp8x.canvas_width, 1);
assert_eq!(vp8x.canvas_height, 1);
assert_eq!(c.chunks[1].payload(&bytes), &[0u8; 5]);
}
#[test]
fn build_webp_file_file_size_field_matches_parsed_value() {
let bytes = build_webp_file(&[0u8; 7], ImageKind::Lossy, 0, 0).unwrap();
let declared = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
assert_eq!(declared, 20);
let bytes = build_webp_file(&[0u8; 8], ImageKind::ExtendedLossy, 100, 100).unwrap();
let declared = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
assert_eq!(declared, 38);
assert_eq!((declared as usize) + 8, bytes.len());
}
#[test]
fn build_webp_file_extended_propagates_canvas_validation_errors() {
assert_eq!(
build_webp_file(&[0u8; 4], ImageKind::ExtendedLossy, 0, 1).unwrap_err(),
BuildError::CanvasDimZero { which: "width" }
);
assert_eq!(
build_webp_file(&[0u8; 4], ImageKind::ExtendedLossless, 65_536, 65_536).unwrap_err(),
BuildError::CanvasTooLarge {
canvas_width: 65_536,
canvas_height: 65_536,
}
);
}
#[test]
fn build_webp_file_empty_payload_is_a_well_formed_empty_chunk() {
let bytes = build_webp_file(&[], ImageKind::Lossy, 0, 0).unwrap();
let c = parse(&bytes).unwrap();
assert_eq!(c.chunks.len(), 1);
assert_eq!(c.chunks[0].size, 0);
assert!(c.chunks[0].payload(&bytes).is_empty());
}
#[test]
fn build_webp_file_round_trip_preserves_64kib_payload_byte_for_byte() {
let mut payload = Vec::with_capacity(65_535);
for i in 0..65_535 {
payload.push((i & 0xFF) as u8);
}
let bytes = build_webp_file(&payload, ImageKind::Lossless, 0, 0).unwrap();
let c = parse(&bytes).expect("64 KiB payload parses");
assert_eq!(c.chunks.len(), 1);
assert_eq!(c.chunks[0].size as usize, payload.len());
assert_eq!(c.chunks[0].payload(&bytes), payload.as_slice());
}
#[test]
fn build_with_metadata_emits_vp8x_then_payload_when_no_metadata() {
let payload = vec![0u8; 6];
let bytes = build_webp_file_with_metadata(
&payload,
ImageKind::ExtendedLossy,
64,
32,
false,
FileMetadata::default(),
)
.unwrap();
let c = parse(&bytes).unwrap();
assert_eq!(c.chunks.len(), 2);
assert_eq!(c.chunks[0].fourcc, fourcc::VP8X);
assert_eq!(c.chunks[1].fourcc, fourcc::VP8);
let vp8x = Vp8xHeader::parse(c.chunks[0].payload(&bytes)).unwrap();
assert!(!vp8x.has_iccp);
assert!(!vp8x.has_alpha);
assert!(!vp8x.has_exif);
assert!(!vp8x.has_xmp);
assert!(!vp8x.has_animation);
}
#[test]
fn build_with_metadata_iccp_only_round_trips() {
let payload = vec![0u8; 4];
let iccp = b"icc-profile-bytes".to_vec();
let bytes = build_webp_file_with_metadata(
&payload,
ImageKind::ExtendedLossless,
16,
16,
false,
FileMetadata {
iccp: Some(&iccp),
..Default::default()
},
)
.unwrap();
let c = parse(&bytes).unwrap();
assert_eq!(c.chunks.len(), 3);
assert_eq!(c.chunks[0].fourcc, fourcc::VP8X);
assert_eq!(c.chunks[1].fourcc, fourcc::ICCP);
assert_eq!(c.chunks[2].fourcc, fourcc::VP8L);
let vp8x = Vp8xHeader::parse(c.chunks[0].payload(&bytes)).unwrap();
assert!(vp8x.has_iccp);
assert!(!vp8x.has_exif);
assert!(!vp8x.has_xmp);
let m = crate::extract_metadata(&bytes).unwrap();
assert_eq!(m.icc.as_deref(), Some(&iccp[..]));
assert_eq!(m.exif, None);
assert_eq!(m.xmp, None);
}
#[test]
fn build_with_metadata_exif_only_round_trips() {
let payload = vec![0u8; 4];
let exif = b"Exif\x00\x00MM\x00*".to_vec();
let bytes = build_webp_file_with_metadata(
&payload,
ImageKind::ExtendedLossless,
8,
8,
false,
FileMetadata {
exif: Some(&exif),
..Default::default()
},
)
.unwrap();
let c = parse(&bytes).unwrap();
assert_eq!(c.chunks.len(), 3);
assert_eq!(c.chunks[0].fourcc, fourcc::VP8X);
assert_eq!(c.chunks[1].fourcc, fourcc::VP8L);
assert_eq!(c.chunks[2].fourcc, fourcc::EXIF);
let vp8x = Vp8xHeader::parse(c.chunks[0].payload(&bytes)).unwrap();
assert!(!vp8x.has_iccp);
assert!(vp8x.has_exif);
assert!(!vp8x.has_xmp);
let m = crate::extract_metadata(&bytes).unwrap();
assert_eq!(m.icc, None);
assert_eq!(m.exif.as_deref(), Some(&exif[..]));
assert_eq!(m.xmp, None);
}
#[test]
fn build_with_metadata_xmp_only_round_trips() {
let payload = vec![0u8; 4];
let xmp = b"<?xpacket begin?>".to_vec();
let bytes = build_webp_file_with_metadata(
&payload,
ImageKind::ExtendedLossless,
8,
8,
false,
FileMetadata {
xmp: Some(&xmp),
..Default::default()
},
)
.unwrap();
let c = parse(&bytes).unwrap();
assert_eq!(c.chunks.len(), 3);
assert_eq!(c.chunks[0].fourcc, fourcc::VP8X);
assert_eq!(c.chunks[1].fourcc, fourcc::VP8L);
assert_eq!(c.chunks[2].fourcc, fourcc::XMP);
let vp8x = Vp8xHeader::parse(c.chunks[0].payload(&bytes)).unwrap();
assert!(vp8x.has_xmp);
let m = crate::extract_metadata(&bytes).unwrap();
assert_eq!(m.xmp.as_deref(), Some(&xmp[..]));
}
#[test]
fn build_with_metadata_iccp_plus_exif_round_trips() {
let payload = vec![0u8; 4];
let iccp = b"icc".to_vec();
let exif = b"Exif\x00\x00MM\x00*more".to_vec();
let bytes = build_webp_file_with_metadata(
&payload,
ImageKind::ExtendedLossless,
8,
8,
false,
FileMetadata {
iccp: Some(&iccp),
exif: Some(&exif),
..Default::default()
},
)
.unwrap();
let c = parse(&bytes).unwrap();
assert_eq!(c.chunks.len(), 4);
assert_eq!(c.chunks[0].fourcc, fourcc::VP8X);
assert_eq!(c.chunks[1].fourcc, fourcc::ICCP);
assert_eq!(c.chunks[2].fourcc, fourcc::VP8L);
assert_eq!(c.chunks[3].fourcc, fourcc::EXIF);
let vp8x = Vp8xHeader::parse(c.chunks[0].payload(&bytes)).unwrap();
assert!(vp8x.has_iccp);
assert!(vp8x.has_exif);
assert!(!vp8x.has_xmp);
let m = crate::extract_metadata(&bytes).unwrap();
assert_eq!(m.icc.as_deref(), Some(&iccp[..]));
assert_eq!(m.exif.as_deref(), Some(&exif[..]));
}
#[test]
fn build_with_metadata_iccp_plus_xmp_round_trips() {
let payload = vec![0u8; 4];
let iccp = b"icc-bytes-here".to_vec();
let xmp = b"<xmp/>".to_vec();
let bytes = build_webp_file_with_metadata(
&payload,
ImageKind::ExtendedLossless,
8,
8,
false,
FileMetadata {
iccp: Some(&iccp),
xmp: Some(&xmp),
..Default::default()
},
)
.unwrap();
let c = parse(&bytes).unwrap();
assert_eq!(c.chunks.len(), 4);
assert_eq!(c.chunks[0].fourcc, fourcc::VP8X);
assert_eq!(c.chunks[1].fourcc, fourcc::ICCP);
assert_eq!(c.chunks[2].fourcc, fourcc::VP8L);
assert_eq!(c.chunks[3].fourcc, fourcc::XMP);
let vp8x = Vp8xHeader::parse(c.chunks[0].payload(&bytes)).unwrap();
assert!(vp8x.has_iccp);
assert!(!vp8x.has_exif);
assert!(vp8x.has_xmp);
let m = crate::extract_metadata(&bytes).unwrap();
assert_eq!(m.icc.as_deref(), Some(&iccp[..]));
assert_eq!(m.xmp.as_deref(), Some(&xmp[..]));
}
#[test]
fn build_with_metadata_exif_plus_xmp_round_trips() {
let payload = vec![0u8; 4];
let exif = b"E".to_vec();
let xmp = b"X".to_vec();
let bytes = build_webp_file_with_metadata(
&payload,
ImageKind::ExtendedLossless,
8,
8,
false,
FileMetadata {
exif: Some(&exif),
xmp: Some(&xmp),
..Default::default()
},
)
.unwrap();
let c = parse(&bytes).unwrap();
assert_eq!(c.chunks.len(), 4);
assert_eq!(c.chunks[0].fourcc, fourcc::VP8X);
assert_eq!(c.chunks[1].fourcc, fourcc::VP8L);
assert_eq!(c.chunks[2].fourcc, fourcc::EXIF);
assert_eq!(c.chunks[3].fourcc, fourcc::XMP);
let m = crate::extract_metadata(&bytes).unwrap();
assert_eq!(m.exif.as_deref(), Some(&exif[..]));
assert_eq!(m.xmp.as_deref(), Some(&xmp[..]));
}
#[test]
fn build_with_metadata_all_three_round_trip_in_canonical_order() {
let payload = vec![0u8; 4];
let iccp = b"ICC-profile-blob".to_vec();
let exif = b"Exif\x00\x00II*\x00".to_vec();
let xmp = b"<x:xmpmeta/>".to_vec();
let bytes = build_webp_file_with_metadata(
&payload,
ImageKind::ExtendedLossless,
16,
16,
true, FileMetadata {
iccp: Some(&iccp),
exif: Some(&exif),
xmp: Some(&xmp),
},
)
.unwrap();
let c = parse(&bytes).unwrap();
assert_eq!(c.chunks.len(), 5);
assert_eq!(c.chunks[0].fourcc, fourcc::VP8X);
assert_eq!(c.chunks[1].fourcc, fourcc::ICCP);
assert_eq!(c.chunks[2].fourcc, fourcc::VP8L);
assert_eq!(c.chunks[3].fourcc, fourcc::EXIF);
assert_eq!(c.chunks[4].fourcc, fourcc::XMP);
let vp8x = Vp8xHeader::parse(c.chunks[0].payload(&bytes)).unwrap();
assert!(vp8x.has_iccp);
assert!(vp8x.has_alpha);
assert!(vp8x.has_exif);
assert!(vp8x.has_xmp);
assert!(!vp8x.has_animation);
assert_eq!(vp8x.canvas_width, 16);
assert_eq!(vp8x.canvas_height, 16);
let m = crate::extract_metadata(&bytes).unwrap();
assert_eq!(m.icc.as_deref(), Some(&iccp[..]));
assert_eq!(m.exif.as_deref(), Some(&exif[..]));
assert_eq!(m.xmp.as_deref(), Some(&xmp[..]));
}
#[test]
fn build_with_metadata_odd_payloads_get_pad_bytes() {
let payload = vec![0xABu8, 0xCD, 0xEF, 0x01, 0x02]; let iccp = b"AAA".to_vec(); let exif = b"BBBBB".to_vec(); let xmp = b"CCCCCCC".to_vec(); let bytes = build_webp_file_with_metadata(
&payload,
ImageKind::ExtendedLossless,
8,
8,
false,
FileMetadata {
iccp: Some(&iccp),
exif: Some(&exif),
xmp: Some(&xmp),
},
)
.unwrap();
let declared = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
assert_eq!(declared, 78);
let c = parse(&bytes).unwrap();
assert_eq!(c.chunks[1].size, 3, "ICCP §Size excludes pad byte");
assert_eq!(c.chunks[2].size, 5, "VP8L §Size excludes pad byte");
assert_eq!(c.chunks[3].size, 5, "EXIF §Size excludes pad byte");
assert_eq!(c.chunks[4].size, 7, "XMP §Size excludes pad byte");
assert_eq!(c.chunks[1].payload(&bytes), &iccp[..]);
assert_eq!(c.chunks[2].payload(&bytes), &payload[..]);
assert_eq!(c.chunks[3].payload(&bytes), &exif[..]);
assert_eq!(c.chunks[4].payload(&bytes), &xmp[..]);
}
#[test]
fn build_with_metadata_flag_bits_match_field_presence() {
let payload = vec![0u8; 2];
for has_icc in [false, true] {
for has_exif_p in [false, true] {
for has_xmp_p in [false, true] {
for has_alpha in [false, true] {
let icc_blob: &[u8] = &[0xAA];
let exif_blob: &[u8] = &[0xBB];
let xmp_blob: &[u8] = &[0xCC];
let metadata = FileMetadata {
iccp: has_icc.then_some(icc_blob),
exif: has_exif_p.then_some(exif_blob),
xmp: has_xmp_p.then_some(xmp_blob),
};
let bytes = build_webp_file_with_metadata(
&payload,
ImageKind::ExtendedLossless,
4,
4,
has_alpha,
metadata,
)
.unwrap();
let c = parse(&bytes).unwrap();
let vp8x = Vp8xHeader::parse(c.chunks[0].payload(&bytes)).unwrap();
assert_eq!(
vp8x.has_iccp, has_icc,
"I flag (has_icc={has_icc}, has_exif={has_exif_p}, has_xmp={has_xmp_p}, has_alpha={has_alpha})"
);
assert_eq!(
vp8x.has_alpha, has_alpha,
"L flag (has_icc={has_icc}, has_exif={has_exif_p}, has_xmp={has_xmp_p}, has_alpha={has_alpha})"
);
assert_eq!(
vp8x.has_exif, has_exif_p,
"E flag (has_icc={has_icc}, has_exif={has_exif_p}, has_xmp={has_xmp_p}, has_alpha={has_alpha})"
);
assert_eq!(
vp8x.has_xmp, has_xmp_p,
"X flag (has_icc={has_icc}, has_exif={has_exif_p}, has_xmp={has_xmp_p}, has_alpha={has_alpha})"
);
assert!(!vp8x.has_animation);
}
}
}
}
}
#[test]
fn build_with_metadata_propagates_canvas_validation_errors() {
assert_eq!(
build_webp_file_with_metadata(
&[0u8; 4],
ImageKind::ExtendedLossless,
0,
1,
false,
FileMetadata::default(),
)
.unwrap_err(),
BuildError::CanvasDimZero { which: "width" }
);
assert_eq!(
build_webp_file_with_metadata(
&[0u8; 4],
ImageKind::ExtendedLossless,
65_536,
65_536,
false,
FileMetadata::default(),
)
.unwrap_err(),
BuildError::CanvasTooLarge {
canvas_width: 65_536,
canvas_height: 65_536,
}
);
}
#[test]
fn build_with_metadata_empty_metadata_omits_optional_chunks() {
assert!(FileMetadata::default().is_empty());
let payload = vec![0u8; 8];
let bytes = build_webp_file_with_metadata(
&payload,
ImageKind::ExtendedLossless,
4,
4,
false,
FileMetadata::default(),
)
.unwrap();
let c = parse(&bytes).unwrap();
assert_eq!(c.chunks.len(), 2);
assert_eq!(c.chunks[0].fourcc, fourcc::VP8X);
assert_eq!(c.chunks[1].fourcc, fourcc::VP8L);
let m = crate::extract_metadata(&bytes).unwrap();
assert!(m.icc.is_none());
assert!(m.exif.is_none());
assert!(m.xmp.is_none());
}
#[test]
fn parser_still_rejects_corrupt_size_field_after_builder_round_trip() {
let mut bytes = build_webp_file(&[0u8; 8], ImageKind::Lossy, 0, 0).unwrap();
let chunk_size_off = 12 + 4;
bytes[chunk_size_off..chunk_size_off + 4].copy_from_slice(&100_000u32.to_le_bytes());
match parse(&bytes) {
Err(ContainerError::ChunkPayloadOverflowsRiff { .. }) => {}
other => panic!("expected ChunkPayloadOverflowsRiff, got {other:?}"),
}
}
}