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