use alloc::vec::Vec;
#[derive(Debug, Clone)]
pub struct WebPProbe {
pub width: u32,
pub height: u32,
pub has_alpha: bool,
pub has_animation: bool,
pub frame_count: u32,
pub bitstream: BitstreamType,
pub icc_profile: Option<Vec<u8>>,
}
#[derive(Debug, Clone)]
pub enum BitstreamType {
Lossy {
quality_estimate: f32,
quantizer_index: u8,
has_segment_quant: bool,
filter_level: u8,
sharpness_level: u8,
},
Lossless,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ProbeError {
TooShort,
NotWebP,
Truncated,
InvalidVP8Magic,
}
impl core::fmt::Display for ProbeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::TooShort => write!(f, "data too short to be a WebP file"),
Self::NotWebP => write!(f, "not a WebP file (missing RIFF/WEBP signature)"),
Self::Truncated => write!(f, "truncated WebP bitstream header"),
Self::InvalidVP8Magic => write!(f, "invalid VP8 magic bytes"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for ProbeError {}
pub fn probe(data: &[u8]) -> Result<WebPProbe, ProbeError> {
if data.len() < 12 {
return Err(ProbeError::TooShort);
}
if &data[0..4] != b"RIFF" || &data[8..12] != b"WEBP" {
return Err(ProbeError::NotWebP);
}
let mut pos = 12;
let mut is_extended = false;
let mut has_alpha = false;
let mut has_animation = false;
let mut frame_count = 1u32;
let mut canvas_width = 0u32;
let mut canvas_height = 0u32;
let mut icc_profile = None;
let mut bitstream_result = None;
while pos + 8 <= data.len() {
let fourcc = &data[pos..pos + 4];
let chunk_size =
u32::from_le_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]])
as usize;
let payload_start = pos + 8;
let payload_end = (payload_start + chunk_size).min(data.len());
match fourcc {
b"VP8 "
if bitstream_result.is_none() => {
bitstream_result = Some(parse_vp8_header(&data[payload_start..payload_end])?);
}
b"VP8L"
if bitstream_result.is_none() => {
if payload_end - payload_start >= 5 {
let signature = data[payload_start];
if signature == 0x2f {
let bits = u32::from_le_bytes([
data[payload_start + 1],
data[payload_start + 2],
data[payload_start + 3],
data[payload_start + 4],
]);
let w = (bits & 0x3FFF) + 1;
let h = ((bits >> 14) & 0x3FFF) + 1;
has_alpha = ((bits >> 28) & 1) != 0;
canvas_width = w;
canvas_height = h;
}
}
bitstream_result = Some(ParsedBitstream::Lossless);
}
b"VP8X" => {
is_extended = true;
if payload_end - payload_start >= 10 {
let flags = data[payload_start];
has_alpha = (flags & 0x10) != 0;
has_animation = (flags & 0x02) != 0;
let has_icc = (flags & 0x20) != 0;
let _ = has_icc; canvas_width = u32::from_le_bytes([
data[payload_start + 4],
data[payload_start + 5],
data[payload_start + 6],
0,
]) + 1;
canvas_height = u32::from_le_bytes([
data[payload_start + 7],
data[payload_start + 8],
data[payload_start + 9],
0,
]) + 1;
}
}
b"ANIM" => {
}
b"ANMF"
if has_animation => {
frame_count = frame_count.max(1);
if bitstream_result.is_none() && payload_end - payload_start >= 24 {
let sub_start = payload_start + 16;
if sub_start + 8 <= payload_end {
let sub_fourcc = &data[sub_start..sub_start + 4];
let sub_size = u32::from_le_bytes([
data[sub_start + 4],
data[sub_start + 5],
data[sub_start + 6],
data[sub_start + 7],
]) as usize;
let sub_payload_start = sub_start + 8;
let sub_payload_end = (sub_payload_start + sub_size).min(payload_end);
match sub_fourcc {
b"VP8 " => {
bitstream_result = Some(parse_vp8_header(
&data[sub_payload_start..sub_payload_end],
)?);
}
b"VP8L" => {
bitstream_result = Some(ParsedBitstream::Lossless);
}
_ => {}
}
}
}
}
b"ICCP"
if payload_end > payload_start => {
icc_profile = Some(data[payload_start..payload_end].to_vec());
}
_ => {}
}
pos = payload_start + ((chunk_size + 1) & !1);
}
if has_animation {
frame_count = 0;
let mut scan_pos = 12;
while scan_pos + 8 <= data.len() {
let fourcc = &data[scan_pos..scan_pos + 4];
let chunk_size = u32::from_le_bytes([
data[scan_pos + 4],
data[scan_pos + 5],
data[scan_pos + 6],
data[scan_pos + 7],
]) as usize;
if fourcc == b"ANMF" {
frame_count += 1;
}
scan_pos = scan_pos + 8 + ((chunk_size + 1) & !1);
}
}
let parsed = bitstream_result.ok_or(ProbeError::Truncated)?;
let bitstream = match parsed {
ParsedBitstream::Lossy(info) => {
if !is_extended {
canvas_width = info.width;
canvas_height = info.height;
}
BitstreamType::Lossy {
quality_estimate: quant_index_to_quality(info.yac_abs),
quantizer_index: info.yac_abs,
has_segment_quant: info.has_segment_quant,
filter_level: info.filter_level,
sharpness_level: info.sharpness_level,
}
}
ParsedBitstream::Lossless => BitstreamType::Lossless,
};
Ok(WebPProbe {
width: canvas_width,
height: canvas_height,
has_alpha,
has_animation,
frame_count,
bitstream,
icc_profile,
})
}
impl WebPProbe {
pub fn estimated_quality(&self) -> Option<f32> {
match &self.bitstream {
BitstreamType::Lossy {
quality_estimate, ..
} => Some(*quality_estimate),
BitstreamType::Lossless => None,
}
}
pub fn recommended_quality(&self) -> Option<f32> {
match &self.bitstream {
BitstreamType::Lossy {
quality_estimate, ..
} => {
Some(*quality_estimate)
}
BitstreamType::Lossless => None,
}
}
pub fn is_lossless(&self) -> bool {
matches!(self.bitstream, BitstreamType::Lossless)
}
}
enum ParsedBitstream {
Lossy(Vp8HeaderInfo),
Lossless,
}
struct Vp8HeaderInfo {
width: u32,
height: u32,
yac_abs: u8,
has_segment_quant: bool,
filter_level: u8,
sharpness_level: u8,
}
fn parse_vp8_header(data: &[u8]) -> Result<ParsedBitstream, ProbeError> {
if data.len() < 10 {
return Err(ProbeError::Truncated);
}
let tag = u32::from(data[0]) | (u32::from(data[1]) << 8) | (u32::from(data[2]) << 16);
let keyframe = tag & 1 == 0;
if !keyframe {
return Err(ProbeError::Truncated);
}
let first_partition_size = (tag >> 5) as usize;
if data[3..6] != [0x9d, 0x01, 0x2a] {
return Err(ProbeError::InvalidVP8Magic);
}
let w = u16::from_le_bytes([data[6], data[7]]) & 0x3FFF;
let h = u16::from_le_bytes([data[8], data[9]]) & 0x3FFF;
let part_start = 10;
let part_end = (part_start + first_partition_size).min(data.len());
if part_end <= part_start {
return Err(ProbeError::Truncated);
}
let part_data = &data[part_start..part_end];
let mut reader = MiniBoolReader::new(part_data)?;
reader.read_literal(1)?;
reader.read_literal(1)?;
let segments_enabled = reader.read_flag()?;
let mut has_segment_quant = false;
if segments_enabled {
let update_map = reader.read_flag()?;
let update_data = reader.read_flag()?;
if update_data {
has_segment_quant = true;
let absolute_delta = reader.read_flag()?;
let _ = absolute_delta;
for _ in 0..4 {
let present = reader.read_flag()?;
if present {
reader.read_literal(7)?; reader.read_literal(1)?; }
}
for _ in 0..4 {
let present = reader.read_flag()?;
if present {
reader.read_literal(6)?; reader.read_literal(1)?; }
}
}
if update_map {
for _ in 0..3 {
let present = reader.read_flag()?;
if present {
reader.read_literal(8)?;
}
}
}
}
let _filter_type = reader.read_flag()?;
let filter_level = reader.read_literal(6)?;
let sharpness_level = reader.read_literal(3)?;
let lf_adj_enabled = reader.read_flag()?;
if lf_adj_enabled {
let lf_adj_update = reader.read_flag()?;
if lf_adj_update {
for _ in 0..4 {
let present = reader.read_flag()?;
if present {
reader.read_literal(6)?;
reader.read_literal(1)?;
}
}
for _ in 0..4 {
let present = reader.read_flag()?;
if present {
reader.read_literal(6)?;
reader.read_literal(1)?;
}
}
}
}
reader.read_literal(2)?;
let yac_abs = reader.read_literal(7)?;
Ok(ParsedBitstream::Lossy(Vp8HeaderInfo {
width: w as u32,
height: h as u32,
yac_abs,
has_segment_quant,
filter_level,
sharpness_level,
}))
}
fn quant_index_to_quality(qi: u8) -> f32 {
if qi == 0 {
return 100.0;
}
if qi >= 127 {
return 0.0;
}
let compression = 1.0 - (qi as f64 / 127.0);
let linear_c = compression * compression * compression;
let quality = if linear_c < 0.5 {
linear_c * 150.0
} else {
(linear_c + 1.0) * 50.0
};
quality.clamp(0.0, 100.0) as f32
}
struct MiniBoolReader<'a> {
data: &'a [u8],
pos: usize,
range: u32,
value: u32,
bits_left: i32,
}
impl<'a> MiniBoolReader<'a> {
fn new(data: &'a [u8]) -> Result<Self, ProbeError> {
if data.len() < 2 {
return Err(ProbeError::Truncated);
}
let value = (u32::from(data[0]) << 8) | u32::from(data[1]);
Ok(Self {
data,
pos: 2,
range: 255,
value,
bits_left: 16,
})
}
fn read_flag(&mut self) -> Result<bool, ProbeError> {
Ok(self.read_bool(128)? != 0)
}
fn read_bool(&mut self, prob: u8) -> Result<u8, ProbeError> {
let split = 1 + (((self.range - 1) * u32::from(prob)) >> 8);
let big = self.value >= (split << 8);
let (new_range, bit) = if big {
(self.range - split, 1u8)
} else {
(split, 0u8)
};
if big {
self.value -= split << 8;
}
self.range = new_range;
while self.range < 128 {
self.range <<= 1;
self.value <<= 1;
self.bits_left -= 1;
if self.bits_left <= 0 {
if self.pos < self.data.len() {
self.value |= u32::from(self.data[self.pos]);
self.pos += 1;
}
self.bits_left = 8;
}
}
Ok(bit)
}
fn read_literal(&mut self, n: u8) -> Result<u8, ProbeError> {
let mut val = 0u8;
for _ in 0..n {
val = (val << 1) | self.read_bool(128)?;
}
Ok(val)
}
}
#[cfg(feature = "zencodec")]
impl zencodec::SourceEncodingDetails for WebPProbe {
fn source_generic_quality(&self) -> Option<f32> {
self.estimated_quality()
}
fn is_lossless(&self) -> bool {
matches!(self.bitstream, BitstreamType::Lossless)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_quant_roundtrip() {
use crate::encoder::fast_math::quality_to_quant_index;
for q in [0, 10, 25, 50, 75, 80, 85, 90, 95, 100] {
let qi = quality_to_quant_index(q);
let recovered = quant_index_to_quality(qi);
let diff = (recovered - q as f32).abs();
assert!(
diff < 3.0,
"Q{q} → qi={qi} → recovered={recovered:.1}, diff={diff:.1}"
);
}
}
#[test]
fn test_quant_to_quality_boundaries() {
assert_eq!(quant_index_to_quality(0), 100.0);
assert_eq!(quant_index_to_quality(127), 0.0);
let mid = quant_index_to_quality(63);
assert!(mid > 10.0 && mid < 80.0, "qi=63 → {mid}");
}
#[test]
fn test_probe_too_short() {
assert_eq!(probe(&[]).unwrap_err(), ProbeError::TooShort);
assert_eq!(probe(&[0; 11]).unwrap_err(), ProbeError::TooShort);
}
#[test]
fn test_probe_not_webp() {
let bad = b"RIFF\x00\x00\x00\x00NOPE";
assert_eq!(probe(bad).unwrap_err(), ProbeError::NotWebP);
}
}