#[cfg(feature = "registry")]
use std::io::{Read, SeekFrom};
#[cfg(feature = "registry")]
use oxideav_core::{
CodecId, CodecParameters, CodecResolver, MediaType, Packet, PixelFormat, StreamInfo, TimeBase,
};
#[cfg(feature = "registry")]
use oxideav_core::{ContainerRegistry, Demuxer, ProbeData, ReadSeek};
use crate::error::{Result, WebpError as Error};
pub const WEBP_CODEC_ID: &str = "webp";
#[cfg(feature = "registry")]
pub fn register(reg: &mut ContainerRegistry) {
reg.register_demuxer("webp", open);
reg.register_extension("webp", "webp");
reg.register_probe("webp", probe);
}
#[cfg(feature = "registry")]
fn probe(p: &ProbeData) -> u8 {
if p.buf.len() < 12 {
return 0;
}
if &p.buf[0..4] != b"RIFF" {
return 0;
}
if &p.buf[8..12] != b"WEBP" {
return 0;
}
100
}
#[cfg(feature = "registry")]
pub fn open_boxed(input: Box<dyn ReadSeek>) -> oxideav_core::Result<Box<dyn Demuxer>> {
open(input, &oxideav_core::NullCodecResolver)
}
#[cfg(feature = "registry")]
fn open(
mut input: Box<dyn ReadSeek>,
_codecs: &dyn CodecResolver,
) -> oxideav_core::Result<Box<dyn Demuxer>> {
let mut buf = Vec::new();
input.seek(SeekFrom::Start(0))?;
input.read_to_end(&mut buf)?;
drop(input);
if buf.len() < 12 || &buf[0..4] != b"RIFF" || &buf[8..12] != b"WEBP" {
return Err(oxideav_core::Error::invalid("WebP: bad RIFF/WEBP magic"));
}
let riff_size = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]) as usize;
let end = (8 + riff_size).min(buf.len());
let body: Vec<u8> = buf[12..end].to_vec();
let parsed = parse_webp_body_lazy(&body)?;
let (w, h) = parsed.canvas;
let mut params = CodecParameters::video(CodecId::new(WEBP_CODEC_ID));
params.media_type = MediaType::Video;
params.width = Some(w);
params.height = Some(h);
params.pixel_format = Some(PixelFormat::Rgba);
let time_base = TimeBase::new(1, 1000);
let stream = StreamInfo {
index: 0,
time_base,
duration: Some(parsed.total_duration_ms as i64),
start_time: Some(0),
params,
};
Ok(Box::new(WebpDemuxer {
stream,
body,
parsed,
time_base,
pos: 0,
pts: 0,
}))
}
#[derive(Debug)]
pub(crate) struct ParsedContainer {
pub canvas: (u32, u32),
pub frames: Vec<ParsedFrame>,
#[allow(dead_code)]
pub total_duration_ms: u32,
pub metadata: WebpFileMetadata,
pub anim_background_bgra: Option<[u8; 4]>,
pub anim_loop_count: Option<u16>,
}
pub(crate) fn bgra_to_rgba(bgra: [u8; 4]) -> [u8; 4] {
[bgra[2], bgra[1], bgra[0], bgra[3]]
}
#[derive(Debug, Clone, Default)]
pub struct WebpFileMetadata {
pub icc: Option<Vec<u8>>,
pub exif: Option<Vec<u8>>,
pub xmp: Option<Vec<u8>>,
}
impl WebpFileMetadata {
pub fn any(&self) -> bool {
self.icc.is_some() || self.exif.is_some() || self.xmp.is_some()
}
}
pub fn extract_metadata(buf: &[u8]) -> Result<WebpFileMetadata> {
if buf.len() < 12 || &buf[0..4] != b"RIFF" || &buf[8..12] != b"WEBP" {
return Err(Error::invalid("WebP: bad RIFF/WEBP magic"));
}
let riff_size = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]) as usize;
let end = (8 + riff_size).min(buf.len());
let body = &buf[12..end];
let mut chunks = RiffChunks::new(body);
let Some(first) = chunks.next().transpose()? else {
return Ok(WebpFileMetadata::default());
};
if &first.id != b"VP8X" {
return Ok(WebpFileMetadata::default());
}
let mut meta = WebpFileMetadata::default();
while let Some(c) = chunks.next().transpose()? {
match &c.id {
b"ICCP" => meta.icc = Some(c.data.to_vec()),
b"EXIF" => meta.exif = Some(c.data.to_vec()),
b"XMP " => meta.xmp = Some(c.data.to_vec()),
_ => {}
}
}
Ok(meta)
}
#[derive(Debug)]
pub(crate) struct ParsedFrame {
pub image: ImagePayload,
pub alph: Option<AlphChunk>,
pub x_offset: u32,
pub y_offset: u32,
pub width: u32,
pub height: u32,
pub duration_ms: u32,
pub dispose_to_background: bool,
pub blend_with_previous: bool,
}
#[derive(Debug)]
pub(crate) enum ImagePayload {
Vp8(Vec<u8>),
Vp8l(Vec<u8>),
}
#[derive(Debug)]
pub(crate) struct AlphChunk {
#[allow(dead_code)]
pub pre_processing: u8,
pub filtering: u8,
pub compression: u8,
pub data: Vec<u8>,
}
#[derive(Debug)]
pub(crate) struct LazyParsedContainer {
pub canvas: (u32, u32),
pub frames: Vec<LazyParsedFrame>,
pub total_duration_ms: u32,
pub metadata: WebpFileMetadata,
pub anim_background_bgra: Option<[u8; 4]>,
pub anim_loop_count: Option<u16>,
}
#[derive(Debug, Clone)]
pub(crate) struct LazyParsedFrame {
pub image: LazyImageRef,
pub alph: Option<LazyAlphRef>,
pub x_offset: u32,
pub y_offset: u32,
pub width: u32,
pub height: u32,
pub duration_ms: u32,
pub dispose_to_background: bool,
pub blend_with_previous: bool,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum LazyImageRef {
Vp8 { offset: usize, len: usize },
Vp8l { offset: usize, len: usize },
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct LazyAlphRef {
#[allow(dead_code)]
pub pre_processing: u8,
pub filtering: u8,
pub compression: u8,
pub data_offset: usize,
pub data_len: usize,
}
#[cfg(feature = "registry")]
pub(crate) fn encode_lazy_frame_payload(
f: &LazyParsedFrame,
body: &[u8],
canvas: (u32, u32),
anim_background_bgra: Option<[u8; 4]>,
) -> Result<Vec<u8>> {
let (img_bytes, is_vp8l) = match f.image {
LazyImageRef::Vp8 { offset, len } => {
if offset + len > body.len() {
return Err(Error::invalid("WebP: VP8 ref out of bounds"));
}
(&body[offset..offset + len], false)
}
LazyImageRef::Vp8l { offset, len } => {
if offset + len > body.len() {
return Err(Error::invalid("WebP: VP8L ref out of bounds"));
}
(&body[offset..offset + len], true)
}
};
let alph_bytes = if let Some(a) = &f.alph {
if a.data_offset + a.data_len > body.len() {
return Err(Error::invalid("WebP: ALPH ref out of bounds"));
}
Some(&body[a.data_offset..a.data_offset + a.data_len])
} else {
None
};
let mut out = Vec::with_capacity(
64 + img_bytes.len() + f.alph.as_ref().map(|a| a.data_len + 16).unwrap_or(0) + 8,
);
out.extend_from_slice(b"OWEB");
out.push(2);
let mut flags = 0u8;
if f.alph.is_some() {
flags |= 0x01;
}
if is_vp8l {
flags |= 0x02;
}
if f.dispose_to_background {
flags |= 0x04;
}
if f.blend_with_previous {
flags |= 0x08;
}
if anim_background_bgra.is_some() {
flags |= 0x10;
}
out.push(flags);
for v in [
canvas.0, canvas.1, f.x_offset, f.y_offset, f.width, f.height,
] {
out.extend_from_slice(&v.to_le_bytes());
}
out.extend_from_slice(&f.duration_ms.to_le_bytes());
out.extend_from_slice(&(img_bytes.len() as u32).to_le_bytes());
out.extend_from_slice(img_bytes);
if let (Some(a), Some(ab)) = (&f.alph, alph_bytes) {
out.push(a.pre_processing);
out.push(a.filtering);
out.push(a.compression);
out.extend_from_slice(&(ab.len() as u32).to_le_bytes());
out.extend_from_slice(ab);
}
if let Some(bgra) = anim_background_bgra {
out.extend_from_slice(&bgra);
}
Ok(out)
}
pub(crate) fn decode_frame_payload(buf: &[u8]) -> Result<DecodedPayload<'_>> {
if buf.len() < 4 + 1 + 1 + 6 * 4 + 4 + 4 {
return Err(Error::invalid("WebP: frame payload too short"));
}
if &buf[0..4] != b"OWEB" {
return Err(Error::invalid("WebP: bad frame payload magic"));
}
let version = buf[4];
if version != 1 && version != 2 {
return Err(Error::invalid("WebP: unknown frame payload version"));
}
let flags = buf[5];
let mut p = 6usize;
let read_u32 = |p: &mut usize, buf: &[u8]| -> u32 {
let v = u32::from_le_bytes([buf[*p], buf[*p + 1], buf[*p + 2], buf[*p + 3]]);
*p += 4;
v
};
let canvas_w = read_u32(&mut p, buf);
let canvas_h = read_u32(&mut p, buf);
let x_off = read_u32(&mut p, buf);
let y_off = read_u32(&mut p, buf);
let frame_w = read_u32(&mut p, buf);
let frame_h = read_u32(&mut p, buf);
let duration_ms = read_u32(&mut p, buf);
let img_len = read_u32(&mut p, buf) as usize;
if p + img_len > buf.len() {
return Err(Error::invalid("WebP: image chunk extends past payload"));
}
let image = &buf[p..p + img_len];
p += img_len;
let alph = if flags & 0x01 != 0 {
if p + 3 + 4 > buf.len() {
return Err(Error::invalid("WebP: truncated ALPH header"));
}
let pre = buf[p];
let filt = buf[p + 1];
let comp = buf[p + 2];
p += 3;
let alen = read_u32(&mut p, buf) as usize;
if p + alen > buf.len() {
return Err(Error::invalid("WebP: ALPH data extends past payload"));
}
let a = &buf[p..p + alen];
p += alen;
Some(DecodedAlph {
pre_processing: pre,
filtering: filt,
compression: comp,
data: a,
})
} else {
None
};
let anim_background_bgra = if version >= 2 && (flags & 0x10) != 0 {
if p + 4 > buf.len() {
return Err(Error::invalid("WebP: truncated ANIM bg in payload"));
}
Some([buf[p], buf[p + 1], buf[p + 2], buf[p + 3]])
} else {
None
};
Ok(DecodedPayload {
is_vp8l: flags & 0x02 != 0,
dispose_to_background: flags & 0x04 != 0,
blend_with_previous: flags & 0x08 != 0,
canvas: (canvas_w, canvas_h),
x_offset: x_off,
y_offset: y_off,
width: frame_w,
height: frame_h,
duration_ms,
image,
alph,
anim_background_bgra,
})
}
pub(crate) struct DecodedPayload<'a> {
pub is_vp8l: bool,
pub dispose_to_background: bool,
pub blend_with_previous: bool,
pub canvas: (u32, u32),
pub x_offset: u32,
pub y_offset: u32,
pub width: u32,
pub height: u32,
#[allow(dead_code)]
pub duration_ms: u32,
pub image: &'a [u8],
pub alph: Option<DecodedAlph<'a>>,
pub anim_background_bgra: Option<[u8; 4]>,
}
pub(crate) struct DecodedAlph<'a> {
#[allow(dead_code)]
pub pre_processing: u8,
pub filtering: u8,
pub compression: u8,
pub data: &'a [u8],
}
pub(crate) fn parse_webp_body(body: &[u8]) -> Result<ParsedContainer> {
let mut chunks = RiffChunks::new(body);
let first = chunks
.next()
.transpose()?
.ok_or_else(|| Error::invalid("WebP: empty RIFF body"))?;
match &first.id {
b"VP8 " => {
let (w, h) = parse_vp8_keyframe_dims(first.data)?;
let frame = ParsedFrame {
image: ImagePayload::Vp8(first.data.to_vec()),
alph: None,
x_offset: 0,
y_offset: 0,
width: w,
height: h,
duration_ms: 0,
dispose_to_background: false,
blend_with_previous: false,
};
Ok(ParsedContainer {
canvas: (w, h),
frames: vec![frame],
total_duration_ms: 0,
metadata: WebpFileMetadata::default(),
anim_background_bgra: None,
anim_loop_count: None,
})
}
b"VP8L" => {
let (w, h) = parse_vp8l_dims(first.data)?;
let frame = ParsedFrame {
image: ImagePayload::Vp8l(first.data.to_vec()),
alph: None,
x_offset: 0,
y_offset: 0,
width: w,
height: h,
duration_ms: 0,
dispose_to_background: false,
blend_with_previous: false,
};
Ok(ParsedContainer {
canvas: (w, h),
frames: vec![frame],
total_duration_ms: 0,
metadata: WebpFileMetadata::default(),
anim_background_bgra: None,
anim_loop_count: None,
})
}
b"VP8X" => parse_extended(first.data, &mut chunks),
other => Err(Error::invalid(format!(
"WebP: unexpected first chunk {:?}",
std::str::from_utf8(other).unwrap_or("???")
))),
}
}
fn parse_extended(vp8x: &[u8], chunks: &mut RiffChunks<'_>) -> Result<ParsedContainer> {
if vp8x.len() < 10 {
return Err(Error::invalid("WebP: VP8X chunk too short"));
}
let flags = vp8x[0];
let has_anim = flags & 0x02 != 0;
let canvas_w = (u32::from_le_bytes([vp8x[4], vp8x[5], vp8x[6], 0]) & 0x00FF_FFFF) + 1;
let canvas_h = (u32::from_le_bytes([vp8x[7], vp8x[8], vp8x[9], 0]) & 0x00FF_FFFF) + 1;
let mut frames: Vec<ParsedFrame> = Vec::new();
let mut pending_alph: Option<AlphChunk> = None;
let mut pending_image: Option<ImagePayload> = None;
let mut metadata = WebpFileMetadata::default();
let mut anim_background_bgra: Option<[u8; 4]> = None;
let mut anim_loop_count: Option<u16> = None;
let mut total_duration = 0u32;
while let Some(c) = chunks.next().transpose()? {
match &c.id {
b"VP8 " => {
pending_image = Some(ImagePayload::Vp8(c.data.to_vec()));
}
b"VP8L" => {
pending_image = Some(ImagePayload::Vp8l(c.data.to_vec()));
}
b"ALPH" => {
if c.data.is_empty() {
return Err(Error::invalid("WebP: ALPH chunk empty"));
}
let hdr = c.data[0];
let pre = (hdr >> 4) & 0x3;
let filt = (hdr >> 2) & 0x3;
let comp = hdr & 0x3;
pending_alph = Some(AlphChunk {
pre_processing: pre,
filtering: filt,
compression: comp,
data: c.data[1..].to_vec(),
});
}
b"ANMF" => {
let anmf = parse_anmf(c.data)?;
let f = anmf.into_frame();
total_duration = total_duration.saturating_add(f.duration_ms);
frames.push(f);
}
b"ICCP" => metadata.icc = Some(c.data.to_vec()),
b"EXIF" => metadata.exif = Some(c.data.to_vec()),
b"XMP " => metadata.xmp = Some(c.data.to_vec()),
b"ANIM" if c.data.len() >= 6 => {
anim_background_bgra = Some([c.data[0], c.data[1], c.data[2], c.data[3]]);
anim_loop_count = Some(u16::from_le_bytes([c.data[4], c.data[5]]));
}
b"ANIM" => {
}
_ => {
}
}
}
if !has_anim {
anim_background_bgra = None;
anim_loop_count = None;
}
if !has_anim {
let image = pending_image
.ok_or_else(|| Error::invalid("WebP: extended file has no image chunk"))?;
let (w, h) = match &image {
ImagePayload::Vp8(v) => parse_vp8_keyframe_dims(v).unwrap_or((canvas_w, canvas_h)),
ImagePayload::Vp8l(v) => parse_vp8l_dims(v).unwrap_or((canvas_w, canvas_h)),
};
let frame = ParsedFrame {
image,
alph: pending_alph.take(),
x_offset: 0,
y_offset: 0,
width: w,
height: h,
duration_ms: 0,
dispose_to_background: false,
blend_with_previous: false,
};
frames.push(frame);
}
Ok(ParsedContainer {
canvas: (canvas_w, canvas_h),
frames,
total_duration_ms: total_duration,
metadata,
anim_background_bgra,
anim_loop_count,
})
}
struct AnmfBundle {
x_offset: u32,
y_offset: u32,
width: u32,
height: u32,
duration_ms: u32,
dispose_to_background: bool,
blend_with_previous: bool,
image: ImagePayload,
alph: Option<AlphChunk>,
}
impl AnmfBundle {
fn into_frame(self) -> ParsedFrame {
ParsedFrame {
image: self.image,
alph: self.alph,
x_offset: self.x_offset,
y_offset: self.y_offset,
width: self.width,
height: self.height,
duration_ms: self.duration_ms,
dispose_to_background: self.dispose_to_background,
blend_with_previous: self.blend_with_previous,
}
}
}
fn parse_anmf(data: &[u8]) -> Result<AnmfBundle> {
if data.len() < 16 {
return Err(Error::invalid("WebP: ANMF header too short"));
}
let x_off = u32::from_le_bytes([data[0], data[1], data[2], 0]) & 0x00FF_FFFF;
let y_off = u32::from_le_bytes([data[3], data[4], data[5], 0]) & 0x00FF_FFFF;
let w = (u32::from_le_bytes([data[6], data[7], data[8], 0]) & 0x00FF_FFFF) + 1;
let h = (u32::from_le_bytes([data[9], data[10], data[11], 0]) & 0x00FF_FFFF) + 1;
let dur = u32::from_le_bytes([data[12], data[13], data[14], 0]) & 0x00FF_FFFF;
let flags = data[15];
let blend_with_previous = flags & 0x01 == 0;
let dispose_to_background = flags & 0x02 != 0;
let mut chunks = RiffChunks::new(&data[16..]);
let mut image: Option<ImagePayload> = None;
let mut alph: Option<AlphChunk> = None;
while let Some(c) = chunks.next().transpose()? {
match &c.id {
b"VP8 " => image = Some(ImagePayload::Vp8(c.data.to_vec())),
b"VP8L" => image = Some(ImagePayload::Vp8l(c.data.to_vec())),
b"ALPH" if !c.data.is_empty() => {
let hdr = c.data[0];
alph = Some(AlphChunk {
pre_processing: (hdr >> 4) & 0x3,
filtering: (hdr >> 2) & 0x3,
compression: hdr & 0x3,
data: c.data[1..].to_vec(),
});
}
_ => {}
}
}
let image = image.ok_or_else(|| Error::invalid("WebP: ANMF has no image chunk"))?;
Ok(AnmfBundle {
x_offset: x_off * 2, y_offset: y_off * 2,
width: w,
height: h,
duration_ms: dur,
dispose_to_background,
blend_with_previous,
image,
alph,
})
}
fn parse_vp8_keyframe_dims(vp8: &[u8]) -> Result<(u32, u32)> {
if vp8.len() < 10 {
return Err(Error::invalid("WebP: VP8 chunk too short"));
}
if vp8[3] != 0x9d || vp8[4] != 0x01 || vp8[5] != 0x2a {
return Err(Error::invalid("WebP: missing VP8 keyframe start code"));
}
let w = u16::from_le_bytes([vp8[6], vp8[7]]) as u32 & 0x3FFF;
let h = u16::from_le_bytes([vp8[8], vp8[9]]) as u32 & 0x3FFF;
Ok((w, h))
}
fn parse_vp8l_dims(vp8l: &[u8]) -> Result<(u32, u32)> {
if vp8l.len() < 5 {
return Err(Error::invalid("WebP: VP8L chunk too short"));
}
if vp8l[0] != 0x2f {
return Err(Error::invalid("WebP: bad VP8L signature"));
}
let bits = u32::from_le_bytes([vp8l[1], vp8l[2], vp8l[3], vp8l[4]]);
let w = (bits & 0x3FFF) + 1;
let h = ((bits >> 14) & 0x3FFF) + 1;
Ok((w, h))
}
pub(crate) fn parse_webp_body_lazy(body: &[u8]) -> Result<LazyParsedContainer> {
let mut chunks = RiffChunks::new(body);
let first = chunks
.next()
.transpose()?
.ok_or_else(|| Error::invalid("WebP: empty RIFF body"))?;
match &first.id {
b"VP8 " => {
let (w, h) = parse_vp8_keyframe_dims(first.data)?;
let frame = LazyParsedFrame {
image: LazyImageRef::Vp8 {
offset: first.payload_offset,
len: first.data.len(),
},
alph: None,
x_offset: 0,
y_offset: 0,
width: w,
height: h,
duration_ms: 0,
dispose_to_background: false,
blend_with_previous: false,
};
Ok(LazyParsedContainer {
canvas: (w, h),
frames: vec![frame],
total_duration_ms: 0,
metadata: WebpFileMetadata::default(),
anim_background_bgra: None,
anim_loop_count: None,
})
}
b"VP8L" => {
let (w, h) = parse_vp8l_dims(first.data)?;
let frame = LazyParsedFrame {
image: LazyImageRef::Vp8l {
offset: first.payload_offset,
len: first.data.len(),
},
alph: None,
x_offset: 0,
y_offset: 0,
width: w,
height: h,
duration_ms: 0,
dispose_to_background: false,
blend_with_previous: false,
};
Ok(LazyParsedContainer {
canvas: (w, h),
frames: vec![frame],
total_duration_ms: 0,
metadata: WebpFileMetadata::default(),
anim_background_bgra: None,
anim_loop_count: None,
})
}
b"VP8X" => parse_extended_lazy(first.data, &mut chunks),
other => Err(Error::invalid(format!(
"WebP: unexpected first chunk {:?}",
std::str::from_utf8(other).unwrap_or("???")
))),
}
}
fn parse_extended_lazy(vp8x: &[u8], chunks: &mut RiffChunks<'_>) -> Result<LazyParsedContainer> {
if vp8x.len() < 10 {
return Err(Error::invalid("WebP: VP8X chunk too short"));
}
let flags = vp8x[0];
let has_anim = flags & 0x02 != 0;
let canvas_w = (u32::from_le_bytes([vp8x[4], vp8x[5], vp8x[6], 0]) & 0x00FF_FFFF) + 1;
let canvas_h = (u32::from_le_bytes([vp8x[7], vp8x[8], vp8x[9], 0]) & 0x00FF_FFFF) + 1;
let mut frames: Vec<LazyParsedFrame> = Vec::new();
let mut pending_alph: Option<LazyAlphRef> = None;
let mut pending_image: Option<LazyImageRef> = None;
let mut metadata = WebpFileMetadata::default();
let mut anim_background_bgra: Option<[u8; 4]> = None;
let mut anim_loop_count: Option<u16> = None;
let mut total_duration = 0u32;
while let Some(c) = chunks.next().transpose()? {
match &c.id {
b"VP8 " => {
pending_image = Some(LazyImageRef::Vp8 {
offset: c.payload_offset,
len: c.data.len(),
});
}
b"VP8L" => {
pending_image = Some(LazyImageRef::Vp8l {
offset: c.payload_offset,
len: c.data.len(),
});
}
b"ALPH" => {
if c.data.is_empty() {
return Err(Error::invalid("WebP: ALPH chunk empty"));
}
let hdr = c.data[0];
pending_alph = Some(LazyAlphRef {
pre_processing: (hdr >> 4) & 0x3,
filtering: (hdr >> 2) & 0x3,
compression: hdr & 0x3,
data_offset: c.payload_offset + 1,
data_len: c.data.len() - 1,
});
}
b"ANMF" => {
let f = parse_anmf_lazy(c.data, c.payload_offset)?;
total_duration = total_duration.saturating_add(f.duration_ms);
frames.push(f);
}
b"ICCP" => metadata.icc = Some(c.data.to_vec()),
b"EXIF" => metadata.exif = Some(c.data.to_vec()),
b"XMP " => metadata.xmp = Some(c.data.to_vec()),
b"ANIM" if c.data.len() >= 6 => {
anim_background_bgra = Some([c.data[0], c.data[1], c.data[2], c.data[3]]);
anim_loop_count = Some(u16::from_le_bytes([c.data[4], c.data[5]]));
}
b"ANIM" => {}
_ => {}
}
}
if !has_anim {
anim_background_bgra = None;
anim_loop_count = None;
}
if !has_anim {
let image = pending_image
.ok_or_else(|| Error::invalid("WebP: extended file has no image chunk"))?;
let body_full_view = chunks.body;
let (w, h) = match image {
LazyImageRef::Vp8 { offset, len } => {
parse_vp8_keyframe_dims(&body_full_view[offset..offset + len])
.unwrap_or((canvas_w, canvas_h))
}
LazyImageRef::Vp8l { offset, len } => {
parse_vp8l_dims(&body_full_view[offset..offset + len])
.unwrap_or((canvas_w, canvas_h))
}
};
frames.push(LazyParsedFrame {
image,
alph: pending_alph.take(),
x_offset: 0,
y_offset: 0,
width: w,
height: h,
duration_ms: 0,
dispose_to_background: false,
blend_with_previous: false,
});
}
Ok(LazyParsedContainer {
canvas: (canvas_w, canvas_h),
frames,
total_duration_ms: total_duration,
metadata,
anim_background_bgra,
anim_loop_count,
})
}
fn parse_anmf_lazy(data: &[u8], anmf_payload_offset: usize) -> Result<LazyParsedFrame> {
if data.len() < 16 {
return Err(Error::invalid("WebP: ANMF header too short"));
}
let x_off = u32::from_le_bytes([data[0], data[1], data[2], 0]) & 0x00FF_FFFF;
let y_off = u32::from_le_bytes([data[3], data[4], data[5], 0]) & 0x00FF_FFFF;
let w = (u32::from_le_bytes([data[6], data[7], data[8], 0]) & 0x00FF_FFFF) + 1;
let h = (u32::from_le_bytes([data[9], data[10], data[11], 0]) & 0x00FF_FFFF) + 1;
let dur = u32::from_le_bytes([data[12], data[13], data[14], 0]) & 0x00FF_FFFF;
let flags = data[15];
let blend_with_previous = flags & 0x01 == 0;
let dispose_to_background = flags & 0x02 != 0;
let mut chunks = RiffChunks::new(&data[16..]);
let mut image: Option<LazyImageRef> = None;
let mut alph: Option<LazyAlphRef> = None;
while let Some(c) = chunks.next().transpose()? {
let abs_offset = anmf_payload_offset + 16 + c.payload_offset;
match &c.id {
b"VP8 " => {
image = Some(LazyImageRef::Vp8 {
offset: abs_offset,
len: c.data.len(),
});
}
b"VP8L" => {
image = Some(LazyImageRef::Vp8l {
offset: abs_offset,
len: c.data.len(),
});
}
b"ALPH" if !c.data.is_empty() => {
let hdr = c.data[0];
alph = Some(LazyAlphRef {
pre_processing: (hdr >> 4) & 0x3,
filtering: (hdr >> 2) & 0x3,
compression: hdr & 0x3,
data_offset: abs_offset + 1,
data_len: c.data.len() - 1,
});
}
_ => {}
}
}
let image = image.ok_or_else(|| Error::invalid("WebP: ANMF has no image chunk"))?;
Ok(LazyParsedFrame {
image,
alph,
x_offset: x_off * 2,
y_offset: y_off * 2,
width: w,
height: h,
duration_ms: dur,
dispose_to_background,
blend_with_previous,
})
}
struct RiffChunks<'a> {
body: &'a [u8],
pos: usize,
}
impl<'a> RiffChunks<'a> {
fn new(body: &'a [u8]) -> Self {
Self { body, pos: 0 }
}
}
struct ChunkRef<'a> {
id: [u8; 4],
data: &'a [u8],
payload_offset: usize,
}
impl<'a> Iterator for RiffChunks<'a> {
type Item = Result<ChunkRef<'a>>;
fn next(&mut self) -> Option<Self::Item> {
if self.pos + 8 > self.body.len() {
return None;
}
let id = [
self.body[self.pos],
self.body[self.pos + 1],
self.body[self.pos + 2],
self.body[self.pos + 3],
];
let size = u32::from_le_bytes([
self.body[self.pos + 4],
self.body[self.pos + 5],
self.body[self.pos + 6],
self.body[self.pos + 7],
]) as usize;
let payload_start = self.pos + 8;
let payload_end = payload_start.saturating_add(size);
if payload_end > self.body.len() {
return Some(Err(Error::invalid("WebP: chunk extends past RIFF body")));
}
let data = &self.body[payload_start..payload_end];
let payload_offset = payload_start;
let padded = (size + (size & 1)).min(self.body.len().saturating_sub(payload_start));
self.pos = payload_start + padded;
Some(Ok(ChunkRef {
id,
data,
payload_offset,
}))
}
}
#[cfg(feature = "registry")]
struct WebpDemuxer {
stream: StreamInfo,
body: Vec<u8>,
parsed: LazyParsedContainer,
time_base: TimeBase,
pos: usize,
pts: i64,
}
#[cfg(feature = "registry")]
impl Demuxer for WebpDemuxer {
fn format_name(&self) -> &str {
"webp"
}
fn streams(&self) -> &[StreamInfo] {
std::slice::from_ref(&self.stream)
}
fn next_packet(&mut self) -> oxideav_core::Result<Packet> {
if self.pos >= self.parsed.frames.len() {
return Err(oxideav_core::Error::Eof);
}
let i = self.pos;
let f = &self.parsed.frames[i];
let duration = f.duration_ms;
let data = encode_lazy_frame_payload(
f,
&self.body,
self.parsed.canvas,
self.parsed.anim_background_bgra,
)?;
let mut pkt = Packet::new(0, self.time_base, data);
pkt.pts = Some(self.pts);
pkt.dts = Some(self.pts);
pkt.duration = Some(duration.max(1) as i64);
pkt.flags.keyframe = i == 0;
self.pts += duration.max(1) as i64;
self.pos += 1;
Ok(pkt)
}
fn duration_micros(&self) -> Option<i64> {
self.stream.duration.map(|d| d * 1000)
}
}
#[cfg(all(test, feature = "registry"))]
mod tests {
use super::*;
#[test]
fn probe_recognises_webp() {
let mut buf = vec![0u8; 16];
buf[..4].copy_from_slice(b"RIFF");
buf[8..12].copy_from_slice(b"WEBP");
let p = ProbeData {
buf: &buf,
ext: None,
};
assert_eq!(probe(&p), 100);
}
#[test]
fn probe_rejects_non_webp_riff() {
let mut buf = vec![0u8; 16];
buf[..4].copy_from_slice(b"RIFF");
buf[8..12].copy_from_slice(b"AVI ");
let p = ProbeData {
buf: &buf,
ext: None,
};
assert_eq!(probe(&p), 0);
}
fn three_frame_anim_blob() -> Vec<u8> {
use crate::encoder_anim::{build_animated_webp, AnimFrame};
const W: u32 = 8;
const H: u32 = 8;
let solid = |rgba: [u8; 4]| -> Vec<u8> {
let n = (W as usize) * (H as usize);
let mut v = Vec::with_capacity(n * 4);
for _ in 0..n {
v.extend_from_slice(&rgba);
}
v
};
let red = solid([0xff, 0, 0, 0xff]);
let green = solid([0, 0xff, 0, 0xff]);
let blue = solid([0, 0, 0xff, 0xff]);
let frames = [
AnimFrame {
width: W,
height: H,
x_offset: 0,
y_offset: 0,
duration_ms: 30,
blend: false,
dispose_to_background: false,
rgba: &red,
},
AnimFrame {
width: W,
height: H,
x_offset: 0,
y_offset: 0,
duration_ms: 40,
blend: false,
dispose_to_background: false,
rgba: &green,
},
AnimFrame {
width: W,
height: H,
x_offset: 0,
y_offset: 0,
duration_ms: 50,
blend: false,
dispose_to_background: false,
rgba: &blue,
},
];
build_animated_webp(W, H, [0, 0, 0, 0], 0, &frames).expect("encode")
}
#[test]
fn streaming_demuxer_yields_frames_in_order_with_pts() {
let blob = three_frame_anim_blob();
let cursor = std::io::Cursor::new(blob);
let mut demux = open_boxed(Box::new(cursor)).expect("open");
assert_eq!(demux.streams().len(), 1);
let p0 = demux.next_packet().expect("first");
assert_eq!(p0.pts, Some(0));
assert_eq!(p0.duration, Some(30));
assert!(p0.flags.keyframe);
let p1 = demux.next_packet().expect("second");
assert_eq!(p1.pts, Some(30));
assert_eq!(p1.duration, Some(40));
assert!(!p1.flags.keyframe);
let p2 = demux.next_packet().expect("third");
assert_eq!(p2.pts, Some(70));
assert_eq!(p2.duration, Some(50));
assert!(matches!(demux.next_packet(), Err(oxideav_core::Error::Eof)));
}
#[test]
fn streaming_demuxer_packet_payload_round_trips_through_decoder() {
let blob = three_frame_anim_blob();
let cursor = std::io::Cursor::new(blob);
let mut demux = open_boxed(Box::new(cursor)).expect("open");
for i in 0..3 {
let pkt = demux.next_packet().expect("ok");
let parsed = decode_frame_payload(pkt.data.as_slice()).expect("OWEB parse");
assert_eq!(parsed.canvas, (8, 8), "frame {i} canvas");
assert_eq!(parsed.width, 8);
assert_eq!(parsed.height, 8);
assert!(!parsed.image.is_empty());
}
}
#[test]
fn streaming_demuxer_does_not_pre_allocate_packet_vec() {
let blob = three_frame_anim_blob();
let cursor = std::io::Cursor::new(blob);
let demux = open_boxed(Box::new(cursor)).expect("open");
drop(demux);
}
#[test]
fn streaming_demuxer_eof_is_repeatable() {
let blob = three_frame_anim_blob();
let cursor = std::io::Cursor::new(blob);
let mut demux = open_boxed(Box::new(cursor)).expect("open");
for _ in 0..3 {
let _ = demux.next_packet().expect("ok");
}
for _ in 0..5 {
assert!(matches!(demux.next_packet(), Err(oxideav_core::Error::Eof)));
}
}
}