Skip to main content

rust_ppm/
ppm.rs

1//! Binary PPM (`P6`) encoding and decoding.
2
3use std::fs::File;
4use std::io::{self, BufRead, BufReader, BufWriter, Write};
5use std::path::Path;
6
7pub use crate::{Image, Pixel};
8
9/// Writes an image as a binary `P6` PPM file.
10pub fn write(image: &Image, path: impl AsRef<Path>) -> io::Result<()> {
11    let file = File::create(path)?;
12    let mut writer = BufWriter::new(file);
13
14    write_to(image, &mut writer)
15}
16
17/// Writes an image to any writer in the binary `P6` PPM format.
18pub fn write_to(image: &Image, mut writer: impl Write) -> io::Result<()> {
19    writeln!(writer, "P6")?;
20    writeln!(writer, "{} {}", image.width, image.height)?;
21    writeln!(writer, "255")?;
22
23    for pixel in image.pixels() {
24        writer.write_all(&[pixel.r, pixel.g, pixel.b])?;
25    }
26
27    Ok(())
28}
29
30/// Reads a binary `P6` PPM file with a maximum channel value of `255`.
31pub fn read(path: impl AsRef<Path>) -> io::Result<Image> {
32    let file = File::open(path)?;
33    let mut reader = BufReader::new(file);
34
35    read_from(&mut reader)
36}
37
38/// Reads a binary `P6` PPM image from any buffered reader.
39pub fn read_from(mut reader: impl BufRead) -> io::Result<Image> {
40    let mut header = String::new();
41
42    reader.read_line(&mut header)?;
43    if header.trim() != "P6" {
44        return Err(io::Error::new(io::ErrorKind::InvalidData, "Not a PPM file"));
45    }
46
47    let mut dimensions = String::new();
48    reader.read_line(&mut dimensions)?;
49    let dims: Vec<&str> = dimensions.split_whitespace().collect();
50    if dims.len() != 2 {
51        return Err(io::Error::new(
52            io::ErrorKind::InvalidData,
53            "Invalid dimensions",
54        ));
55    }
56    let width: usize = dims[0]
57        .parse()
58        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid width"))?;
59    let height: usize = dims[1]
60        .parse()
61        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid height"))?;
62
63    let mut max_color_value = String::new();
64    reader.read_line(&mut max_color_value)?;
65    if max_color_value.trim() != "255" {
66        return Err(io::Error::new(
67            io::ErrorKind::InvalidData,
68            "Unsupported max color value",
69        ));
70    }
71
72    let pixel_count = width
73        .checked_mul(height)
74        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Image dimensions overflow"))?;
75    let mut pixels = Vec::with_capacity(pixel_count);
76    for _ in 0..pixel_count {
77        let mut rgb = [0u8; 3];
78        reader.read_exact(&mut rgb)?;
79        pixels.push(Pixel {
80            r: rgb[0],
81            g: rgb[1],
82            b: rgb[2],
83        });
84    }
85
86    Ok(Image::from_pixels(width, height, pixels))
87}
88
89#[cfg(test)]
90mod tests {
91    use std::io::{BufReader, Cursor};
92
93    use super::*;
94
95    #[test]
96    fn image_round_trips_through_binary_ppm() {
97        let image = Image::from_pixels(2, 1, vec![Pixel::rgb(1, 2, 3), Pixel::rgb(250, 251, 252)]);
98        let mut encoded = Vec::new();
99        write_to(&image, &mut encoded).unwrap();
100
101        let decoded = read_from(BufReader::new(Cursor::new(encoded))).unwrap();
102        assert_eq!(decoded, image);
103    }
104
105    #[test]
106    fn oversized_dimensions_return_invalid_data() {
107        let input = format!("P6\n{} 2\n255\n", usize::MAX);
108        let error = read_from(BufReader::new(Cursor::new(input))).unwrap_err();
109
110        assert_eq!(error.kind(), io::ErrorKind::InvalidData);
111    }
112
113    #[test]
114    fn truncated_pixel_data_returns_unexpected_eof() {
115        let input = b"P6\n1 1\n255\n\x01\x02";
116        let error = read_from(BufReader::new(Cursor::new(input))).unwrap_err();
117
118        assert_eq!(error.kind(), io::ErrorKind::UnexpectedEof);
119    }
120}