nullgeo-cli 0.2.0

Command-line ray tracer for null geodesics in arbitrary spacetimes
use std::fs::File;
use std::io::{BufWriter, Write};

pub fn write_ppm_gray(
    path: &str,
    width: usize,
    height: usize,
    pixels: &[u8],
) -> std::io::Result<()> {
    if pixels.len() != width * height {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            "pixel buffer size mismatch",
        ));
    }
    let mut f = BufWriter::new(File::create(path)?);
    writeln!(f, "P5")?;
    writeln!(f, "{} {}", width, height)?;
    writeln!(f, "255")?;
    f.write_all(pixels)?;
    Ok(())
}

pub fn write_ppm_rgb(
    path: &str,
    width: usize,
    height: usize,
    pixels: &[[u8; 3]],
) -> std::io::Result<()> {
    if pixels.len() != width * height {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            "pixel buffer size mismatch",
        ));
    }
    let mut f = BufWriter::new(File::create(path)?);
    writeln!(f, "P6")?;
    writeln!(f, "{} {}", width, height)?;
    writeln!(f, "255")?;
    for px in pixels {
        f.write_all(px)?;
    }
    Ok(())
}

pub fn write_pfm_gray(
    path: &str,
    width: usize,
    height: usize,
    values: &[f64],
) -> std::io::Result<()> {
    if values.len() != width * height {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            "value buffer size mismatch",
        ));
    }
    let mut f = BufWriter::new(File::create(path)?);
    write!(f, "Pf\n{} {}\n-1.0\n", width, height)?;
    for row in (0..height).rev() {
        for &v in &values[row * width..(row + 1) * width] {
            f.write_all(&(v as f32).to_le_bytes())?;
        }
    }
    Ok(())
}

pub fn write_pfm_rgb(
    path: &str,
    width: usize,
    height: usize,
    pixels: &[[f32; 3]],
) -> std::io::Result<()> {
    if pixels.len() != width * height {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            "pixel buffer size mismatch",
        ));
    }
    let mut f = BufWriter::new(File::create(path)?);
    write!(f, "PF\n{} {}\n-1.0\n", width, height)?;
    for row in (0..height).rev() {
        for px in &pixels[row * width..(row + 1) * width] {
            for channel in px {
                f.write_all(&channel.to_le_bytes())?;
            }
        }
    }
    Ok(())
}

pub fn write_csv_matrix(
    path: &str,
    width: usize,
    height: usize,
    values: &[f64],
) -> std::io::Result<()> {
    if values.len() != width * height {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            "value buffer size mismatch",
        ));
    }
    let mut f = BufWriter::new(File::create(path)?);
    for row in values.chunks(width) {
        let line: Vec<String> = row.iter().map(|v| v.to_string()).collect();
        writeln!(f, "{}", line.join(","))?;
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn pfm_gray_layout_is_bottom_up_little_endian() {
        let dir = std::env::temp_dir().join("nullgeo_io_pfm_gray");
        std::fs::create_dir_all(&dir).unwrap();
        let path = dir.join("map.pfm");
        let path = path.to_str().unwrap();
        write_pfm_gray(path, 2, 2, &[1.0, 2.0, 3.0, 4.0]).unwrap();
        let bytes = std::fs::read(path).unwrap();
        let header = b"Pf\n2 2\n-1.0\n";
        assert_eq!(&bytes[..header.len()], header);
        let mut floats = bytes[header.len()..]
            .chunks(4)
            .map(|c| f32::from_le_bytes(c.try_into().unwrap()));
        assert_eq!(floats.next(), Some(3.0));
        assert_eq!(floats.next(), Some(4.0));
        assert_eq!(floats.next(), Some(1.0));
        assert_eq!(floats.next(), Some(2.0));
        assert_eq!(floats.next(), None);
    }

    #[test]
    fn csv_matrix_rows_match_image_rows() {
        let dir = std::env::temp_dir().join("nullgeo_io_csv");
        std::fs::create_dir_all(&dir).unwrap();
        let path = dir.join("map.csv");
        let path = path.to_str().unwrap();
        write_csv_matrix(path, 3, 2, &[1.0, 2.5, f64::NAN, 4.0, 5.0, 6.0]).unwrap();
        let text = std::fs::read_to_string(path).unwrap();
        assert_eq!(text, "1,2.5,NaN\n4,5,6\n");
    }
}