diff --git a/ffimage-app/Cargo.toml b/ffimage-app/Cargo.toml index bbbdce9..b561feb 100644 --- a/ffimage-app/Cargo.toml +++ b/ffimage-app/Cargo.toml @@ -11,7 +11,6 @@ repository= "https://github.com/raymanfx/ffimage" [dependencies] async-std = "1.12.0" -atty = "0.2.14" iced = { version = "0.7.0", features = ["async-std", "image"] } ffimage = { version = "0.10.0", path = "../ffimage" } diff --git a/ffimage-app/src/main.rs b/ffimage-app/src/main.rs index 419dd81..cab1d48 100644 --- a/ffimage-app/src/main.rs +++ b/ffimage-app/src/main.rs @@ -1,5 +1,6 @@ use std::{env, io, io::Read}; +use ffimage_yuv::yuv420::Yuv420p; use iced::{ executor, widget::{column, container, image, text::Text}, @@ -11,7 +12,9 @@ use ffimage::{ iter::{BytesExt, ColorConvertExt, PixelsExt}, }; +mod parser; mod ppm; +mod y4m; mod rgba; use rgba::Rgba; @@ -125,25 +128,38 @@ pub struct Image { } async fn load_from_stdin() -> Result { - if atty::isnt(atty::Stream::Stdin) { - return Err("stdin is no tty"); - } - // read bytes from stdin - let stdin = io::stdin().lock(); - let bytes = io::BufReader::new(stdin).bytes(); - let bytes = bytes.filter_map(|res| match res { - Ok(byte) => Some(byte), - Err(_) => None, - }); - - let res = ppm::read(bytes); - match res { - Ok(ppm) => Ok(Image { + let mut stdin = io::stdin().lock(); + let mut bytes = Vec::new(); + stdin + .read_to_end(&mut bytes) + .or(Err("could not read bytes from stdin"))?; + + if let Some(res) = ppm::read(bytes.iter().copied()) { + let ppm = res?; + + return Ok(Image { width: ppm.width, height: ppm.height, rgb: ppm.bytes, - }), - Err(e) => Err(e), + }); } + + if let Some(res) = y4m::read(bytes.iter().copied()) { + let y4m = res?; + let rgb: Vec = Yuv420p::pack(&y4m.bytes, y4m.width, y4m.height) + .into_iter() + .colorconvert::>() + .bytes() + .flatten() + .collect(); + + return Ok(Image { + width: y4m.width, + height: y4m.height, + rgb, + }); + } + + Err("unknown image format") } diff --git a/ffimage-app/src/parser.rs b/ffimage-app/src/parser.rs new file mode 100644 index 0000000..298cfca --- /dev/null +++ b/ffimage-app/src/parser.rs @@ -0,0 +1,56 @@ +use std::str::FromStr; + +pub fn parse_char(bytes: &mut impl Iterator) -> Option> { + let byte = bytes.next()?; + match byte { + b'A'..=b'Z' | b'a'..=b'z' => Some(Ok(byte as char)), + _ => Some(Err(byte)), + } +} + +pub fn parse_digit(bytes: &mut impl Iterator) -> Option> { + let byte = bytes.next()?; + match byte { + b'0'..=b'9' => Some(Ok(byte as char)), + _ => Some(Err(byte)), + } +} + +pub fn parse_seq( + bytes: &mut I, + predicate: impl Fn(&mut I) -> Option>, +) -> Option<(String, u8)> +where + I: Iterator, +{ + let mut seq = String::new(); + + loop { + let res = predicate(bytes)?; + match res { + Ok(val) => seq.push(val), + Err(b) => return Some((seq, b)), + } + } +} + +pub fn parse_ascii(bytes: &mut impl Iterator) -> Option<(String, u8)> { + parse_seq(bytes, |iter| { + let b = iter.next()?; + match b { + b' ' => Some(Err(b)), + b'\n' => Some(Err(b)), + _ => Some(Ok(b as char)), + } + }) +} + +pub fn parse_u32( + bytes: &mut impl Iterator, +) -> Option<(Result::Err>, u8)> { + let (word, other) = parse_seq(bytes, parse_digit)?; + match word.parse::() { + Ok(number) => Some((Ok(number), other)), + Err(e) => Some((Err(e), other)), + } +} diff --git a/ffimage-app/src/ppm.rs b/ffimage-app/src/ppm.rs index ce3bdf5..42b7546 100644 --- a/ffimage-app/src/ppm.rs +++ b/ffimage-app/src/ppm.rs @@ -1,3 +1,5 @@ +use crate::parser::*; + pub struct Ppm { pub width: u32, pub height: u32, @@ -5,7 +7,7 @@ pub struct Ppm { pub bytes: Vec, } -pub fn read(bytes: impl IntoIterator) -> Result { +pub fn read(bytes: impl IntoIterator) -> Option> { let mut bytes = bytes.into_iter(); // parse format from first line @@ -13,75 +15,59 @@ pub fn read(bytes: impl IntoIterator) -> Result { magic[0] = if let Some(byte) = bytes.next() { byte } else { - return Err("ppm: not enough bytes"); + return None; }; magic[1] = if let Some(byte) = bytes.next() { byte } else { - return Err("ppm: not enough bytes"); + return None; }; // is this a P6 PPM? if magic != *b"P6" { - return Err("ppm: cannot handle magic"); + return None; } - fn real_bytes(iter: &mut impl Iterator, limit: usize) -> Vec { - let mut bytes = Vec::new(); - for byte in iter { - if bytes.len() == limit { - break; - } - - if byte == b' ' || byte == b'\n' { - if !bytes.is_empty() { - break; - } - } else { - bytes.push(byte); - } - } - bytes + match bytes.next()? { + b' ' | b'\n' => {} + _ => return Some(Err("ppm: expected whitespace")), } // parse width - let width_bytes = real_bytes(&mut bytes, 10); - let width = std::str::from_utf8(&width_bytes) - .expect("bytes should contain ASCII data") - .parse::() - .expect("value should be integer"); + let width = match parse_u32(&mut bytes)?.0 { + Ok(val) => val, + Err(_e) => return Some(Err("ppm: failed to parse width")), + }; // parse height - let height_bytes = real_bytes(&mut bytes, 10); - let height = std::str::from_utf8(&height_bytes) - .expect("bytes should contain ASCII data") - .parse::() - .expect("value should be integer"); + let height = match parse_u32(&mut bytes)?.0 { + Ok(val) => val, + Err(_e) => return Some(Err("ppm: failed to parse height")), + }; // parse range - let range_bytes = real_bytes(&mut bytes, 10); - let range = std::str::from_utf8(&range_bytes) - .expect("bytes should contain ASCII data") - .parse::() - .expect("value should be integer"); + let range = match parse_u32(&mut bytes)?.0 { + Ok(val) => val, + Err(_e) => return Some(Err("ppm: failed to parse range")), + }; if range > 255 { - return Err("ppm: cannot handle range: {range}"); + return Some(Err("ppm: cannot handle range: {range}")); } // take only as many bytes as we expect there to be in the image - let ppm_len = width * height * 3; + let ppm_len = (width * height * 3) as usize; let bytes: Vec = bytes.take(ppm_len).collect(); // verify buffer length - if bytes.len() != width * height * 3 { - return Err("ppm: invalid length"); + if bytes.len() != ppm_len { + return Some(Err("ppm: invalid length")); } - Ok(Ppm { + Some(Ok(Ppm { width: width as u32, height: height as u32, range: range as u32, bytes, - }) + })) } diff --git a/ffimage-app/src/y4m.rs b/ffimage-app/src/y4m.rs new file mode 100644 index 0000000..a8b612a --- /dev/null +++ b/ffimage-app/src/y4m.rs @@ -0,0 +1,218 @@ +use crate::parser::*; + +#[derive(Debug, Clone)] +pub struct Y4m { + pub width: u32, + pub height: u32, + pub framerate: (u32, u32), + pub interlacing: char, + pub aspect_ratio: (u32, u32), + pub color_space: ColorSpace, + pub bytes: Vec, +} + +pub fn read(bytes: impl IntoIterator) -> Option> { + let mut bytes = bytes.into_iter(); + + // check signature + let mut signature = [0u8; 10]; + for byte in &mut signature { + *byte = match bytes.next() { + Some(byte) => byte, + None => return Some(Err("y4m: no signature")), + } + } + if signature != *b"YUV4MPEG2 " { + return Some(Err("y4m: invalid signature")); + } + + let mut y4m = Y4m { + width: 0, + height: 0, + framerate: (0, 0), + interlacing: ' ', + aspect_ratio: (0, 0), + color_space: ColorSpace::C420jpeg, + bytes: Vec::new(), + }; + + // parse parameters + loop { + let (res, other) = parse_params(&mut bytes)?; + let param = match res { + Ok(param) => param, + Err(e) => return Some(Err(e)), + }; + match param { + Param::Width(width) => y4m.width = width, + Param::Height(height) => y4m.height = height, + Param::Framerate((num, denom)) => y4m.framerate = (num, denom), + Param::Interlacing(interlacing) => y4m.interlacing = interlacing, + Param::AspectRatio(ratio) => y4m.aspect_ratio = ratio, + Param::ColorSpace(colorspace) => y4m.color_space = colorspace, + Param::Unknown(word) => println!("y4m: unknown tag: {}", word), + } + + if other == b'\n' { + break; + } + } + + // parse frame + let mut marker = [0u8; 5]; + for byte in &mut marker { + *byte = match bytes.next() { + Some(byte) => byte, + None => return Some(Err("y4m: missing frame marker")), + } + } + if marker != *b"FRAME" { + return Some(Err("y4m: invalid frame marker")); + } + + // check for frame parameters + match bytes.next() { + Some(b' ') => todo!("y4m: parse frame parameters"), + Some(b'\n') => {} + Some(_) => return Some(Err("y4m: unexpected byte after frame marker")), + None => return Some(Err("y4m: missing frame marker")), + }; + + // take only as many bytes as we expect there to be in the image + let y4m_len = (y4m.width * y4m.height * y4m.color_space.bpp() / 8) as usize; + y4m.bytes = bytes.take(y4m_len).collect(); + + // verify buffer length + if y4m.bytes.len() != y4m_len { + return Some(Err("y4m: unexpected EOF")); + } + + Some(Ok(y4m)) +} + +#[derive(Debug, Clone)] +pub enum Param { + Unknown(String), + Width(u32), + Height(u32), + Framerate((u32, u32)), + Interlacing(char), + AspectRatio((u32, u32)), + ColorSpace(ColorSpace), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ColorSpace { + C420jpeg, + C420paldv, + C420, + C422, + C444, + Cmono, +} + +impl ColorSpace { + pub fn parse(value: &str) -> Option { + match value { + "420jpeg" => Some(ColorSpace::C420jpeg), + "420paldv" => Some(ColorSpace::C420paldv), + "420" => Some(ColorSpace::C420), + "422" => Some(ColorSpace::C422), + "444" => Some(ColorSpace::C444), + "mono" => Some(ColorSpace::Cmono), + _ => None, + } + } + + pub fn bpp(&self) -> u32 { + match self { + ColorSpace::C420jpeg | ColorSpace::C420paldv | ColorSpace::C420 => 12, + ColorSpace::C422 => 16, + ColorSpace::C444 => 24, + ColorSpace::Cmono => 8, + } + } +} + +pub fn parse_params( + bytes: &mut impl Iterator, +) -> Option<(Result, u8)> { + fn parse_fraction( + bytes: &mut impl Iterator, + ) -> Option<(Result<(u32, u32), &'static str>, u8)> { + let (res, other) = parse_u32(bytes)?; + let num = match res { + Ok(num) => num, + Err(_e) => return Some((Err("y4m: faild to parse integer"), other)), + }; + if other != b':' { + return Some((Err("y4m: expected fraction delimiter (:)"), other)); + } + let (res, other) = parse_u32(bytes)?; + let denom = match res { + Ok(num) => num, + Err(_e) => return Some((Err("y4m: faild to parse integer"), other)), + }; + + Some((Ok((num, denom)), other)) + } + + let (param, other) = match bytes.next()? { + b'W' => { + let (res, other) = parse_u32(bytes)?; + let val = match res { + Ok(val) => val, + Err(_e) => return Some((Err("y4m: failed to parse integer"), other)), + }; + (Param::Width(val), other) + } + b'H' => { + let (res, other) = parse_u32(bytes)?; + let val = match res { + Ok(val) => val, + Err(_e) => return Some((Err("y4m: failed to parse integer"), other)), + }; + (Param::Height(val), other) + } + b'F' => { + let (res, other) = parse_fraction(bytes)?; + let val = match res { + Ok(val) => val, + Err(e) => return Some((Err(e), other)), + }; + (Param::Framerate(val), other) + } + b'I' => { + let res = parse_char(bytes)?; + let val = match res { + Ok(val) => val, + Err(e) => return Some((Err("y4m: failed to parse char"), e)), + }; + let other = bytes.next()?; + (Param::Interlacing(val), other) + } + b'A' => { + let (res, other) = parse_fraction(bytes)?; + let val = match res { + Ok(val) => val, + Err(e) => return Some((Err(e), other)), + }; + (Param::AspectRatio(val), other) + } + b'C' => { + let (word, other) = parse_ascii(bytes)?; + let res = ColorSpace::parse(&word).ok_or("y4m: failed to parse colorspace"); + let val = match res { + Ok(val) => val, + Err(e) => return Some((Err(e), other)), + }; + (Param::ColorSpace(val), other) + } + _ => { + let (word, other) = parse_ascii(bytes)?; + (Param::Unknown(word), other) + } + }; + + Some((Ok(param), other)) +}