use std::io::{BufRead, BufReader, Read};
use oximedia_core::{OxiError, OxiResult};
const Y4M_MAGIC: &str = "YUV4MPEG2";
const FRAME_TAG: &str = "FRAME";
const MAX_HEADER_LINE_LEN: usize = 4096;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Y4mChroma {
C420jpeg,
C420mpeg2,
C420paldv,
C422,
C444,
C444alpha,
Mono,
}
impl Y4mChroma {
#[must_use]
pub fn bytes_per_frame(self, width: u32, height: u32) -> usize {
let w = width as usize;
let h = height as usize;
let luma = w * h;
match self {
Self::C420jpeg | Self::C420mpeg2 | Self::C420paldv => {
let chroma_w = (w + 1) / 2;
let chroma_h = (h + 1) / 2;
luma + 2 * chroma_w * chroma_h
}
Self::C422 => {
let chroma_w = (w + 1) / 2;
luma + 2 * chroma_w * h
}
Self::C444 => {
luma * 3
}
Self::C444alpha => {
luma * 4
}
Self::Mono => {
luma
}
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::C420jpeg => "420jpeg",
Self::C420mpeg2 => "420mpeg2",
Self::C420paldv => "420paldv",
Self::C422 => "422",
Self::C444 => "444",
Self::C444alpha => "444alpha",
Self::Mono => "mono",
}
}
#[must_use]
pub fn from_str(s: &str) -> Option<Self> {
match s {
"420jpeg" | "420" => Some(Self::C420jpeg),
"420mpeg2" => Some(Self::C420mpeg2),
"420paldv" => Some(Self::C420paldv),
"422" => Some(Self::C422),
"444" => Some(Self::C444),
"444alpha" => Some(Self::C444alpha),
"mono" => Some(Self::Mono),
_ => None,
}
}
}
impl std::fmt::Display for Y4mChroma {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Y4mInterlace {
Progressive,
TopFirst,
BottomFirst,
Mixed,
}
impl Y4mInterlace {
#[must_use]
pub const fn as_char(self) -> char {
match self {
Self::Progressive => 'p',
Self::TopFirst => 't',
Self::BottomFirst => 'b',
Self::Mixed => 'm',
}
}
#[must_use]
pub fn from_char(c: char) -> Option<Self> {
match c {
'p' => Some(Self::Progressive),
't' => Some(Self::TopFirst),
'b' => Some(Self::BottomFirst),
'm' => Some(Self::Mixed),
_ => None,
}
}
}
impl std::fmt::Display for Y4mInterlace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_char())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Y4mHeader {
pub width: u32,
pub height: u32,
pub fps_num: u32,
pub fps_den: u32,
pub interlace: Y4mInterlace,
pub par_num: u32,
pub par_den: u32,
pub chroma: Y4mChroma,
pub comment: Option<String>,
}
impl Y4mHeader {
#[must_use]
pub fn frame_size(&self) -> usize {
self.chroma.bytes_per_frame(self.width, self.height)
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn fps(&self) -> f64 {
if self.fps_den == 0 {
0.0
} else {
self.fps_num as f64 / self.fps_den as f64
}
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn pixel_aspect_ratio(&self) -> f64 {
if self.par_den == 0 {
1.0
} else {
self.par_num as f64 / self.par_den as f64
}
}
}
impl Default for Y4mHeader {
fn default() -> Self {
Self {
width: 0,
height: 0,
fps_num: 25,
fps_den: 1,
interlace: Y4mInterlace::Progressive,
par_num: 0,
par_den: 0,
chroma: Y4mChroma::C420jpeg,
comment: None,
}
}
}
fn parse_header(line: &str) -> OxiResult<Y4mHeader> {
let line = line.trim_end_matches(['\n', '\r']);
if !line.starts_with(Y4M_MAGIC) {
return Err(OxiError::Parse {
offset: 0,
message: format!(
"Expected Y4M magic '{}', got '{}'",
Y4M_MAGIC,
&line[..line.len().min(20)]
),
});
}
let params_str = &line[Y4M_MAGIC.len()..];
let mut header = Y4mHeader::default();
let mut found_width = false;
let mut found_height = false;
for token in params_str.split_whitespace() {
if token.is_empty() {
continue;
}
let tag = token.as_bytes()[0];
let value = &token[1..];
match tag {
b'W' => {
header.width = value.parse::<u32>().map_err(|_| OxiError::Parse {
offset: 0,
message: format!("Invalid width: '{value}'"),
})?;
if header.width == 0 {
return Err(OxiError::Parse {
offset: 0,
message: "Width must be greater than zero".into(),
});
}
found_width = true;
}
b'H' => {
header.height = value.parse::<u32>().map_err(|_| OxiError::Parse {
offset: 0,
message: format!("Invalid height: '{value}'"),
})?;
if header.height == 0 {
return Err(OxiError::Parse {
offset: 0,
message: "Height must be greater than zero".into(),
});
}
found_height = true;
}
b'F' => {
let (num, den) = parse_ratio(value).ok_or_else(|| OxiError::Parse {
offset: 0,
message: format!("Invalid frame rate: '{value}'"),
})?;
if den == 0 {
return Err(OxiError::Parse {
offset: 0,
message: "Frame rate denominator must be non-zero".into(),
});
}
header.fps_num = num;
header.fps_den = den;
}
b'I' => {
if let Some(first_char) = value.chars().next() {
header.interlace =
Y4mInterlace::from_char(first_char).ok_or_else(|| OxiError::Parse {
offset: 0,
message: format!("Invalid interlace mode: '{first_char}'"),
})?;
}
}
b'A' => {
let (num, den) = parse_ratio(value).ok_or_else(|| OxiError::Parse {
offset: 0,
message: format!("Invalid pixel aspect ratio: '{value}'"),
})?;
header.par_num = num;
header.par_den = den;
}
b'C' => {
header.chroma = Y4mChroma::from_str(value).ok_or_else(|| OxiError::Parse {
offset: 0,
message: format!("Unknown chroma subsampling: '{value}'"),
})?;
}
b'X' => {
header.comment = Some(value.to_string());
}
_ => {
}
}
}
if !found_width {
return Err(OxiError::Parse {
offset: 0,
message: "Missing required width parameter (W)".into(),
});
}
if !found_height {
return Err(OxiError::Parse {
offset: 0,
message: "Missing required height parameter (H)".into(),
});
}
Ok(header)
}
fn parse_ratio(s: &str) -> Option<(u32, u32)> {
let mut parts = s.splitn(2, ':');
let num_str = parts.next()?;
let den_str = parts.next()?;
let num = num_str.parse::<u32>().ok()?;
let den = den_str.parse::<u32>().ok()?;
Some((num, den))
}
fn validate_frame_tag(line: &str) -> OxiResult<()> {
let trimmed = line.trim_end_matches(['\n', '\r']);
if !trimmed.starts_with(FRAME_TAG) {
return Err(OxiError::Parse {
offset: 0,
message: format!(
"Expected FRAME tag, got '{}'",
&trimmed[..trimmed.len().min(20)]
),
});
}
let rest = &trimmed[FRAME_TAG.len()..];
if !rest.is_empty() && !rest.starts_with(' ') {
return Err(OxiError::Parse {
offset: 0,
message: format!("Invalid FRAME tag: '{}'", &trimmed[..trimmed.len().min(30)]),
});
}
Ok(())
}
pub struct Y4mDemuxer<R: Read> {
reader: BufReader<R>,
header: Y4mHeader,
frame_count: u64,
frame_size: usize,
eof: bool,
line_buf: String,
}
impl<R: Read> Y4mDemuxer<R> {
pub fn new(reader: R) -> OxiResult<Self> {
let mut buf_reader = BufReader::new(reader);
let mut line = String::with_capacity(256);
let bytes_read = buf_reader.read_line(&mut line).map_err(OxiError::Io)?;
if bytes_read == 0 {
return Err(OxiError::UnexpectedEof);
}
if bytes_read > MAX_HEADER_LINE_LEN {
return Err(OxiError::Parse {
offset: 0,
message: format!(
"Header line too long: {bytes_read} bytes (max {MAX_HEADER_LINE_LEN})"
),
});
}
let header = parse_header(&line)?;
let frame_size = header.frame_size();
Ok(Self {
reader: buf_reader,
header,
frame_count: 0,
frame_size,
eof: false,
line_buf: String::with_capacity(128),
})
}
#[must_use]
pub fn header(&self) -> &Y4mHeader {
&self.header
}
#[must_use]
pub fn width(&self) -> u32 {
self.header.width
}
#[must_use]
pub fn height(&self) -> u32 {
self.header.height
}
#[must_use]
pub fn fps(&self) -> (u32, u32) {
(self.header.fps_num, self.header.fps_den)
}
#[must_use]
pub fn chroma(&self) -> Y4mChroma {
self.header.chroma
}
#[must_use]
pub fn frame_count(&self) -> u64 {
self.frame_count
}
#[must_use]
pub fn frame_size(&self) -> usize {
self.frame_size
}
#[must_use]
pub fn is_eof(&self) -> bool {
self.eof
}
pub fn read_frame(&mut self) -> OxiResult<Option<Vec<u8>>> {
if self.eof {
return Ok(None);
}
self.line_buf.clear();
let bytes_read = self
.reader
.read_line(&mut self.line_buf)
.map_err(OxiError::Io)?;
if bytes_read == 0 {
self.eof = true;
return Ok(None);
}
validate_frame_tag(&self.line_buf)?;
let mut frame_data = vec![0u8; self.frame_size];
self.read_exact_into(&mut frame_data)?;
self.frame_count += 1;
Ok(Some(frame_data))
}
pub fn read_all_frames(&mut self) -> OxiResult<Vec<Vec<u8>>> {
let mut frames = Vec::new();
while let Some(frame) = self.read_frame()? {
frames.push(frame);
}
Ok(frames)
}
fn read_exact_into(&mut self, buf: &mut [u8]) -> OxiResult<()> {
let mut read = 0;
while read < buf.len() {
let n = self.reader.read(&mut buf[read..]).map_err(OxiError::Io)?;
if n == 0 {
self.eof = true;
return Err(OxiError::Parse {
offset: 0,
message: format!(
"Truncated frame data: expected {} bytes, got {}",
buf.len(),
read
),
});
}
read += n;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
#[ignore]
fn test_parse_header_minimal() {
let header = parse_header("YUV4MPEG2 W320 H240").expect("should parse");
assert_eq!(header.width, 320);
assert_eq!(header.height, 240);
assert_eq!(header.fps_num, 25);
assert_eq!(header.fps_den, 1);
assert_eq!(header.interlace, Y4mInterlace::Progressive);
assert_eq!(header.chroma, Y4mChroma::C420jpeg);
}
#[test]
#[ignore]
fn test_parse_header_full() {
let line = "YUV4MPEG2 W1920 H1080 F30000:1001 It A1:1 C422 Xtest_comment";
let header = parse_header(line).expect("should parse");
assert_eq!(header.width, 1920);
assert_eq!(header.height, 1080);
assert_eq!(header.fps_num, 30000);
assert_eq!(header.fps_den, 1001);
assert_eq!(header.interlace, Y4mInterlace::TopFirst);
assert_eq!(header.par_num, 1);
assert_eq!(header.par_den, 1);
assert_eq!(header.chroma, Y4mChroma::C422);
assert_eq!(header.comment.as_deref(), Some("test_comment"));
}
#[test]
#[ignore]
fn test_parse_header_all_chroma() {
for (cs, expected) in &[
("420jpeg", Y4mChroma::C420jpeg),
("420", Y4mChroma::C420jpeg),
("420mpeg2", Y4mChroma::C420mpeg2),
("420paldv", Y4mChroma::C420paldv),
("422", Y4mChroma::C422),
("444", Y4mChroma::C444),
("444alpha", Y4mChroma::C444alpha),
("mono", Y4mChroma::Mono),
] {
let line = format!("YUV4MPEG2 W8 H8 C{cs}");
let header = parse_header(&line).unwrap_or_else(|e| {
panic!("Failed to parse chroma '{cs}': {e}");
});
assert_eq!(header.chroma, *expected, "chroma mismatch for '{cs}'");
}
}
#[test]
#[ignore]
fn test_parse_header_all_interlace() {
for (c, expected) in &[
('p', Y4mInterlace::Progressive),
('t', Y4mInterlace::TopFirst),
('b', Y4mInterlace::BottomFirst),
('m', Y4mInterlace::Mixed),
] {
let line = format!("YUV4MPEG2 W8 H8 I{c}");
let header = parse_header(&line).unwrap_or_else(|e| {
panic!("Failed to parse interlace '{c}': {e}");
});
assert_eq!(header.interlace, *expected, "interlace mismatch for '{c}'");
}
}
#[test]
#[ignore]
fn test_parse_header_missing_magic() {
let result = parse_header("NOT_Y4M W320 H240");
assert!(result.is_err());
}
#[test]
#[ignore]
fn test_parse_header_missing_width() {
let result = parse_header("YUV4MPEG2 H240");
assert!(result.is_err());
}
#[test]
#[ignore]
fn test_parse_header_missing_height() {
let result = parse_header("YUV4MPEG2 W320");
assert!(result.is_err());
}
#[test]
#[ignore]
fn test_parse_header_zero_width() {
let result = parse_header("YUV4MPEG2 W0 H240");
assert!(result.is_err());
}
#[test]
#[ignore]
fn test_parse_header_zero_height() {
let result = parse_header("YUV4MPEG2 W320 H0");
assert!(result.is_err());
}
#[test]
#[ignore]
fn test_parse_header_invalid_width() {
let result = parse_header("YUV4MPEG2 Wabc H240");
assert!(result.is_err());
}
#[test]
#[ignore]
fn test_parse_header_invalid_fps() {
let result = parse_header("YUV4MPEG2 W320 H240 Fabc:def");
assert!(result.is_err());
}
#[test]
#[ignore]
fn test_parse_header_zero_fps_den() {
let result = parse_header("YUV4MPEG2 W320 H240 F30:0");
assert!(result.is_err());
}
#[test]
#[ignore]
fn test_parse_header_unknown_chroma() {
let result = parse_header("YUV4MPEG2 W320 H240 Cunknown");
assert!(result.is_err());
}
#[test]
#[ignore]
fn test_parse_header_invalid_interlace() {
let result = parse_header("YUV4MPEG2 W320 H240 Ix");
assert!(result.is_err());
}
#[test]
#[ignore]
fn test_parse_header_unknown_params_ignored() {
let header = parse_header("YUV4MPEG2 W8 H8 Zunknown").expect("should parse");
assert_eq!(header.width, 8);
assert_eq!(header.height, 8);
}
#[test]
#[ignore]
fn test_parse_header_trailing_newline() {
let header = parse_header("YUV4MPEG2 W320 H240\n").expect("should parse");
assert_eq!(header.width, 320);
assert_eq!(header.height, 240);
}
#[test]
#[ignore]
fn test_chroma_bytes_per_frame_420() {
assert_eq!(Y4mChroma::C420jpeg.bytes_per_frame(8, 8), 96);
assert_eq!(Y4mChroma::C420jpeg.bytes_per_frame(1920, 1080), 3110400);
}
#[test]
#[ignore]
fn test_chroma_bytes_per_frame_420_odd() {
assert_eq!(Y4mChroma::C420jpeg.bytes_per_frame(7, 7), 81);
}
#[test]
#[ignore]
fn test_chroma_bytes_per_frame_422() {
assert_eq!(Y4mChroma::C422.bytes_per_frame(8, 8), 128);
}
#[test]
#[ignore]
fn test_chroma_bytes_per_frame_444() {
assert_eq!(Y4mChroma::C444.bytes_per_frame(8, 8), 192);
}
#[test]
#[ignore]
fn test_chroma_bytes_per_frame_444alpha() {
assert_eq!(Y4mChroma::C444alpha.bytes_per_frame(8, 8), 256);
}
#[test]
#[ignore]
fn test_chroma_bytes_per_frame_mono() {
assert_eq!(Y4mChroma::Mono.bytes_per_frame(8, 8), 64);
}
fn build_y4m_stream(width: u32, height: u32, chroma: Y4mChroma, frame_count: usize) -> Vec<u8> {
let mut data = Vec::new();
let header_line = format!(
"YUV4MPEG2 W{width} H{height} F30:1 Ip C{}\n",
chroma.as_str()
);
data.extend_from_slice(header_line.as_bytes());
let frame_size = chroma.bytes_per_frame(width, height);
for i in 0..frame_count {
data.extend_from_slice(b"FRAME\n");
let fill_byte = (i & 0xFF) as u8;
data.extend(std::iter::repeat(fill_byte).take(frame_size));
}
data
}
#[test]
#[ignore]
fn test_demuxer_empty_stream() {
let result = Y4mDemuxer::new(Cursor::new(Vec::<u8>::new()));
assert!(result.is_err());
}
#[test]
#[ignore]
fn test_demuxer_header_only() {
let data = b"YUV4MPEG2 W8 H8 F25:1 Ip C420jpeg\n";
let mut demuxer = Y4mDemuxer::new(Cursor::new(data.to_vec())).expect("should parse header");
assert_eq!(demuxer.width(), 8);
assert_eq!(demuxer.height(), 8);
assert_eq!(demuxer.fps(), (25, 1));
assert_eq!(demuxer.chroma(), Y4mChroma::C420jpeg);
assert_eq!(demuxer.frame_count(), 0);
let frame = demuxer.read_frame().expect("should not error");
assert!(frame.is_none());
assert!(demuxer.is_eof());
}
#[test]
#[ignore]
fn test_demuxer_single_frame_420() {
let data = build_y4m_stream(4, 4, Y4mChroma::C420jpeg, 1);
let mut demuxer = Y4mDemuxer::new(Cursor::new(data)).expect("should parse header");
let frame = demuxer.read_frame().expect("should read frame");
assert!(frame.is_some());
let frame = frame.expect("frame should be Some");
assert_eq!(frame.len(), 24);
assert!(frame.iter().all(|&b| b == 0));
assert_eq!(demuxer.frame_count(), 1);
let frame = demuxer.read_frame().expect("should not error");
assert!(frame.is_none());
}
#[test]
#[ignore]
fn test_demuxer_multiple_frames() {
let num_frames = 5;
let data = build_y4m_stream(4, 4, Y4mChroma::C420jpeg, num_frames);
let mut demuxer = Y4mDemuxer::new(Cursor::new(data)).expect("should parse header");
for i in 0..num_frames {
let frame = demuxer
.read_frame()
.expect("should read frame")
.unwrap_or_else(|| panic!("frame {i} should exist"));
assert!(
frame.iter().all(|&b| b == i as u8),
"frame {i} data mismatch"
);
}
assert_eq!(demuxer.frame_count(), num_frames as u64);
assert!(demuxer.read_frame().expect("should not error").is_none());
}
#[test]
#[ignore]
fn test_demuxer_422() {
let data = build_y4m_stream(8, 8, Y4mChroma::C422, 2);
let mut demuxer = Y4mDemuxer::new(Cursor::new(data)).expect("should parse header");
assert_eq!(demuxer.chroma(), Y4mChroma::C422);
let frame = demuxer
.read_frame()
.expect("should read")
.expect("frame should exist");
assert_eq!(frame.len(), 128);
}
#[test]
#[ignore]
fn test_demuxer_444() {
let data = build_y4m_stream(4, 4, Y4mChroma::C444, 1);
let mut demuxer = Y4mDemuxer::new(Cursor::new(data)).expect("should parse header");
let frame = demuxer
.read_frame()
.expect("should read")
.expect("frame should exist");
assert_eq!(frame.len(), 48);
}
#[test]
#[ignore]
fn test_demuxer_mono() {
let data = build_y4m_stream(4, 4, Y4mChroma::Mono, 1);
let mut demuxer = Y4mDemuxer::new(Cursor::new(data)).expect("should parse header");
let frame = demuxer
.read_frame()
.expect("should read")
.expect("frame should exist");
assert_eq!(frame.len(), 16);
}
#[test]
#[ignore]
fn test_demuxer_444alpha() {
let data = build_y4m_stream(4, 4, Y4mChroma::C444alpha, 1);
let mut demuxer = Y4mDemuxer::new(Cursor::new(data)).expect("should parse header");
let frame = demuxer
.read_frame()
.expect("should read")
.expect("frame should exist");
assert_eq!(frame.len(), 64);
}
#[test]
#[ignore]
fn test_demuxer_truncated_frame() {
let mut data = Vec::new();
data.extend_from_slice(b"YUV4MPEG2 W4 H4 C420jpeg\n");
data.extend_from_slice(b"FRAME\n");
data.extend_from_slice(&[0u8; 10]);
let mut demuxer = Y4mDemuxer::new(Cursor::new(data)).expect("should parse header");
let result = demuxer.read_frame();
assert!(result.is_err());
}
#[test]
#[ignore]
fn test_demuxer_missing_frame_tag() {
let mut data = Vec::new();
data.extend_from_slice(b"YUV4MPEG2 W4 H4 C420jpeg\n");
data.extend_from_slice(b"NOT_FRAME\n");
data.extend_from_slice(&[0u8; 24]);
let mut demuxer = Y4mDemuxer::new(Cursor::new(data)).expect("should parse header");
let result = demuxer.read_frame();
assert!(result.is_err());
}
#[test]
#[ignore]
fn test_demuxer_frame_with_params() {
let mut data = Vec::new();
data.extend_from_slice(b"YUV4MPEG2 W4 H4 C420jpeg\n");
data.extend_from_slice(b"FRAME Xsome_comment\n");
data.extend_from_slice(&[128u8; 24]);
let mut demuxer = Y4mDemuxer::new(Cursor::new(data)).expect("should parse header");
let frame = demuxer
.read_frame()
.expect("should read frame")
.expect("frame should exist");
assert_eq!(frame.len(), 24);
assert!(frame.iter().all(|&b| b == 128));
}
#[test]
#[ignore]
fn test_demuxer_read_all_frames() {
let data = build_y4m_stream(4, 4, Y4mChroma::C420jpeg, 3);
let mut demuxer = Y4mDemuxer::new(Cursor::new(data)).expect("should parse header");
let frames = demuxer.read_all_frames().expect("should read all");
assert_eq!(frames.len(), 3);
for (i, frame) in frames.iter().enumerate() {
assert_eq!(frame.len(), 24);
assert!(
frame.iter().all(|&b| b == i as u8),
"frame {i} data mismatch"
);
}
}
#[test]
#[ignore]
fn test_demuxer_ntsc_framerate() {
let data = b"YUV4MPEG2 W1920 H1080 F30000:1001\n";
let demuxer = Y4mDemuxer::new(Cursor::new(data.to_vec())).expect("should parse");
assert_eq!(demuxer.fps(), (30000, 1001));
let fps = demuxer.header().fps();
assert!((fps - 29.97).abs() < 0.01);
}
#[test]
#[ignore]
fn test_demuxer_pixel_aspect_ratio() {
let data = b"YUV4MPEG2 W720 H480 A10:11\n";
let demuxer = Y4mDemuxer::new(Cursor::new(data.to_vec())).expect("should parse");
assert_eq!(demuxer.header().par_num, 10);
assert_eq!(demuxer.header().par_den, 11);
let par = demuxer.header().pixel_aspect_ratio();
assert!((par - 10.0 / 11.0).abs() < 0.001);
}
#[test]
#[ignore]
fn test_header_frame_size() {
let header = Y4mHeader {
width: 1920,
height: 1080,
chroma: Y4mChroma::C420jpeg,
..Y4mHeader::default()
};
assert_eq!(header.frame_size(), 3110400);
}
#[test]
#[ignore]
fn test_validate_frame_tag_valid() {
assert!(validate_frame_tag("FRAME\n").is_ok());
assert!(validate_frame_tag("FRAME").is_ok());
assert!(validate_frame_tag("FRAME Xcomment\n").is_ok());
}
#[test]
#[ignore]
fn test_validate_frame_tag_invalid() {
assert!(validate_frame_tag("FRAMES\n").is_err());
assert!(validate_frame_tag("FRAMEX\n").is_err());
assert!(validate_frame_tag("frame\n").is_err());
assert!(validate_frame_tag("").is_err());
}
#[test]
#[ignore]
fn test_chroma_display() {
assert_eq!(format!("{}", Y4mChroma::C420jpeg), "420jpeg");
assert_eq!(format!("{}", Y4mChroma::C422), "422");
assert_eq!(format!("{}", Y4mChroma::C444), "444");
assert_eq!(format!("{}", Y4mChroma::Mono), "mono");
}
#[test]
#[ignore]
fn test_interlace_display() {
assert_eq!(format!("{}", Y4mInterlace::Progressive), "p");
assert_eq!(format!("{}", Y4mInterlace::TopFirst), "t");
assert_eq!(format!("{}", Y4mInterlace::BottomFirst), "b");
assert_eq!(format!("{}", Y4mInterlace::Mixed), "m");
}
#[test]
#[ignore]
fn test_parse_ratio() {
assert_eq!(parse_ratio("30:1"), Some((30, 1)));
assert_eq!(parse_ratio("30000:1001"), Some((30000, 1001)));
assert_eq!(parse_ratio("0:0"), Some((0, 0)));
assert_eq!(parse_ratio("30"), None);
assert_eq!(parse_ratio("abc:def"), None);
assert_eq!(parse_ratio(":1"), None);
}
#[test]
#[ignore]
fn test_header_default_fps_zero_den() {
let mut header = Y4mHeader::default();
header.width = 8;
header.height = 8;
header.fps_den = 0;
assert_eq!(header.fps(), 0.0);
}
#[test]
#[ignore]
fn test_header_default_par_zero_den() {
let mut header = Y4mHeader::default();
header.par_den = 0;
assert_eq!(header.pixel_aspect_ratio(), 1.0);
}
}