use gamut_core::{Error, Result};
use super::bool_coder::{BoolDecoder, BoolEncoder};
use super::tokens::{self, CoeffProbs, DEFAULT_COEFF_PROBS};
pub const VP8_KEYFRAME_START_CODE: [u8; 3] = [0x9d, 0x01, 0x2a];
pub const UNCOMPRESSED_CHUNK_LEN: usize = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Segmentation {
pub enabled: bool,
pub update_map: bool,
pub abs_delta: bool,
pub quantizer: [i8; 4],
pub filter_strength: [i8; 4],
pub tree_probs: [u8; 3],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct LoopFilterParams {
pub simple: bool,
pub level: u8,
pub sharpness: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct QuantIndices {
pub y_ac: u8,
pub y_dc_delta: i8,
pub y2_dc_delta: i8,
pub y2_ac_delta: i8,
pub uv_dc_delta: i8,
pub uv_ac_delta: i8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Vp8FrameHeader {
pub width: u16,
pub height: u16,
pub horizontal_scale: u8,
pub vertical_scale: u8,
pub version: u8,
pub color_space: u8,
pub clamp_required: bool,
pub segmentation: Segmentation,
pub loop_filter: LoopFilterParams,
pub token_partitions: u8,
pub quant: QuantIndices,
pub refresh_entropy_probs: bool,
pub mb_no_skip_coeff: bool,
pub prob_skip_false: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UncompressedChunk {
pub is_key_frame: bool,
pub version: u8,
pub show_frame: bool,
pub first_partition_size: u32,
pub width: u16,
pub height: u16,
pub horizontal_scale: u8,
pub vertical_scale: u8,
}
fn log2_partitions(count: u8) -> u32 {
debug_assert!(
matches!(count, 1 | 2 | 4 | 8),
"token partition count must be 1, 2, 4, or 8"
);
u32::from(count).trailing_zeros()
}
pub fn write_uncompressed_chunk(
header: &Vp8FrameHeader,
first_partition_size: u32,
out: &mut Vec<u8>,
) {
let tag = (u32::from(header.version) << 1) | (1 << 4) | (first_partition_size << 5);
out.push((tag & 0xff) as u8);
out.push(((tag >> 8) & 0xff) as u8);
out.push(((tag >> 16) & 0xff) as u8);
out.extend_from_slice(&VP8_KEYFRAME_START_CODE);
let h = u32::from(header.width) | (u32::from(header.horizontal_scale) << 14);
out.push((h & 0xff) as u8);
out.push(((h >> 8) & 0xff) as u8);
let v = u32::from(header.height) | (u32::from(header.vertical_scale) << 14);
out.push((v & 0xff) as u8);
out.push(((v >> 8) & 0xff) as u8);
}
pub fn read_uncompressed_chunk(data: &[u8]) -> Result<UncompressedChunk> {
if data.len() < 3 {
return Err(Error::InvalidInput("VP8: truncated frame tag"));
}
let tag = u32::from(data[0]) | (u32::from(data[1]) << 8) | (u32::from(data[2]) << 16);
let is_key_frame = (tag & 1) == 0;
if !is_key_frame {
return Err(Error::Unsupported(
"VP8: only intra key frames are supported",
));
}
if data.len() < UNCOMPRESSED_CHUNK_LEN {
return Err(Error::InvalidInput("VP8: truncated key-frame header"));
}
if data[3..6] != VP8_KEYFRAME_START_CODE {
return Err(Error::InvalidInput("VP8: bad key-frame start code"));
}
let hsc = u32::from(data[6]) | (u32::from(data[7]) << 8);
let vsc = u32::from(data[8]) | (u32::from(data[9]) << 8);
Ok(UncompressedChunk {
is_key_frame,
version: ((tag >> 1) & 0x7) as u8,
show_frame: (tag >> 4) & 1 != 0,
first_partition_size: (tag >> 5) & 0x7_FFFF,
width: (hsc & 0x3FFF) as u16,
horizontal_scale: (hsc >> 14) as u8,
height: (vsc & 0x3FFF) as u16,
vertical_scale: (vsc >> 14) as u8,
})
}
fn write_delta(enc: &mut BoolEncoder, delta: i8) {
if delta == 0 {
enc.put_flag(false);
} else {
enc.put_flag(true);
enc.put_literal(u32::from(delta.unsigned_abs()), 4);
enc.put_flag(delta < 0);
}
}
fn read_delta(dec: &mut BoolDecoder) -> i8 {
if dec.get_flag() {
let magnitude = dec.get_literal(4) as i8;
if dec.get_flag() {
-magnitude
} else {
magnitude
}
} else {
0
}
}
fn write_quant_indices(enc: &mut BoolEncoder, quant: &QuantIndices) {
enc.put_literal(u32::from(quant.y_ac), 7);
write_delta(enc, quant.y_dc_delta);
write_delta(enc, quant.y2_dc_delta);
write_delta(enc, quant.y2_ac_delta);
write_delta(enc, quant.uv_dc_delta);
write_delta(enc, quant.uv_ac_delta);
}
fn read_quant_indices(dec: &mut BoolDecoder) -> QuantIndices {
QuantIndices {
y_ac: dec.get_literal(7) as u8,
y_dc_delta: read_delta(dec),
y2_dc_delta: read_delta(dec),
y2_ac_delta: read_delta(dec),
uv_dc_delta: read_delta(dec),
uv_ac_delta: read_delta(dec),
}
}
fn write_update_segmentation(enc: &mut BoolEncoder, seg: &Segmentation) {
enc.put_flag(seg.update_map);
let update_data = seg.abs_delta || seg.quantizer != [0; 4] || seg.filter_strength != [0; 4];
enc.put_flag(update_data);
if update_data {
enc.put_flag(seg.abs_delta); for q in seg.quantizer {
write_segment_feature(enc, q, 7);
}
for f in seg.filter_strength {
write_segment_feature(enc, f, 6);
}
}
if seg.update_map {
for p in seg.tree_probs {
if p == 255 {
enc.put_flag(false); } else {
enc.put_flag(true);
enc.put_literal(u32::from(p), 8);
}
}
}
}
fn write_segment_feature(enc: &mut BoolEncoder, value: i8, bits: u32) {
if value == 0 {
enc.put_flag(false);
} else {
enc.put_flag(true);
enc.put_literal(u32::from(value.unsigned_abs()), bits);
enc.put_flag(value < 0);
}
}
fn read_update_segmentation(dec: &mut BoolDecoder) -> Segmentation {
let update_map = dec.get_flag();
let mut seg = Segmentation {
enabled: true,
update_map,
tree_probs: [255; 3],
..Segmentation::default()
};
if dec.get_flag() {
seg.abs_delta = dec.get_flag();
for q in &mut seg.quantizer {
*q = read_segment_feature(dec, 7);
}
for f in &mut seg.filter_strength {
*f = read_segment_feature(dec, 6);
}
}
if update_map {
for p in &mut seg.tree_probs {
if dec.get_flag() {
*p = dec.get_literal(8) as u8;
}
}
}
seg
}
fn read_segment_feature(dec: &mut BoolDecoder, bits: u32) -> i8 {
if dec.get_flag() {
let mag = dec.get_literal(bits) as i8;
if dec.get_flag() { -mag } else { mag }
} else {
0
}
}
pub fn write_frame_header(enc: &mut BoolEncoder, header: &Vp8FrameHeader) {
enc.put_literal(u32::from(header.color_space), 1);
enc.put_flag(!header.clamp_required); enc.put_flag(header.segmentation.enabled);
if header.segmentation.enabled {
write_update_segmentation(enc, &header.segmentation);
}
enc.put_flag(header.loop_filter.simple); enc.put_literal(u32::from(header.loop_filter.level), 6);
enc.put_literal(u32::from(header.loop_filter.sharpness), 3);
enc.put_flag(false); enc.put_literal(log2_partitions(header.token_partitions), 2);
write_quant_indices(enc, &header.quant);
enc.put_flag(header.refresh_entropy_probs);
tokens::write_coeff_prob_updates(enc, &DEFAULT_COEFF_PROBS, &DEFAULT_COEFF_PROBS);
enc.put_flag(header.mb_no_skip_coeff);
if header.mb_no_skip_coeff {
enc.put_literal(u32::from(header.prob_skip_false), 8);
}
}
pub fn read_frame_header(
chunk: &UncompressedChunk,
dec: &mut BoolDecoder,
) -> Result<(Vp8FrameHeader, CoeffProbs)> {
let color_space = dec.get_literal(1) as u8;
let clamp_required = !dec.get_flag();
let segmentation = if dec.get_flag() {
read_update_segmentation(dec)
} else {
Segmentation::default()
};
let loop_filter = LoopFilterParams {
simple: dec.get_flag(),
level: dec.get_literal(6) as u8,
sharpness: dec.get_literal(3) as u8,
};
if dec.get_flag() {
return Err(Error::Unsupported(
"VP8: loop-filter adjustments not yet supported",
));
}
let token_partitions = 1u8 << dec.get_literal(2);
let quant = read_quant_indices(dec);
let refresh_entropy_probs = dec.get_flag();
let mut coeff_probs = DEFAULT_COEFF_PROBS;
tokens::read_coeff_prob_updates(dec, &mut coeff_probs);
let mb_no_skip_coeff = dec.get_flag();
let prob_skip_false = if mb_no_skip_coeff {
dec.get_literal(8) as u8
} else {
0
};
let header = Vp8FrameHeader {
width: chunk.width,
height: chunk.height,
horizontal_scale: chunk.horizontal_scale,
vertical_scale: chunk.vertical_scale,
version: chunk.version,
color_space,
clamp_required,
segmentation,
loop_filter,
token_partitions,
quant,
refresh_entropy_probs,
mb_no_skip_coeff,
prob_skip_false,
};
Ok((header, coeff_probs))
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_header() -> Vp8FrameHeader {
Vp8FrameHeader {
width: 176,
height: 144,
horizontal_scale: 0,
vertical_scale: 0,
version: 0,
color_space: 0,
clamp_required: true,
segmentation: Segmentation::default(),
loop_filter: LoopFilterParams {
simple: false,
level: 0,
sharpness: 0,
},
token_partitions: 1,
quant: QuantIndices::default(),
refresh_entropy_probs: true,
mb_no_skip_coeff: false,
prob_skip_false: 0,
}
}
fn roundtrip(header: &Vp8FrameHeader) {
let mut enc = BoolEncoder::new();
write_frame_header(&mut enc, header);
let part0 = enc.finish();
let mut stream = Vec::new();
write_uncompressed_chunk(header, part0.len() as u32, &mut stream);
stream.extend_from_slice(&part0);
let chunk = read_uncompressed_chunk(&stream).expect("chunk");
assert!(chunk.is_key_frame);
assert_eq!((chunk.width, chunk.height), (header.width, header.height));
assert_eq!(chunk.first_partition_size as usize, part0.len());
let end = UNCOMPRESSED_CHUNK_LEN + chunk.first_partition_size as usize;
let mut dec = BoolDecoder::new(&stream[UNCOMPRESSED_CHUNK_LEN..end]);
let (decoded, probs) = read_frame_header(&chunk, &mut dec).expect("header");
assert_eq!(&decoded, header);
assert_eq!(
probs, DEFAULT_COEFF_PROBS,
"minimal header carries no prob updates"
);
}
#[test]
fn minimal_header_round_trips() {
roundtrip(&sample_header());
}
#[test]
fn dimensions_and_scale_round_trip() {
for (w, h, hs, vs) in [
(1u16, 1u16, 0u8, 0u8),
(16, 16, 0, 0),
(16383, 1, 3, 0),
(17, 9, 1, 2),
] {
let mut header = sample_header();
header.width = w;
header.height = h;
header.horizontal_scale = hs;
header.vertical_scale = vs;
roundtrip(&header);
}
}
#[test]
fn quant_filter_and_flags_round_trip() {
let mut header = sample_header();
header.quant = QuantIndices {
y_ac: 100,
y_dc_delta: 7,
y2_dc_delta: -8,
y2_ac_delta: 15,
uv_dc_delta: -1,
uv_ac_delta: 0,
};
header.loop_filter = LoopFilterParams {
simple: true,
level: 47,
sharpness: 5,
};
header.color_space = 1;
header.clamp_required = false;
header.refresh_entropy_probs = false;
header.version = 3;
roundtrip(&header);
}
#[test]
fn skip_probability_round_trips() {
let mut header = sample_header();
header.mb_no_skip_coeff = true;
header.prob_skip_false = 210;
roundtrip(&header);
}
#[test]
fn partition_counts_round_trip() {
for count in [1u8, 2, 4, 8] {
let mut header = sample_header();
header.token_partitions = count;
roundtrip(&header);
}
}
#[test]
fn rejects_inter_frame_and_bad_start_code() {
assert!(matches!(
read_uncompressed_chunk(&[0x01, 0, 0, 0x9d, 0x01, 0x2a, 0, 0, 0, 0]),
Err(Error::Unsupported(_))
));
assert!(matches!(
read_uncompressed_chunk(&[0x00, 0, 0, 0x9d, 0x01, 0x2b, 16, 0, 16, 0]),
Err(Error::InvalidInput(_))
));
assert!(read_uncompressed_chunk(&[0x00, 0, 0]).is_err());
}
#[test]
fn rejects_unsupported_lf_adjust() {
let chunk = UncompressedChunk {
is_key_frame: true,
version: 0,
show_frame: true,
first_partition_size: 0,
width: 16,
height: 16,
horizontal_scale: 0,
vertical_scale: 0,
};
let mut lf = BoolEncoder::new();
lf.put_literal(0, 1);
lf.put_flag(true);
lf.put_flag(false);
lf.put_flag(false);
lf.put_literal(0, 6);
lf.put_literal(0, 3);
lf.put_flag(true);
let bytes = lf.finish();
assert!(matches!(
read_frame_header(&chunk, &mut BoolDecoder::new(&bytes)),
Err(Error::Unsupported(_))
));
}
#[test]
fn segmentation_round_trips() {
let mut header = sample_header();
header.segmentation = Segmentation {
enabled: true,
update_map: true,
abs_delta: false,
quantizer: [-8, -2, 5, 12],
filter_strength: [0; 4],
tree_probs: [120, 200, 64],
};
let chunk = UncompressedChunk {
is_key_frame: true,
version: 0,
show_frame: true,
first_partition_size: 0,
width: header.width,
height: header.height,
horizontal_scale: 0,
vertical_scale: 0,
};
let mut enc = BoolEncoder::new();
write_frame_header(&mut enc, &header);
let bytes = enc.finish();
let (decoded, _) = read_frame_header(&chunk, &mut BoolDecoder::new(&bytes)).unwrap();
assert_eq!(decoded.segmentation, header.segmentation);
}
}