Skip to main content

rust_ppm/
image.rs

1//! RGB pixel and image storage.
2
3use std::io;
4use std::path::Path;
5
6/// An 8-bit red, green, and blue pixel.
7#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
8pub struct Pixel {
9    /// Red channel value in the range `[0, 255]`.
10    pub r: u8,
11    /// Green channel value in the range `[0, 255]`.
12    pub g: u8,
13    /// Blue channel value in the range `[0, 255]`.
14    pub b: u8,
15}
16
17impl Pixel {
18    /// Black pixel.
19    pub const BLACK: Self = Self::rgb(0, 0, 0);
20    /// White pixel.
21    pub const WHITE: Self = Self::rgb(255, 255, 255);
22
23    /// Creates a pixel from red, green, and blue channel values.
24    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
25        Self { r, g, b }
26    }
27}
28
29/// A row-major RGB image stored in memory.
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct Image {
32    /// Width of the image in pixels.
33    pub width: usize,
34    /// Height of the image in pixels.
35    pub height: usize,
36    pixels: Vec<Pixel>,
37}
38
39impl Image {
40    /// Creates a new image filled with a black pixel color.
41    pub fn new(width: usize, height: usize) -> Self {
42        Self::from_color(width, height, Pixel::BLACK)
43    }
44
45    /// Creates a new image filled with black.
46    pub fn new_black(width: usize, height: usize) -> Self {
47        Self::from_color(width, height, Pixel::BLACK)
48    }
49
50    /// Creates a new image filled with white.
51    pub fn new_white(width: usize, height: usize) -> Self {
52        Self::from_color(width, height, Pixel::WHITE)
53    }
54
55    /// Creates an image filled with a single color.
56    pub fn from_color(width: usize, height: usize, color: Pixel) -> Self {
57        let pixels = vec![color; pixel_count(width, height)];
58        Self {
59            width,
60            height,
61            pixels,
62        }
63    }
64
65    /// Creates an image from a flat pixel buffer.
66    pub fn from_pixels(width: usize, height: usize, pixels: Vec<Pixel>) -> Self {
67        assert_eq!(
68            pixels.len(),
69            pixel_count(width, height),
70            "pixel count must match image dimensions"
71        );
72        Self {
73            width,
74            height,
75            pixels,
76        }
77    }
78
79    /// Builds an image by evaluating a callback for every `(x, y)` pixel coordinate.
80    pub fn from_pixel_fn(
81        width: usize,
82        height: usize,
83        mut pixel_fn: impl FnMut(usize, usize) -> Pixel,
84    ) -> Self {
85        let mut pixels = Vec::with_capacity(pixel_count(width, height));
86        for y in 0..height {
87            for x in 0..width {
88                pixels.push(pixel_fn(x, y));
89            }
90        }
91        Self::from_pixels(width, height, pixels)
92    }
93
94    /// Opens an image from the given path using the binary `P6` PPM format.
95    pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
96        crate::ppm::read(path)
97    }
98
99    /// Saves the image to the given path as a binary `P6` PPM file.
100    pub fn save(&self, path: impl AsRef<Path>) -> io::Result<()> {
101        crate::ppm::write(self, path)
102    }
103
104    /// Loads an image from a file using the file name string.
105    pub fn from_file(filename: &str) -> io::Result<Self> {
106        Self::open(filename)
107    }
108
109    /// Saves this image to a file using the file name string.
110    pub fn to_file(&self, filename: &str) -> io::Result<()> {
111        self.save(filename)
112    }
113
114    /// Clones an image from an existing one.
115    pub fn from_image(image: &Self) -> Self {
116        image.clone()
117    }
118
119    /// Returns the backing pixel storage.
120    pub fn pixels(&self) -> &[Pixel] {
121        &self.pixels
122    }
123
124    /// Reads a pixel from the image at `(x, y)`.
125    pub fn get_pixel(&self, x: usize, y: usize) -> Option<&Pixel> {
126        self.pixel_index(x, y).map(|index| &self.pixels[index])
127    }
128
129    /// Sets a pixel in the image at `(x, y)`.
130    pub fn set_pixel(&mut self, x: usize, y: usize, pixel: Pixel) {
131        if let Some(index) = self.pixel_index(x, y) {
132            self.pixels[index] = pixel;
133        }
134    }
135
136    fn pixel_index(&self, x: usize, y: usize) -> Option<usize> {
137        (x < self.width && y < self.height).then_some(y * self.width + x)
138    }
139}
140
141fn pixel_count(width: usize, height: usize) -> usize {
142    width
143        .checked_mul(height)
144        .expect("image dimensions overflow")
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn pixels_are_row_major_and_bounds_checked() {
153        let mut image = Image::new_white(2, 2);
154        image.set_pixel(1, 0, Pixel::rgb(1, 2, 3));
155        image.set_pixel(2, 0, Pixel::BLACK);
156
157        assert_eq!(image.get_pixel(1, 0), Some(&Pixel::rgb(1, 2, 3)));
158        assert_eq!(image.get_pixel(0, 1), Some(&Pixel::WHITE));
159        assert_eq!(image.get_pixel(2, 0), None);
160        assert_eq!(image.pixels().len(), 4);
161    }
162
163    #[test]
164    #[should_panic(expected = "pixel count must match image dimensions")]
165    fn from_pixels_rejects_wrong_pixel_count() {
166        Image::from_pixels(2, 2, vec![Pixel::BLACK]);
167    }
168}