use std::fs;
use std::path::PathBuf;
use oxideav_prores::alpha::{decode_scanned_alpha, AlphaChannelType};
use oxideav_prores::decoder::{decode_packet_with_depth, BitDepth};
use oxideav_prores::frame::ChromaFormat;
const MB_SIDE_PX: usize = 16;
fn promote_16(alpha: u16, out_max: u64) -> u16 {
let mask = 65535u64;
let num = out_max * alpha as u64;
((num + mask / 2) / mask) as u16
}
fn out_max_for(depth: BitDepth) -> u64 {
match depth {
BitDepth::Eight => 255,
BitDepth::Ten => 1023,
BitDepth::Twelve => 4095,
BitDepth::Sixteen => 65535,
_ => unreachable!("variant not exercised by this test"),
}
}
fn first_prores_frame(container: &[u8]) -> &[u8] {
let needle = b"icpf";
let mut i = 4usize;
while i + 4 <= container.len() {
if &container[i..i + 4] == needle {
let size_off = i - 4;
let frame_size =
u32::from_be_bytes(container[size_off..size_off + 4].try_into().unwrap()) as usize;
let end = size_off + frame_size;
if frame_size >= 8 && end <= container.len() {
return &container[size_off..end];
}
}
i += 1;
}
panic!("no ProRes 'icpf' frame found in fixture container");
}
struct Picture {
mbs_per_slice: usize,
num_slices: usize,
slice_sizes: Vec<usize>,
first_slice_off: usize,
width: usize,
height: usize,
}
fn parse_picture(frame: &[u8]) -> Picture {
let hdr_size = u16::from_be_bytes([frame[8], frame[9]]) as usize;
let width = u16::from_be_bytes([frame[16], frame[17]]) as usize;
let height = u16::from_be_bytes([frame[18], frame[19]]) as usize;
let pic = 8 + hdr_size;
let pic_hdr_bits = frame[pic] as usize;
assert_eq!(pic_hdr_bits % 8, 0, "picture header must be byte-aligned");
let pic_hdr_len = pic_hdr_bits / 8;
let num_slices = u16::from_be_bytes([frame[pic + 5], frame[pic + 6]]) as usize;
let mbs_per_slice = 1usize << (frame[pic + 7] >> 4);
let table = pic + pic_hdr_len;
let mut slice_sizes = Vec::with_capacity(num_slices);
for s in 0..num_slices {
slice_sizes
.push(u16::from_be_bytes([frame[table + s * 2], frame[table + s * 2 + 1]]) as usize);
}
let first_slice_off = table + num_slices * 2;
Picture {
mbs_per_slice,
num_slices,
slice_sizes,
first_slice_off,
width,
height,
}
}
fn slice_alpha<'a>(frame: &'a [u8], pic: &Picture, idx: usize) -> &'a [u8] {
let mut off = pic.first_slice_off;
for &sz in &pic.slice_sizes[..idx] {
off += sz;
}
let slice = &frame[off..off + pic.slice_sizes[idx]];
let shs = (slice[0] >> 3) as usize;
let y = u16::from_be_bytes([slice[2], slice[3]]) as usize;
let u = u16::from_be_bytes([slice[4], slice[5]]) as usize;
let v = u16::from_be_bytes([slice[6], slice[7]]) as usize;
&slice[shs + y + u + v..]
}
fn reconstruct_alpha_plane(frame: &[u8], pic: &Picture, out_max: u64) -> Vec<u16> {
let width = pic.width;
let height = pic.height;
let mut plane = vec![0u16; width * height];
let mbs_x = width.div_ceil(MB_SIDE_PX);
let slices_per_row = mbs_x.div_ceil(pic.mbs_per_slice);
assert_eq!(
pic.num_slices % slices_per_row,
0,
"slice count must be a whole number of MB rows"
);
for idx in 0..pic.num_slices {
let mb_row = idx / slices_per_row;
let col_slice = idx % slices_per_row;
let mx = col_slice * pic.mbs_per_slice;
let mbs_this = pic.mbs_per_slice.min(mbs_x - mx);
let cols = mbs_this * MB_SIDE_PX;
let blob = slice_alpha(frame, pic, idx);
let values = decode_scanned_alpha(blob, cols * MB_SIDE_PX, AlphaChannelType::Sixteen)
.unwrap_or_else(|e| panic!("slice {idx} alpha decode: {e}"));
let y0 = mb_row * MB_SIDE_PX;
let x0 = mx * MB_SIDE_PX;
for r in 0..MB_SIDE_PX {
let frame_row = y0 + r;
if frame_row >= height {
break; }
for c in 0..cols {
let frame_col = x0 + c;
if frame_col >= width {
break; }
plane[frame_row * width + frame_col] = promote_16(values[r * cols + c], out_max);
}
}
}
plane
}
fn fixture_mov() -> Option<Vec<u8>> {
let p = PathBuf::from("../../docs/video/prores/fixtures/4444-with-alpha/input.mov");
match fs::read(&p) {
Ok(b) => Some(b),
Err(e) => {
eprintln!(
"skip: missing {} ({e}). docs/ fixtures live in the workspace \
umbrella repo — the standalone crate checkout has no corpus.",
p.display()
);
None
}
}
}
fn check_at_depth(frame: &[u8], pic: &Picture, depth: BitDepth) {
let out_max = out_max_for(depth);
let reference = reconstruct_alpha_plane(frame, pic, out_max);
let vf = decode_packet_with_depth(frame, Some(0), Some((depth, ChromaFormat::Y444)))
.unwrap_or_else(|e| panic!("4444-with-alpha frame must decode at {depth:?}: {e:?}"));
assert_eq!(vf.planes.len(), 4, "alpha-bearing frame must emit 4 planes");
let a = &vf.planes[3];
let bps = depth.bytes_per_sample();
let stride_samples = a.stride / bps;
assert!(
stride_samples >= pic.width,
"{depth:?}: alpha stride {} samples < width {}",
stride_samples,
pic.width
);
let mut mismatches = 0usize;
let mut first: Option<(usize, usize, u16, u16)> = None;
for y in 0..pic.height {
let row = &a.data[y * a.stride..y * a.stride + pic.width * bps];
for x in 0..pic.width {
let got = match depth {
BitDepth::Eight => row[x] as u16,
BitDepth::Ten | BitDepth::Twelve | BitDepth::Sixteen => {
u16::from_le_bytes([row[x * 2], row[x * 2 + 1]])
}
_ => unreachable!("variant not exercised by this test"),
};
let want = reference[y * pic.width + x];
if got != want {
if first.is_none() {
first = Some((x, y, got, want));
}
mismatches += 1;
}
}
}
assert_eq!(
mismatches, 0,
"{depth:?}: decoder alpha plane diverged from the independent \
§7.5.2/§7.5.3 reconstruction in {mismatches} samples; first at {:?} \
(got vs want)",
first
);
}
#[test]
fn decoder_alpha_plane_matches_independent_reconstruction() {
let Some(mov) = fixture_mov() else { return };
let frame = first_prores_frame(&mov);
let pic = parse_picture(frame);
assert_eq!((pic.width, pic.height), (1920, 1080));
assert_eq!(pic.mbs_per_slice, 8);
assert_eq!(pic.num_slices, 1020);
for depth in [BitDepth::Twelve, BitDepth::Ten, BitDepth::Eight] {
check_at_depth(frame, &pic, depth);
}
}
#[test]
fn promotion_endpoints_match_spec() {
for &m in &[255u64, 1023, 4095] {
assert_eq!(promote_16(0, m), 0);
assert_eq!(promote_16(0xFFFF, m), m as u16);
}
assert_eq!(promote_16(0x8000, 4095), 2048);
assert_eq!(promote_16(0x4000, 4095), 1024);
assert_eq!(promote_16(0xFF00, 255), 254);
assert_eq!(promote_16(0x0100, 255), 1);
}