Skip to main content

cranpose_ui_graphics/
image.rs

1//! Image bitmap primitives used by render backends.
2
3use crate::{Color, Size};
4use std::hash::{Hash, Hasher};
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::sync::Arc;
7use thiserror::Error;
8
9static NEXT_IMAGE_BITMAP_ID: AtomicU64 = AtomicU64::new(1);
10
11/// Errors returned while constructing an [`ImageBitmap`].
12#[derive(Debug, Clone, PartialEq, Eq, Error)]
13pub enum ImageBitmapError {
14    #[error("image dimensions must be greater than zero")]
15    InvalidDimensions,
16    #[error("image dimensions are too large")]
17    DimensionsTooLarge,
18    #[error("pixel data length mismatch: expected {expected} bytes, got {actual}")]
19    PixelDataLengthMismatch { expected: usize, actual: usize },
20}
21
22/// Immutable RGBA image data used by UI primitives and render backends.
23#[derive(Clone, Debug)]
24pub struct ImageBitmap {
25    id: u64,
26    width: u32,
27    height: u32,
28    pixels: Arc<[u8]>,
29}
30
31/// Simple image color filter model.
32#[derive(Clone, Copy, Debug, PartialEq)]
33pub enum ColorFilter {
34    /// Multiplies sampled RGBA channels by the tint color.
35    Tint(Color),
36}
37
38impl ColorFilter {
39    /// Creates a tint filter that multiplies sampled channels by `color`.
40    pub fn tint(color: Color) -> Self {
41        Self::Tint(color)
42    }
43
44    pub fn apply_rgba(self, rgba: [f32; 4]) -> [f32; 4] {
45        match self {
46            Self::Tint(tint) => [
47                rgba[0] * tint.r(),
48                rgba[1] * tint.g(),
49                rgba[2] * tint.b(),
50                rgba[3] * tint.a(),
51            ],
52        }
53    }
54}
55
56impl ImageBitmap {
57    /// Creates a bitmap from tightly packed RGBA8 pixels.
58    pub fn from_rgba8(width: u32, height: u32, pixels: Vec<u8>) -> Result<Self, ImageBitmapError> {
59        Self::from_rgba8_slice(width, height, &pixels)
60    }
61
62    /// Creates a bitmap from tightly packed RGBA8 pixels.
63    pub fn from_rgba8_slice(
64        width: u32,
65        height: u32,
66        pixels: &[u8],
67    ) -> Result<Self, ImageBitmapError> {
68        if width == 0 || height == 0 {
69            return Err(ImageBitmapError::InvalidDimensions);
70        }
71        let expected = (width as usize)
72            .checked_mul(height as usize)
73            .and_then(|value| value.checked_mul(4))
74            .ok_or(ImageBitmapError::DimensionsTooLarge)?;
75
76        if pixels.len() != expected {
77            return Err(ImageBitmapError::PixelDataLengthMismatch {
78                expected,
79                actual: pixels.len(),
80            });
81        }
82
83        Ok(Self {
84            id: NEXT_IMAGE_BITMAP_ID.fetch_add(1, Ordering::Relaxed),
85            width,
86            height,
87            pixels: Arc::from(pixels),
88        })
89    }
90
91    /// Stable bitmap identity used by caches.
92    pub fn id(&self) -> u64 {
93        self.id
94    }
95
96    /// Width in pixels.
97    pub fn width(&self) -> u32 {
98        self.width
99    }
100
101    /// Height in pixels.
102    pub fn height(&self) -> u32 {
103        self.height
104    }
105
106    /// Returns the raw RGBA8 pixel data.
107    pub fn pixels(&self) -> &[u8] {
108        &self.pixels
109    }
110
111    /// Returns intrinsic size in logical units.
112    pub fn intrinsic_size(&self) -> Size {
113        Size {
114            width: self.width as f32,
115            height: self.height as f32,
116        }
117    }
118}
119
120impl PartialEq for ImageBitmap {
121    fn eq(&self, other: &Self) -> bool {
122        self.id == other.id
123    }
124}
125
126impl Eq for ImageBitmap {}
127
128impl Hash for ImageBitmap {
129    fn hash<H: Hasher>(&self, state: &mut H) {
130        self.id.hash(state);
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn from_rgba8_accepts_valid_data() {
140        let bitmap = ImageBitmap::from_rgba8(2, 1, vec![255, 0, 0, 255, 0, 255, 0, 255])
141            .expect("valid bitmap");
142
143        assert_eq!(bitmap.width(), 2);
144        assert_eq!(bitmap.height(), 1);
145        assert_eq!(bitmap.pixels().len(), 8);
146    }
147
148    #[test]
149    fn from_rgba8_rejects_zero_dimensions() {
150        let err = ImageBitmap::from_rgba8(0, 2, vec![]).expect_err("must fail");
151        assert_eq!(err, ImageBitmapError::InvalidDimensions);
152    }
153
154    #[test]
155    fn from_rgba8_rejects_wrong_pixel_length() {
156        let err = ImageBitmap::from_rgba8(2, 2, vec![0; 15]).expect_err("must fail");
157        assert_eq!(
158            err,
159            ImageBitmapError::PixelDataLengthMismatch {
160                expected: 16,
161                actual: 15,
162            }
163        );
164    }
165
166    #[test]
167    fn from_rgba8_slice_accepts_valid_data() {
168        let pixels = [255u8, 0, 0, 255];
169        let bitmap = ImageBitmap::from_rgba8_slice(1, 1, &pixels).expect("valid bitmap");
170        assert_eq!(bitmap.pixels(), &pixels);
171    }
172
173    #[test]
174    fn ids_are_unique() {
175        let a = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 0, 255]).expect("bitmap a");
176        let b = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 0, 255]).expect("bitmap b");
177        assert_ne!(a.id(), b.id());
178    }
179
180    #[test]
181    fn intrinsic_size_matches_dimensions() {
182        let bitmap = ImageBitmap::from_rgba8(3, 4, vec![255; 3 * 4 * 4]).expect("bitmap");
183        assert_eq!(bitmap.intrinsic_size(), Size::new(3.0, 4.0));
184    }
185
186    #[test]
187    fn tint_filter_multiplies_channels() {
188        let filter = ColorFilter::tint(Color::from_rgba_u8(128, 255, 64, 128));
189        let tinted = filter.apply_rgba([1.0, 0.5, 1.0, 1.0]);
190        assert!((tinted[0] - (128.0 / 255.0)).abs() < 1e-5);
191        assert!((tinted[1] - 0.5).abs() < 1e-5);
192        assert!((tinted[2] - (64.0 / 255.0)).abs() < 1e-5);
193        assert!((tinted[3] - (128.0 / 255.0)).abs() < 1e-5);
194    }
195
196    #[test]
197    fn tint_constructor_matches_variant() {
198        let color = Color::from_rgba_u8(10, 20, 30, 40);
199        assert_eq!(ColorFilter::tint(color), ColorFilter::Tint(color));
200    }
201}