rust-ppm 0.1.1

Small RGB image and plotting library for generating PPM graphics
Documentation
//! RGB pixel and image storage.

use std::io;
use std::path::Path;

/// An 8-bit red, green, and blue pixel.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Pixel {
    /// Red channel value in the range `[0, 255]`.
    pub r: u8,
    /// Green channel value in the range `[0, 255]`.
    pub g: u8,
    /// Blue channel value in the range `[0, 255]`.
    pub b: u8,
}

impl Pixel {
    /// Black pixel.
    pub const BLACK: Self = Self::rgb(0, 0, 0);
    /// White pixel.
    pub const WHITE: Self = Self::rgb(255, 255, 255);

    /// Creates a pixel from red, green, and blue channel values.
    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
        Self { r, g, b }
    }
}

/// A row-major RGB image stored in memory.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Image {
    /// Width of the image in pixels.
    pub width: usize,
    /// Height of the image in pixels.
    pub height: usize,
    pixels: Vec<Pixel>,
}

impl Image {
    /// Creates a new image filled with a black pixel color.
    pub fn new(width: usize, height: usize) -> Self {
        Self::from_color(width, height, Pixel::BLACK)
    }

    /// Creates a new image filled with black.
    pub fn new_black(width: usize, height: usize) -> Self {
        Self::from_color(width, height, Pixel::BLACK)
    }

    /// Creates a new image filled with white.
    pub fn new_white(width: usize, height: usize) -> Self {
        Self::from_color(width, height, Pixel::WHITE)
    }

    /// Creates an image filled with a single color.
    pub fn from_color(width: usize, height: usize, color: Pixel) -> Self {
        let pixels = vec![color; pixel_count(width, height)];
        Self {
            width,
            height,
            pixels,
        }
    }

    /// Creates an image from a flat pixel buffer.
    pub fn from_pixels(width: usize, height: usize, pixels: Vec<Pixel>) -> Self {
        assert_eq!(
            pixels.len(),
            pixel_count(width, height),
            "pixel count must match image dimensions"
        );
        Self {
            width,
            height,
            pixels,
        }
    }

    /// Builds an image by evaluating a callback for every `(x, y)` pixel coordinate.
    pub fn from_pixel_fn(
        width: usize,
        height: usize,
        mut pixel_fn: impl FnMut(usize, usize) -> Pixel,
    ) -> Self {
        let mut pixels = Vec::with_capacity(pixel_count(width, height));
        for y in 0..height {
            for x in 0..width {
                pixels.push(pixel_fn(x, y));
            }
        }
        Self::from_pixels(width, height, pixels)
    }

    /// Opens an image from the given path using the binary `P6` PPM format.
    pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
        crate::ppm::read(path)
    }

    /// Saves the image to the given path as a binary `P6` PPM file.
    pub fn save(&self, path: impl AsRef<Path>) -> io::Result<()> {
        crate::ppm::write(self, path)
    }

    /// Loads an image from a file using the file name string.
    pub fn from_file(filename: &str) -> io::Result<Self> {
        Self::open(filename)
    }

    /// Saves this image to a file using the file name string.
    pub fn to_file(&self, filename: &str) -> io::Result<()> {
        self.save(filename)
    }

    /// Clones an image from an existing one.
    pub fn from_image(image: &Self) -> Self {
        image.clone()
    }

    /// Returns the backing pixel storage.
    pub fn pixels(&self) -> &[Pixel] {
        &self.pixels
    }

    /// Reads a pixel from the image at `(x, y)`.
    pub fn get_pixel(&self, x: usize, y: usize) -> Option<&Pixel> {
        self.pixel_index(x, y).map(|index| &self.pixels[index])
    }

    /// Sets a pixel in the image at `(x, y)`.
    pub fn set_pixel(&mut self, x: usize, y: usize, pixel: Pixel) {
        if let Some(index) = self.pixel_index(x, y) {
            self.pixels[index] = pixel;
        }
    }

    fn pixel_index(&self, x: usize, y: usize) -> Option<usize> {
        (x < self.width && y < self.height).then_some(y * self.width + x)
    }
}

fn pixel_count(width: usize, height: usize) -> usize {
    width
        .checked_mul(height)
        .expect("image dimensions overflow")
}

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

    #[test]
    fn pixels_are_row_major_and_bounds_checked() {
        let mut image = Image::new_white(2, 2);
        image.set_pixel(1, 0, Pixel::rgb(1, 2, 3));
        image.set_pixel(2, 0, Pixel::BLACK);

        assert_eq!(image.get_pixel(1, 0), Some(&Pixel::rgb(1, 2, 3)));
        assert_eq!(image.get_pixel(0, 1), Some(&Pixel::WHITE));
        assert_eq!(image.get_pixel(2, 0), None);
        assert_eq!(image.pixels().len(), 4);
    }

    #[test]
    #[should_panic(expected = "pixel count must match image dimensions")]
    fn from_pixels_rejects_wrong_pixel_count() {
        Image::from_pixels(2, 2, vec![Pixel::BLACK]);
    }
}