use oxideav_core::{Error, Result};
pub const FRAME_IDENTIFIER: &[u8; 4] = b"icpf";
pub const PRORES_RAW_FRAME_IDENTIFIER: &[u8; 4] = b"aprh";
pub const ENCODER_IDENTIFIER: &[u8; 4] = b"oxav";
pub const CHROMA_FMT_422_CODE: u8 = 2;
pub const CHROMA_FMT_444_CODE: u8 = 3;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ChromaFormat {
Y422,
Y444,
}
impl ChromaFormat {
pub fn from_code(c: u8) -> Result<Self> {
match c {
CHROMA_FMT_422_CODE => Ok(Self::Y422),
CHROMA_FMT_444_CODE => Ok(Self::Y444),
other => Err(Error::unsupported(format!(
"prores: chroma_format {other} not supported"
))),
}
}
pub fn code(self) -> u8 {
match self {
Self::Y422 => CHROMA_FMT_422_CODE,
Self::Y444 => CHROMA_FMT_444_CODE,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Profile {
Proxy,
Lt,
Standard,
Hq,
Prores4444,
Prores4444Xq,
}
impl Profile {
pub fn fourcc(self) -> &'static [u8; 4] {
match self {
Profile::Proxy => b"apco",
Profile::Lt => b"apcs",
Profile::Standard => b"apcn",
Profile::Hq => b"apch",
Profile::Prores4444 => b"ap4h",
Profile::Prores4444Xq => b"ap4x",
}
}
pub fn chroma_format(self) -> ChromaFormat {
match self {
Profile::Proxy | Profile::Lt | Profile::Standard | Profile::Hq => ChromaFormat::Y422,
Profile::Prores4444 | Profile::Prores4444Xq => ChromaFormat::Y444,
}
}
pub fn default_quant_index(self) -> u8 {
match self {
Profile::Proxy => 8,
Profile::Lt => 6,
Profile::Standard => 4,
Profile::Hq => 2,
Profile::Prores4444 => 2,
Profile::Prores4444Xq => 1,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum InterlaceMode {
Progressive = 0,
TopFieldFirst = 1,
BottomFieldFirst = 2,
}
impl InterlaceMode {
pub fn code(self) -> u8 {
self as u8
}
pub fn is_interlaced(self) -> bool {
!matches!(self, Self::Progressive)
}
}
pub fn interlace_mode_from_code(code: u8) -> Option<InterlaceMode> {
match code {
0 => Some(InterlaceMode::Progressive),
1 => Some(InterlaceMode::TopFieldFirst),
2 => Some(InterlaceMode::BottomFieldFirst),
_ => None,
}
}
#[derive(Clone, Debug)]
pub struct FrameHeader {
pub frame_size: u32,
pub frame_header_size: u16,
pub bitstream_version: u8,
pub encoder_identifier: [u8; 4],
pub width: u16,
pub height: u16,
pub chroma_format: ChromaFormat,
pub interlace_mode: u8,
pub aspect_ratio_information: u8,
pub frame_rate_code: u8,
pub color_primaries: u8,
pub transfer_characteristic: u8,
pub matrix_coefficients: u8,
pub alpha_channel_type: u8,
pub load_luma_quantization_matrix: bool,
pub load_chroma_quantization_matrix: bool,
pub luma_qmat: [u8; 64],
pub chroma_qmat: [u8; 64],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum QuantizationMatrixSource {
CustomChroma,
LumaCustom,
Default,
}
impl FrameHeader {
pub fn picture_count(&self) -> u32 {
if self.interlace_mode == 0 {
1
} else {
2
}
}
pub fn interlace_kind(&self) -> Option<InterlaceMode> {
interlace_mode_from_code(self.interlace_mode)
}
pub fn alpha_kind(&self) -> Option<AlphaChannelType> {
alpha_channel_type_from_code(self.alpha_channel_type)
}
pub fn color_primaries_kind(&self) -> Option<ColorPrimaries> {
color_primaries_from_code(self.color_primaries)
}
pub fn matrix_coefficients_kind(&self) -> Option<MatrixCoefficients> {
matrix_coefficients_from_code(self.matrix_coefficients)
}
pub fn transfer_characteristic_kind(&self) -> Option<TransferCharacteristic> {
transfer_characteristic_from_code(self.transfer_characteristic)
}
pub fn frame_rate(&self) -> Option<oxideav_core::Rational> {
rational_from_frame_rate_code(self.frame_rate_code)
}
pub fn aspect_ratio(&self) -> Option<oxideav_core::Rational> {
aspect_ratio_from_code(self.aspect_ratio_information)
}
pub fn meta(&self) -> FrameMeta {
FrameMeta {
aspect_ratio_information: self.aspect_ratio_information,
frame_rate_code: self.frame_rate_code,
color_primaries: self.color_primaries,
transfer_characteristic: self.transfer_characteristic,
matrix_coefficients: self.matrix_coefficients,
}
}
pub fn encoder_identifier(&self) -> [u8; 4] {
self.encoder_identifier
}
pub fn encoder_identifier_str(&self) -> Option<&str> {
if self
.encoder_identifier
.iter()
.all(|&b| (0x20..=0x7E).contains(&b))
{
std::str::from_utf8(&self.encoder_identifier).ok()
} else {
None
}
}
pub fn quantization_matrix_source(&self) -> QuantizationMatrixSource {
if self.load_chroma_quantization_matrix {
QuantizationMatrixSource::CustomChroma
} else if self.load_luma_quantization_matrix {
QuantizationMatrixSource::LumaCustom
} else {
QuantizationMatrixSource::Default
}
}
pub fn picture_geometry(&self) -> PictureGeometry {
let horizontal_size = self.width as usize;
let vertical_size = self.height as usize;
let width_in_mb = horizontal_size.div_ceil(MB_SIDE_PX);
let (picture_vertical_size, second_picture_vertical_size) = if self.interlace_mode == 0 {
(vertical_size, None)
} else {
let top = vertical_size.div_ceil(2); let bottom = vertical_size / 2;
match self.interlace_mode {
1 => (top, Some(bottom)),
_ => (bottom, Some(top)),
}
};
let height_in_mb = picture_vertical_size.div_ceil(MB_SIDE_PX);
let coded_width = width_in_mb * MB_SIDE_PX;
let coded_height = height_in_mb * MB_SIDE_PX;
PictureGeometry {
width_in_mb,
height_in_mb,
picture_vertical_size,
second_picture_vertical_size,
picture_count: self.picture_count(),
right_crop: coded_width - horizontal_size,
bottom_crop: coded_height - picture_vertical_size,
}
}
}
const MB_SIDE_PX: usize = 16;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PictureGeometry {
pub width_in_mb: usize,
pub height_in_mb: usize,
pub picture_vertical_size: usize,
pub second_picture_vertical_size: Option<usize>,
pub picture_count: u32,
pub right_crop: usize,
pub bottom_crop: usize,
}
impl PictureGeometry {
pub fn slice_count(&self, log2_desired_slice_size_in_mb: u8) -> usize {
slice_count(
self.width_in_mb,
log2_desired_slice_size_in_mb,
self.height_in_mb,
)
}
pub fn slices_per_mb_row(&self, log2_desired_slice_size_in_mb: u8) -> usize {
compute_slice_sizes(self.width_in_mb, log2_desired_slice_size_in_mb).len()
}
}
pub fn parse_frame(data: &[u8]) -> Result<(FrameHeader, &[u8])> {
if data.len() < 8 {
return Err(Error::invalid("prores: frame truncated (need 8 bytes)"));
}
let frame_size = u32::from_be_bytes(data[0..4].try_into().unwrap());
if &data[4..8] != FRAME_IDENTIFIER {
if &data[4..8] == PRORES_RAW_FRAME_IDENTIFIER {
return Err(Error::unsupported(
"prores: ProRes RAW sample ('aprh' marker) is not decodable — \
ProRes RAW is a separate Apple format (single-plane Bayer/CFA \
sensor data) outside the scope of SMPTE RDD 36",
));
}
return Err(Error::invalid("prores: frame magic mismatch (not 'icpf')"));
}
if (frame_size as usize) > data.len() {
return Err(Error::invalid(
"prores: frame_size exceeds available buffer",
));
}
if (frame_size as usize) < 8 {
return Err(Error::invalid(
"prores: frame_size below the 8-byte size+magic prefix",
));
}
let frame_data = &data[..frame_size as usize];
let after_magic = &frame_data[8..];
let (fh, after_fh) = parse_frame_header(after_magic)?;
Ok((fh, after_fh))
}
pub fn parse_frame_header(data: &[u8]) -> Result<(FrameHeader, &[u8])> {
if data.len() < 20 {
return Err(Error::invalid("prores: frame header truncated"));
}
let frame_header_size = u16::from_be_bytes(data[0..2].try_into().unwrap());
if (frame_header_size as usize) < 20 || (frame_header_size as usize) > data.len() {
return Err(Error::invalid("prores: bad frame_header_size"));
}
let _reserved = data[2];
let bitstream_version = data[3];
if bitstream_version > 1 {
return Err(Error::unsupported(format!(
"prores: unsupported bitstream_version {bitstream_version} \
(RDD 36 specifies versions 0 and 1)"
)));
}
let encoder_identifier: [u8; 4] = data[4..8].try_into().unwrap();
let width = u16::from_be_bytes(data[8..10].try_into().unwrap());
let height = u16::from_be_bytes(data[10..12].try_into().unwrap());
let b12 = data[12];
let chroma_code = (b12 >> 6) & 0x3;
let interlace_mode = (b12 >> 2) & 0x3;
let chroma_format = ChromaFormat::from_code(chroma_code)?;
if interlace_mode == 3 {
return Err(Error::invalid(
"prores: interlace_mode 3 is reserved (RDD 36 §6.1.1 Table 2)",
));
}
let b13 = data[13];
let aspect_ratio_information = (b13 >> 4) & 0xF;
let frame_rate_code = b13 & 0xF;
let color_primaries = data[14];
let transfer_characteristic = data[15];
let matrix_coefficients = data[16];
let b17 = data[17];
let alpha_channel_type = b17 & 0xF;
if bitstream_version == 0 {
if chroma_format != ChromaFormat::Y422 {
return Err(Error::invalid(format!(
"prores: bitstream_version 0 requires chroma_format=2 (4:2:2), got code {chroma_code} \
(RDD 36 §6.4)"
)));
}
if alpha_channel_type != 0 {
return Err(Error::invalid(format!(
"prores: bitstream_version 0 requires alpha_channel_type=0, got {alpha_channel_type} \
(RDD 36 §6.4)"
)));
}
}
let b19 = data[19];
let load_luma = (b19 >> 1) & 1;
let load_chroma = b19 & 1;
let mut luma_qmat = [4u8; 64];
let mut chroma_qmat = [4u8; 64];
let mut cursor = 20usize;
if load_luma == 1 {
if data.len() < cursor + 64 {
return Err(Error::invalid("prores: luma_qmat truncated"));
}
luma_qmat.copy_from_slice(&data[cursor..cursor + 64]);
cursor += 64;
if let Some(&bad) = luma_qmat.iter().find(|&&w| !(2..=63).contains(&w)) {
return Err(Error::invalid(format!(
"prores: luma_quantization_matrix entry {bad} out of range 2..=63 \
(RDD 36 §6.1.1)"
)));
}
}
if load_chroma == 1 {
if data.len() < cursor + 64 {
return Err(Error::invalid("prores: chroma_qmat truncated"));
}
chroma_qmat.copy_from_slice(&data[cursor..cursor + 64]);
cursor += 64;
if let Some(&bad) = chroma_qmat.iter().find(|&&w| !(2..=63).contains(&w)) {
return Err(Error::invalid(format!(
"prores: chroma_quantization_matrix entry {bad} out of range 2..=63 \
(RDD 36 §6.1.1)"
)));
}
} else if load_luma == 1 {
chroma_qmat = luma_qmat;
}
if cursor > frame_header_size as usize {
return Err(Error::invalid(
"prores: frame header parser overran declared size",
));
}
Ok((
FrameHeader {
frame_size: 0,
frame_header_size,
bitstream_version,
encoder_identifier,
width,
height,
chroma_format,
interlace_mode,
aspect_ratio_information,
frame_rate_code,
color_primaries,
transfer_characteristic,
matrix_coefficients,
alpha_channel_type,
load_luma_quantization_matrix: load_luma == 1,
load_chroma_quantization_matrix: load_chroma == 1,
luma_qmat,
chroma_qmat,
},
&data[frame_header_size as usize..],
))
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct FrameMeta {
pub aspect_ratio_information: u8,
pub frame_rate_code: u8,
pub color_primaries: u8,
pub transfer_characteristic: u8,
pub matrix_coefficients: u8,
}
impl FrameMeta {
pub fn unknown() -> Self {
Self::default()
}
pub fn is_unknown(self) -> bool {
self.aspect_ratio_information == 0
&& self.frame_rate_code == 0
&& self.color_primaries == 0
&& self.transfer_characteristic == 0
&& self.matrix_coefficients == 0
}
}
pub fn frame_rate_code_from_rational(r: oxideav_core::Rational) -> u8 {
if r.num <= 0 || r.den <= 0 {
return 0;
}
let num = r.num as i128;
let den = r.den as i128;
let candidates: &[(u8, i128, i128)] = &[
(1, 24_000, 1001),
(2, 24, 1),
(3, 25, 1),
(4, 30_000, 1001),
(5, 30, 1),
(6, 50, 1),
(7, 60_000, 1001),
(8, 60, 1),
(9, 100, 1),
(10, 120_000, 1001),
(11, 120, 1),
];
for &(code, n, d) in candidates {
if num * d == n * den {
return code;
}
}
0
}
pub fn rational_from_frame_rate_code(code: u8) -> Option<oxideav_core::Rational> {
match code {
1 => Some(oxideav_core::Rational::new(24_000, 1001)),
2 => Some(oxideav_core::Rational::new(24, 1)),
3 => Some(oxideav_core::Rational::new(25, 1)),
4 => Some(oxideav_core::Rational::new(30_000, 1001)),
5 => Some(oxideav_core::Rational::new(30, 1)),
6 => Some(oxideav_core::Rational::new(50, 1)),
7 => Some(oxideav_core::Rational::new(60_000, 1001)),
8 => Some(oxideav_core::Rational::new(60, 1)),
9 => Some(oxideav_core::Rational::new(100, 1)),
10 => Some(oxideav_core::Rational::new(120_000, 1001)),
11 => Some(oxideav_core::Rational::new(120, 1)),
_ => None,
}
}
pub fn aspect_ratio_from_code(code: u8) -> Option<oxideav_core::Rational> {
match code {
1 => Some(oxideav_core::Rational::new(1, 1)),
2 => Some(oxideav_core::Rational::new(4, 3)),
3 => Some(oxideav_core::Rational::new(16, 9)),
_ => None,
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ColorPrimaries {
Bt709 = 1,
Bt601_625 = 5,
Bt601_525 = 6,
Bt2020 = 9,
DciP3 = 11,
P3D65 = 12,
}
impl ColorPrimaries {
pub fn code(self) -> u8 {
self as u8
}
}
pub fn color_primaries_from_code(code: u8) -> Option<ColorPrimaries> {
match code {
1 => Some(ColorPrimaries::Bt709),
5 => Some(ColorPrimaries::Bt601_625),
6 => Some(ColorPrimaries::Bt601_525),
9 => Some(ColorPrimaries::Bt2020),
11 => Some(ColorPrimaries::DciP3),
12 => Some(ColorPrimaries::P3D65),
_ => None,
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum MatrixCoefficients {
Bt709 = 1,
Bt601 = 6,
Bt2020Ncl = 9,
}
impl MatrixCoefficients {
pub fn code(self) -> u8 {
self as u8
}
pub fn luma_coefficients(self) -> (f64, f64, f64) {
match self {
Self::Bt709 => (0.2126, 0.7152, 0.0722),
Self::Bt601 => (0.299, 0.587, 0.114),
Self::Bt2020Ncl => (0.2627, 0.6780, 0.0593),
}
}
}
pub fn matrix_coefficients_from_code(code: u8) -> Option<MatrixCoefficients> {
match code {
1 => Some(MatrixCoefficients::Bt709),
6 => Some(MatrixCoefficients::Bt601),
9 => Some(MatrixCoefficients::Bt2020Ncl),
_ => None,
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum TransferCharacteristic {
Bt1886 = 1,
St2084 = 16,
Hlg = 18,
}
impl TransferCharacteristic {
pub fn code(self) -> u8 {
self as u8
}
}
pub fn transfer_characteristic_from_code(code: u8) -> Option<TransferCharacteristic> {
match code {
1 => Some(TransferCharacteristic::Bt1886),
16 => Some(TransferCharacteristic::St2084),
18 => Some(TransferCharacteristic::Hlg),
_ => None,
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum AlphaChannelType {
None = 0,
Bits8 = 1,
Bits16 = 2,
}
impl AlphaChannelType {
pub fn code(self) -> u8 {
self as u8
}
pub fn has_alpha(self) -> bool {
!matches!(self, Self::None)
}
}
pub fn alpha_channel_type_from_code(code: u8) -> Option<AlphaChannelType> {
match code {
0 => Some(AlphaChannelType::None),
1 => Some(AlphaChannelType::Bits8),
2 => Some(AlphaChannelType::Bits16),
_ => None,
}
}
#[allow(clippy::too_many_arguments)]
pub fn write_frame(
out: &mut Vec<u8>,
total_frame_size: u32,
width: u16,
height: u16,
chroma_format: ChromaFormat,
interlace_mode: u8,
luma_qmat: &[u8; 64],
chroma_qmat: &[u8; 64],
load_luma: bool,
load_chroma: bool,
) {
write_frame_with_meta(
out,
total_frame_size,
width,
height,
chroma_format,
interlace_mode,
luma_qmat,
chroma_qmat,
load_luma,
load_chroma,
0,
FrameMeta::default(),
)
}
#[allow(clippy::too_many_arguments)]
pub fn write_frame_with_alpha(
out: &mut Vec<u8>,
total_frame_size: u32,
width: u16,
height: u16,
chroma_format: ChromaFormat,
interlace_mode: u8,
luma_qmat: &[u8; 64],
chroma_qmat: &[u8; 64],
load_luma: bool,
load_chroma: bool,
alpha_channel_type: u8,
) {
write_frame_with_meta(
out,
total_frame_size,
width,
height,
chroma_format,
interlace_mode,
luma_qmat,
chroma_qmat,
load_luma,
load_chroma,
alpha_channel_type,
FrameMeta::default(),
)
}
#[allow(clippy::too_many_arguments)]
pub fn write_frame_with_meta(
out: &mut Vec<u8>,
total_frame_size: u32,
width: u16,
height: u16,
chroma_format: ChromaFormat,
interlace_mode: u8,
luma_qmat: &[u8; 64],
chroma_qmat: &[u8; 64],
load_luma: bool,
load_chroma: bool,
alpha_channel_type: u8,
meta: FrameMeta,
) {
debug_assert!(alpha_channel_type <= 2);
debug_assert!(
interlace_mode <= 2,
"prores: interlace_mode {interlace_mode} is reserved (RDD 36 §6.1.1 Table 2)"
);
if load_luma {
debug_assert!(
luma_qmat.iter().all(|&w| (2..=63).contains(&w)),
"prores: luma_qmat entry out of range 2..=63 (RDD 36 §6.1.1)"
);
}
if load_chroma {
debug_assert!(
chroma_qmat.iter().all(|&w| (2..=63).contains(&w)),
"prores: chroma_qmat entry out of range 2..=63 (RDD 36 §6.1.1)"
);
}
out.extend_from_slice(&total_frame_size.to_be_bytes());
out.extend_from_slice(FRAME_IDENTIFIER);
let fh_size: u16 = 20 + if load_luma { 64 } else { 0 } + if load_chroma { 64 } else { 0 };
out.extend_from_slice(&fh_size.to_be_bytes());
out.push(0); let bitstream_version: u8 = if alpha_channel_type != 0 {
1
} else {
match chroma_format {
ChromaFormat::Y422 => 0,
ChromaFormat::Y444 => 1,
}
};
out.push(bitstream_version);
out.extend_from_slice(ENCODER_IDENTIFIER);
out.extend_from_slice(&width.to_be_bytes());
out.extend_from_slice(&height.to_be_bytes());
out.push((chroma_format.code() << 6) | ((interlace_mode & 0x3) << 2));
out.push(((meta.aspect_ratio_information & 0x0F) << 4) | (meta.frame_rate_code & 0x0F));
out.push(meta.color_primaries);
out.push(meta.transfer_characteristic);
out.push(meta.matrix_coefficients);
out.push(alpha_channel_type & 0x0F); out.push(0); let lb = ((load_luma as u8) << 1) | (load_chroma as u8);
out.push(lb);
if load_luma {
out.extend_from_slice(luma_qmat);
}
if load_chroma {
out.extend_from_slice(chroma_qmat);
}
}
#[derive(Clone, Debug)]
pub struct PictureHeader {
pub picture_header_size: u8,
pub picture_size: u32,
pub deprecated_number_of_slices: u16,
pub log2_desired_slice_size_in_mb: u8,
}
impl PictureHeader {
pub fn mbs_per_slice(&self) -> Option<u8> {
match self.log2_desired_slice_size_in_mb {
0 => Some(1),
1 => Some(2),
2 => Some(4),
3 => Some(8),
_ => None,
}
}
pub fn deprecated_slice_count(&self) -> u16 {
self.deprecated_number_of_slices
}
}
pub fn parse_picture_header(data: &[u8]) -> Result<(PictureHeader, &[u8])> {
if data.len() < 8 {
return Err(Error::invalid("prores: picture header truncated"));
}
let b0 = data[0];
let picture_header_size = (b0 >> 3) & 0x1F;
if picture_header_size < 8 {
return Err(Error::invalid("prores: picture_header_size < 8"));
}
let picture_size = u32::from_be_bytes(data[1..5].try_into().unwrap());
let deprecated_number_of_slices = u16::from_be_bytes(data[5..7].try_into().unwrap());
let b7 = data[7];
let log2_desired_slice_size_in_mb = (b7 >> 4) & 0x3;
if data.len() < picture_header_size as usize {
return Err(Error::invalid("prores: picture header overruns buffer"));
}
Ok((
PictureHeader {
picture_header_size,
picture_size,
deprecated_number_of_slices,
log2_desired_slice_size_in_mb,
},
&data[picture_header_size as usize..],
))
}
pub fn write_picture_header(
out: &mut Vec<u8>,
picture_size: u32,
deprecated_number_of_slices: u16,
log2_desired_slice_size_in_mb: u8,
) {
let picture_header_size: u8 = 8;
out.push(picture_header_size << 3);
out.extend_from_slice(&picture_size.to_be_bytes());
out.extend_from_slice(&deprecated_number_of_slices.to_be_bytes());
out.push((log2_desired_slice_size_in_mb & 0x3) << 4);
}
#[derive(Clone, Debug)]
pub struct SliceHeader {
pub slice_header_size: u8,
pub quantization_index: u8,
pub coded_size_of_y_data: u16,
pub coded_size_of_cb_data: u16,
pub coded_size_of_cr_data: Option<u16>,
}
impl SliceHeader {
pub fn qscale(&self) -> Option<i32> {
if (1..=224).contains(&self.quantization_index) {
Some(crate::quant::qscale(self.quantization_index))
} else {
None
}
}
}
pub fn parse_slice_header(data: &[u8], has_alpha: bool) -> Result<(SliceHeader, &[u8])> {
let min = if has_alpha { 8 } else { 6 };
if data.len() < min {
return Err(Error::invalid("prores: slice header truncated"));
}
let b0 = data[0];
let slice_header_size = (b0 >> 3) & 0x1F;
let quantization_index = data[1];
if !(1..=224).contains(&quantization_index) {
return Err(Error::invalid(
"prores: quantization_index out of range (1..=224)",
));
}
let coded_size_of_y_data = u16::from_be_bytes(data[2..4].try_into().unwrap());
let coded_size_of_cb_data = u16::from_be_bytes(data[4..6].try_into().unwrap());
let coded_size_of_cr_data = if has_alpha {
Some(u16::from_be_bytes(data[6..8].try_into().unwrap()))
} else {
None
};
let consumed = slice_header_size as usize;
if consumed < min {
return Err(Error::invalid("prores: slice_header_size < required"));
}
if data.len() < consumed {
return Err(Error::invalid("prores: slice header overruns buffer"));
}
Ok((
SliceHeader {
slice_header_size,
quantization_index,
coded_size_of_y_data,
coded_size_of_cb_data,
coded_size_of_cr_data,
},
&data[consumed..],
))
}
pub fn write_slice_header(
out: &mut Vec<u8>,
quantization_index: u8,
coded_size_of_y_data: u16,
coded_size_of_cb_data: u16,
coded_size_of_cr_data: Option<u16>,
) {
let slice_header_size: u8 = if coded_size_of_cr_data.is_some() {
8
} else {
6
};
out.push(slice_header_size << 3);
out.push(quantization_index);
out.extend_from_slice(&coded_size_of_y_data.to_be_bytes());
out.extend_from_slice(&coded_size_of_cb_data.to_be_bytes());
if let Some(cr) = coded_size_of_cr_data {
out.extend_from_slice(&cr.to_be_bytes());
}
}
pub fn compute_slice_sizes(width_in_mb: usize, log2_desired_slice_size_in_mb: u8) -> Vec<usize> {
let mut sizes = Vec::new();
let mut slice_size = 1usize << log2_desired_slice_size_in_mb;
let mut remaining = width_in_mb;
loop {
while remaining >= slice_size {
sizes.push(slice_size);
remaining -= slice_size;
}
slice_size /= 2;
if remaining == 0 {
break;
}
if slice_size == 0 {
break;
}
}
sizes
}
pub fn slice_count(
width_in_mb: usize,
log2_desired_slice_size_in_mb: u8,
height_in_mb: usize,
) -> usize {
compute_slice_sizes(width_in_mb, log2_desired_slice_size_in_mb).len() * height_in_mb
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_frame_rejects_prores_raw_marker() {
let mut buf = Vec::new();
buf.extend_from_slice(&16u32.to_be_bytes());
buf.extend_from_slice(PRORES_RAW_FRAME_IDENTIFIER);
buf.extend_from_slice(&[0u8; 8]);
let err = parse_frame(&buf).expect_err("ProRes RAW must be rejected");
assert!(
err.to_string().contains("ProRes RAW"),
"error should name ProRes RAW, got: {err}"
);
}
#[test]
fn parse_frame_generic_magic_mismatch_is_not_reported_as_raw() {
let mut buf = Vec::new();
buf.extend_from_slice(&16u32.to_be_bytes());
buf.extend_from_slice(b"junk");
buf.extend_from_slice(&[0u8; 8]);
let err = parse_frame(&buf).expect_err("non-ProRes bytes must be rejected");
let msg = err.to_string();
assert!(msg.contains("magic mismatch"), "got: {msg}");
assert!(!msg.contains("ProRes RAW"), "got: {msg}");
}
#[test]
fn frame_roundtrip_422() {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let mut buf = Vec::new();
write_frame(
&mut buf,
0,
128,
128,
ChromaFormat::Y422,
0,
&luma,
&chroma,
false,
false,
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.width, 128);
assert_eq!(fh.height, 128);
assert_eq!(fh.chroma_format, ChromaFormat::Y422);
assert_eq!(fh.bitstream_version, 0);
assert_eq!(fh.luma_qmat, [4u8; 64]);
assert_eq!(fh.chroma_qmat, [4u8; 64]);
}
#[test]
fn frame_roundtrip_444_with_qmats() {
let mut luma = [0u8; 64];
let mut chroma = [0u8; 64];
for i in 0..64 {
luma[i] = 2 + (i as u8 % 62); chroma[i] = 2 + ((i as u8 + 31) % 62); }
let mut buf = Vec::new();
write_frame(
&mut buf,
0,
64,
64,
ChromaFormat::Y444,
0,
&luma,
&chroma,
true,
true,
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.chroma_format, ChromaFormat::Y444);
assert_eq!(fh.bitstream_version, 1);
assert_eq!(fh.luma_qmat, luma);
assert_eq!(fh.chroma_qmat, chroma);
}
#[test]
fn picture_header_roundtrip() {
let mut buf = Vec::new();
write_picture_header(&mut buf, 1234, 12, 3);
let (ph, _) = parse_picture_header(&buf).unwrap();
assert_eq!(ph.picture_header_size, 8);
assert_eq!(ph.picture_size, 1234);
assert_eq!(ph.deprecated_number_of_slices, 12);
assert_eq!(ph.log2_desired_slice_size_in_mb, 3);
}
#[test]
fn slice_header_roundtrip_no_alpha() {
let mut buf = Vec::new();
write_slice_header(&mut buf, 4, 100, 50, None);
let (sh, _) = parse_slice_header(&buf, false).unwrap();
assert_eq!(sh.slice_header_size, 6);
assert_eq!(sh.quantization_index, 4);
assert_eq!(sh.coded_size_of_y_data, 100);
assert_eq!(sh.coded_size_of_cb_data, 50);
assert!(sh.coded_size_of_cr_data.is_none());
}
#[test]
fn slice_header_qscale_accessor_matches_table15() {
for (qi, want) in [
(1u8, 1i32),
(2, 2),
(128, 128),
(129, 132),
(130, 136),
(223, 508),
(224, 512),
] {
let mut buf = Vec::new();
write_slice_header(&mut buf, qi, 100, 50, None);
let (sh, _) = parse_slice_header(&buf, false).unwrap();
assert_eq!(sh.quantization_index, qi);
assert_eq!(sh.qscale(), Some(want), "qi={qi}");
assert_eq!(sh.qscale(), Some(crate::quant::qscale(qi)));
}
}
#[test]
fn slice_header_qscale_none_for_reserved_codes() {
for qi in [0u8, 225, 255] {
let sh = SliceHeader {
slice_header_size: 6,
quantization_index: qi,
coded_size_of_y_data: 0,
coded_size_of_cb_data: 0,
coded_size_of_cr_data: None,
};
assert_eq!(sh.qscale(), None, "qi={qi}");
}
let sh = SliceHeader {
slice_header_size: 6,
quantization_index: 1,
coded_size_of_y_data: 0,
coded_size_of_cb_data: 0,
coded_size_of_cr_data: None,
};
assert_eq!(sh.qscale(), Some(1));
}
#[test]
fn compute_slice_sizes_examples() {
assert_eq!(compute_slice_sizes(45, 3), vec![8, 8, 8, 8, 8, 4, 1]);
assert_eq!(compute_slice_sizes(8, 3), vec![8]);
assert_eq!(compute_slice_sizes(1, 3), vec![1]);
assert_eq!(compute_slice_sizes(5, 0), vec![1, 1, 1, 1, 1]);
}
#[test]
fn slice_count_matches_rdd36_corpus_geometries() {
assert_eq!(slice_count(120, 3, 68), 1020);
assert_eq!(slice_count(80, 3, 45), 450);
assert_eq!(compute_slice_sizes(20, 3), vec![8, 8, 4]);
assert_eq!(slice_count(20, 3, 15), 45);
assert_eq!(slice_count(120, 3, 34), 510);
assert_eq!(
slice_count(120, 3, 68),
compute_slice_sizes(120, 3).len() * 68
);
}
fn geom_header(width: u16, height: u16, interlace_mode: u8) -> FrameHeader {
FrameHeader {
frame_size: 0,
frame_header_size: 20,
bitstream_version: if interlace_mode == 0 { 0 } else { 1 },
encoder_identifier: *b"oxav",
width,
height,
chroma_format: ChromaFormat::Y422,
interlace_mode,
aspect_ratio_information: 0,
frame_rate_code: 0,
color_primaries: 0,
transfer_characteristic: 0,
matrix_coefficients: 0,
alpha_channel_type: 0,
load_luma_quantization_matrix: false,
load_chroma_quantization_matrix: false,
luma_qmat: [4u8; 64],
chroma_qmat: [4u8; 64],
}
}
#[test]
fn picture_geometry_progressive_corpus() {
let g = geom_header(1920, 1080, 0).picture_geometry();
assert_eq!(g.width_in_mb, 120);
assert_eq!(g.height_in_mb, 68);
assert_eq!(g.picture_vertical_size, 1080);
assert_eq!(g.second_picture_vertical_size, None);
assert_eq!(g.picture_count, 1);
assert_eq!(g.right_crop, 0);
assert_eq!(g.bottom_crop, 68 * 16 - 1080); assert_eq!(g.slices_per_mb_row(3), 15);
assert_eq!(g.slice_count(3), 1020);
let g = geom_header(1280, 720, 0).picture_geometry();
assert_eq!((g.width_in_mb, g.height_in_mb), (80, 45));
assert_eq!((g.right_crop, g.bottom_crop), (0, 0));
assert_eq!(g.slice_count(3), 450);
let g = geom_header(320, 240, 0).picture_geometry();
assert_eq!(g.slices_per_mb_row(3), 3);
assert_eq!(g.slice_count(3), 45);
}
#[test]
fn picture_geometry_interlaced_field_split() {
let g = geom_header(1920, 1080, 1).picture_geometry();
assert_eq!(g.width_in_mb, 120);
assert_eq!(g.height_in_mb, 34);
assert_eq!(g.picture_vertical_size, 540); assert_eq!(g.second_picture_vertical_size, Some(540));
assert_eq!(g.picture_count, 2);
assert_eq!(g.right_crop, 0);
assert_eq!(g.bottom_crop, 34 * 16 - 540);
assert_eq!(g.slice_count(3), 510);
let g = geom_header(1920, 1080, 2).picture_geometry();
assert_eq!(g.picture_vertical_size, 540);
assert_eq!(g.second_picture_vertical_size, Some(540));
}
#[test]
fn picture_geometry_odd_height_puts_extra_row_in_top_field() {
let g = geom_header(640, 487, 1).picture_geometry(); assert_eq!(g.picture_vertical_size, 244); assert_eq!(g.second_picture_vertical_size, Some(243));
let g = geom_header(640, 487, 2).picture_geometry(); assert_eq!(g.picture_vertical_size, 243); assert_eq!(g.second_picture_vertical_size, Some(244)); }
#[test]
fn picture_geometry_crops_non_multiple_of_16() {
let g = geom_header(1366, 766, 0).picture_geometry();
assert_eq!(g.width_in_mb, 86);
assert_eq!(g.height_in_mb, 48);
assert_eq!(g.right_crop, 86 * 16 - 1366);
assert_eq!(g.bottom_crop, 48 * 16 - 766);
}
#[test]
fn deprecated_slice_count_accessor_surfaces_wire_field() {
let mut buf = Vec::new();
write_picture_header(
&mut buf, 64, 1020, 3,
);
let (ph, _) = parse_picture_header(&buf).unwrap();
assert_eq!(ph.deprecated_slice_count(), 1020);
assert_eq!(ph.deprecated_number_of_slices, 1020);
assert_eq!(
ph.deprecated_slice_count() as usize,
slice_count(120, ph.log2_desired_slice_size_in_mb, 68)
);
let mut bogus = Vec::new();
write_picture_header(&mut bogus, 64, 7, 3);
let (ph2, _) = parse_picture_header(&bogus).unwrap();
assert_eq!(ph2.deprecated_slice_count(), 7);
assert_ne!(
ph2.deprecated_slice_count() as usize,
slice_count(120, ph2.log2_desired_slice_size_in_mb, 68)
);
}
#[test]
fn frame_rate_code_named_rates_match_spec_table_4() {
use oxideav_core::Rational;
let cases: &[(Rational, u8)] = &[
(Rational::new(24_000, 1001), 1),
(Rational::new(24, 1), 2),
(Rational::new(25, 1), 3),
(Rational::new(30_000, 1001), 4),
(Rational::new(30, 1), 5),
(Rational::new(50, 1), 6),
(Rational::new(60_000, 1001), 7),
(Rational::new(60, 1), 8),
(Rational::new(100, 1), 9),
(Rational::new(120_000, 1001), 10),
(Rational::new(120, 1), 11),
];
for &(r, expected) in cases {
let got = frame_rate_code_from_rational(r);
assert_eq!(
got, expected,
"rate {}/{} must map to {expected}",
r.num, r.den
);
}
}
#[test]
fn frame_rate_code_unnormalised_fractions_match() {
use oxideav_core::Rational;
assert_eq!(frame_rate_code_from_rational(Rational::new(60, 2)), 5);
assert_eq!(
frame_rate_code_from_rational(Rational::new(50_000, 1000)),
6
);
assert_eq!(
frame_rate_code_from_rational(Rational::new(48_000, 1001)),
0
);
}
#[test]
fn frame_rate_code_unknown_rates_map_to_zero() {
use oxideav_core::Rational;
assert_eq!(frame_rate_code_from_rational(Rational::new(48, 1)), 0);
assert_eq!(frame_rate_code_from_rational(Rational::new(90, 1)), 0);
assert_eq!(frame_rate_code_from_rational(Rational::new(0, 0)), 0);
assert_eq!(frame_rate_code_from_rational(Rational::new(-30, 1)), 0);
}
#[test]
fn frame_meta_is_unknown_helpers() {
assert!(FrameMeta::default().is_unknown());
assert!(FrameMeta::unknown().is_unknown());
let m = FrameMeta {
frame_rate_code: 5,
..FrameMeta::default()
};
assert!(!m.is_unknown());
}
#[test]
fn frame_with_meta_roundtrips_all_fields() {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let meta = FrameMeta {
aspect_ratio_information: 3, frame_rate_code: 4, color_primaries: 9, transfer_characteristic: 16, matrix_coefficients: 9, };
let mut buf = Vec::new();
write_frame_with_meta(
&mut buf,
0,
1920,
1080,
ChromaFormat::Y422,
0,
&luma,
&chroma,
false,
false,
0,
meta,
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.aspect_ratio_information, meta.aspect_ratio_information);
assert_eq!(fh.frame_rate_code, meta.frame_rate_code);
assert_eq!(fh.color_primaries, meta.color_primaries);
assert_eq!(fh.transfer_characteristic, meta.transfer_characteristic);
assert_eq!(fh.matrix_coefficients, meta.matrix_coefficients);
}
#[test]
fn rational_from_frame_rate_code_table_4_round_trip() {
use oxideav_core::Rational;
let cases: &[(u8, Rational)] = &[
(1, Rational::new(24_000, 1001)),
(2, Rational::new(24, 1)),
(3, Rational::new(25, 1)),
(4, Rational::new(30_000, 1001)),
(5, Rational::new(30, 1)),
(6, Rational::new(50, 1)),
(7, Rational::new(60_000, 1001)),
(8, Rational::new(60, 1)),
(9, Rational::new(100, 1)),
(10, Rational::new(120_000, 1001)),
(11, Rational::new(120, 1)),
];
for &(code, expected) in cases {
let got = rational_from_frame_rate_code(code).unwrap_or_else(|| {
panic!("code {code} must resolve to Some(_)");
});
assert_eq!(
got, expected,
"code {code} must map to {}/{} verbatim",
expected.num, expected.den
);
assert_eq!(
frame_rate_code_from_rational(got),
code,
"code {code} must symmetrically reverse",
);
}
}
#[test]
fn rational_from_frame_rate_code_unknown_and_reserved_are_none() {
assert!(rational_from_frame_rate_code(0).is_none());
for reserved in 12u8..=15 {
assert!(
rational_from_frame_rate_code(reserved).is_none(),
"code {reserved} is reserved and must be None"
);
}
assert!(rational_from_frame_rate_code(16).is_none());
assert!(rational_from_frame_rate_code(255).is_none());
}
#[test]
fn aspect_ratio_from_code_table_3_named_values() {
use oxideav_core::Rational;
assert_eq!(aspect_ratio_from_code(1), Some(Rational::new(1, 1)));
assert_eq!(aspect_ratio_from_code(2), Some(Rational::new(4, 3)));
assert_eq!(aspect_ratio_from_code(3), Some(Rational::new(16, 9)));
}
#[test]
fn aspect_ratio_from_code_unknown_and_reserved_are_none() {
assert!(aspect_ratio_from_code(0).is_none());
for reserved in 4u8..=15 {
assert!(
aspect_ratio_from_code(reserved).is_none(),
"code {reserved} is reserved and must be None"
);
}
assert!(aspect_ratio_from_code(16).is_none());
assert!(aspect_ratio_from_code(255).is_none());
}
#[test]
fn parsed_frame_header_meta_decodes_to_rational() {
use oxideav_core::Rational;
let luma = [4u8; 64];
let chroma = [4u8; 64];
let meta = FrameMeta {
aspect_ratio_information: 3, frame_rate_code: 8, color_primaries: 1,
transfer_characteristic: 1,
matrix_coefficients: 1,
};
let mut buf = Vec::new();
write_frame_with_meta(
&mut buf,
0,
1920,
1080,
ChromaFormat::Y422,
0,
&luma,
&chroma,
false,
false,
0,
meta,
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(
rational_from_frame_rate_code(fh.frame_rate_code),
Some(Rational::new(60, 1)),
);
assert_eq!(
aspect_ratio_from_code(fh.aspect_ratio_information),
Some(Rational::new(16, 9)),
);
}
#[test]
fn parsed_frame_header_unknown_meta_is_none_through_helpers() {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let mut buf = Vec::new();
write_frame_with_meta(
&mut buf,
0,
64,
48,
ChromaFormat::Y422,
0,
&luma,
&chroma,
false,
false,
0,
FrameMeta::default(),
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(rational_from_frame_rate_code(fh.frame_rate_code), None);
assert_eq!(aspect_ratio_from_code(fh.aspect_ratio_information), None);
}
#[test]
fn frame_with_alpha_back_compat_zeros_meta() {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let mut buf = Vec::new();
write_frame_with_alpha(
&mut buf,
0,
64,
64,
ChromaFormat::Y444,
0,
&luma,
&chroma,
false,
false,
2, );
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.aspect_ratio_information, 0);
assert_eq!(fh.frame_rate_code, 0);
assert_eq!(fh.color_primaries, 0);
assert_eq!(fh.transfer_characteristic, 0);
assert_eq!(fh.matrix_coefficients, 0);
assert_eq!(fh.alpha_channel_type, 2);
}
#[test]
fn color_primaries_from_code_named_codes_table_5() {
assert_eq!(color_primaries_from_code(1), Some(ColorPrimaries::Bt709));
assert_eq!(
color_primaries_from_code(5),
Some(ColorPrimaries::Bt601_625)
);
assert_eq!(
color_primaries_from_code(6),
Some(ColorPrimaries::Bt601_525)
);
assert_eq!(color_primaries_from_code(9), Some(ColorPrimaries::Bt2020));
assert_eq!(color_primaries_from_code(11), Some(ColorPrimaries::DciP3));
assert_eq!(color_primaries_from_code(12), Some(ColorPrimaries::P3D65));
}
#[test]
fn color_primaries_from_code_unknown_and_reserved_are_none() {
assert!(color_primaries_from_code(0).is_none());
assert!(color_primaries_from_code(2).is_none());
for reserved in [3u8, 4, 7, 8, 10] {
assert!(
color_primaries_from_code(reserved).is_none(),
"code {reserved} is reserved per Table 5"
);
}
for code in 13u16..=255 {
assert!(color_primaries_from_code(code as u8).is_none());
}
}
#[test]
fn color_primaries_code_round_trip() {
for v in [
ColorPrimaries::Bt709,
ColorPrimaries::Bt601_625,
ColorPrimaries::Bt601_525,
ColorPrimaries::Bt2020,
ColorPrimaries::DciP3,
ColorPrimaries::P3D65,
] {
assert_eq!(color_primaries_from_code(v.code()), Some(v));
}
}
#[test]
fn matrix_coefficients_from_code_named_codes_table_6() {
assert_eq!(
matrix_coefficients_from_code(1),
Some(MatrixCoefficients::Bt709)
);
assert_eq!(
matrix_coefficients_from_code(6),
Some(MatrixCoefficients::Bt601)
);
assert_eq!(
matrix_coefficients_from_code(9),
Some(MatrixCoefficients::Bt2020Ncl)
);
}
#[test]
fn matrix_coefficients_from_code_unknown_and_reserved_are_none() {
assert!(matrix_coefficients_from_code(0).is_none());
assert!(matrix_coefficients_from_code(2).is_none());
for reserved in [3u8, 4, 5, 7, 8] {
assert!(
matrix_coefficients_from_code(reserved).is_none(),
"code {reserved} is reserved per Table 6"
);
}
for code in 10u16..=255 {
assert!(matrix_coefficients_from_code(code as u8).is_none());
}
}
#[test]
fn matrix_coefficients_code_round_trip() {
for v in [
MatrixCoefficients::Bt709,
MatrixCoefficients::Bt601,
MatrixCoefficients::Bt2020Ncl,
] {
assert_eq!(matrix_coefficients_from_code(v.code()), Some(v));
}
}
#[test]
fn matrix_coefficients_luma_coefficients_match_table_6() {
assert_eq!(
MatrixCoefficients::Bt709.luma_coefficients(),
(0.2126, 0.7152, 0.0722),
);
assert_eq!(
MatrixCoefficients::Bt601.luma_coefficients(),
(0.299, 0.587, 0.114),
);
assert_eq!(
MatrixCoefficients::Bt2020Ncl.luma_coefficients(),
(0.2627, 0.6780, 0.0593),
);
for v in [
MatrixCoefficients::Bt709,
MatrixCoefficients::Bt601,
MatrixCoefficients::Bt2020Ncl,
] {
let (k_r, k_g, k_b) = v.luma_coefficients();
assert!(
(k_r + k_g + k_b - 1.0).abs() < 1e-12,
"{v:?}: K_R + K_G + K_B = {} but must = 1",
k_r + k_g + k_b,
);
}
}
#[test]
fn alpha_channel_type_from_code_named_codes_table_7() {
assert_eq!(
alpha_channel_type_from_code(0),
Some(AlphaChannelType::None)
);
assert_eq!(
alpha_channel_type_from_code(1),
Some(AlphaChannelType::Bits8)
);
assert_eq!(
alpha_channel_type_from_code(2),
Some(AlphaChannelType::Bits16)
);
}
#[test]
fn alpha_channel_type_from_code_reserved_are_none() {
for reserved in 3u8..=15 {
assert!(
alpha_channel_type_from_code(reserved).is_none(),
"code {reserved} is reserved per Table 7"
);
}
assert!(alpha_channel_type_from_code(16).is_none());
assert!(alpha_channel_type_from_code(255).is_none());
}
#[test]
fn alpha_channel_type_has_alpha_predicate() {
assert!(!AlphaChannelType::None.has_alpha());
assert!(AlphaChannelType::Bits8.has_alpha());
assert!(AlphaChannelType::Bits16.has_alpha());
}
#[test]
fn alpha_channel_type_code_round_trip() {
for v in [
AlphaChannelType::None,
AlphaChannelType::Bits8,
AlphaChannelType::Bits16,
] {
assert_eq!(alpha_channel_type_from_code(v.code()), Some(v));
}
}
#[test]
fn parsed_frame_header_color_metadata_decodes_to_named_variants() {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let meta = FrameMeta {
aspect_ratio_information: 3,
frame_rate_code: 8,
color_primaries: 9, transfer_characteristic: 16, matrix_coefficients: 9, };
let mut buf = Vec::new();
write_frame_with_meta(
&mut buf,
0,
1920,
1080,
ChromaFormat::Y422,
0,
&luma,
&chroma,
false,
false,
1, meta,
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(
color_primaries_from_code(fh.color_primaries),
Some(ColorPrimaries::Bt2020),
);
assert_eq!(
matrix_coefficients_from_code(fh.matrix_coefficients),
Some(MatrixCoefficients::Bt2020Ncl),
);
assert_eq!(
alpha_channel_type_from_code(fh.alpha_channel_type),
Some(AlphaChannelType::Bits8),
);
assert_eq!(fh.transfer_characteristic, 16);
}
#[test]
fn parsed_frame_header_unknown_color_metadata_is_none_through_helpers() {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let mut buf = Vec::new();
write_frame_with_meta(
&mut buf,
0,
64,
48,
ChromaFormat::Y422,
0,
&luma,
&chroma,
false,
false,
0,
FrameMeta::default(),
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(color_primaries_from_code(fh.color_primaries), None);
assert_eq!(matrix_coefficients_from_code(fh.matrix_coefficients), None);
assert_eq!(
alpha_channel_type_from_code(fh.alpha_channel_type),
Some(AlphaChannelType::None),
);
assert!(!AlphaChannelType::None.has_alpha());
}
fn build_with_alpha(code: u8, chroma: ChromaFormat) -> Vec<u8> {
let luma = [4u8; 64];
let cma = [4u8; 64];
let mut buf = Vec::new();
write_frame_with_alpha(
&mut buf, 0, 64, 48, chroma, 0, &luma, &cma, false, false, code,
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
buf
}
#[test]
fn alpha_kind_accessor_recognises_all_three_named_codes() {
let buf0 = build_with_alpha(0, ChromaFormat::Y422);
let (fh0, _) = parse_frame(&buf0).unwrap();
assert_eq!(fh0.alpha_kind(), Some(AlphaChannelType::None));
assert_eq!(fh0.alpha_channel_type, 0);
assert_eq!(fh0.bitstream_version, 0);
assert!(!fh0.alpha_kind().unwrap().has_alpha());
let buf1 = build_with_alpha(1, ChromaFormat::Y444);
let (fh1, _) = parse_frame(&buf1).unwrap();
assert_eq!(fh1.alpha_kind(), Some(AlphaChannelType::Bits8));
assert_eq!(fh1.alpha_channel_type, 1);
assert_eq!(fh1.bitstream_version, 1);
assert!(fh1.alpha_kind().unwrap().has_alpha());
let buf2 = build_with_alpha(2, ChromaFormat::Y444);
let (fh2, _) = parse_frame(&buf2).unwrap();
assert_eq!(fh2.alpha_kind(), Some(AlphaChannelType::Bits16));
assert_eq!(fh2.alpha_channel_type, 2);
assert_eq!(fh2.bitstream_version, 1);
assert!(fh2.alpha_kind().unwrap().has_alpha());
assert_eq!(fh0.alpha_kind().unwrap().code(), fh0.alpha_channel_type);
assert_eq!(fh1.alpha_kind().unwrap().code(), fh1.alpha_channel_type);
assert_eq!(fh2.alpha_kind().unwrap().code(), fh2.alpha_channel_type);
}
#[test]
fn alpha_kind_accessor_returns_none_for_reserved_codes() {
let fh_reserved = FrameHeader {
frame_size: 0,
frame_header_size: 20,
bitstream_version: 1,
encoder_identifier: *ENCODER_IDENTIFIER,
width: 64,
height: 48,
chroma_format: ChromaFormat::Y444,
interlace_mode: 0,
aspect_ratio_information: 0,
frame_rate_code: 0,
color_primaries: 0,
transfer_characteristic: 0,
matrix_coefficients: 0,
alpha_channel_type: 7, load_luma_quantization_matrix: false,
load_chroma_quantization_matrix: false,
luma_qmat: [4u8; 64],
chroma_qmat: [4u8; 64],
};
assert_eq!(fh_reserved.alpha_kind(), None);
for code in 3u8..=15 {
let mut fh = fh_reserved.clone();
fh.alpha_channel_type = code;
assert_eq!(
fh.alpha_kind(),
None,
"reserved code {code} must surface as outer-Option None",
);
}
}
#[test]
fn interlace_kind_accessor_recognises_all_three_named_codes() {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let build = |mode: u8| -> Vec<u8> {
let mut buf = Vec::new();
write_frame(
&mut buf,
0,
64,
48,
ChromaFormat::Y422,
mode,
&luma,
&chroma,
false,
false,
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
buf
};
let buf0 = build(0);
let (fh0, _) = parse_frame(&buf0).unwrap();
assert_eq!(fh0.interlace_kind(), Some(InterlaceMode::Progressive));
assert_eq!(fh0.interlace_mode, 0);
assert_eq!(fh0.picture_count(), 1);
assert!(!fh0.interlace_kind().unwrap().is_interlaced());
let buf1 = build(1);
let (fh1, _) = parse_frame(&buf1).unwrap();
assert_eq!(fh1.interlace_kind(), Some(InterlaceMode::TopFieldFirst));
assert_eq!(fh1.interlace_mode, 1);
assert_eq!(fh1.picture_count(), 2);
assert!(fh1.interlace_kind().unwrap().is_interlaced());
let buf2 = build(2);
let (fh2, _) = parse_frame(&buf2).unwrap();
assert_eq!(fh2.interlace_kind(), Some(InterlaceMode::BottomFieldFirst));
assert_eq!(fh2.interlace_mode, 2);
assert_eq!(fh2.picture_count(), 2);
assert!(fh2.interlace_kind().unwrap().is_interlaced());
assert_eq!(fh0.interlace_kind().unwrap().code(), fh0.interlace_mode,);
assert_eq!(fh1.interlace_kind().unwrap().code(), fh1.interlace_mode,);
assert_eq!(fh2.interlace_kind().unwrap().code(), fh2.interlace_mode,);
}
#[test]
fn interlace_mode_from_code_reserved_and_out_of_range_are_none() {
assert_eq!(
interlace_mode_from_code(0),
Some(InterlaceMode::Progressive),
);
assert_eq!(
interlace_mode_from_code(1),
Some(InterlaceMode::TopFieldFirst),
);
assert_eq!(
interlace_mode_from_code(2),
Some(InterlaceMode::BottomFieldFirst),
);
assert_eq!(interlace_mode_from_code(3), None);
for code in 4u8..=255 {
assert_eq!(
interlace_mode_from_code(code),
None,
"out-of-u2 code {code} must surface as None",
);
}
let fh_reserved = FrameHeader {
frame_size: 0,
frame_header_size: 20,
bitstream_version: 1,
encoder_identifier: *ENCODER_IDENTIFIER,
width: 64,
height: 48,
chroma_format: ChromaFormat::Y444,
interlace_mode: 3, aspect_ratio_information: 0,
frame_rate_code: 0,
color_primaries: 0,
transfer_characteristic: 0,
matrix_coefficients: 0,
alpha_channel_type: 0,
load_luma_quantization_matrix: false,
load_chroma_quantization_matrix: false,
luma_qmat: [4u8; 64],
chroma_qmat: [4u8; 64],
};
assert_eq!(fh_reserved.interlace_kind(), None);
}
#[test]
fn parse_frame_header_rejects_interlace_mode_3() {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let mut buf = Vec::new();
write_frame(
&mut buf,
0,
64,
48,
ChromaFormat::Y422,
0, &luma,
&chroma,
false,
false,
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
let byte_12 = &mut buf[20];
*byte_12 = (*byte_12 & !0b0000_1100) | (3 << 2);
let err = parse_frame(&buf).expect_err("interlace_mode 3 must be rejected");
assert!(
err.to_string().contains("interlace_mode 3"),
"error should cite the reserved interlace_mode, got: {err}"
);
}
#[test]
fn color_primaries_kind_accessor_recognises_all_six_named_codes() {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let build = |code: u8| -> Vec<u8> {
let mut buf = Vec::new();
write_frame_with_meta(
&mut buf,
0,
64,
48,
ChromaFormat::Y422,
0,
&luma,
&chroma,
false,
false,
0,
FrameMeta {
color_primaries: code,
..FrameMeta::default()
},
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
buf
};
let cases = [
(1u8, ColorPrimaries::Bt709),
(5u8, ColorPrimaries::Bt601_625),
(6u8, ColorPrimaries::Bt601_525),
(9u8, ColorPrimaries::Bt2020),
(11u8, ColorPrimaries::DciP3),
(12u8, ColorPrimaries::P3D65),
];
for (code, named) in cases {
let buf = build(code);
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.color_primaries, code);
assert_eq!(
fh.color_primaries_kind(),
Some(named),
"code {code} should surface as {named:?} via the accessor",
);
assert_eq!(
fh.color_primaries_kind().unwrap().code(),
fh.color_primaries
);
}
}
#[test]
fn color_primaries_kind_accessor_returns_none_for_unknown_and_reserved_codes() {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let build = |code: u8| -> Vec<u8> {
let mut buf = Vec::new();
write_frame_with_meta(
&mut buf,
0,
64,
48,
ChromaFormat::Y422,
0,
&luma,
&chroma,
false,
false,
0,
FrameMeta {
color_primaries: code,
..FrameMeta::default()
},
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
buf
};
for code in 0u8..=255 {
let is_named = matches!(code, 1 | 5 | 6 | 9 | 11 | 12);
if is_named {
continue;
}
let buf = build(code);
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.color_primaries, code);
assert_eq!(
fh.color_primaries_kind(),
None,
"unknown/reserved code {code} must surface as None via the accessor",
);
}
let fh_unknown = FrameHeader {
frame_size: 0,
frame_header_size: 20,
bitstream_version: 1,
encoder_identifier: *ENCODER_IDENTIFIER,
width: 64,
height: 48,
chroma_format: ChromaFormat::Y444,
interlace_mode: 0,
aspect_ratio_information: 0,
frame_rate_code: 0,
color_primaries: 0, transfer_characteristic: 0,
matrix_coefficients: 0,
alpha_channel_type: 0,
load_luma_quantization_matrix: false,
load_chroma_quantization_matrix: false,
luma_qmat: [4u8; 64],
chroma_qmat: [4u8; 64],
};
assert_eq!(fh_unknown.color_primaries_kind(), None);
}
#[test]
fn matrix_coefficients_kind_accessor_recognises_all_three_named_codes() {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let build = |code: u8| -> Vec<u8> {
let mut buf = Vec::new();
write_frame_with_meta(
&mut buf,
0,
64,
48,
ChromaFormat::Y422,
0,
&luma,
&chroma,
false,
false,
0,
FrameMeta {
matrix_coefficients: code,
..FrameMeta::default()
},
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
buf
};
let cases = [
(1u8, MatrixCoefficients::Bt709),
(6u8, MatrixCoefficients::Bt601),
(9u8, MatrixCoefficients::Bt2020Ncl),
];
for (code, named) in cases {
let buf = build(code);
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.matrix_coefficients, code);
assert_eq!(
fh.matrix_coefficients_kind(),
Some(named),
"code {code} should surface as {named:?} via the accessor",
);
assert_eq!(
fh.matrix_coefficients_kind().unwrap().code(),
fh.matrix_coefficients
);
assert_eq!(
fh.matrix_coefficients_kind().unwrap().luma_coefficients(),
named.luma_coefficients()
);
}
}
#[test]
fn matrix_coefficients_kind_accessor_returns_none_for_unknown_and_reserved_codes() {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let build = |code: u8| -> Vec<u8> {
let mut buf = Vec::new();
write_frame_with_meta(
&mut buf,
0,
64,
48,
ChromaFormat::Y422,
0,
&luma,
&chroma,
false,
false,
0,
FrameMeta {
matrix_coefficients: code,
..FrameMeta::default()
},
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
buf
};
for code in 0u8..=255 {
let is_named = matches!(code, 1 | 6 | 9);
if is_named {
continue;
}
let buf = build(code);
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.matrix_coefficients, code);
assert_eq!(
fh.matrix_coefficients_kind(),
None,
"unknown/reserved code {code} must surface as None via the accessor",
);
}
let fh_unknown = FrameHeader {
frame_size: 0,
frame_header_size: 20,
bitstream_version: 1,
encoder_identifier: *ENCODER_IDENTIFIER,
width: 64,
height: 48,
chroma_format: ChromaFormat::Y444,
interlace_mode: 0,
aspect_ratio_information: 0,
frame_rate_code: 0,
color_primaries: 0,
transfer_characteristic: 0,
matrix_coefficients: 0, alpha_channel_type: 0,
load_luma_quantization_matrix: false,
load_chroma_quantization_matrix: false,
luma_qmat: [4u8; 64],
chroma_qmat: [4u8; 64],
};
assert_eq!(fh_unknown.matrix_coefficients_kind(), None);
}
#[test]
fn transfer_characteristic_kind_accessor_recognises_all_three_named_codes() {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let build = |code: u8| -> Vec<u8> {
let mut buf = Vec::new();
write_frame_with_meta(
&mut buf,
0,
64,
48,
ChromaFormat::Y422,
0,
&luma,
&chroma,
false,
false,
0,
FrameMeta {
transfer_characteristic: code,
..FrameMeta::default()
},
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
buf
};
let cases = [
(1u8, TransferCharacteristic::Bt1886),
(16u8, TransferCharacteristic::St2084),
(18u8, TransferCharacteristic::Hlg),
];
for (code, named) in cases {
let buf = build(code);
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.transfer_characteristic, code);
assert_eq!(
fh.transfer_characteristic_kind(),
Some(named),
"code {code} should surface as {named:?} via the accessor",
);
assert_eq!(
fh.transfer_characteristic_kind().unwrap().code(),
fh.transfer_characteristic
);
}
}
#[test]
fn transfer_characteristic_kind_accessor_returns_none_for_unknown_and_reserved_codes() {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let build = |code: u8| -> Vec<u8> {
let mut buf = Vec::new();
write_frame_with_meta(
&mut buf,
0,
64,
48,
ChromaFormat::Y422,
0,
&luma,
&chroma,
false,
false,
0,
FrameMeta {
transfer_characteristic: code,
..FrameMeta::default()
},
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
buf
};
for code in 0u8..=255 {
let is_named = matches!(code, 1 | 16 | 18);
if is_named {
continue;
}
let buf = build(code);
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.transfer_characteristic, code);
assert_eq!(
fh.transfer_characteristic_kind(),
None,
"unknown/reserved code {code} must surface as None via the accessor",
);
}
let fh_unknown = FrameHeader {
frame_size: 0,
frame_header_size: 20,
bitstream_version: 1,
encoder_identifier: *ENCODER_IDENTIFIER,
width: 64,
height: 48,
chroma_format: ChromaFormat::Y444,
interlace_mode: 0,
aspect_ratio_information: 0,
frame_rate_code: 0,
color_primaries: 0,
transfer_characteristic: 0, matrix_coefficients: 0,
alpha_channel_type: 0,
load_luma_quantization_matrix: false,
load_chroma_quantization_matrix: false,
luma_qmat: [4u8; 64],
chroma_qmat: [4u8; 64],
};
assert_eq!(fh_unknown.transfer_characteristic_kind(), None);
}
#[test]
fn frame_rate_accessor_recognises_all_eleven_named_codes() {
use oxideav_core::Rational;
let luma = [4u8; 64];
let chroma = [4u8; 64];
let build = |code: u8| -> Vec<u8> {
let mut buf = Vec::new();
write_frame_with_meta(
&mut buf,
0,
64,
48,
ChromaFormat::Y422,
0,
&luma,
&chroma,
false,
false,
0,
FrameMeta {
frame_rate_code: code,
..FrameMeta::default()
},
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
buf
};
let cases = [
(1u8, Rational::new(24_000, 1001)),
(2u8, Rational::new(24, 1)),
(3u8, Rational::new(25, 1)),
(4u8, Rational::new(30_000, 1001)),
(5u8, Rational::new(30, 1)),
(6u8, Rational::new(50, 1)),
(7u8, Rational::new(60_000, 1001)),
(8u8, Rational::new(60, 1)),
(9u8, Rational::new(100, 1)),
(10u8, Rational::new(120_000, 1001)),
(11u8, Rational::new(120, 1)),
];
for (code, named) in cases {
let buf = build(code);
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.frame_rate_code, code);
assert_eq!(
fh.frame_rate(),
Some(named),
"code {code} should surface as {named:?} via the accessor",
);
assert_eq!(
frame_rate_code_from_rational(fh.frame_rate().unwrap()),
fh.frame_rate_code
);
}
}
#[test]
fn frame_rate_accessor_returns_none_for_unknown_and_reserved_codes() {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let build = |code: u8| -> Vec<u8> {
let mut buf = Vec::new();
write_frame_with_meta(
&mut buf,
0,
64,
48,
ChromaFormat::Y422,
0,
&luma,
&chroma,
false,
false,
0,
FrameMeta {
frame_rate_code: code,
..FrameMeta::default()
},
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
buf
};
let buf = build(0);
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.frame_rate_code, 0);
assert_eq!(fh.frame_rate(), None);
for code in 12u8..=15 {
let buf = build(code);
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.frame_rate_code, code);
assert_eq!(
fh.frame_rate(),
None,
"reserved code {code} must surface as outer-Option None",
);
}
for code in [0u8, 12, 13, 14, 15, 16, 100, 255] {
let fh = FrameHeader {
frame_size: 0,
frame_header_size: 20,
bitstream_version: 1,
encoder_identifier: *ENCODER_IDENTIFIER,
width: 64,
height: 48,
chroma_format: ChromaFormat::Y444,
interlace_mode: 0,
aspect_ratio_information: 0,
frame_rate_code: code,
color_primaries: 0,
transfer_characteristic: 0,
matrix_coefficients: 0,
alpha_channel_type: 0,
load_luma_quantization_matrix: false,
load_chroma_quantization_matrix: false,
luma_qmat: [4u8; 64],
chroma_qmat: [4u8; 64],
};
assert_eq!(
fh.frame_rate(),
None,
"code {code} must surface as outer-Option None on hand-built struct",
);
}
}
#[test]
fn aspect_ratio_accessor_named_codes_round_trip_through_parse() {
use oxideav_core::Rational;
let luma = [4u8; 64];
let chroma = [4u8; 64];
let cases: &[(u8, Rational)] = &[
(1, Rational::new(1, 1)),
(2, Rational::new(4, 3)),
(3, Rational::new(16, 9)),
];
for &(code, expected) in cases {
let meta = FrameMeta {
aspect_ratio_information: code,
..FrameMeta::default()
};
let mut buf = Vec::new();
write_frame_with_meta(
&mut buf,
0,
1920,
1080,
ChromaFormat::Y422,
0,
&luma,
&chroma,
false,
false,
0,
meta,
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.aspect_ratio_information, code);
assert_eq!(
fh.aspect_ratio(),
Some(expected),
"code {code} must lift to {}/{} via the typed accessor",
expected.num,
expected.den,
);
assert_eq!(
aspect_ratio_from_code(fh.aspect_ratio_information),
fh.aspect_ratio(),
"typed accessor must agree with aspect_ratio_from_code for code {code}",
);
}
}
#[test]
fn aspect_ratio_accessor_returns_none_for_unknown_and_reserved_codes() {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let build = |code: u8| -> Vec<u8> {
let mut buf = Vec::new();
write_frame_with_meta(
&mut buf,
0,
64,
48,
ChromaFormat::Y422,
0,
&luma,
&chroma,
false,
false,
0,
FrameMeta {
aspect_ratio_information: code,
..FrameMeta::default()
},
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
buf
};
let buf = build(0);
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.aspect_ratio_information, 0);
assert_eq!(fh.aspect_ratio(), None);
for code in 4u8..=15 {
let buf = build(code);
let (fh, _) = parse_frame(&buf).unwrap();
assert_eq!(fh.aspect_ratio_information, code);
assert_eq!(
fh.aspect_ratio(),
None,
"reserved code {code} must surface as outer-Option None",
);
}
for code in [0u8, 4, 5, 14, 15, 16, 100, 255] {
let fh = FrameHeader {
frame_size: 0,
frame_header_size: 20,
bitstream_version: 1,
encoder_identifier: *ENCODER_IDENTIFIER,
width: 64,
height: 48,
chroma_format: ChromaFormat::Y444,
interlace_mode: 0,
aspect_ratio_information: code,
frame_rate_code: 0,
color_primaries: 0,
transfer_characteristic: 0,
matrix_coefficients: 0,
alpha_channel_type: 0,
load_luma_quantization_matrix: false,
load_chroma_quantization_matrix: false,
luma_qmat: [4u8; 64],
chroma_qmat: [4u8; 64],
};
assert_eq!(
fh.aspect_ratio(),
None,
"code {code} must surface as outer-Option None on hand-built struct",
);
}
}
#[test]
fn mbs_per_slice_accessor_recognises_all_four_named_codes() {
let cases = [(0u8, 1u8), (1, 2), (2, 4), (3, 8)];
for (code, expected_mbs) in cases {
let mut buf = Vec::new();
write_picture_header(&mut buf, 4096, 1, code);
let (ph, _) = parse_picture_header(&buf).unwrap();
assert_eq!(ph.log2_desired_slice_size_in_mb, code);
assert_eq!(
ph.mbs_per_slice(),
Some(expected_mbs),
"code {code} should surface as {expected_mbs}-MBs-per-slice via the accessor",
);
assert_eq!(
ph.mbs_per_slice().unwrap(),
1u8 << ph.log2_desired_slice_size_in_mb,
"accessor must agree with `1 << log2_desired_slice_size_in_mb` for code {code}",
);
}
}
#[test]
fn mbs_per_slice_accessor_returns_none_for_out_of_range_codes() {
for code in [4u8, 5, 7, 8, 15, 16, 100, 255] {
let ph = PictureHeader {
picture_header_size: 8,
picture_size: 0,
deprecated_number_of_slices: 0,
log2_desired_slice_size_in_mb: code,
};
assert_eq!(
ph.mbs_per_slice(),
None,
"code {code} must surface as outer-Option None on hand-built struct",
);
}
for code in 0u8..=3 {
let mut buf = Vec::new();
write_picture_header(&mut buf, 0, 0, code);
let (ph, _) = parse_picture_header(&buf).unwrap();
assert!(
ph.mbs_per_slice().is_some(),
"every parsed picture header should have a defined slice width (code {code})",
);
}
}
fn parse_header_with_meta(meta: FrameMeta) -> FrameHeader {
let luma = [4u8; 64];
let chroma = [4u8; 64];
let mut buf = Vec::new();
write_frame_with_meta(
&mut buf,
0,
64,
48,
ChromaFormat::Y422,
0,
&luma,
&chroma,
false,
false,
0,
meta,
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
let (fh, _) = parse_frame(&buf).unwrap();
fh
}
#[test]
fn meta_accessor_round_trips_named_codes_through_parse() {
use oxideav_core::Rational;
let src = FrameMeta {
aspect_ratio_information: 3,
frame_rate_code: 8,
color_primaries: 9,
transfer_characteristic: 16,
matrix_coefficients: 9,
};
let fh = parse_header_with_meta(src);
let meta = fh.meta();
assert_eq!(meta, src, "fh.meta() must equal the written FrameMeta");
assert_eq!(meta.aspect_ratio_information, fh.aspect_ratio_information);
assert_eq!(meta.frame_rate_code, fh.frame_rate_code);
assert_eq!(meta.color_primaries, fh.color_primaries);
assert_eq!(meta.transfer_characteristic, fh.transfer_characteristic);
assert_eq!(meta.matrix_coefficients, fh.matrix_coefficients);
assert_eq!(fh.aspect_ratio(), Some(Rational::new(16, 9)));
assert_eq!(fh.frame_rate(), Some(Rational::new(60, 1)));
assert_eq!(fh.color_primaries_kind(), Some(ColorPrimaries::Bt2020));
assert_eq!(
fh.transfer_characteristic_kind(),
Some(TransferCharacteristic::St2084)
);
assert_eq!(
fh.matrix_coefficients_kind(),
Some(MatrixCoefficients::Bt2020Ncl)
);
assert!(!meta.is_unknown());
}
#[test]
fn meta_accessor_preserves_unknown_and_reserved_codes_verbatim() {
let fh = parse_header_with_meta(FrameMeta::default());
assert_eq!(fh.meta(), FrameMeta::unknown());
assert!(fh.meta().is_unknown());
let src = FrameMeta {
aspect_ratio_information: 15,
frame_rate_code: 12,
color_primaries: 3,
transfer_characteristic: 17,
matrix_coefficients: 4,
};
let fh = parse_header_with_meta(src);
assert_eq!(fh.meta(), src, "reserved codes must fold through verbatim",);
assert_eq!(fh.aspect_ratio(), None);
assert_eq!(fh.frame_rate(), None);
assert_eq!(fh.color_primaries_kind(), None);
assert_eq!(fh.transfer_characteristic_kind(), None);
assert_eq!(fh.matrix_coefficients_kind(), None);
assert!(!fh.meta().is_unknown());
}
#[test]
fn encoder_identifier_round_trips_through_parse() {
let fh = parse_header_with_meta(FrameMeta::default());
assert_eq!(
fh.encoder_identifier(),
*ENCODER_IDENTIFIER,
"parsed header must surface the written encoder_identifier verbatim"
);
assert_eq!(
fh.encoder_identifier_str(),
Some("oxav"),
"printable-ASCII encoder_identifier lifts to its FourCC string"
);
let fh2 = parse_header_with_meta(FrameMeta {
aspect_ratio_information: 3,
frame_rate_code: 8,
color_primaries: 9,
transfer_characteristic: 16,
matrix_coefficients: 9,
});
assert_eq!(fh2.encoder_identifier(), *ENCODER_IDENTIFIER);
assert_eq!(fh2.encoder_identifier_str(), Some("oxav"));
}
#[test]
fn encoder_identifier_str_rejects_non_printable_bytes() {
let mut hdr = vec![0u8; 20];
hdr[0] = 0; hdr[1] = 20; hdr[2] = 0; hdr[3] = 0; hdr[4] = b'A';
hdr[5] = 0x00; hdr[6] = b'p';
hdr[7] = b'l';
hdr[8] = 0;
hdr[9] = 64;
hdr[10] = 0;
hdr[11] = 48;
hdr[12] = 0b1000_0000;
let (fh, _) = parse_frame_header(&hdr).unwrap();
assert_eq!(
fh.encoder_identifier(),
[b'A', 0x00, b'p', b'l'],
"raw encoder_identifier bytes are surfaced even when non-printable"
);
assert_eq!(
fh.encoder_identifier_str(),
None,
"a non-printable byte makes the string accessor return None"
);
}
#[test]
fn quantization_matrix_source_reflects_load_flags() {
let mut custom_luma = [4u8; 64];
for (i, w) in custom_luma.iter_mut().enumerate() {
*w = 2 + (i as u8 % 62); }
let mut custom_chroma = [4u8; 64];
for (i, w) in custom_chroma.iter_mut().enumerate() {
*w = 63 - (i as u8 % 62); }
let build = |load_luma: bool, load_chroma: bool| -> FrameHeader {
let mut buf = Vec::new();
write_frame(
&mut buf,
0,
64,
48,
ChromaFormat::Y444,
0,
&custom_luma,
&custom_chroma,
load_luma,
load_chroma,
);
let total = buf.len() as u32;
buf[0..4].copy_from_slice(&total.to_be_bytes());
parse_frame(&buf).expect("parse").0
};
let fh = build(true, true);
assert!(fh.load_luma_quantization_matrix);
assert!(fh.load_chroma_quantization_matrix);
assert_eq!(
fh.quantization_matrix_source(),
QuantizationMatrixSource::CustomChroma
);
assert_eq!(fh.chroma_qmat, custom_chroma);
assert_eq!(fh.luma_qmat, custom_luma);
let fh = build(true, false);
assert!(fh.load_luma_quantization_matrix);
assert!(!fh.load_chroma_quantization_matrix);
assert_eq!(
fh.quantization_matrix_source(),
QuantizationMatrixSource::LumaCustom
);
assert_eq!(fh.chroma_qmat, custom_luma);
assert_eq!(fh.luma_qmat, custom_luma);
let fh = build(false, false);
assert!(!fh.load_luma_quantization_matrix);
assert!(!fh.load_chroma_quantization_matrix);
assert_eq!(
fh.quantization_matrix_source(),
QuantizationMatrixSource::Default
);
assert_eq!(fh.luma_qmat, [4u8; 64]);
assert_eq!(fh.chroma_qmat, [4u8; 64]);
}
}