Skip to main content

tessera_components/pipelines/image/
command.rs

1use std::{
2    hash::{Hash, Hasher},
3    sync::Arc,
4};
5
6use tessera_ui::DrawCommand;
7
8/// Image pixel data for rendering.
9///
10/// Prefer [`ImageData::from_rgba`] to construct image content. Cloning shares
11/// pixel storage; independently allocated images compare by dimensions and pixels.
12#[derive(Debug, Clone)]
13pub struct ImageData {
14    /// Premultiplied sRGB RGBA pixels.
15    pub data: Arc<Vec<u8>>,
16    /// Image width in pixels.
17    pub width: u32,
18    /// Image height in pixels.
19    pub height: u32,
20    pub(super) content_hash: u64,
21}
22
23impl ImageData {
24    /// Builds image data from straight-alpha sRGB RGBA pixels.
25    pub fn from_rgba(data: impl Into<Arc<Vec<u8>>>, width: u32, height: u32) -> Self {
26        let mut data: Arc<Vec<u8>> = data.into();
27        if data.chunks_exact(4).any(|pixel| pixel[3] != 255) {
28            premultiply(Arc::make_mut(&mut data).as_mut_slice());
29        }
30        let content_hash = content_hash(data.as_ref());
31        Self {
32            data,
33            width,
34            height,
35            content_hash,
36        }
37    }
38}
39
40fn premultiply(pixels: &mut [u8]) {
41    for pixel in pixels.chunks_exact_mut(4) {
42        let alpha = f32::from(pixel[3]) / 255.0;
43        if alpha == 1.0 {
44            continue;
45        }
46        for channel in &mut pixel[..3] {
47            let srgb = f32::from(*channel) / 255.0;
48            let linear = if srgb <= 0.04045 {
49                srgb / 12.92
50            } else {
51                ((srgb + 0.055) / 1.055).powf(2.4)
52            } * alpha;
53            let encoded = if linear <= 0.0031308 {
54                linear * 12.92
55            } else {
56                1.055 * linear.powf(1.0 / 2.4) - 0.055
57            };
58            *channel = (encoded * 255.0).round() as u8;
59        }
60    }
61}
62
63fn content_hash(bytes: &[u8]) -> u64 {
64    let mut hasher = std::collections::hash_map::DefaultHasher::new();
65    bytes.hash(&mut hasher);
66    hasher.finish()
67}
68
69impl Hash for ImageData {
70    fn hash<H: Hasher>(&self, state: &mut H) {
71        self.content_hash.hash(state);
72        self.width.hash(state);
73        self.height.hash(state);
74    }
75}
76
77impl PartialEq for ImageData {
78    fn eq(&self, other: &Self) -> bool {
79        self.width == other.width
80            && self.height == other.height
81            && self.content_hash == other.content_hash
82            && (Arc::ptr_eq(&self.data, &other.data) || self.data == other.data)
83    }
84}
85
86impl Eq for ImageData {}
87
88/// Command for rendering an image in a UI component.
89#[derive(Debug, Clone, PartialEq)]
90pub struct ImageCommand {
91    /// Image buffer used by the draw pass.
92    pub data: ImageData,
93    /// Opacity multiplier applied to the sampled image.
94    pub opacity: f32,
95}
96
97impl DrawCommand for ImageCommand {
98    fn apply_opacity(&mut self, opacity: f32) {
99        self.opacity = (self.opacity * opacity).clamp(0.0, 1.0);
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use std::sync::Arc;
106
107    use super::ImageData;
108
109    #[test]
110    fn independently_allocated_images_compare_pixels_even_when_hashes_collide() {
111        let first = ImageData::from_rgba(vec![255, 0, 0, 255], 1, 1);
112        let equal = ImageData::from_rgba(vec![255, 0, 0, 255], 1, 1);
113        assert_eq!(first, equal);
114        let mut collision = ImageData::from_rgba(vec![0, 0, 255, 255], 1, 1);
115        collision.content_hash = first.content_hash;
116        assert_ne!(first, collision);
117    }
118
119    #[test]
120    fn construction_premultiplies_in_linear_light_and_keeps_opaque_buffers() {
121        let opaque = Arc::new(vec![128, 64, 192, 255]);
122        assert!(Arc::ptr_eq(
123            &ImageData::from_rgba(Arc::clone(&opaque), 1, 1).data,
124            &opaque
125        ));
126        let translucent = ImageData::from_rgba(vec![255, 0, 0, 128, 255, 128, 64, 0], 2, 1);
127        assert_eq!(*translucent.data, [188, 0, 0, 128, 0, 0, 0, 0]);
128    }
129
130    #[test]
131    fn shared_pixels_with_different_dimensions_are_distinct_images() {
132        let pixels = Arc::new(vec![255, 0, 0, 255, 0, 0, 255, 255]);
133        let horizontal = ImageData::from_rgba(Arc::clone(&pixels), 2, 1);
134        let vertical = ImageData::from_rgba(pixels, 1, 2);
135        assert_ne!(horizontal, vertical);
136    }
137}