1use std::fs::File;
4use std::io::{self, BufRead, BufReader, BufWriter, Write};
5use std::path::Path;
6
7pub use crate::{Image, Pixel};
8
9pub 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
17pub 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
30pub 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
38pub 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}