use display_types::ColorFormat;
use crate::decoded::Decoded;
use crate::encode::{IntoPackets, SinglePacketIter};
use crate::error::DecodeError;
use crate::warn::AviWarning;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum BarInfo {
NotPresent,
VerticalBarsPresent,
HorizontalBarsPresent,
BothPresent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScanInfo {
NoData,
Overscanned,
Underscanned,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Colorimetry {
NoData,
Bt601,
Bt709,
Extended,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ExtendedColorimetry {
XvYCC601,
XvYCC709,
SyCC601,
OpYCC601,
OpRgb,
Bt2020cYCC,
Bt2020YCC,
AdditionalColorimetryExtension,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PictureAspectRatio {
NoData,
FourByThree,
SixteenByNine,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum RgbQuantization {
Default,
LimitedRange,
FullRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum YccQuantization {
LimitedRange,
FullRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum NonUniformScaling {
None,
Horizontal,
Vertical,
Both,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ItContentType {
Graphics,
Photo,
Cinema,
Game,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AviInfoFrame {
pub color_format: ColorFormat,
pub active_format_present: bool,
pub bar_info: BarInfo,
pub scan_info: ScanInfo,
pub colorimetry: Colorimetry,
pub extended_colorimetry: ExtendedColorimetry,
pub picture_aspect_ratio: PictureAspectRatio,
pub active_format_aspect_ratio: u8,
pub it_content: bool,
pub rgb_quantization: RgbQuantization,
pub non_uniform_scaling: NonUniformScaling,
pub vic: u8,
pub ycc_quantization: YccQuantization,
pub it_content_type: ItContentType,
pub pixel_repetition: u8,
pub top_bar: u16,
pub bottom_bar: u16,
pub left_bar: u16,
pub right_bar: u16,
}
impl AviInfoFrame {
pub fn decode(packet: &[u8; 31]) -> Result<Decoded<AviInfoFrame, AviWarning>, DecodeError> {
let length = packet[2];
if length > 27 {
return Err(DecodeError::Truncated { claimed: length });
}
let mut decoded = Decoded::new(AviInfoFrame {
color_format: ColorFormat::Rgb444,
active_format_present: false,
bar_info: BarInfo::NotPresent,
scan_info: ScanInfo::NoData,
colorimetry: Colorimetry::NoData,
extended_colorimetry: ExtendedColorimetry::XvYCC601,
picture_aspect_ratio: PictureAspectRatio::NoData,
active_format_aspect_ratio: 0,
it_content: false,
rgb_quantization: RgbQuantization::Default,
non_uniform_scaling: NonUniformScaling::None,
vic: 0,
ycc_quantization: YccQuantization::LimitedRange,
it_content_type: ItContentType::Graphics,
pixel_repetition: 0,
top_bar: 0,
bottom_bar: 0,
left_bar: 0,
right_bar: 0,
});
let total: u8 = packet.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
if total != 0x00 {
let expected = crate::checksum::compute_checksum(packet[..30].try_into().unwrap());
decoded.push_warning(AviWarning::ChecksumMismatch {
expected,
found: packet[3],
});
}
let pb1 = packet[4];
let y_raw = (pb1 >> 5) & 0x07;
decoded.value.color_format = match y_raw {
0 => ColorFormat::Rgb444,
1 => ColorFormat::YCbCr422,
2 => ColorFormat::YCbCr444,
3 => ColorFormat::YCbCr420,
_ => {
decoded.push_warning(AviWarning::UnknownEnumValue {
field: "color_format",
raw: y_raw,
});
ColorFormat::Rgb444
}
};
decoded.value.active_format_present = pb1 & 0x10 != 0;
decoded.value.bar_info = match (pb1 >> 2) & 0x03 {
0 => BarInfo::NotPresent,
1 => BarInfo::VerticalBarsPresent,
2 => BarInfo::HorizontalBarsPresent,
_ => BarInfo::BothPresent,
};
let s_raw = pb1 & 0x03;
decoded.value.scan_info = match s_raw {
0 => ScanInfo::NoData,
1 => ScanInfo::Overscanned,
2 => ScanInfo::Underscanned,
_ => {
decoded.push_warning(AviWarning::UnknownEnumValue {
field: "scan_info",
raw: s_raw,
});
ScanInfo::NoData
}
};
let pb2 = packet[5];
decoded.value.colorimetry = match (pb2 >> 6) & 0x03 {
0 => Colorimetry::NoData,
1 => Colorimetry::Bt601,
2 => Colorimetry::Bt709,
_ => Colorimetry::Extended,
};
let m_raw = (pb2 >> 4) & 0x03;
decoded.value.picture_aspect_ratio = match m_raw {
0 => PictureAspectRatio::NoData,
1 => PictureAspectRatio::FourByThree,
2 => PictureAspectRatio::SixteenByNine,
_ => {
decoded.push_warning(AviWarning::UnknownEnumValue {
field: "picture_aspect_ratio",
raw: m_raw,
});
PictureAspectRatio::NoData
}
};
decoded.value.active_format_aspect_ratio = pb2 & 0x0F;
let pb3 = packet[6];
decoded.value.it_content = pb3 & 0x80 != 0;
decoded.value.extended_colorimetry = match (pb3 >> 4) & 0x07 {
0 => ExtendedColorimetry::XvYCC601,
1 => ExtendedColorimetry::XvYCC709,
2 => ExtendedColorimetry::SyCC601,
3 => ExtendedColorimetry::OpYCC601,
4 => ExtendedColorimetry::OpRgb,
5 => ExtendedColorimetry::Bt2020cYCC,
6 => ExtendedColorimetry::Bt2020YCC,
_ => ExtendedColorimetry::AdditionalColorimetryExtension,
};
let q_raw = (pb3 >> 2) & 0x03;
decoded.value.rgb_quantization = match q_raw {
0 => RgbQuantization::Default,
1 => RgbQuantization::LimitedRange,
2 => RgbQuantization::FullRange,
_ => {
decoded.push_warning(AviWarning::UnknownEnumValue {
field: "rgb_quantization",
raw: q_raw,
});
RgbQuantization::Default
}
};
decoded.value.non_uniform_scaling = match pb3 & 0x03 {
0 => NonUniformScaling::None,
1 => NonUniformScaling::Horizontal,
2 => NonUniformScaling::Vertical,
_ => NonUniformScaling::Both,
};
let pb4 = packet[7];
if pb4 & 0x80 != 0 {
decoded.push_warning(AviWarning::ReservedFieldNonZero { byte: 7, bit: 7 });
}
decoded.value.vic = pb4 & 0x7F;
let pb5 = packet[8];
let yq_raw = (pb5 >> 6) & 0x03;
decoded.value.ycc_quantization = match yq_raw {
0 => YccQuantization::LimitedRange,
1 => YccQuantization::FullRange,
_ => {
decoded.push_warning(AviWarning::UnknownEnumValue {
field: "ycc_quantization",
raw: yq_raw,
});
YccQuantization::LimitedRange
}
};
decoded.value.it_content_type = match (pb5 >> 4) & 0x03 {
0 => ItContentType::Graphics,
1 => ItContentType::Photo,
2 => ItContentType::Cinema,
_ => ItContentType::Game,
};
decoded.value.pixel_repetition = pb5 & 0x0F;
if length >= 7 {
decoded.value.top_bar = u16::from_le_bytes([packet[9], packet[10]]);
}
if length >= 9 {
decoded.value.bottom_bar = u16::from_le_bytes([packet[11], packet[12]]);
}
if length >= 11 {
decoded.value.left_bar = u16::from_le_bytes([packet[13], packet[14]]);
}
if length >= 13 {
decoded.value.right_bar = u16::from_le_bytes([packet[15], packet[16]]);
}
Ok(decoded)
}
}
impl IntoPackets for AviInfoFrame {
type Iter = SinglePacketIter;
type Warning = AviWarning;
fn into_packets(self) -> crate::decoded::Decoded<SinglePacketIter, AviWarning> {
let vic = self.vic;
let mut hp = [0u8; 30];
hp[0] = 0x82; hp[1] = 0x02; hp[2] = 13;
let y_raw: u8 = match self.color_format {
ColorFormat::Rgb444 => 0,
ColorFormat::YCbCr422 => 1,
ColorFormat::YCbCr444 => 2,
ColorFormat::YCbCr420 => 3,
_ => 0,
};
let b_raw: u8 = match self.bar_info {
BarInfo::NotPresent => 0,
BarInfo::VerticalBarsPresent => 1,
BarInfo::HorizontalBarsPresent => 2,
BarInfo::BothPresent => 3,
};
let s_raw: u8 = match self.scan_info {
ScanInfo::NoData => 0,
ScanInfo::Overscanned => 1,
ScanInfo::Underscanned => 2,
};
hp[3] = (y_raw << 5) | ((self.active_format_present as u8) << 4) | (b_raw << 2) | s_raw;
let c_raw: u8 = match self.colorimetry {
Colorimetry::NoData => 0,
Colorimetry::Bt601 => 1,
Colorimetry::Bt709 => 2,
Colorimetry::Extended => 3,
};
let m_raw: u8 = match self.picture_aspect_ratio {
PictureAspectRatio::NoData => 0,
PictureAspectRatio::FourByThree => 1,
PictureAspectRatio::SixteenByNine => 2,
};
hp[4] = (c_raw << 6) | (m_raw << 4) | (self.active_format_aspect_ratio & 0x0F);
let ec_raw: u8 = match self.extended_colorimetry {
ExtendedColorimetry::XvYCC601 => 0,
ExtendedColorimetry::XvYCC709 => 1,
ExtendedColorimetry::SyCC601 => 2,
ExtendedColorimetry::OpYCC601 => 3,
ExtendedColorimetry::OpRgb => 4,
ExtendedColorimetry::Bt2020cYCC => 5,
ExtendedColorimetry::Bt2020YCC => 6,
ExtendedColorimetry::AdditionalColorimetryExtension => 7,
};
let q_raw: u8 = match self.rgb_quantization {
RgbQuantization::Default => 0,
RgbQuantization::LimitedRange => 1,
RgbQuantization::FullRange => 2,
};
let sc_raw: u8 = match self.non_uniform_scaling {
NonUniformScaling::None => 0,
NonUniformScaling::Horizontal => 1,
NonUniformScaling::Vertical => 2,
NonUniformScaling::Both => 3,
};
hp[5] = ((self.it_content as u8) << 7) | (ec_raw << 4) | (q_raw << 2) | sc_raw;
hp[6] = self.vic & 0x7F;
let yq_raw: u8 = match self.ycc_quantization {
YccQuantization::LimitedRange => 0,
YccQuantization::FullRange => 1,
};
let cn_raw: u8 = match self.it_content_type {
ItContentType::Graphics => 0,
ItContentType::Photo => 1,
ItContentType::Cinema => 2,
ItContentType::Game => 3,
};
hp[7] = (yq_raw << 6) | (cn_raw << 4) | (self.pixel_repetition & 0x0F);
let [lo, hi] = self.top_bar.to_le_bytes();
hp[8] = lo;
hp[9] = hi;
let [lo, hi] = self.bottom_bar.to_le_bytes();
hp[10] = lo;
hp[11] = hi;
let [lo, hi] = self.left_bar.to_le_bytes();
hp[12] = lo;
hp[13] = hi;
let [lo, hi] = self.right_bar.to_le_bytes();
hp[14] = lo;
hp[15] = hi;
let checksum = crate::checksum::compute_checksum(&hp);
let mut packet = [0u8; 31];
packet[..3].copy_from_slice(&hp[..3]);
packet[3] = checksum;
packet[4..].copy_from_slice(&hp[3..]);
let mut result = crate::decoded::Decoded::new(SinglePacketIter::new(packet));
if vic > 127 {
result.push_warning(AviWarning::UnknownEnumValue {
field: "vic",
raw: vic,
});
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::encode::IntoPackets;
fn full_frame() -> AviInfoFrame {
AviInfoFrame {
color_format: ColorFormat::YCbCr444,
active_format_present: true,
bar_info: BarInfo::BothPresent,
scan_info: ScanInfo::Underscanned,
colorimetry: Colorimetry::Extended,
extended_colorimetry: ExtendedColorimetry::Bt2020YCC,
picture_aspect_ratio: PictureAspectRatio::SixteenByNine,
active_format_aspect_ratio: 0x08,
it_content: true,
rgb_quantization: RgbQuantization::FullRange,
non_uniform_scaling: NonUniformScaling::None,
vic: 16,
ycc_quantization: YccQuantization::LimitedRange,
it_content_type: ItContentType::Cinema,
pixel_repetition: 0,
top_bar: 0x1234,
bottom_bar: 0x5678,
left_bar: 0x9ABC,
right_bar: 0xDEF0,
}
}
#[test]
fn round_trip() {
let frame = full_frame();
let packet = frame.clone().into_packets().value.next().unwrap();
let decoded = AviInfoFrame::decode(&packet).unwrap();
assert!(decoded.iter_warnings().next().is_none());
assert_eq!(decoded.value, frame);
}
#[test]
fn checksum_mismatch_warning() {
let mut packet = full_frame().into_packets().value.next().unwrap();
packet[3] = packet[3].wrapping_add(1);
let decoded = AviInfoFrame::decode(&packet).unwrap();
assert!(
decoded
.iter_warnings()
.any(|w| matches!(w, AviWarning::ChecksumMismatch { .. }))
);
assert_eq!(decoded.value, full_frame());
}
#[test]
fn truncated_length_is_error() {
let mut packet = full_frame().into_packets().value.next().unwrap();
packet[2] = 28;
assert!(matches!(
AviInfoFrame::decode(&packet),
Err(DecodeError::Truncated { claimed: 28 })
));
}
#[test]
fn reserved_bit_warning() {
let mut packet = full_frame().into_packets().value.next().unwrap();
packet[7] |= 0x80; let sum: u8 = packet.iter().fold(0u8, |a, &b| a.wrapping_add(b));
packet[3] = packet[3].wrapping_sub(sum);
let decoded = AviInfoFrame::decode(&packet).unwrap();
assert!(
decoded
.iter_warnings()
.any(|w| matches!(w, AviWarning::ReservedFieldNonZero { byte: 7, bit: 7 }))
);
}
#[test]
fn color_format_variants_round_trip() {
for fmt in [
ColorFormat::Rgb444,
ColorFormat::YCbCr422,
ColorFormat::YCbCr420,
] {
let frame = AviInfoFrame {
color_format: fmt,
..full_frame()
};
let packet = frame.clone().into_packets().value.next().unwrap();
let decoded = AviInfoFrame::decode(&packet).unwrap();
assert_eq!(decoded.value.color_format, fmt);
}
}
#[test]
fn scan_info_and_colorimetry_variants_round_trip() {
for (scan, c) in [
(ScanInfo::NoData, Colorimetry::NoData),
(ScanInfo::Overscanned, Colorimetry::Bt601),
(ScanInfo::Underscanned, Colorimetry::Bt709),
(ScanInfo::NoData, Colorimetry::Extended),
] {
let frame = AviInfoFrame {
scan_info: scan,
colorimetry: c,
..full_frame()
};
let packet = frame.clone().into_packets().value.next().unwrap();
let decoded = AviInfoFrame::decode(&packet).unwrap();
assert_eq!(decoded.value.scan_info, scan);
assert_eq!(decoded.value.colorimetry, c);
}
}
#[test]
fn bar_info_and_aspect_ratio_variants_round_trip() {
for (bar, aspect) in [
(BarInfo::NotPresent, PictureAspectRatio::NoData),
(
BarInfo::VerticalBarsPresent,
PictureAspectRatio::FourByThree,
),
(
BarInfo::HorizontalBarsPresent,
PictureAspectRatio::SixteenByNine,
),
(BarInfo::BothPresent, PictureAspectRatio::SixteenByNine),
] {
let frame = AviInfoFrame {
bar_info: bar,
picture_aspect_ratio: aspect,
..full_frame()
};
let packet = frame.clone().into_packets().value.next().unwrap();
let decoded = AviInfoFrame::decode(&packet).unwrap();
assert_eq!(decoded.value.bar_info, bar);
assert_eq!(decoded.value.picture_aspect_ratio, aspect);
}
}
#[test]
fn quantization_and_scaling_variants_round_trip() {
for (rgb_q, ycc_q, sc) in [
(
RgbQuantization::Default,
YccQuantization::FullRange,
NonUniformScaling::Horizontal,
),
(
RgbQuantization::LimitedRange,
YccQuantization::LimitedRange,
NonUniformScaling::Vertical,
),
(
RgbQuantization::FullRange,
YccQuantization::LimitedRange,
NonUniformScaling::Both,
),
] {
let frame = AviInfoFrame {
rgb_quantization: rgb_q,
ycc_quantization: ycc_q,
non_uniform_scaling: sc,
..full_frame()
};
let packet = frame.clone().into_packets().value.next().unwrap();
let decoded = AviInfoFrame::decode(&packet).unwrap();
assert_eq!(decoded.value.rgb_quantization, rgb_q);
assert_eq!(decoded.value.ycc_quantization, ycc_q);
assert_eq!(decoded.value.non_uniform_scaling, sc);
}
}
#[test]
fn it_content_type_variants_round_trip() {
for cn in [
ItContentType::Graphics,
ItContentType::Photo,
ItContentType::Cinema,
ItContentType::Game,
] {
let frame = AviInfoFrame {
it_content: true,
it_content_type: cn,
..full_frame()
};
let packet = frame.clone().into_packets().value.next().unwrap();
let decoded = AviInfoFrame::decode(&packet).unwrap();
assert_eq!(decoded.value.it_content_type, cn);
}
}
#[test]
fn extended_colorimetry_variants_round_trip() {
for ec in [
ExtendedColorimetry::XvYCC601,
ExtendedColorimetry::XvYCC709,
ExtendedColorimetry::SyCC601,
ExtendedColorimetry::OpYCC601,
ExtendedColorimetry::OpRgb,
ExtendedColorimetry::Bt2020cYCC,
ExtendedColorimetry::Bt2020YCC,
ExtendedColorimetry::AdditionalColorimetryExtension,
] {
let frame = AviInfoFrame {
colorimetry: Colorimetry::Extended,
extended_colorimetry: ec,
..full_frame()
};
let packet = frame.clone().into_packets().value.next().unwrap();
let decoded = AviInfoFrame::decode(&packet).unwrap();
assert_eq!(decoded.value.extended_colorimetry, ec);
}
}
#[test]
fn unknown_scan_info_warns() {
let mut packet = full_frame().into_packets().value.next().unwrap();
packet[4] = (packet[4] & !0x03) | 0x03; let sum: u8 = packet.iter().fold(0u8, |a, &b| a.wrapping_add(b));
packet[3] = packet[3].wrapping_sub(sum);
let decoded = AviInfoFrame::decode(&packet).unwrap();
assert!(decoded.iter_warnings().any(|w| matches!(
w,
AviWarning::UnknownEnumValue {
field: "scan_info",
raw: 3
}
)));
assert_eq!(decoded.value.scan_info, ScanInfo::NoData);
}
#[test]
fn unknown_color_format_warns() {
let mut packet = full_frame().into_packets().value.next().unwrap();
packet[4] = (packet[4] & !0xE0) | (5u8 << 5);
let sum: u8 = packet.iter().fold(0u8, |a, &b| a.wrapping_add(b));
packet[3] = packet[3].wrapping_sub(sum);
let decoded = AviInfoFrame::decode(&packet).unwrap();
assert!(decoded.iter_warnings().any(|w| matches!(
w,
AviWarning::UnknownEnumValue {
field: "color_format",
raw: 5
}
)));
assert_eq!(decoded.value.color_format, ColorFormat::Rgb444);
}
#[test]
fn unknown_rgb_quantization_warns() {
let mut packet = full_frame().into_packets().value.next().unwrap();
packet[6] = (packet[6] & !0x0C) | 0x0C; let sum: u8 = packet.iter().fold(0u8, |a, &b| a.wrapping_add(b));
packet[3] = packet[3].wrapping_sub(sum);
let decoded = AviInfoFrame::decode(&packet).unwrap();
assert!(decoded.iter_warnings().any(|w| matches!(
w,
AviWarning::UnknownEnumValue {
field: "rgb_quantization",
raw: 3
}
)));
assert_eq!(decoded.value.rgb_quantization, RgbQuantization::Default);
}
#[test]
fn unknown_ycc_quantization_warns() {
let mut packet = full_frame().into_packets().value.next().unwrap();
packet[8] = (packet[8] & !0xC0) | 0x80; let sum: u8 = packet.iter().fold(0u8, |a, &b| a.wrapping_add(b));
packet[3] = packet[3].wrapping_sub(sum);
let decoded = AviInfoFrame::decode(&packet).unwrap();
assert!(decoded.iter_warnings().any(|w| matches!(
w,
AviWarning::UnknownEnumValue {
field: "ycc_quantization",
raw: 2
}
)));
assert_eq!(
decoded.value.ycc_quantization,
YccQuantization::LimitedRange
);
}
#[test]
fn unknown_picture_aspect_ratio_warns() {
let mut packet = full_frame().into_packets().value.next().unwrap();
packet[5] = (packet[5] & !0x30) | 0x30; let sum: u8 = packet.iter().fold(0u8, |a, &b| a.wrapping_add(b));
packet[3] = packet[3].wrapping_sub(sum);
let decoded = AviInfoFrame::decode(&packet).unwrap();
assert!(decoded.iter_warnings().any(|w| matches!(
w,
AviWarning::UnknownEnumValue {
field: "picture_aspect_ratio",
raw: 3
}
)));
assert_eq!(
decoded.value.picture_aspect_ratio,
PictureAspectRatio::NoData
);
}
#[test]
fn vic_out_of_range_warns_on_encode() {
let frame = AviInfoFrame {
vic: 200,
..full_frame()
};
let mut encoded = frame.into_packets();
assert!(encoded.iter_warnings().any(|w| matches!(
w,
AviWarning::UnknownEnumValue {
field: "vic",
raw: 200
}
)));
let packet = encoded.value.next().unwrap();
assert_eq!(packet[7] & 0x7F, 72);
}
#[test]
fn short_packet_leaves_bar_data_zeroed() {
let mut packet = full_frame().into_packets().value.next().unwrap();
packet[2] = 5;
let sum: u8 = packet.iter().fold(0u8, |a, &b| a.wrapping_add(b));
packet[3] = packet[3].wrapping_sub(sum);
let decoded = AviInfoFrame::decode(&packet).unwrap();
assert_eq!(decoded.value.top_bar, 0);
assert_eq!(decoded.value.bottom_bar, 0);
assert_eq!(decoded.value.left_bar, 0);
assert_eq!(decoded.value.right_bar, 0);
}
}