#[derive(Default, Clone)]
pub struct WebpMetadata<'a> {
pub icc: Option<&'a [u8]>,
pub exif: Option<&'a [u8]>,
pub xmp: Option<&'a [u8]>,
}
impl WebpMetadata<'_> {
pub fn any(&self) -> bool {
self.icc.is_some() || self.exif.is_some() || self.xmp.is_some()
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ImageKind {
Vp8Lossy,
Vp8lLossless,
}
impl ImageKind {
fn fourcc(self) -> &'static [u8; 4] {
match self {
ImageKind::Vp8Lossy => b"VP8 ",
ImageKind::Vp8lLossless => b"VP8L",
}
}
}
pub fn build_webp_file(
kind: ImageKind,
image_bytes: &[u8],
canvas_w: u32,
canvas_h: u32,
alph: Option<&AlphChunkBytes>,
meta: &WebpMetadata<'_>,
) -> Vec<u8> {
let needs_extended = alph.is_some() || meta.any();
if !needs_extended {
return build_simple(kind, image_bytes);
}
build_extended(kind, image_bytes, canvas_w, canvas_h, alph, meta, false)
}
pub fn build_vp8l_with_alpha(
image_bytes: &[u8],
canvas_w: u32,
canvas_h: u32,
meta: &WebpMetadata<'_>,
) -> Vec<u8> {
build_extended(
ImageKind::Vp8lLossless,
image_bytes,
canvas_w,
canvas_h,
None,
meta,
true,
)
}
pub struct AlphChunkBytes {
pub header_byte: u8,
pub payload: Vec<u8>,
}
fn build_simple(kind: ImageKind, image_bytes: &[u8]) -> Vec<u8> {
let chunk_len = image_bytes.len() as u32;
let pad = (chunk_len & 1) as usize;
let riff_size = 4 + 8 + chunk_len as usize + pad;
let mut out = Vec::with_capacity(8 + riff_size);
out.extend_from_slice(b"RIFF");
out.extend_from_slice(&(riff_size as u32).to_le_bytes());
out.extend_from_slice(b"WEBP");
out.extend_from_slice(kind.fourcc());
out.extend_from_slice(&chunk_len.to_le_bytes());
out.extend_from_slice(image_bytes);
if pad == 1 {
out.push(0);
}
out
}
fn build_extended(
kind: ImageKind,
image_bytes: &[u8],
canvas_w: u32,
canvas_h: u32,
alph: Option<&AlphChunkBytes>,
meta: &WebpMetadata<'_>,
force_alpha_flag: bool,
) -> Vec<u8> {
let mut body: Vec<u8> = Vec::with_capacity(
8 + 10
+ image_bytes.len()
+ alph.map(|a| a.payload.len() + 16).unwrap_or(0)
+ meta.icc.map(|v| v.len() + 16).unwrap_or(0)
+ meta.exif.map(|v| v.len() + 16).unwrap_or(0)
+ meta.xmp.map(|v| v.len() + 16).unwrap_or(0),
);
let mut flags: u8 = 0;
if meta.icc.is_some() {
flags |= 0x20; }
if meta.exif.is_some() {
flags |= 0x08; }
if meta.xmp.is_some() {
flags |= 0x04; }
if alph.is_some() || force_alpha_flag {
flags |= 0x10; }
write_chunk(&mut body, b"VP8X", &vp8x_payload(flags, canvas_w, canvas_h));
if let Some(icc) = meta.icc {
write_chunk(&mut body, b"ICCP", icc);
}
if let Some(a) = alph {
let mut alph_data = Vec::with_capacity(1 + a.payload.len());
alph_data.push(a.header_byte);
alph_data.extend_from_slice(&a.payload);
write_chunk(&mut body, b"ALPH", &alph_data);
}
write_chunk(&mut body, kind.fourcc(), image_bytes);
if let Some(exif) = meta.exif {
write_chunk(&mut body, b"EXIF", exif);
}
if let Some(xmp) = meta.xmp {
write_chunk(&mut body, b"XMP ", xmp);
}
let riff_size = 4 + body.len();
let mut out = Vec::with_capacity(8 + riff_size);
out.extend_from_slice(b"RIFF");
out.extend_from_slice(&(riff_size as u32).to_le_bytes());
out.extend_from_slice(b"WEBP");
out.extend_from_slice(&body);
out
}
fn vp8x_payload(flags: u8, canvas_w: u32, canvas_h: u32) -> [u8; 10] {
let mut out = [0u8; 10];
out[0] = flags;
let w_minus_1 = canvas_w.saturating_sub(1) & 0x00FF_FFFF;
let h_minus_1 = canvas_h.saturating_sub(1) & 0x00FF_FFFF;
out[4] = (w_minus_1 & 0xff) as u8;
out[5] = ((w_minus_1 >> 8) & 0xff) as u8;
out[6] = ((w_minus_1 >> 16) & 0xff) as u8;
out[7] = (h_minus_1 & 0xff) as u8;
out[8] = ((h_minus_1 >> 8) & 0xff) as u8;
out[9] = ((h_minus_1 >> 16) & 0xff) as u8;
out
}
fn write_chunk(out: &mut Vec<u8>, fourcc: &[u8; 4], payload: &[u8]) {
out.extend_from_slice(fourcc);
out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
out.extend_from_slice(payload);
if payload.len() & 1 == 1 {
out.push(0);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple_layout_vp8l_no_extras() {
let payload = vec![0x2fu8; 10];
let out = build_webp_file(
ImageKind::Vp8lLossless,
&payload,
32,
32,
None,
&WebpMetadata::default(),
);
assert_eq!(&out[0..4], b"RIFF");
assert_eq!(&out[8..12], b"WEBP");
assert_eq!(&out[12..16], b"VP8L");
}
#[test]
fn extended_layout_emits_vp8x_first() {
let payload = vec![0x2fu8; 10];
let meta = WebpMetadata {
icc: Some(b"fake-icc"),
..Default::default()
};
let out = build_webp_file(ImageKind::Vp8lLossless, &payload, 64, 48, None, &meta);
assert_eq!(&out[0..4], b"RIFF");
assert_eq!(&out[8..12], b"WEBP");
assert_eq!(&out[12..16], b"VP8X");
assert_eq!(out[20], 0x20);
let w_minus_1 = u32::from_le_bytes([out[24], out[25], out[26], 0]) & 0x00FF_FFFF;
let h_minus_1 = u32::from_le_bytes([out[27], out[28], out[29], 0]) & 0x00FF_FFFF;
assert_eq!(w_minus_1, 63);
assert_eq!(h_minus_1, 47);
}
#[test]
fn extended_layout_with_alph_flags_both_bits() {
let payload = vec![0x11u8; 5];
let alph = AlphChunkBytes {
header_byte: 0,
payload: vec![0xffu8; 16],
};
let meta = WebpMetadata {
exif: Some(b"exif!"),
..Default::default()
};
let out = build_webp_file(ImageKind::Vp8Lossy, &payload, 16, 16, Some(&alph), &meta);
assert_eq!(out[20], 0x18);
}
}