From 25f2db976503637e41a6d4a6d053c929dbe19531 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sat, 24 Oct 2026 13:31:44 +0100 Subject: [PATCH] Implement Farbfeld format --- src/image.rs | 4 + src/image/farbfeld.rs | 227 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 231 insertions(+) create mode 100644 src/image/farbfeld.rs diff --git a/src/image.rs b/src/image.rs index 6d8104c..7b75d56 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,3 +1,7 @@ +pub mod farbfeld; + +pub use farbfeld::*; + #[derive(Clone, Copy, Debug, PartialEq)] pub struct Pixel { pub r: f32, diff --git a/src/image/farbfeld.rs b/src/image/farbfeld.rs new file mode 100644 index 0000000..2d3c872 --- /dev/null +++ b/src/image/farbfeld.rs @@ -0,0 +1,227 @@ +use super::*; + +pub struct Farbfeld; + +fn read_header(input: &mut I) -> Result +where + I: std::io::Read, +{ + let mut bytes: [u8; 16] = [0; 16]; + if input.read(&mut bytes)? < 16 || &bytes[0..8] != b"farbfeld" { + Err(Error::Invalid) + } else { + Ok(Extent { + x: u32::from_be_bytes(bytes[8..12].try_into().unwrap()), + y: u32::from_be_bytes(bytes[12..16].try_into().unwrap()), + }) + } +} + +fn read_pixels(input: &mut I, extent: Extent) -> Result, Error> +where + I: std::io::Read, +{ + let mut pixels = Vec::with_capacity(extent.size() as usize); + for _ in 0..extent.size() { + pixels.push(read_pixel(input)?); + } + Ok(pixels) +} + +fn read_pixel(input: &mut I) -> Result +where + I: std::io::Read, +{ + let mut bytes: [u8; 8] = [0; 8]; + if input.read(&mut bytes)? < 8 { + Err(Error::Invalid) + } else { + Ok(Pixel { + r: to_channel(bytes[0..2].try_into().unwrap()), + g: to_channel(bytes[2..4].try_into().unwrap()), + b: to_channel(bytes[4..6].try_into().unwrap()), + a: to_channel(bytes[6..8].try_into().unwrap()), + }) + } +} + +fn to_channel(bytes: [u8; 2]) -> f32 { + u16::from_be_bytes(bytes) as f32 / u16::MAX as f32 +} + +fn from_channel(channel: f32) -> [u8; 2] { + ((channel * u16::MAX as f32) as u16).to_be_bytes() +} + +impl Format for Farbfeld { + fn read(input: &mut I) -> Result + where + I: std::io::Read, + { + let extent = read_header(input)?; + Ok(Image { + extent, + pixels: read_pixels(input, extent)?, + }) + } + + fn write(output: &mut O, img: &Image) -> std::io::Result<()> + where + O: std::io::Write, + { + output.write_all(b"farbfeld")?; + output.write_all(&img.extent.x.to_be_bytes())?; + output.write_all(&img.extent.y.to_be_bytes())?; + for pixel in &img.pixels { + output.write_all(&from_channel(pixel.r))?; + output.write_all(&from_channel(pixel.g))?; + output.write_all(&from_channel(pixel.b))?; + output.write_all(&from_channel(pixel.a))?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const EPSILON: f32 = 1e-3; + + macro_rules! assert_matches { + ( $act:expr, $pat:pat ) => { + match $act { + $pat => (), + _ => panic!( + "assertion `left matches right` failed\n\ + {:>6}: {:?}\n{:>6}: {}\n", + "left", + $act, + "right", + stringify!($pat) + ), + } + }; + } + + macro_rules! assert_approx { + ( $act:expr, $exp:expr ) => { + if ($act - $exp).abs() >= EPSILON { + panic!( + "assertion `left ~= right` failed\n\ + {:>6}: {:?}\n{:>6}: {:?}\n", + "left", $act, "right", $exp + ); + } + }; + } + + #[test] + fn read_invalid_magic() { + let mut ff = std::io::Cursor::new( + [&b"forbfeld"[..], &[0, 0, 0, 1], &[0, 0, 0, 1], &[0; 8]] + .concat(), + ); + let err = Farbfeld::read(&mut ff).unwrap_err(); + assert_matches!(err, Error::Invalid); + } + + #[test] + fn read_extent_1x2() { + let mut ff = std::io::Cursor::new( + [&b"farbfeld"[..], &[0, 0, 0, 1], &[0, 0, 0, 2], &[0; 16]] + .concat(), + ); + let img = Farbfeld::read(&mut ff).unwrap(); + assert_eq!(img.extent.x, 1); + assert_eq!(img.extent.y, 2); + } + + #[test] + fn read_incomplete_header() { + let mut ff = std::io::Cursor::new(b"farbfeld\x00\x00\x00\x01"); + let err = Farbfeld::read(&mut ff).unwrap_err(); + assert_matches!(err, Error::Invalid); + } + + #[test] + fn read_too_few_pixels() { + let mut ff = std::io::Cursor::new( + [&b"farbfeld"[..], &[0, 0, 0, 2], &[0, 0, 0, 2], &[0; 24]] + .concat(), + ); + let err = Farbfeld::read(&mut ff).unwrap_err(); + assert_matches!(err, Error::Invalid); + } + + #[test] + fn read_single_pixel() { + let mut ff = std::io::Cursor::new( + [ + &b"farbfeld"[..], + &[0, 0, 0, 1], + &[0, 0, 0, 1], + &[0x00, 0x00, 0x40, 0x00, 0x80, 0x00, 0xff, 0xff], + ] + .concat(), + ); + let img = Farbfeld::read(&mut ff).unwrap(); + assert_eq!(img.pixels.len(), 1); + assert_approx!(img.pixels[0].r, 0.0); + assert_approx!(img.pixels[0].g, 0.25); + assert_approx!(img.pixels[0].b, 0.5); + assert_approx!(img.pixels[0].a, 1.0); + } + + #[test] + fn write_magic() { + let img = Image { + extent: Extent { x: 1, y: 1 }, + pixels: vec![Pixel { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0, + }], + }; + let mut out = Vec::new(); + Farbfeld::write(&mut std::io::Cursor::new(&mut out), &img).unwrap(); + assert_eq!(&out[0..8], b"farbfeld"); + } + + #[test] + fn write_extent() { + let img = Image { + extent: Extent { x: 1, y: 2 }, + pixels: vec![Pixel { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0, + }], + }; + let mut out = Vec::new(); + Farbfeld::write(&mut std::io::Cursor::new(&mut out), &img).unwrap(); + assert_eq!(out[8..12], [0, 0, 0, 1]); + assert_eq!(out[12..16], [0, 0, 0, 2]); + } + + #[test] + fn write_pixel() { + let img = Image { + extent: Extent { x: 1, y: 1 }, + pixels: vec![Pixel { + r: 0.0, + g: 0.25, + b: 0.5, + a: 1.0, + }], + }; + let mut out = Vec::new(); + Farbfeld::write(&mut std::io::Cursor::new(&mut out), &img).unwrap(); + assert_eq!(out[16..18], [0, 0]); + assert_eq!(out[18..20], [0x3f, 0xff]); + assert_eq!(out[20..22], [0x7f, 0xff]); + assert_eq!(out[22..24], [0xff, 0xff]); + } +}