glance_core/img/
mod.rs

1//! A high-level image handling module
2//!
3//! This crate provides an [`Image`] struct that can open, display and save images.
4
5use crate::drawing::traits::Drawable;
6use crate::utils;
7use crate::{CoreError, Result};
8pub mod iterators;
9
10use image::{ImageBuffer, ImageReader, Rgba};
11use minifb::{Key, Window, WindowOptions};
12use std::path::Path;
13
14/// A struct that provides image handling functionality
15///
16/// # Examples
17///
18/// ```
19/// // Open and display an image
20/// use glance_core::img::Image;
21///
22/// if let Ok(img) = Image::open("path/to/image.jpg") {
23///     let _ = img.display("Example Image");
24/// }
25/// ```
26pub struct Image {
27    width: u32,
28    height: u32,
29    data: Vec<u8>,
30}
31
32impl Image {
33    /// Opens an image from the given path.
34    ///
35    /// Returns an error if the file does not exist or cannot be decoded.
36    /// Supports all formats recognized by the `image` crate.
37    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
38        let dyn_img = ImageReader::open(path)?.decode()?;
39        let rgba = dyn_img.to_rgba8();
40        let (width, height) = rgba.dimensions();
41        Ok(Image {
42            width,
43            height,
44            data: rgba.into_raw(),
45        })
46    }
47
48    /// Saves an image to the given path
49    ///
50    /// Returns an error if the file cannot be created or buffer is invalid.
51    /// Format is recognized from file extension (see [`image::ImageBuffer::save`] for more info).
52    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
53        let buffer: ImageBuffer<Rgba<u8>, _> =
54            ImageBuffer::from_raw(self.width, self.height, self.data.clone())
55                .ok_or_else(|| std::io::Error::other("Invalid buffer"))?;
56
57        buffer.save(path)?;
58
59        Ok(())
60    }
61
62    /// Creates a new Image from width, height, and a data buffer (must be RGBA, width*height*4 bytes).
63    pub fn new(width: u32, height: u32, data: Vec<u8>) -> Result<Self> {
64        if data.len() != (width as usize) * (height as usize) * 4 {
65            return Err(std::io::Error::new(
66                std::io::ErrorKind::InvalidInput,
67                "Data buffer length does not match width * height * 4",
68            )
69            .into());
70        }
71
72        Ok(Image {
73            width,
74            height,
75            data,
76        })
77    }
78
79    /// Displays the image (as RGBA8) in a window until Escape is pressed.
80    /// Returns an error if the window cannot be created.
81    /// The window runs at 1 FPS to minimize CPU usage.
82    /// Uses `minifb` for cross-platform windowing.
83    pub fn display(&self, title: &str) -> Result<()> {
84        let dims = self.dimensions();
85        let width = dims[0] as usize;
86        let height = dims[1] as usize;
87
88        // Create window
89        let mut window = Window::new(
90            title,
91            width,
92            height,
93            WindowOptions {
94                resize: false,
95                ..Default::default()
96            },
97        )?;
98        window.set_target_fps(1);
99
100        // Populate framebuffer
101        let rgba_bytes: &[u8] = &self.data;
102        let mut buffer: Vec<u32> = Vec::with_capacity(rgba_bytes.len() / 4);
103        for chunk in rgba_bytes.chunks(4) {
104            buffer.push(u32::from_be_bytes([chunk[3], chunk[0], chunk[1], chunk[2]]));
105        }
106
107        while window.is_open() && !window.is_key_down(Key::Escape) {
108            window.update_with_buffer(&buffer, width, height)?;
109        }
110
111        Ok(())
112    }
113
114    /// Gets the color of a pixel. Top left is treated as origin. Right is positive x, down is
115    /// positive y.
116    pub fn get_pixel(&self, position: [u32; 2]) -> Result<[u8; 4]> {
117        let dims = self.dimensions();
118        if position[0] >= dims[0] || position[1] >= dims[1] {
119            return Err(CoreError::OutOfBounds(format!(
120                "The image dimensions are {dims:?}. Getting pixel {position:?} is not possible."
121            )));
122        }
123
124        let idx = ((position[1] * dims[0] + position[0]) * 4) as usize;
125        let pixel = [
126            self.data[idx],
127            self.data[idx + 1],
128            self.data[idx + 2],
129            self.data[idx + 3],
130        ];
131        Ok(pixel)
132    }
133
134    /// Sets a pixel to the given color. Top left is treated as origin, x-axis goes horizontally.
135    pub fn set_pixel(&mut self, position: [u32; 2], color: [u8; 4]) -> Result<()> {
136        let dims = self.dimensions();
137        if position[0] >= dims[0] || position[1] >= dims[1] {
138            return Err(CoreError::OutOfBounds(format!(
139                "The image dimensions are {dims:?}. Setting pixel {position:?} is not possible."
140            )));
141        }
142
143        let idx = ((position[1] * dims[0] + position[0]) * 4) as usize;
144        self.data[idx..idx + 4].copy_from_slice(&color);
145
146        Ok(())
147    }
148
149    /// Linearly interpolate a pixel color with the given color
150    pub fn alpha_blend_pixel(&mut self, position: [u32; 2], color: [u8; 4]) -> Result<()> {
151        let dims = self.dimensions();
152        if position[0] >= dims[0] || position[1] >= dims[1] {
153            return Err(CoreError::OutOfBounds(format!(
154                "The image dimensions are {dims:?}. Setting pixel {position:?} is not possible."
155            )));
156        }
157
158        let color_fg = color;
159        let color_bg = self.get_pixel(position)?;
160        let blend_color = utils::alpha_blend(color_fg, color_bg);
161
162        self.set_pixel(position, blend_color)?;
163
164        Ok(())
165    }
166
167    /// Draw a shape (any struct that implements the [`drawing::traits::Drawable`] trait)
168    pub fn draw<D: Drawable>(&mut self, shape: D) -> Result<()> {
169        shape.draw_on(self)?;
170        Ok(())
171    }
172
173    /// Returns true if the image contains no pixel data.
174    pub fn is_empty(&self) -> bool {
175        self.data.is_empty()
176    }
177
178    /// Returns the image dimensions as (width, height).
179    pub fn dimensions(&self) -> [u32; 2] {
180        [self.width, self.height]
181    }
182}