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