Skip to content

Commit

Permalink
app: Add support for y4m files
Browse files Browse the repository at this point in the history
Signed-off-by: Christopher N. Hesse <[email protected]>
  • Loading branch information
raymanfx committed Oct 29, 2023
1 parent 2f386c1 commit 04c2895
Show file tree
Hide file tree
Showing 5 changed files with 333 additions and 58 deletions.
1 change: 0 additions & 1 deletion ffimage-app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
48 changes: 32 additions & 16 deletions ffimage-app/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{env, io, io::Read};

use ffimage_yuv::yuv420::Yuv420p;
use iced::{
executor,
widget::{column, container, image, text::Text},
Expand All @@ -11,7 +12,9 @@ use ffimage::{
iter::{BytesExt, ColorConvertExt, PixelsExt},
};

mod parser;
mod ppm;
mod y4m;

mod rgba;
use rgba::Rgba;
Expand Down Expand Up @@ -125,25 +128,38 @@ pub struct Image {
}

async fn load_from_stdin() -> Result<Image, &'static str> {
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<u8> = Yuv420p::pack(&y4m.bytes, y4m.width, y4m.height)
.into_iter()
.colorconvert::<Rgb<u8>>()
.bytes()
.flatten()
.collect();

return Ok(Image {
width: y4m.width,
height: y4m.height,
rgb,
});
}

Err("unknown image format")
}
56 changes: 56 additions & 0 deletions ffimage-app/src/parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use std::str::FromStr;

pub fn parse_char(bytes: &mut impl Iterator<Item = u8>) -> Option<Result<char, u8>> {
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<Item = u8>) -> Option<Result<char, u8>> {
let byte = bytes.next()?;
match byte {
b'0'..=b'9' => Some(Ok(byte as char)),
_ => Some(Err(byte)),
}
}

pub fn parse_seq<I>(
bytes: &mut I,
predicate: impl Fn(&mut I) -> Option<Result<char, u8>>,
) -> Option<(String, u8)>
where
I: Iterator<Item = u8>,
{
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<Item = u8>) -> 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<Item = u8>,
) -> Option<(Result<u32, <u32 as FromStr>::Err>, u8)> {
let (word, other) = parse_seq(bytes, parse_digit)?;
match word.parse::<u32>() {
Ok(number) => Some((Ok(number), other)),
Err(e) => Some((Err(e), other)),
}
}
68 changes: 27 additions & 41 deletions ffimage-app/src/ppm.rs
Original file line number Diff line number Diff line change
@@ -1,87 +1,73 @@
use crate::parser::*;

pub struct Ppm {
pub width: u32,
pub height: u32,
pub range: u32,
pub bytes: Vec<u8>,
}

pub fn read(bytes: impl IntoIterator<Item = u8>) -> Result<Ppm, &'static str> {
pub fn read(bytes: impl IntoIterator<Item = u8>) -> Option<Result<Ppm, &'static str>> {
let mut bytes = bytes.into_iter();

// parse format from first line
let mut magic = [0u8; 2];
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<Item = u8>, limit: usize) -> Vec<u8> {
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::<usize>()
.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::<usize>()
.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::<usize>()
.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<u8> = 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,
})
}))
}
Loading

0 comments on commit 04c2895

Please sign in to comment.