Skip to main content

cranpose_ui_graphics/
image.rs

1//! Image bitmap primitives used by render backends.
2
3use std::{
4    any::Any,
5    borrow::Cow,
6    hash::{BuildHasher, Hash, Hasher},
7    panic::AssertUnwindSafe,
8    sync::{
9        Arc,
10        atomic::{AtomicU64, Ordering},
11    },
12};
13
14use thiserror::Error;
15
16use crate::{BlendMode, Color, Size};
17
18/// Errors returned while constructing an [`ImageBitmap`].
19#[derive(Debug, Clone, PartialEq, Eq, Error)]
20pub enum ImageBitmapError {
21    #[error("image dimensions must be greater than zero")]
22    InvalidDimensions,
23    #[error("image dimensions are too large")]
24    DimensionsTooLarge,
25    #[error("pixel data length mismatch: expected {expected} bytes, got {actual}")]
26    PixelDataLengthMismatch { expected: usize, actual: usize },
27}
28
29/// Storage format of an immutable bitmap.
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31pub enum ImagePixelFormat {
32    /// Four bytes per pixel, ordered red, green, blue, alpha.
33    Rgba8,
34    /// One alpha byte per pixel and a constant, unpremultiplied RGB color.
35    Alpha8 {
36        /// RGB channels shared by every pixel, including transparent pixels.
37        color: [u8; 3],
38    },
39    /// No pixels in memory: a GPU texture another renderer draws, made by
40    /// [`ExternalTexture`]. Software renderers draw it transparent.
41    External,
42}
43
44/// Immutable image data used by UI primitives and render backends.
45#[derive(Clone, Debug)]
46pub struct ImageBitmap {
47    data: Arc<ImageBitmapData>,
48}
49
50#[derive(Debug)]
51struct ImageBitmapData {
52    width: u32,
53    height: u32,
54    id: u64,
55    opaque: bool,
56    format: ImagePixelFormat,
57    storage: ImageStorage,
58}
59
60enum ImageStorage {
61    Pixels(Box<[u8]>),
62    External { key: u64, handle: ExternalHandle },
63}
64
65type ExternalHandle = Arc<AssertUnwindSafe<Box<dyn Any + Send + Sync>>>;
66
67impl std::fmt::Debug for ImageStorage {
68    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        match self {
70            Self::Pixels(pixels) => write!(formatter, "Pixels({} bytes)", pixels.len()),
71            Self::External { key, .. } => write!(formatter, "External({key:#x})"),
72        }
73    }
74}
75
76/// A GPU texture that another renderer, such as a game engine, draws into,
77/// shown through Cranpose's image primitives.
78///
79/// The handle belongs to the renderer that draws the scene: the wgpu
80/// renderer draws a `wgpu::TextureView` and samples it as stored, so an sRGB
81/// texture needs a non-sRGB view. Software renderers draw it transparent.
82///
83/// Draw [`frame`](Self::frame) with the number of the texture's current
84/// content. Image ids include that number, so every cache keyed by an image
85/// id draws the new content once the number changes.
86#[derive(Clone)]
87pub struct ExternalTexture {
88    key: u64,
89    width: u32,
90    height: u32,
91    opaque: bool,
92    handle: ExternalHandle,
93}
94
95impl std::fmt::Debug for ExternalTexture {
96    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        formatter
98            .debug_struct("ExternalTexture")
99            .field("key", &self.key)
100            .field("width", &self.width)
101            .field("height", &self.height)
102            .finish_non_exhaustive()
103    }
104}
105
106impl ExternalTexture {
107    /// Wraps a renderer's texture handle of `width` by `height` pixels.
108    /// `opaque` promises that every pixel the texture holds has full alpha.
109    pub fn new(
110        width: u32,
111        height: u32,
112        opaque: bool,
113        handle: impl Any + Send + Sync,
114    ) -> Result<Self, ImageBitmapError> {
115        if width == 0 || height == 0 {
116            return Err(ImageBitmapError::InvalidDimensions);
117        }
118        let serial = NEXT_EXTERNAL_TEXTURE.fetch_add(1, Ordering::Relaxed);
119        let mut hasher = foldhash::quality::FixedState::default().build_hasher();
120        ImagePixelFormat::External.hash(&mut hasher);
121        serial.hash(&mut hasher);
122        Ok(Self {
123            key: hasher.finish(),
124            width,
125            height,
126            opaque,
127            handle: Arc::new(AssertUnwindSafe(Box::new(handle))),
128        })
129    }
130
131    /// The bitmap that draws content number `frame` of this texture.
132    pub fn frame(&self, frame: u64) -> ImageBitmap {
133        ImageBitmap {
134            data: Arc::new(ImageBitmapData {
135                width: self.width,
136                height: self.height,
137                id: self.key ^ frame.wrapping_mul(0x9E37_79B9_7F4A_7C15),
138                opaque: self.opaque,
139                format: ImagePixelFormat::External,
140                storage: ImageStorage::External {
141                    key: self.key,
142                    handle: Arc::clone(&self.handle),
143                },
144            }),
145        }
146    }
147
148    /// Width in pixels.
149    pub fn width(&self) -> u32 {
150        self.width
151    }
152
153    /// Height in pixels.
154    pub fn height(&self) -> u32 {
155        self.height
156    }
157
158    /// Whether a bitmap from [`frame`](Self::frame), or another clone of this
159    /// texture, is still alive. A renderer draws the texture only through such
160    /// a bitmap, so once none is alive, no draw of it is waiting for the GPU
161    /// queue.
162    pub fn bitmaps_alive(&self) -> bool {
163        Arc::strong_count(&self.handle) > 1
164    }
165}
166
167static NEXT_EXTERNAL_TEXTURE: AtomicU64 = AtomicU64::new(0);
168
169/// Texture sampling mode for image primitives.
170#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
171pub enum ImageSampling {
172    /// Preserve source texels exactly. Use this for atlases, pixel art, and UI skins.
173    #[default]
174    Nearest,
175    /// Interpolate adjacent texels. Use this for photographic or continuously scaled images.
176    Linear,
177}
178
179/// Simple image color filter model.
180#[derive(Clone, Copy, Debug, PartialEq)]
181pub enum ColorFilter {
182    /// Compose-style tint using `BlendMode::SrcIn`.
183    Tint(Color),
184    /// Explicit per-channel modulation (multiply behavior).
185    Modulate(Color),
186    /// 4x5 color matrix in row-major order.
187    ///
188    /// Rows map output RGBA channels, columns map input RGBA plus constant term:
189    /// `out = M * [r, g, b, a, 1]`.
190    Matrix([f32; 20]),
191}
192
193impl ColorFilter {
194    /// Creates a Compose-style tint filter (`SrcIn`).
195    pub fn tint(color: Color) -> Self {
196        Self::Tint(color)
197    }
198
199    /// Creates an explicit modulation filter that multiplies channels by `color`.
200    pub fn modulate(color: Color) -> Self {
201        Self::Modulate(color)
202    }
203
204    /// Creates a filter from a 4x5 color matrix.
205    pub fn matrix(matrix: [f32; 20]) -> Self {
206        Self::Matrix(matrix)
207    }
208
209    pub fn compose(self, next: ColorFilter) -> ColorFilter {
210        ColorFilter::Matrix(compose_color_matrices(self.as_matrix(), next.as_matrix()))
211    }
212
213    pub fn as_matrix(self) -> [f32; 20] {
214        match self {
215            Self::Tint(tint) => [
216                0.0,
217                0.0,
218                0.0,
219                tint.r(),
220                0.0,
221                0.0,
222                0.0,
223                0.0,
224                tint.g(),
225                0.0,
226                0.0,
227                0.0,
228                0.0,
229                tint.b(),
230                0.0,
231                0.0,
232                0.0,
233                0.0,
234                tint.a(),
235                0.0,
236            ],
237            Self::Modulate(modulate) => [
238                modulate.r(),
239                0.0,
240                0.0,
241                0.0,
242                0.0,
243                0.0,
244                modulate.g(),
245                0.0,
246                0.0,
247                0.0,
248                0.0,
249                0.0,
250                modulate.b(),
251                0.0,
252                0.0,
253                0.0,
254                0.0,
255                0.0,
256                modulate.a(),
257                0.0,
258            ],
259            Self::Matrix(matrix) => matrix,
260        }
261    }
262
263    pub fn apply_rgba(self, rgba: [f32; 4]) -> [f32; 4] {
264        apply_color_matrix(self.as_matrix(), rgba)
265    }
266
267    pub fn supports_gpu_vertex_modulation(self) -> bool {
268        matches!(self, Self::Modulate(_))
269    }
270
271    pub fn gpu_vertex_tint(self) -> Option<[f32; 4]> {
272        match self {
273            Self::Modulate(tint) => Some([tint.r(), tint.g(), tint.b(), tint.a()]),
274            _ => None,
275        }
276    }
277
278    pub fn blend_mode(self) -> BlendMode {
279        match self {
280            Self::Tint(_) => BlendMode::SrcIn,
281            Self::Modulate(_) => BlendMode::Modulate,
282            Self::Matrix(_) => BlendMode::SrcOver,
283        }
284    }
285}
286
287fn apply_color_matrix(matrix: [f32; 20], rgba: [f32; 4]) -> [f32; 4] {
288    let r = rgba[0];
289    let g = rgba[1];
290    let b = rgba[2];
291    let a = rgba[3];
292    [
293        (matrix[0] * r + matrix[1] * g + matrix[2] * b + matrix[3] * a + matrix[4]).clamp(0.0, 1.0),
294        (matrix[5] * r + matrix[6] * g + matrix[7] * b + matrix[8] * a + matrix[9]).clamp(0.0, 1.0),
295        (matrix[10] * r + matrix[11] * g + matrix[12] * b + matrix[13] * a + matrix[14])
296            .clamp(0.0, 1.0),
297        (matrix[15] * r + matrix[16] * g + matrix[17] * b + matrix[18] * a + matrix[19])
298            .clamp(0.0, 1.0),
299    ]
300}
301
302fn compose_color_matrices(first: [f32; 20], second: [f32; 20]) -> [f32; 20] {
303    let mut composed = [0.0f32; 20];
304    for row in 0..4 {
305        let row_base = row * 5;
306        let s0 = second[row_base];
307        let s1 = second[row_base + 1];
308        let s2 = second[row_base + 2];
309        let s3 = second[row_base + 3];
310        let s4 = second[row_base + 4];
311
312        composed[row_base] = s0 * first[0] + s1 * first[5] + s2 * first[10] + s3 * first[15];
313        composed[row_base + 1] = s0 * first[1] + s1 * first[6] + s2 * first[11] + s3 * first[16];
314        composed[row_base + 2] = s0 * first[2] + s1 * first[7] + s2 * first[12] + s3 * first[17];
315        composed[row_base + 3] = s0 * first[3] + s1 * first[8] + s2 * first[13] + s3 * first[18];
316        composed[row_base + 4] =
317            s0 * first[4] + s1 * first[9] + s2 * first[14] + s3 * first[19] + s4;
318    }
319    composed
320}
321
322impl ImageBitmap {
323    /// Creates a bitmap by taking ownership of tightly packed RGBA8 pixels.
324    /// Any spare capacity in the pixel buffer is released.
325    pub fn from_rgba8(width: u32, height: u32, pixels: Vec<u8>) -> Result<Self, ImageBitmapError> {
326        Self::from_pixels(width, height, ImagePixelFormat::Rgba8, pixels)
327    }
328
329    /// Copies tightly packed RGBA8 pixels into a new bitmap.
330    pub fn from_rgba8_slice(
331        width: u32,
332        height: u32,
333        pixels: &[u8],
334    ) -> Result<Self, ImageBitmapError> {
335        Self::from_pixels(width, height, ImagePixelFormat::Rgba8, pixels)
336    }
337
338    /// Takes ownership of one alpha byte per pixel with a constant RGB color.
339    /// Any spare capacity in the alpha buffer is released.
340    pub fn from_alpha8(
341        width: u32,
342        height: u32,
343        color: [u8; 3],
344        alpha: Vec<u8>,
345    ) -> Result<Self, ImageBitmapError> {
346        Self::from_pixels(width, height, ImagePixelFormat::Alpha8 { color }, alpha)
347    }
348
349    fn from_pixels(
350        width: u32,
351        height: u32,
352        format: ImagePixelFormat,
353        pixels: impl AsRef<[u8]> + Into<Box<[u8]>>,
354    ) -> Result<Self, ImageBitmapError> {
355        if width == 0 || height == 0 {
356            return Err(ImageBitmapError::InvalidDimensions);
357        }
358        let expected = (width as usize)
359            .checked_mul(height as usize)
360            .and_then(|value| value.checked_mul(4))
361            .ok_or(ImageBitmapError::DimensionsTooLarge)?;
362        let expected = match format {
363            ImagePixelFormat::Rgba8 => expected,
364            ImagePixelFormat::Alpha8 { .. } => expected / 4,
365            ImagePixelFormat::External => 0,
366        };
367
368        let bytes = pixels.as_ref();
369        if bytes.len() != expected {
370            return Err(ImageBitmapError::PixelDataLengthMismatch {
371                expected,
372                actual: bytes.len(),
373            });
374        }
375
376        let id = bitmap_content_id(width, height, format, bytes);
377        let opaque = match format {
378            ImagePixelFormat::Rgba8 => bytes
379                .as_chunks::<4>()
380                .0
381                .iter()
382                .all(|pixel| pixel[3] == u8::MAX),
383            ImagePixelFormat::Alpha8 { .. } => bytes.iter().all(|alpha| *alpha == u8::MAX),
384            ImagePixelFormat::External => false,
385        };
386        Ok(Self {
387            data: Arc::new(ImageBitmapData {
388                width,
389                height,
390                id,
391                opaque,
392                format,
393                storage: ImageStorage::Pixels(pixels.into()),
394            }),
395        })
396    }
397
398    /// Content-derived bitmap identity used by renderer caches. Each frame
399    /// of an [`ExternalTexture`] has its own id.
400    pub fn id(&self) -> u64 {
401        self.data.id
402    }
403
404    /// Identity of the bitmap's storage: the same for every frame of an
405    /// [`ExternalTexture`], equal to [`Self::id`] for pixel bitmaps.
406    pub fn storage_id(&self) -> u64 {
407        match &self.data.storage {
408            ImageStorage::External { key, .. } => *key,
409            ImageStorage::Pixels(_) => self.data.id,
410        }
411    }
412
413    /// The renderer handle of an [`ExternalTexture`] frame, when it is a `T`.
414    pub fn external_handle<T: 'static>(&self) -> Option<&T> {
415        match &self.data.storage {
416            ImageStorage::External { handle, .. } => handle.downcast_ref(),
417            ImageStorage::Pixels(_) => None,
418        }
419    }
420
421    /// Width in pixels.
422    pub fn width(&self) -> u32 {
423        self.data.width
424    }
425
426    /// Height in pixels.
427    pub fn height(&self) -> u32 {
428        self.data.height
429    }
430
431    /// Returns the raw pixel bytes in [`Self::format`], empty for an
432    /// [`ExternalTexture`] frame.
433    pub fn pixels(&self) -> &[u8] {
434        match &self.data.storage {
435            ImageStorage::Pixels(pixels) => pixels,
436            ImageStorage::External { .. } => &[],
437        }
438    }
439
440    /// Returns the storage format of [`Self::pixels`].
441    pub fn format(&self) -> ImagePixelFormat {
442        self.data.format
443    }
444
445    /// Returns the unpremultiplied RGBA8 pixel at a row-major index;
446    /// transparent for an [`ExternalTexture`] frame.
447    ///
448    /// # Panics
449    /// Panics when `index` is outside the image.
450    pub fn rgba8_pixel(&self, index: usize) -> [u8; 4] {
451        match self.format() {
452            ImagePixelFormat::Rgba8 => self.pixels().as_chunks::<4>().0[index],
453            ImagePixelFormat::Alpha8 { color: [r, g, b] } => [r, g, b, self.pixels()[index]],
454            ImagePixelFormat::External => [0; 4],
455        }
456    }
457
458    /// Returns tightly packed RGBA8 bytes, borrowing RGBA images and expanding
459    /// alpha masks; empty for an [`ExternalTexture`] frame.
460    pub fn rgba8_pixels(&self) -> Cow<'_, [u8]> {
461        match self.format() {
462            ImagePixelFormat::Rgba8 | ImagePixelFormat::External => Cow::Borrowed(self.pixels()),
463            ImagePixelFormat::Alpha8 { color: [r, g, b] } => {
464                let mut pixels = Vec::with_capacity(self.pixels().len() * 4);
465                for &alpha in self.pixels() {
466                    pixels.extend_from_slice(&[r, g, b, alpha]);
467                }
468                Cow::Owned(pixels)
469            }
470        }
471    }
472
473    /// Returns true when every source pixel has full alpha.
474    pub fn is_opaque(&self) -> bool {
475        self.data.opaque
476    }
477
478    /// Returns intrinsic size in logical units.
479    pub fn intrinsic_size(&self) -> Size {
480        Size {
481            width: self.data.width as f32,
482            height: self.data.height as f32,
483        }
484    }
485}
486
487impl PartialEq for ImageBitmap {
488    fn eq(&self, other: &Self) -> bool {
489        self.id() == other.id()
490    }
491}
492
493impl Eq for ImageBitmap {}
494
495impl Hash for ImageBitmap {
496    fn hash<H: Hasher>(&self, state: &mut H) {
497        self.id().hash(state);
498    }
499}
500
501fn bitmap_content_id(width: u32, height: u32, format: ImagePixelFormat, pixels: &[u8]) -> u64 {
502    let mut hasher = foldhash::quality::FixedState::default().build_hasher();
503    width.hash(&mut hasher);
504    height.hash(&mut hasher);
505    format.hash(&mut hasher);
506    pixels.hash(&mut hasher);
507    hasher.finish()
508}
509
510#[cfg(test)]
511#[path = "tests/image_tests.rs"]
512mod tests;