tessera_components/pipelines/image/
command.rs1use std::{
2 hash::{Hash, Hasher},
3 sync::Arc,
4};
5
6use tessera_ui::DrawCommand;
7
8#[derive(Debug, Clone)]
13pub struct ImageData {
14 pub data: Arc<Vec<u8>>,
16 pub width: u32,
18 pub height: u32,
20 pub(super) content_hash: u64,
21}
22
23impl ImageData {
24 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#[derive(Debug, Clone, PartialEq)]
90pub struct ImageCommand {
91 pub data: ImageData,
93 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}