use crate::headers::{self, Av1StillConfig};
use crate::tile::FrameEncoder;
use gamut_color::cicp::{ColourPrimaries, MatrixCoefficients, TransferCharacteristics};
use gamut_color::{BitDepth, Planar8};
use gamut_core::{Error, Result};
#[derive(Debug, Clone)]
#[must_use]
pub struct EncodedStill {
pub obus: Vec<u8>,
pub config: Av1StillConfig,
}
#[derive(Debug, Clone)]
#[must_use]
pub struct ReconImage {
pub width: u32,
pub height: u32,
pub bit_depth: BitDepth,
pub planes: [Vec<u16>; 3],
}
pub fn encode_still_lossless_identity(planes: &Planar8) -> Result<EncodedStill> {
Ok(encode_still_intra(planes, 0)?.0)
}
pub fn encode_still_intra(planes: &Planar8, qindex: u8) -> Result<(EncodedStill, ReconImage)> {
encode_with(planes, qindex, None)
}
pub fn encode_still_intra_superres(
planes: &Planar8,
qindex: u8,
coded_denom: u8,
) -> Result<(EncodedStill, ReconImage)> {
encode_with(planes, qindex, Some(coded_denom))
}
fn encode_with(
planes: &Planar8,
qindex: u8,
coded_denom: Option<u8>,
) -> Result<(EncodedStill, ReconImage)> {
let width = planes.width();
let height = planes.height();
if width == 0 || height == 0 {
return Err(Error::InvalidInput("image has a zero dimension"));
}
let (coded_w, coded_src) = match coded_denom {
Some(cd) => {
let denom = cd as usize + 9;
let dw = crate::filter::superres_downscaled_width(width as usize, denom);
let dp: [Vec<u8>; 3] = std::array::from_fn(|i| {
crate::filter::superres_downscale_plane(
planes.plane(i),
width as usize,
dw,
height as usize,
)
});
(dw as u32, Planar8::from_planes(dw as u32, height, dp)?)
}
None => (width, planes.clone()),
};
let config = Av1StillConfig {
seq_profile: 1,
seq_level_idx_0: headers::pick_level(width, height)?,
seq_tier_0: 0,
high_bitdepth: false,
twelve_bit: false,
monochrome: false,
chroma_subsampling_x: 0,
chroma_subsampling_y: 0,
chroma_sample_position: 0,
color_primaries: ColourPrimaries::Bt709.code_point(),
transfer_characteristics: TransferCharacteristics::Srgb.code_point(),
matrix_coefficients: MatrixCoefficients::Identity.code_point(),
full_range: true,
};
let mi_cols = 2 * ((coded_w + 7) >> 3);
let mi_rows = 2 * ((height + 7) >> 3);
let seq_payload =
headers::sequence_header_payload(&config, width, height, qindex > 0, coded_denom.is_some());
let mut frame_payload =
headers::frame_header_payload(coded_w, height, mi_cols, mi_rows, qindex, coded_denom);
let (tile_bytes, recon) = FrameEncoder::new(&coded_src, qindex).encode();
for (i, tile) in tile_bytes.iter().enumerate() {
if i + 1 < tile_bytes.len() {
let sz = (tile.len() - 1) as u32;
frame_payload.extend_from_slice(&sz.to_le_bytes()[..headers::TILE_SIZE_BYTES]);
}
frame_payload.extend_from_slice(tile);
}
let (uw, uh) = (width as usize, height as usize);
let recon_planes: [Vec<u16>; 3] = if qindex == 0 {
std::array::from_fn(|i| {
crop(planes.plane(i), width, planes.width(), height)
.into_iter()
.map(u16::from)
.collect()
})
} else if coded_denom.is_some() {
let mut up: [Vec<u16>; 3] = std::array::from_fn(|i| {
crate::filter::superres_upscale_plane(
&recon.planes[i],
recon.coded_w,
coded_w as usize,
uw,
uh,
)
});
let deblock_up = crate::filter::superres_upscale_plane(
&recon.deblocked_luma,
recon.coded_w,
coded_w as usize,
uw,
uh,
);
crate::filter::loop_restore_wiener_luma(
&mut up[0],
&deblock_up,
uw,
uw,
uh,
crate::filter::WIENER_DEFAULT,
crate::filter::WIENER_DEFAULT,
);
up
} else {
let mut planes = recon.planes.clone();
crate::filter::loop_restore_wiener_luma(
&mut planes[0],
&recon.deblocked_luma,
recon.coded_w,
uw,
uh,
crate::filter::WIENER_DEFAULT,
crate::filter::WIENER_DEFAULT,
);
std::array::from_fn(|i| crop(&planes[i], width, recon.coded_w as u32, height))
};
let still = EncodedStill {
obus: headers::assemble_temporal_unit(&seq_payload, &frame_payload),
config,
};
let recon = ReconImage {
width,
height,
bit_depth: BitDepth::from_bits(recon.bit_depth).ok_or(Error::Unsupported(
"AV1: unsupported reconstruction bit depth",
))?,
planes: recon_planes,
};
Ok((still, recon))
}
fn crop<T: Copy>(plane: &[T], width: u32, src_stride: u32, height: u32) -> Vec<T> {
let (w, sw, h) = (width as usize, src_stride as usize, height as usize);
let mut out = Vec::with_capacity(w * h);
for y in 0..h {
out.extend_from_slice(&plane[y * sw..y * sw + w]);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use gamut_color::Planar8;
fn planes(w: u32, h: u32, f: impl Fn(u32, u32) -> [u8; 3]) -> Planar8 {
let mut rgb = vec![0u8; (w * h * 3) as usize];
for y in 0..h {
for x in 0..w {
let i = ((y * w + x) * 3) as usize;
let p = f(x, y);
rgb[i..i + 3].copy_from_slice(&p);
}
}
Planar8::from_rgb8_identity(&rgb, w, h).unwrap()
}
fn parse_obus(d: &[u8]) -> Vec<(u8, usize)> {
let mut out = Vec::new();
let mut i = 0;
while i < d.len() {
let hb = d[i];
i += 1;
let obu_type = (hb >> 3) & 0xf;
let has_size = (hb >> 1) & 1;
assert_eq!(has_size, 1, "M0 always sets obu_has_size_field");
let mut size = 0usize;
let mut shift = 0;
loop {
let b = d[i];
i += 1;
size |= usize::from(b & 0x7f) << shift;
shift += 7;
if b & 0x80 == 0 {
break;
}
}
out.push((obu_type, size));
i += size;
}
assert_eq!(i, d.len(), "OBUs must tile the temporal unit exactly");
out
}
#[test]
fn obu_stream_is_seq_then_frame() {
let p = planes(40, 24, |x, y| [(x * 3) as u8, (y * 5) as u8, (x + y) as u8]);
let e = encode_still_lossless_identity(&p).unwrap();
assert_eq!(e.config.seq_profile, 1);
assert_eq!(e.config.matrix_coefficients, 0);
assert_eq!(e.config.chroma_subsampling_x, 0);
assert!(e.config.full_range);
let obus = parse_obus(&e.obus);
assert_eq!(obus.len(), 2);
assert_eq!(obus[0].0, 1, "first OBU is the sequence header");
assert_eq!(obus[1].0, 6, "second OBU is the frame");
}
#[test]
fn deterministic_output() {
let p = planes(33, 17, |x, y| [(x ^ y) as u8, (x * 7) as u8, (y * 3) as u8]);
assert_eq!(
encode_still_lossless_identity(&p).unwrap().obus,
encode_still_lossless_identity(&p).unwrap().obus
);
}
#[test]
fn solid_color_uses_all_zero_path() {
let e = encode_still_lossless_identity(&planes(64, 64, |_, _| [200, 100, 50])).unwrap();
assert!(!e.obus.is_empty());
}
#[test]
fn high_contrast_exercises_golomb() {
let e = encode_still_lossless_identity(&planes(48, 48, |x, y| {
let v = if (x + y) % 2 == 0 { 0 } else { 255 };
[v, 255 - v, v]
}))
.unwrap();
assert!(!e.obus.is_empty());
}
#[test]
fn assorted_sizes_encode() {
for (w, h) in [
(1, 1),
(7, 3),
(64, 1),
(1, 64),
(100, 80),
(130, 70),
(256, 256),
] {
let p = planes(w, h, |x, y| [(x * 11) as u8, (y * 13) as u8, (x * y) as u8]);
let e = encode_still_lossless_identity(&p).unwrap();
assert_eq!(parse_obus(&e.obus).len(), 2);
}
}
#[test]
fn rejects_zero_dimension() {
let p = Planar8::from_rgb8_identity(&[], 0, 0).unwrap();
assert!(encode_still_lossless_identity(&p).is_err());
}
#[test]
fn lossy_encode_structure_and_determinism() {
for &q in &[1u8, 8, 20, 40, 90, 200, 255] {
for (w, h) in [(1, 1), (8, 8), (17, 13), (40, 24), (130, 70)] {
let p = planes(w, h, |x, y| {
[(x * 7 + y) as u8, (x ^ (y * 3)) as u8, (x + y * 5) as u8]
});
let (still, recon) = encode_still_intra(&p, q).unwrap();
assert_eq!(parse_obus(&still.obus).len(), 2, "{w}x{h} q{q}");
assert_eq!(recon.width, w);
assert_eq!(recon.height, h);
for plane in &recon.planes {
assert_eq!(plane.len(), (w * h) as usize);
}
let (again, _) = encode_still_intra(&p, q).unwrap();
assert_eq!(still.obus, again.obus, "{w}x{h} q{q} not deterministic");
}
}
}
#[test]
fn lossy_flat_image_reconstructs_near_source() {
let (_, recon) = encode_still_intra(&planes(48, 40, |_, _| [200, 100, 50]), 12).unwrap();
for (plane, &want) in recon.planes.iter().zip(&[100u8, 50, 200]) {
for &got in plane {
assert!(
i32::from(got).abs_diff(i32::from(want)) <= 3,
"flat recon {got} far from {want}"
);
}
}
}
#[test]
fn lossy_high_contrast_encodes() {
let (still, recon) = encode_still_intra(
&planes(48, 48, |x, y| {
let v = if (x + y) % 2 == 0 { 0 } else { 255 };
[v, 255 - v, v]
}),
16,
)
.unwrap();
assert_eq!(parse_obus(&still.obus).len(), 2);
assert_eq!(recon.planes[0].len(), 48 * 48);
}
}