use std::fs;
use std::path::PathBuf;
use oxideav_prores::alpha::{decode_scanned_alpha, AlphaChannelType};
const MB_SIDE_PX: usize = 16;
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 SliceAlpha<'a> {
mb_count: usize,
blob: &'a [u8],
}
fn extract_slice_alpha(frame: &[u8], target_idx: usize) -> SliceAlpha<'_> {
let hdr_size = u16::from_be_bytes([frame[8], frame[9]]) as usize;
let pic = 8 + hdr_size;
let pic_hdr_bits = frame[pic] as usize;
assert_eq!(
pic_hdr_bits % 8,
0,
"picture-header size must be a whole number of bytes"
);
let pic_hdr_len = pic_hdr_bits / 8;
let num_slices = u16::from_be_bytes([frame[pic + 5], frame[pic + 6]]) as usize;
let table = pic + pic_hdr_len;
let mut sizes = Vec::with_capacity(num_slices);
for s in 0..num_slices {
sizes.push(u16::from_be_bytes([frame[table + s * 2], frame[table + s * 2 + 1]]) as usize);
}
assert!(target_idx < num_slices, "slice index out of range");
let mut off = table + num_slices * 2;
for &sz in &sizes[..target_idx] {
off += sz;
}
let slice = &frame[off..off + sizes[target_idx]];
let slice_header_size = (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;
let alpha = &slice[slice_header_size + y + u + v..];
SliceAlpha {
mb_count: 1 << (frame[pic + 7] >> 4),
blob: alpha,
}
}
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
}
}
}
const BOTTOM_ROW_FIRST_SLICE: usize = 1005;
#[test]
fn reference_bottom_mb_row_alpha_blob_is_full_height_not_visible_height() {
let Some(mov) = fixture_mov() else { return };
let frame = first_prores_frame(&mov);
let sa = extract_slice_alpha(frame, BOTTOM_ROW_FIRST_SLICE);
assert_eq!(
sa.mb_count, 8,
"expected the default 8-MBs-per-slice layout for this fixture"
);
let cols = sa.mb_count * MB_SIDE_PX;
assert_eq!(cols, 128);
let visible_rows = 1080 - 67 * MB_SIDE_PX;
assert_eq!(visible_rows, 8);
let literal_n = cols * visible_rows; let literal = decode_scanned_alpha(sa.blob, literal_n, AlphaChannelType::Sixteen);
assert!(
literal.is_err(),
"the bottom-MB-row alpha blob decoded cleanly at the §7.5.3-literal \
visible-row length ({literal_n}); the reference bitstream is supposed \
to carry MORE values than that"
);
let full_n = cols * MB_SIDE_PX; let full = decode_scanned_alpha(sa.blob, full_n, AlphaChannelType::Sixteen)
.expect("bottom-MB-row alpha blob must decode at the full 16-row length");
assert_eq!(full.len(), full_n);
assert!(
full.iter().all(|&a| a == 0xFFFF),
"reference bottom-row slice 1005 is fully opaque"
);
}
#[test]
fn reference_bottom_mb_row_full_height_holds_for_every_slice() {
let Some(mov) = fixture_mov() else { return };
let frame = first_prores_frame(&mov);
let slices_per_row = 1920 / MB_SIDE_PX / 8;
assert_eq!(slices_per_row, 15);
let mut full_ok = 0usize;
for j in 0..slices_per_row {
let idx = BOTTOM_ROW_FIRST_SLICE + j;
let sa = extract_slice_alpha(frame, idx);
let cols = sa.mb_count * MB_SIDE_PX;
let full_n = cols * MB_SIDE_PX;
let decoded = decode_scanned_alpha(sa.blob, full_n, AlphaChannelType::Sixteen)
.unwrap_or_else(|e| panic!("slice {idx} failed at full 16-row length: {e}"));
assert_eq!(decoded.len(), full_n, "slice {idx} length");
full_ok += 1;
}
assert_eq!(
full_ok, slices_per_row,
"every bottom-MB-row slice must decode at the full MB-row height"
);
}
#[test]
fn reference_interior_mb_row_alpha_blob_is_full_height() {
let Some(mov) = fixture_mov() else { return };
let frame = first_prores_frame(&mov);
let sa = extract_slice_alpha(frame, 0);
let cols = sa.mb_count * MB_SIDE_PX;
let full_n = cols * MB_SIDE_PX;
let decoded = decode_scanned_alpha(sa.blob, full_n, AlphaChannelType::Sixteen)
.expect("interior-row alpha blob must decode at the full 16-row length");
assert_eq!(decoded.len(), full_n);
}