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, Copy, 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]`.
196    Matrix([f32; 20]),
197}
198
199impl ColorFilter {
200    /// Creates a Compose-style tint filter (`SrcIn`).
201    pub fn tint(color: Color) -> Self {
202        Self::Tint(color)
203    }
204
205    /// Creates an explicit modulation filter that multiplies channels by `color`.
206    pub fn modulate(color: Color) -> Self {
207        Self::Modulate(color)
208    }
209
210    /// Creates a filter from a 4x5 color matrix.
211    pub fn matrix(matrix: [f32; 20]) -> Self {
212        Self::Matrix(matrix)
213    }
214
215    /// Returns a filter that applies `self` first and then `next`, as a single color matrix.
216    pub fn compose(self, next: ColorFilter) -> ColorFilter {
217        ColorFilter::Matrix(compose_color_matrices(self.as_matrix(), next.as_matrix()))
218    }
219
220    /// Returns the filter as a 4x5 color matrix in row-major order, in the layout
221    /// [`ColorFilter::Matrix`] describes.
222    pub fn as_matrix(self) -> [f32; 20] {
223        match self {
224            Self::Tint(tint) => [
225                0.0,
226                0.0,
227                0.0,
228                tint.r(),
229                0.0,
230                0.0,
231                0.0,
232                0.0,
233                tint.g(),
234                0.0,
235                0.0,
236                0.0,
237                0.0,
238                tint.b(),
239                0.0,
240                0.0,
241                0.0,
242                0.0,
243                tint.a(),
244                0.0,
245            ],
246            Self::Modulate(modulate) => [
247                modulate.r(),
248                0.0,
249                0.0,
250                0.0,
251                0.0,
252                0.0,
253                modulate.g(),
254                0.0,
255                0.0,
256                0.0,
257                0.0,
258                0.0,
259                modulate.b(),
260                0.0,
261                0.0,
262                0.0,
263                0.0,
264                0.0,
265                modulate.a(),
266                0.0,
267            ],
268            Self::Matrix(matrix) => matrix,
269        }
270    }
271
272    /// Applies the filter to a color given as red, green, blue and alpha from 0.0 to 1.0. Each
273    /// result channel is clamped to that range.
274    pub fn apply_rgba(self, rgba: [f32; 4]) -> [f32; 4] {
275        apply_color_matrix(self.as_matrix(), rgba)
276    }
277
278    /// Whether the filter is a plain modulation, which a renderer can apply by multiplying vertex
279    /// colors instead of sampling a matrix.
280    pub fn supports_gpu_vertex_modulation(self) -> bool {
281        matches!(self, Self::Modulate(_))
282    }
283
284    /// Returns the red, green, blue and alpha factors to multiply into vertex colors when the
285    /// filter is a modulation, and `None` otherwise.
286    pub fn gpu_vertex_tint(self) -> Option<[f32; 4]> {
287        match self {
288            Self::Modulate(tint) => Some([tint.r(), tint.g(), tint.b(), tint.a()]),
289            _ => None,
290        }
291    }
292
293    /// Returns the blend mode that gives the same result when the filter can be expressed as one:
294    /// `SrcIn` for a tint, `Modulate` for a modulation and `SrcOver` for a matrix.
295    pub fn blend_mode(self) -> BlendMode {
296        match self {
297            Self::Tint(_) => BlendMode::SrcIn,
298            Self::Modulate(_) => BlendMode::Modulate,
299            Self::Matrix(_) => BlendMode::SrcOver,
300        }
301    }
302}
303
304fn apply_color_matrix(matrix: [f32; 20], rgba: [f32; 4]) -> [f32; 4] {
305    let r = rgba[0];
306    let g = rgba[1];
307    let b = rgba[2];
308    let a = rgba[3];
309    [
310        (matrix[0] * r + matrix[1] * g + matrix[2] * b + matrix[3] * a + matrix[4]).clamp(0.0, 1.0),
311        (matrix[5] * r + matrix[6] * g + matrix[7] * b + matrix[8] * a + matrix[9]).clamp(0.0, 1.0),
312        (matrix[10] * r + matrix[11] * g + matrix[12] * b + matrix[13] * a + matrix[14])
313            .clamp(0.0, 1.0),
314        (matrix[15] * r + matrix[16] * g + matrix[17] * b + matrix[18] * a + matrix[19])
315            .clamp(0.0, 1.0),
316    ]
317}
318
319fn compose_color_matrices(first: [f32; 20], second: [f32; 20]) -> [f32; 20] {
320    let mut composed = [0.0f32; 20];
321    for row in 0..4 {
322        let row_base = row * 5;
323        let s0 = second[row_base];
324        let s1 = second[row_base + 1];
325        let s2 = second[row_base + 2];
326        let s3 = second[row_base + 3];
327        let s4 = second[row_base + 4];
328
329        composed[row_base] = s0 * first[0] + s1 * first[5] + s2 * first[10] + s3 * first[15];
330        composed[row_base + 1] = s0 * first[1] + s1 * first[6] + s2 * first[11] + s3 * first[16];
331        composed[row_base + 2] = s0 * first[2] + s1 * first[7] + s2 * first[12] + s3 * first[17];
332        composed[row_base + 3] = s0 * first[3] + s1 * first[8] + s2 * first[13] + s3 * first[18];
333        composed[row_base + 4] =
334            s0 * first[4] + s1 * first[9] + s2 * first[14] + s3 * first[19] + s4;
335    }
336    composed
337}
338
339impl ImageBitmap {
340    /// Creates a bitmap by taking ownership of tightly packed RGBA8 pixels.
341    /// Any spare capacity in the pixel buffer is released.
342    pub fn from_rgba8(width: u32, height: u32, pixels: Vec<u8>) -> Result<Self, ImageBitmapError> {
343        Self::from_pixels(width, height, ImagePixelFormat::Rgba8, pixels)
344    }
345
346    /// Copies tightly packed RGBA8 pixels into a new bitmap.
347    pub fn from_rgba8_slice(
348        width: u32,
349        height: u32,
350        pixels: &[u8],
351    ) -> Result<Self, ImageBitmapError> {
352        Self::from_pixels(width, height, ImagePixelFormat::Rgba8, pixels)
353    }
354
355    /// Takes ownership of one alpha byte per pixel with a constant RGB color.
356    /// Any spare capacity in the alpha buffer is released.
357    pub fn from_alpha8(
358        width: u32,
359        height: u32,
360        color: [u8; 3],
361        alpha: Vec<u8>,
362    ) -> Result<Self, ImageBitmapError> {
363        Self::from_pixels(width, height, ImagePixelFormat::Alpha8 { color }, alpha)
364    }
365
366    fn from_pixels(
367        width: u32,
368        height: u32,
369        format: ImagePixelFormat,
370        pixels: impl AsRef<[u8]> + Into<Box<[u8]>>,
371    ) -> Result<Self, ImageBitmapError> {
372        if width == 0 || height == 0 {
373            return Err(ImageBitmapError::InvalidDimensions);
374        }
375        let expected = (width as usize)
376            .checked_mul(height as usize)
377            .and_then(|value| value.checked_mul(4))
378            .ok_or(ImageBitmapError::DimensionsTooLarge)?;
379        let expected = match format {
380            ImagePixelFormat::Rgba8 => expected,
381            ImagePixelFormat::Alpha8 { .. } => expected / 4,
382            ImagePixelFormat::External => 0,
383        };
384
385        let bytes = pixels.as_ref();
386        if bytes.len() != expected {
387            return Err(ImageBitmapError::PixelDataLengthMismatch {
388                expected,
389                actual: bytes.len(),
390            });
391        }
392
393        let id = bitmap_content_id(width, height, format, bytes);
394        let opaque = match format {
395            ImagePixelFormat::Rgba8 => bytes
396                .as_chunks::<4>()
397                .0
398                .iter()
399                .all(|pixel| pixel[3] == u8::MAX),
400            ImagePixelFormat::Alpha8 { .. } => bytes.iter().all(|alpha| *alpha == u8::MAX),
401            ImagePixelFormat::External => false,
402        };
403        Ok(Self {
404            data: Arc::new(ImageBitmapData {
405                width,
406                height,
407                id,
408                opaque,
409                format,
410                storage: ImageStorage::Pixels(pixels.into()),
411            }),
412        })
413    }
414
415    /// Content-derived bitmap identity used by renderer caches. Each frame
416    /// of an [`ExternalTexture`] has its own id.
417    pub fn id(&self) -> u64 {
418        self.data.id
419    }
420
421    /// Identity of the bitmap's storage: the same for every frame of an
422    /// [`ExternalTexture`], equal to [`Self::id`] for pixel bitmaps.
423    pub fn storage_id(&self) -> u64 {
424        match &self.data.storage {
425            ImageStorage::External { key, .. } => *key,
426            ImageStorage::Pixels(_) => self.data.id,
427        }
428    }
429
430    /// The renderer handle of an [`ExternalTexture`] frame, when it is a `T`.
431    pub fn external_handle<T: 'static>(&self) -> Option<&T> {
432        match &self.data.storage {
433            ImageStorage::External { handle, .. } => handle.downcast_ref(),
434            ImageStorage::Pixels(_) => None,
435        }
436    }
437
438    /// Width in pixels.
439    pub fn width(&self) -> u32 {
440        self.data.width
441    }
442
443    /// Height in pixels.
444    pub fn height(&self) -> u32 {
445        self.data.height
446    }
447
448    /// Returns the raw pixel bytes in [`Self::format`], empty for an
449    /// [`ExternalTexture`] frame.
450    pub fn pixels(&self) -> &[u8] {
451        match &self.data.storage {
452            ImageStorage::Pixels(pixels) => pixels,
453            ImageStorage::External { .. } => &[],
454        }
455    }
456
457    /// Returns the storage format of [`Self::pixels`].
458    pub fn format(&self) -> ImagePixelFormat {
459        self.data.format
460    }
461
462    /// Returns the unpremultiplied RGBA8 pixel at a row-major index;
463    /// transparent for an [`ExternalTexture`] frame.
464    ///
465    /// # Panics
466    /// Panics when `index` is outside the image.
467    pub fn rgba8_pixel(&self, index: usize) -> [u8; 4] {
468        match self.format() {
469            ImagePixelFormat::Rgba8 => self.pixels().as_chunks::<4>().0[index],
470            ImagePixelFormat::Alpha8 { color: [r, g, b] } => [r, g, b, self.pixels()[index]],
471            ImagePixelFormat::External => [0; 4],
472        }
473    }
474
475    /// Returns tightly packed RGBA8 bytes, borrowing RGBA images and expanding
476    /// alpha masks; empty for an [`ExternalTexture`] frame.
477    pub fn rgba8_pixels(&self) -> Cow<'_, [u8]> {
478        match self.format() {
479            ImagePixelFormat::Rgba8 | ImagePixelFormat::External => Cow::Borrowed(self.pixels()),
480            ImagePixelFormat::Alpha8 { color: [r, g, b] } => {
481                let mut pixels = Vec::with_capacity(self.pixels().len() * 4);
482                for &alpha in self.pixels() {
483                    pixels.extend_from_slice(&[r, g, b, alpha]);
484                }
485                Cow::Owned(pixels)
486            }
487        }
488    }
489
490    /// Returns true when every source pixel has full alpha.
491    pub fn is_opaque(&self) -> bool {
492        self.data.opaque
493    }
494
495    /// Returns intrinsic size in logical units.
496    pub fn intrinsic_size(&self) -> Size {
497        Size {
498            width: self.data.width as f32,
499            height: self.data.height as f32,
500        }
501    }
502}
503
504impl PartialEq for ImageBitmap {
505    fn eq(&self, other: &Self) -> bool {
506        self.id() == other.id()
507    }
508}
509
510impl Eq for ImageBitmap {}
511
512impl Hash for ImageBitmap {
513    fn hash<H: Hasher>(&self, state: &mut H) {
514        self.id().hash(state);
515    }
516}
517
518fn bitmap_content_id(width: u32, height: u32, format: ImagePixelFormat, pixels: &[u8]) -> u64 {
519    let mut hasher = foldhash::quality::FixedState::default().build_hasher();
520    width.hash(&mut hasher);
521    height.hash(&mut hasher);
522    format.hash(&mut hasher);
523    pixels.hash(&mut hasher);
524    hasher.finish()
525}
526
527#[cfg(test)]
528#[path = "tests/image_tests.rs"]
529mod tests;