glance_core/img/
mod.rs

1//! This module provides the [`Image`] struct for handling images with generic pixel data.
2//! It includes functionality for loading, saving, displaying images, and manipulating pixel data.
3//! The pixel data is represented by a type that implements the [`Pixel`] trait, allowing for
4//! flexible support of different pixel formats like Rgba and Luma, with u8, u16, f32 (for now).
5//!
6//! ## Examples
7//!
8//! ```
9//! use glance_core::img::{Image, pixel::Rgba};
10//!
11//! // Load an image. Type annotations are required for the pixel type. (Might change in the
12//! // future)
13//! if let Ok(image)= Image::<Rgba<u8>>::open("input.png") {
14//!     let _ = image.display("My Image");
15//! }
16//! ```
17pub mod iterators;
18pub mod pixel;
19
20use std::path::Path;
21
22use crate::{CoreError, Result, drawing::traits::Drawable};
23use image::{ImageBuffer, ImageReader, Rgba as ImageRgba};
24use minifb::{Key, Window, WindowOptions};
25use pixel::{Pixel, Rgba};
26
27/// Image struct represents an image with pixel data of type P
28/// where P implements the [`Pixel`] trait.
29pub struct Image<P: Pixel> {
30    width: usize,
31    height: usize,
32    data: Vec<P>,
33}
34
35impl<P> Image<P>
36where
37    P: Pixel,
38{
39    /// Creates a new empty [`Image`] instance with the specified width and height.
40    pub fn new(width: usize, height: usize) -> Self {
41        Image {
42            width,
43            height,
44            data: vec![P::from_rgba8([0, 0, 0, 0]).unwrap(); width * height],
45        }
46    }
47
48    /// Creates a new [`Image`] instance from the given path.
49    pub fn open<Pth: AsRef<Path>>(path: Pth) -> Result<Self> {
50        let image = ImageReader::open(path)?.decode()?.to_rgba8();
51        let (width, height) = image.dimensions();
52        let width = width as usize;
53        let height = height as usize;
54
55        let data: Result<Vec<P>> = image.pixels().map(|p| P::from_rgba8(p.0)).collect();
56        let data = data?;
57
58        Ok(Image {
59            width,
60            height,
61            data,
62        })
63    }
64
65    /// Saves the image to the specified path. File format is determined by the file extension.
66    /// See [`image::ImageBuffer::save`] for more details.
67    pub fn save<Pth: AsRef<Path>>(&self, path: Pth) -> Result<()> {
68        let rgba8_data: Vec<[u8; 4]> = self.data.iter().map(|pixel| pixel.to_rgba8()).collect();
69        let rgba8_bytes: Vec<u8> = rgba8_data.iter().flatten().copied().collect();
70
71        let buffer = ImageBuffer::<ImageRgba<u8>, _>::from_raw(
72            self.width as u32,
73            self.height as u32,
74            rgba8_bytes,
75        )
76        .ok_or_else(|| std::io::Error::other("Invalid buffer"))?;
77        buffer.save(path)?;
78
79        Ok(())
80    }
81
82    /// Opens an [`Image`] instance and displays it in a window.
83    pub fn display(&self, title: &str) -> Result<()> {
84        let (width, height) = self.dimensions();
85
86        // Create window
87        let mut window = Window::new(
88            title,
89            width,
90            height,
91            WindowOptions {
92                resize: false,
93                ..Default::default()
94            },
95        )?;
96        window.set_target_fps(30);
97
98        // Populate framebuffer
99        let rgba8_data: Vec<[u8; 4]> = self.data.iter().map(|px| px.to_rgba8()).collect();
100
101        let mut buffer: Vec<u32> = Vec::with_capacity(rgba8_data.len());
102        for pixel in rgba8_data.iter() {
103            buffer.push(u32::from_be_bytes([pixel[3], pixel[0], pixel[1], pixel[2]]));
104        }
105
106        while window.is_open() && !window.is_key_down(Key::Escape) {
107            window.update_with_buffer(&buffer, width, height)?;
108        }
109
110        Ok(())
111    }
112
113    /// Returns a reference to the pixel data at the specified position.
114    /// Returns an error if the position is out of bounds.
115    pub fn get_pixel(&self, position: (usize, usize)) -> Result<&P> {
116        let idx = position.1 * self.width + position.0;
117        self.data.get(idx).ok_or_else(|| {
118            CoreError::OutOfBounds(format!(
119                "{:#?} is out of bounds for image of size {:#?}",
120                position,
121                self.dimensions()
122            ))
123        })
124    }
125
126    /// Sets the pixel at the specified position to the given color.
127    /// Colors are of type P, which implements the [`Pixel`] trait.
128    /// Returns an error if the position is out of bounds.
129    pub fn set_pixel(&mut self, position: (usize, usize), color: P) -> Result<()> {
130        let idx = position.1 * self.width + position.0;
131        if let Some(px) = self.data.get_mut(idx) {
132            *px = color;
133        }
134        Ok(())
135    }
136
137    /// Draws a shape on the image. The shape must implement the [`Drawable`] trait.
138    pub fn draw<D: Drawable<P>>(&mut self, shape: D) -> Result<()> {
139        shape.draw_on(self)?;
140        Ok(())
141    }
142
143    /// Returns the dimensions of the image as a tuple (width, height).
144    pub fn dimensions(&self) -> (usize, usize) {
145        (self.width, self.height)
146    }
147
148    /// Returns true if the image is empty
149    pub fn is_empty(&self) -> bool {
150        self.data.is_empty()
151    }
152
153    /// Convert the image to RGBA8 format.
154    pub fn to_rgba8(&self) -> Image<Rgba<u8>> {
155        let rgba_data: Vec<[u8; 4]> = self.data.iter().map(|px| px.to_rgba8()).collect();
156        let rgba_data: Vec<Rgba<u8>> = rgba_data
157            .into_iter()
158            .map(|rgba| Rgba {
159                r: rgba[0],
160                g: rgba[1],
161                b: rgba[2],
162                a: rgba[3],
163            })
164            .collect();
165
166        Image {
167            width: self.width,
168            height: self.height,
169            data: rgba_data,
170        }
171    }
172}