use gamut_core::{Error, Result};
use crate::fourcc::FourCc;
use crate::writer::RiffWriter;
pub const VP8X_PAYLOAD_LEN: usize = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Vp8xHeader {
pub icc_profile: bool,
pub alpha: bool,
pub exif_metadata: bool,
pub xmp_metadata: bool,
pub animation: bool,
pub canvas_width: u32,
pub canvas_height: u32,
}
impl Vp8xHeader {
#[must_use]
pub fn to_payload(&self) -> [u8; VP8X_PAYLOAD_LEN] {
let flags = (u8::from(self.icc_profile) << 5)
| (u8::from(self.alpha) << 4)
| (u8::from(self.exif_metadata) << 3)
| (u8::from(self.xmp_metadata) << 2)
| (u8::from(self.animation) << 1);
let w = self.canvas_width.saturating_sub(1);
let h = self.canvas_height.saturating_sub(1);
[
flags,
0,
0,
0,
w as u8,
(w >> 8) as u8,
(w >> 16) as u8,
h as u8,
(h >> 8) as u8,
(h >> 16) as u8,
]
}
pub fn from_payload(payload: &[u8]) -> Result<Self> {
if payload.len() < VP8X_PAYLOAD_LEN {
return Err(Error::InvalidInput(
"VP8X: chunk payload shorter than 10 bytes",
));
}
let flags = payload[0];
let le24 = |b: &[u8]| u32::from(b[0]) | (u32::from(b[1]) << 8) | (u32::from(b[2]) << 16);
Ok(Self {
icc_profile: flags & 0x20 != 0,
alpha: flags & 0x10 != 0,
exif_metadata: flags & 0x08 != 0,
xmp_metadata: flags & 0x04 != 0,
animation: flags & 0x02 != 0,
canvas_width: le24(&payload[4..7]) + 1,
canvas_height: le24(&payload[7..10]) + 1,
})
}
}
#[must_use]
pub fn write_extended(header: &Vp8xHeader, chunks: &[(FourCc, &[u8])]) -> Vec<u8> {
let mut w = RiffWriter::new();
w.write_chunk(FourCc::VP8X, &header.to_payload());
for (fourcc, payload) in chunks {
w.write_chunk(*fourcc, payload);
}
w.finish()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum WebpChunkId {
Vp8,
Vp8l,
Vp8x,
Alpha,
Iccp,
Exif,
Xmp,
Anim,
Anmf,
Unknown(FourCc),
}
impl From<FourCc> for WebpChunkId {
fn from(fourcc: FourCc) -> Self {
match &fourcc.0 {
b"VP8 " => Self::Vp8,
b"VP8L" => Self::Vp8l,
b"VP8X" => Self::Vp8x,
b"ALPH" => Self::Alpha,
b"ICCP" => Self::Iccp,
b"EXIF" => Self::Exif,
b"XMP " => Self::Xmp,
b"ANIM" => Self::Anim,
b"ANMF" => Self::Anmf,
_ => Self::Unknown(fourcc),
}
}
}
#[must_use]
pub fn write_simple_lossless(vp8l_bitstream: &[u8]) -> Vec<u8> {
let mut w = RiffWriter::new();
w.write_chunk(FourCc::VP8L, vp8l_bitstream);
w.finish()
}
#[must_use]
pub fn write_simple_lossy(vp8_bitstream: &[u8]) -> Vec<u8> {
let mut w = RiffWriter::new();
w.write_chunk(FourCc::VP8, vp8_bitstream);
w.finish()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reader::RiffReader;
#[test]
fn classifies_known_and_unknown_chunks() {
assert_eq!(WebpChunkId::from(FourCc::VP8), WebpChunkId::Vp8);
assert_eq!(WebpChunkId::from(FourCc::VP8L), WebpChunkId::Vp8l);
assert_eq!(WebpChunkId::from(FourCc::VP8X), WebpChunkId::Vp8x);
assert_eq!(WebpChunkId::from(FourCc::ALPH), WebpChunkId::Alpha);
assert_eq!(WebpChunkId::from(FourCc::ICCP), WebpChunkId::Iccp);
assert_eq!(WebpChunkId::from(FourCc::EXIF), WebpChunkId::Exif);
assert_eq!(WebpChunkId::from(FourCc::XMP), WebpChunkId::Xmp);
assert_eq!(WebpChunkId::from(FourCc::ANIM), WebpChunkId::Anim);
assert_eq!(WebpChunkId::from(FourCc::ANMF), WebpChunkId::Anmf);
let weird = FourCc::from(*b"XYZW");
assert_eq!(WebpChunkId::from(weird), WebpChunkId::Unknown(weird));
}
#[test]
fn simple_lossless_wraps_one_vp8l_chunk() {
let bitstream = [0x2f, 0xde, 0xad, 0xbe, 0xef];
let file = write_simple_lossless(&bitstream);
let chunks: Vec<_> = RiffReader::new(&file)
.unwrap()
.map(|c| c.unwrap())
.collect();
assert_eq!(chunks.len(), 1);
assert_eq!(WebpChunkId::from(chunks[0].fourcc), WebpChunkId::Vp8l);
assert_eq!(chunks[0].payload, &bitstream);
}
#[test]
fn vp8x_header_round_trips() {
let h = Vp8xHeader {
icc_profile: false,
alpha: true,
exif_metadata: false,
xmp_metadata: false,
animation: false,
canvas_width: 640,
canvas_height: 481,
};
let payload = h.to_payload();
assert_eq!(payload.len(), VP8X_PAYLOAD_LEN);
assert_eq!(payload[0] & 0x10, 0x10, "alpha (L) flag is bit 4");
assert_eq!(&payload[1..4], &[0, 0, 0], "reserved bytes are zero");
assert_eq!(Vp8xHeader::from_payload(&payload).unwrap(), h);
}
#[test]
fn from_payload_rejects_short_input() {
assert!(Vp8xHeader::from_payload(&[0u8; 9]).is_err());
}
#[test]
fn write_extended_assembles_vp8x_then_chunks() {
let h = Vp8xHeader {
alpha: true,
canvas_width: 16,
canvas_height: 16,
..Default::default()
};
let file = write_extended(
&h,
&[
(FourCc::ALPH, &[1, 2, 3]),
(FourCc::VP8, &[0x9d, 0x01, 0x2a]),
],
);
let chunks: Vec<_> = RiffReader::new(&file)
.unwrap()
.map(|c| c.unwrap())
.collect();
assert_eq!(WebpChunkId::from(chunks[0].fourcc), WebpChunkId::Vp8x);
assert_eq!(WebpChunkId::from(chunks[1].fourcc), WebpChunkId::Alpha);
assert_eq!(WebpChunkId::from(chunks[2].fourcc), WebpChunkId::Vp8);
assert_eq!(Vp8xHeader::from_payload(chunks[0].payload).unwrap(), h);
}
#[test]
fn simple_lossy_wraps_one_vp8_chunk() {
let bitstream = [0x9d, 0x01, 0x2a];
let file = write_simple_lossy(&bitstream);
let chunks: Vec<_> = RiffReader::new(&file)
.unwrap()
.map(|c| c.unwrap())
.collect();
assert_eq!(chunks.len(), 1);
assert_eq!(WebpChunkId::from(chunks[0].fourcc), WebpChunkId::Vp8);
assert_eq!(chunks[0].payload, &bitstream);
}
}