pix_engine/
image.rs

1//! [Image] and [`PixelFormat`] functions.
2
3use crate::{ops::clamp_dimensions, prelude::*, renderer::Rendering};
4#[cfg(not(target_arch = "wasm32"))]
5use anyhow::Context;
6#[cfg(not(target_arch = "wasm32"))]
7use png::{BitDepth, ColorType, Decoder};
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10#[cfg(not(target_arch = "wasm32"))]
11use std::{
12    ffi::OsStr,
13    fs::File,
14    io::{self, BufReader, BufWriter},
15    path::{Path, PathBuf},
16};
17use std::{fmt, iter::Copied, slice};
18
19/// Format for interpreting image data.
20#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
21#[non_exhaustive]
22#[must_use]
23#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
24pub enum PixelFormat {
25    /// 8-bit Red, Green, and Blue
26    Rgb,
27    /// 8-bit Red, Green, Blue, and Alpha
28    Rgba,
29}
30
31impl PixelFormat {
32    /// Returns the number of channels associated with the format.
33    #[inline]
34    #[must_use]
35    pub const fn channels(&self) -> usize {
36        match self {
37            PixelFormat::Rgb => 3,
38            PixelFormat::Rgba => 4,
39        }
40    }
41}
42
43/// The error type returned when a checked conversion from [png::ColorType] fails.
44#[derive(Debug, Copy, Clone, PartialEq, Eq)]
45#[doc(hidden)]
46pub struct TryFromColorTypeError(pub(crate) ());
47
48#[doc(hidden)]
49impl TryFrom<png::ColorType> for PixelFormat {
50    type Error = TryFromColorTypeError;
51    fn try_from(color_type: png::ColorType) -> std::result::Result<Self, Self::Error> {
52        match color_type {
53            png::ColorType::Rgb => Ok(Self::Rgb),
54            png::ColorType::Rgba => Ok(Self::Rgba),
55            _ => Err(TryFromColorTypeError(())),
56        }
57    }
58}
59
60#[doc(hidden)]
61impl From<PixelFormat> for png::ColorType {
62    fn from(format: PixelFormat) -> Self {
63        match format {
64            PixelFormat::Rgb => Self::Rgb,
65            PixelFormat::Rgba => Self::Rgba,
66        }
67    }
68}
69
70impl Default for PixelFormat {
71    fn default() -> Self {
72        Self::Rgba
73    }
74}
75
76/// An `Image` representing a buffer of pixel color values.
77#[derive(Default, Clone)]
78#[must_use]
79pub struct Image {
80    /// `Image` width.
81    width: u32,
82    /// `Image` height.
83    height: u32,
84    /// Raw pixel data.
85    data: Vec<u8>,
86    /// Pixel Format.
87    format: PixelFormat,
88}
89
90impl Image {
91    /// Constructs an empty RGBA `Image` with given `width` and `height`.
92    #[inline]
93    pub fn new(width: u32, height: u32) -> Self {
94        Self::rgba(width, height)
95    }
96
97    /// Constructs an empty RGBA `Image` with given `width` and `height`.
98    ///
99    /// Alias for [Image::new].
100    #[doc(alias = "new")]
101    #[inline]
102    pub fn rgba(width: u32, height: u32) -> Self {
103        let format = PixelFormat::Rgba;
104        let data = vec![0x00; format.channels() * (width * height) as usize];
105        Self::from_vec(width, height, data, format)
106    }
107
108    /// Constructs an empty RGB `Image` with given `width` and `height`.
109    #[inline]
110    pub fn rgb(width: u32, height: u32) -> Self {
111        let format = PixelFormat::Rgb;
112        let data = vec![0x00; format.channels() * (width * height) as usize];
113        Self::from_vec(width, height, data, format)
114    }
115
116    /// Constructs an `Image` from a [u8] [prim@slice] representing RGB/A values.
117    ///
118    /// # Errors
119    ///
120    /// If the bytes length doesn't match the image dimensions and [`PixelFormat`] provided, then
121    /// an error is returned.
122    #[inline]
123    pub fn from_bytes<B: AsRef<[u8]>>(
124        width: u32,
125        height: u32,
126        bytes: B,
127        format: PixelFormat,
128    ) -> PixResult<Self> {
129        let bytes = bytes.as_ref();
130        if bytes.len() != (format.channels() * width as usize * height as usize) {
131            return Err(PixError::InvalidImage {
132                width,
133                height,
134                size: bytes.len(),
135                format,
136            }
137            .into());
138        }
139        Ok(Self::from_vec(width, height, bytes.to_vec(), format))
140    }
141
142    /// Constructs an `Image` from a [Color] [prim@slice] representing RGBA values.
143    ///
144    /// # Errors
145    ///
146    /// If the pixels length doesn't match the image dimensions and [`PixelFormat`] provided, then
147    /// an error is returned.
148    #[inline]
149    pub fn from_pixels<P: AsRef<[Color]>>(
150        width: u32,
151        height: u32,
152        pixels: P,
153        format: PixelFormat,
154    ) -> PixResult<Self> {
155        let pixels = pixels.as_ref();
156        if pixels.len() != (width as usize * height as usize) {
157            return Err(PixError::InvalidImage {
158                width,
159                height,
160                size: pixels.len() * format.channels(),
161                format,
162            }
163            .into());
164        }
165        let bytes: Vec<u8> = match format {
166            PixelFormat::Rgb => pixels
167                .iter()
168                .flat_map(|p| [p.red(), p.green(), p.blue()])
169                .collect(),
170            PixelFormat::Rgba => pixels.iter().flat_map(Color::channels).collect(),
171        };
172        Ok(Self::from_vec(width, height, bytes, format))
173    }
174
175    /// Constructs an `Image` from a [`Vec<u8>`] representing RGB/A values.
176    #[inline]
177    pub fn from_vec(width: u32, height: u32, data: Vec<u8>, format: PixelFormat) -> Self {
178        Self {
179            width,
180            height,
181            data,
182            format,
183        }
184    }
185
186    /// Constructs an `Image` from a [png] file.
187    ///
188    /// # Errors
189    ///
190    /// If the file format is not supported or extension is not `.png`, then an error is returned.
191    #[cfg(not(target_arch = "wasm32"))]
192    pub fn from_file<P: AsRef<Path>>(path: P) -> PixResult<Self> {
193        let path = path.as_ref();
194        let ext = path.extension();
195        if ext != Some(OsStr::new("png")) {
196            return Err(PixError::UnsupportedFileType(ext.map(OsStr::to_os_string)).into());
197        }
198        Self::from_read(File::open(path)?)
199    }
200
201    /// Constructs an `Image` from a [png] reader.
202    ///
203    /// # Errors
204    ///
205    /// If the file format is not supported or there is an [`io::Error`] reading the file then an
206    /// error is returned.
207    #[cfg(not(target_arch = "wasm32"))]
208    pub fn from_read<R: io::Read>(read: R) -> PixResult<Self> {
209        let png_file = BufReader::new(read);
210        let png = Decoder::new(png_file);
211
212        // TODO: Make this machine-dependent to best match display capabilities for performance
213        // EXPL: Switch RGBA32 (RGBA8888) format to ARGB8888 by swapping alpha
214        // EXPL: Expand paletted to RGB and non-8-bit grayscale to 8-bits
215        // png.set_transformations(Transformations::SWAP_ALPHA | Transformations::EXPAND);
216
217        let mut reader = png.read_info().context("failed to read png data")?;
218        let mut buf = vec![0x00; reader.output_buffer_size()];
219        let info = reader
220            .next_frame(&mut buf)
221            .context("failed to read png data frame")?;
222        let bit_depth = info.bit_depth;
223        let color_type = info.color_type;
224        if bit_depth != BitDepth::Eight || !matches!(color_type, ColorType::Rgb | ColorType::Rgba) {
225            return Err(PixError::UnsupportedImageFormat {
226                bit_depth,
227                color_type,
228            }
229            .into());
230        }
231
232        let data = &buf[..info.buffer_size()];
233        let format = info
234            .color_type
235            .try_into()
236            .map_err(|_| PixError::UnsupportedImageFormat {
237                bit_depth,
238                color_type,
239            })?;
240        Self::from_bytes(info.width, info.height, data, format)
241    }
242
243    /// Returns the `Image` width.
244    #[inline]
245    #[must_use]
246    pub const fn width(&self) -> u32 {
247        self.width
248    }
249
250    /// Returns the `Image` height.
251    #[inline]
252    #[must_use]
253    pub const fn height(&self) -> u32 {
254        self.height
255    }
256
257    /// Returns the `Image` dimensions as `(width, height)`.
258    #[inline]
259    #[must_use]
260    pub const fn dimensions(&self) -> (u32, u32) {
261        (self.width, self.height)
262    }
263
264    /// Returns the `pitch` of the image data which is the number of bytes in a row of pixel data,
265    /// including padding between lines.
266    #[inline]
267    #[must_use]
268    pub const fn pitch(&self) -> usize {
269        self.width() as usize * self.format.channels()
270    }
271
272    /// Returns the `Image` bounding [Rect] positioned at `(0, 0)`.
273    ///
274    /// The width and height of the returned rectangle are clamped to ensure that size does not
275    /// exceed [`i32::MAX`]. This could result in unexpected behavior with drawing routines if the
276    /// image size is larger than this.
277    #[inline]
278    pub fn bounding_rect(&self) -> Rect<i32> {
279        let (width, height) = clamp_dimensions(self.width, self.height);
280        rect![0, 0, width, height]
281    }
282
283    /// Returns the `Image` bounding [Rect] positioned at `offset`.
284    #[inline]
285    pub fn bounding_rect_offset<P>(&self, offset: P) -> Rect<i32>
286    where
287        P: Into<Point<i32>>,
288    {
289        let (width, height) = clamp_dimensions(self.width, self.height);
290        rect![offset.into(), width, height]
291    }
292
293    /// Returns the center position as [Point].
294    #[inline]
295    pub fn center(&self) -> Point<i32> {
296        let (width, height) = clamp_dimensions(self.width, self.height);
297        point!(width / 2, height / 2)
298    }
299
300    /// Returns the `Image` pixel data as an iterator of [u8].
301    #[inline]
302    pub fn bytes(&self) -> Bytes<'_> {
303        Bytes(self.as_bytes().iter().copied())
304    }
305
306    /// Returns the `Image` pixel data as a [u8] [prim@slice].
307    #[inline]
308    #[must_use]
309    pub fn as_bytes(&self) -> &[u8] {
310        &self.data
311    }
312
313    /// Returns the `Image` pixel data as a mutable [u8] [prim@slice].
314    #[inline]
315    #[must_use]
316    pub fn as_mut_bytes(&mut self) -> &mut [u8] {
317        &mut self.data
318    }
319
320    /// Returns the `Image` pixel data as a [`Vec<u8>`].
321    ///
322    /// This consumes the `Image`, so we do not need to copy its contents.
323    #[inline]
324    #[must_use]
325    // FIXME: https://github.com/rust-lang/rust-clippy/issues/4979
326    #[allow(clippy::missing_const_for_fn)]
327    pub fn into_bytes(self) -> Vec<u8> {
328        self.data
329    }
330
331    /// Returns the `Image` pixel data as an iterator of [Color]s.
332    #[inline]
333    pub fn pixels(&self) -> Pixels<'_> {
334        Pixels(self.format.channels(), self.as_bytes().iter().copied())
335    }
336
337    /// Returns the `Image` pixel data as a [`Vec<Color>`].
338    ///
339    /// # Panics
340    ///
341    /// Panics if the image has an invalid sequence of bytes given it's [`PixelFormat`].
342    #[inline]
343    #[must_use]
344    pub fn into_pixels(self) -> Vec<Color> {
345        self.data
346            .chunks(self.format.channels())
347            .map(|slice| match *slice {
348                [red, green, blue] => Color::rgb(red, green, blue),
349                [red, green, blue, alpha] => Color::rgba(red, green, blue, alpha),
350                _ => Color::TRANSPARENT,
351            })
352            .collect()
353    }
354
355    /// Returns the color value at the given `(x, y)` position.
356    ///
357    /// # Panics
358    ///
359    /// Panics if the image has an invalid sequence of bytes given it's [`PixelFormat`], or the `(x,
360    /// y`) index is out of range.
361    #[inline]
362    pub fn get_pixel(&self, x: u32, y: u32) -> Color {
363        let idx = self.idx(x, y);
364        let channels = self.format.channels();
365        match self.data.get(idx..idx + channels) {
366            Some([red, green, blue]) => Color::rgb(*red, *green, *blue),
367            Some([red, green, blue, alpha]) => Color::rgba(*red, *green, *blue, *alpha),
368            _ => Color::TRANSPARENT,
369        }
370    }
371
372    /// Sets the color value at the given `(x, y)` position.
373    #[inline]
374    pub fn set_pixel<C: Into<Color>>(&mut self, x: u32, y: u32, color: C) {
375        let color = color.into();
376        let idx = self.idx(x, y);
377        let channels = self.format.channels();
378        self.data[idx..(idx + channels)].clone_from_slice(&color.channels()[..channels]);
379    }
380
381    /// Update the `Image` with a  [u8] [prim@slice] representing RGB/A values.
382    #[inline]
383    pub fn update_bytes<B: AsRef<[u8]>>(&mut self, bytes: B) {
384        self.data.clone_from_slice(bytes.as_ref());
385    }
386
387    /// Returns the `Image` pixel format.
388    #[inline]
389    pub const fn format(&self) -> PixelFormat {
390        self.format
391    }
392
393    /// Save the `Image` to a [png] file.
394    ///
395    /// # Errors
396    ///
397    /// Returns an error for any of the following:
398    ///     - An [`io::Error`] occurs attempting to create the [png] file.
399    ///     - A [`png::EncodingError`] occurs attempting to write image bytes.
400    ///
401    /// # Example
402    ///
403    /// ```
404    /// # use pix_engine::prelude::*;
405    /// # struct App { image: Image };
406    /// # impl PixEngine for App {
407    /// # fn on_update(&mut self, s: &mut PixState) -> PixResult<()> { Ok(()) }
408    /// fn on_key_pressed(&mut self, s: &mut PixState, event: KeyEvent) -> PixResult<bool> {
409    ///     if let Key::S = event.key {
410    ///         self.image.save("test_image.png")?;
411    ///     }
412    ///     Ok(false)
413    /// }
414    /// # }
415    /// ```
416    #[cfg(not(target_arch = "wasm32"))]
417    pub fn save<P>(&self, path: P) -> PixResult<()>
418    where
419        P: AsRef<Path>,
420    {
421        let path = path.as_ref();
422        let png_file = BufWriter::new(File::create(path)?);
423        let mut png = png::Encoder::new(png_file, self.width, self.height);
424        png.set_color(self.format.into());
425        png.set_depth(png::BitDepth::Eight);
426        let mut writer = png
427            .write_header()
428            .with_context(|| format!("failed to write png header: {path:?}"))?;
429        writer
430            .write_image_data(self.as_bytes())
431            .with_context(|| format!("failed to write png data: {path:?}"))
432    }
433}
434
435impl Image {
436    /// Helper function to get the byte array index based on `(x, y)`.
437    #[inline]
438    const fn idx(&self, x: u32, y: u32) -> usize {
439        self.format.channels() * (x + y * self.width) as usize
440    }
441}
442
443impl PixState {
444    /// Draw an [Image] to the current canvas.
445    ///
446    /// # Errors
447    ///
448    /// If the renderer fails to draw to the current render target, then an error is returned.
449    ///
450    /// # Example
451    ///
452    /// ```
453    /// # use pix_engine::prelude::*;
454    /// # struct App { text_field: String, text_area: String};
455    /// # impl PixEngine for App {
456    /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
457    ///     let image = Image::from_file("./some_image.png")?;
458    ///     s.image(&image, [10, 10])?;
459    ///     Ok(())
460    /// }
461    /// # }
462    /// ```
463    pub fn image<P>(&mut self, img: &Image, position: P) -> PixResult<()>
464    where
465        P: Into<Point<i32>>,
466    {
467        let pos = position.into();
468        let dst = img.bounding_rect_offset(pos);
469        self.image_transformed(img, None, dst, 0.0, None, None)
470    }
471
472    /// Draw a transformed [Image] to the current canvas resized to the target `rect`, optionally
473    /// rotated by an `angle` about the `center` point or `flipped`. `angle` can be in either
474    /// radians or degrees based on [`AngleMode`]. [`PixState::image_tint`] can optionally add a tint
475    /// color to the rendered image.
476    ///
477    /// # Errors
478    ///
479    /// If the renderer fails to draw to the current render target, then an error is returned.
480    ///
481    /// # Example
482    ///
483    /// ```
484    /// # use pix_engine::prelude::*;
485    /// # struct App { text_field: String, text_area: String};
486    /// # impl PixEngine for App {
487    /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
488    ///     s.angle_mode(AngleMode::Degrees);
489    ///     let image = Image::from_file("./some_image.png")?;
490    ///     let src = None; // Draw entire image instead of a sub-image
491    ///     let dst = image.bounding_rect_offset([10, 10]); // Draw image at `(10, 10)`.
492    ///     let angle = 10.0;
493    ///     let center = point!(10, 10);
494    ///     s.image_transformed(&image, src, dst, angle, center, Flipped::Horizontal)?;
495    ///     Ok(())
496    /// }
497    /// # }
498    /// ```
499    pub fn image_transformed<R1, R2, A, C, F>(
500        &mut self,
501        img: &Image,
502        src: R1,
503        dst: R2,
504        angle: A,
505        center: C,
506        flipped: F,
507    ) -> PixResult<()>
508    where
509        R1: Into<Option<Rect<i32>>>,
510        R2: Into<Option<Rect<i32>>>,
511        A: Into<Option<f64>>,
512        C: Into<Option<Point<i32>>>,
513        F: Into<Option<Flipped>>,
514    {
515        let s = &self.settings;
516        let mut dst = dst.into();
517        if s.image_mode == ImageMode::Center {
518            dst = dst.map(|dst| Rect::from_center(dst.top_left(), dst.width(), dst.height()));
519        };
520        let mut angle = angle.into().unwrap_or(0.0);
521        if s.angle_mode == AngleMode::Radians {
522            angle = angle.to_degrees();
523        };
524        self.renderer.image(
525            img,
526            src.into(),
527            dst,
528            angle,
529            center.into(),
530            flipped.into(),
531            s.image_tint,
532        )
533    }
534}
535
536impl fmt::Debug for Image {
537    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
538        f.debug_struct("Image")
539            .field("width", &self.width)
540            .field("height", &self.height)
541            .field("size", &self.data.len())
542            .field("format", &self.format)
543            .finish()
544    }
545}
546
547/// An iterator over the bytes of an [Image].
548///
549/// This struct is created by the [`Image::bytes`] method.
550/// See its documentation for more.
551#[derive(Debug, Clone)]
552#[must_use]
553pub struct Bytes<'a>(Copied<slice::Iter<'a, u8>>);
554
555impl Iterator for Bytes<'_> {
556    type Item = u8;
557    #[inline]
558    fn next(&mut self) -> Option<Self::Item> {
559        self.0.next()
560    }
561}
562
563/// An iterator over the [Color] pixels of an [Image].
564///
565/// This struct is created by the [`Image::pixels`] method.
566/// See its documentation for more.
567#[derive(Debug, Clone)]
568#[must_use]
569pub struct Pixels<'a>(usize, Copied<slice::Iter<'a, u8>>);
570
571impl Iterator for Pixels<'_> {
572    type Item = Color;
573    #[inline]
574    fn next(&mut self) -> Option<Self::Item> {
575        let r = self.1.next()?;
576        let g = self.1.next()?;
577        let b = self.1.next()?;
578        let channels = self.0;
579        match channels {
580            3 => Some(Color::rgb(r, g, b)),
581            4 => {
582                let a = self.1.next()?;
583                Some(Color::rgba(r, g, b, a))
584            }
585            _ => Some(Color::TRANSPARENT),
586        }
587    }
588}
589
590/// Represents an image icon source.
591#[derive(Debug, Clone)]
592pub enum Icon {
593    /// An icon image.
594    Image(Image),
595    #[cfg(not(target_arch = "wasm32"))]
596    /// A path to an icon image.
597    Path(PathBuf),
598}
599
600#[cfg(not(target_arch = "wasm32"))]
601impl<T: Into<PathBuf>> From<T> for Icon {
602    fn from(value: T) -> Self {
603        Self::Path(value.into())
604    }
605}
606
607impl From<Image> for Icon {
608    fn from(img: Image) -> Self {
609        Self::Image(img)
610    }
611}