smudge_core/image.rs
1//! Premultiplied RGBA pixel data that widgets paint into rectangles.
2//!
3//! Decoding is not part of the library: an application decodes a file off the
4//! UI thread and hands the pixels over. [`Image`] is `Send` and `Sync`, so a
5//! background thread can hold one, and it is cheap to clone, so a signal can
6//! hold one while the display list borrows it.
7//!
8//! The pixels are premultiplied RGBA8, row-major with no padding: each byte
9//! is a channel value with the red, green and blue channels already
10//! multiplied by the alpha. Premultiplied pixels blend without division. A
11//! buffer that arrives straight-alpha from a decoder goes through
12//! [`Image::from_straight_rgba`], which does the multiplication.
13
14use crate::element::Value;
15use std::sync::Arc;
16use std::sync::atomic::{AtomicU64, Ordering};
17
18/// The source of the identifiers every [`Image`] carries.
19static NEXT_ID: AtomicU64 = AtomicU64::new(1);
20
21/// Premultiplied RGBA8 pixel data, shared by every element that shows it.
22///
23/// The pixels are row-major with no padding, four bytes per pixel in red,
24/// green, blue, alpha order, with the color channels already multiplied by
25/// the alpha. Two images compare equal when they came from the same
26/// [`Image::from_premultiplied_rgba`] call, so a display list that re-emits
27/// the same image damages nothing new.
28///
29/// # Examples
30///
31/// A two by two image, fully opaque red and blue:
32///
33/// ```
34/// use smudge_core::image::Image;
35///
36/// let image = Image::from_premultiplied_rgba(
37/// 2,
38/// 2,
39/// vec![
40/// 255, 0, 0, 255, 255, 0, 0, 255,
41/// 0, 0, 255, 255, 0, 0, 255, 255,
42/// ],
43/// )
44/// .expect("the buffer holds four pixels");
45/// assert_eq!(image.width(), 2);
46/// ```
47#[derive(Clone)]
48pub struct Image {
49 data: Arc<ImageData>,
50}
51
52struct ImageData {
53 id: u64,
54 width: u32,
55 height: u32,
56 pixels: Box<[u8]>,
57}
58
59impl Image {
60 /// Builds an image from premultiplied RGBA8 pixels, row-major with no
61 /// padding, four bytes per pixel in red, green, blue, alpha order, with
62 /// the color channels already multiplied by the alpha.
63 ///
64 /// A color channel above its pixel's alpha cannot come out of a
65 /// multiplication by that alpha, so it is clamped to the alpha. Both
66 /// renderers then blend the same valid pixel. A buffer that holds
67 /// straight alpha belongs in [`Image::from_straight_rgba`] instead.
68 ///
69 /// # Errors
70 ///
71 /// Returns [`ImageError::ZeroSize`] when `width` or `height` is zero, and
72 /// [`ImageError::PixelCount`] when `pixels` does not hold exactly
73 /// `width * height * 4` bytes.
74 ///
75 /// # Examples
76 ///
77 /// ```
78 /// use smudge_core::image::{Image, ImageError};
79 ///
80 /// let error = Image::from_premultiplied_rgba(2, 2, vec![0; 7])
81 /// .expect_err("the buffer is too short");
82 /// assert!(matches!(
83 /// error,
84 /// ImageError::PixelCount {
85 /// expected: 16,
86 /// found: 7,
87 /// }
88 /// ));
89 /// ```
90 pub fn from_premultiplied_rgba(
91 width: u32,
92 height: u32,
93 mut pixels: Vec<u8>,
94 ) -> Result<Image, ImageError> {
95 check_size(width, height, pixels.len())?;
96 for pixel in pixels.as_chunks_mut::<4>().0 {
97 let alpha = pixel[3];
98 for channel in &mut pixel[..3] {
99 *channel = (*channel).min(alpha);
100 }
101 }
102 Ok(Image::new(width, height, pixels))
103 }
104
105 /// Builds an image from straight-alpha RGBA8 pixels, the form most
106 /// decoders produce, by multiplying each color channel by its pixel's
107 /// alpha and rounding to the nearest value.
108 ///
109 /// The layout is the one [`Image::from_premultiplied_rgba`] takes: row
110 /// major with no padding, four bytes per pixel in red, green, blue,
111 /// alpha order. The conversion happens in place, without a second
112 /// buffer.
113 ///
114 /// # Errors
115 ///
116 /// Returns [`ImageError::ZeroSize`] when `width` or `height` is zero, and
117 /// [`ImageError::PixelCount`] when `pixels` does not hold exactly
118 /// `width * height * 4` bytes.
119 ///
120 /// # Examples
121 ///
122 /// White at half alpha becomes half-intensity white:
123 ///
124 /// ```
125 /// use smudge_core::image::Image;
126 ///
127 /// let image = Image::from_straight_rgba(1, 1, vec![255, 255, 255, 128])
128 /// .expect("one pixel fills the buffer");
129 /// assert_eq!(image.pixels(), &[128, 128, 128, 128]);
130 /// ```
131 pub fn from_straight_rgba(
132 width: u32,
133 height: u32,
134 mut pixels: Vec<u8>,
135 ) -> Result<Image, ImageError> {
136 check_size(width, height, pixels.len())?;
137 for pixel in pixels.as_chunks_mut::<4>().0 {
138 let alpha = u32::from(pixel[3]);
139 for channel in &mut pixel[..3] {
140 *channel = ((u32::from(*channel) * alpha + 127) / 255) as u8;
141 }
142 }
143 Ok(Image::new(width, height, pixels))
144 }
145
146 /// Wraps pixels that passed [`check_size`] under a fresh identifier.
147 fn new(width: u32, height: u32, pixels: Vec<u8>) -> Image {
148 Image {
149 data: Arc::new(ImageData {
150 id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
151 width,
152 height,
153 pixels: pixels.into_boxed_slice(),
154 }),
155 }
156 }
157
158 /// The image's width in pixels.
159 pub fn width(&self) -> u32 {
160 self.data.width
161 }
162
163 /// The image's height in pixels.
164 pub fn height(&self) -> u32 {
165 self.data.height
166 }
167
168 /// The premultiplied RGBA8 pixels, row-major with no padding.
169 pub fn pixels(&self) -> &[u8] {
170 &self.data.pixels
171 }
172
173 /// The identifier that names this image's data for the lifetime of the
174 /// process.
175 ///
176 /// Renderers use it as an upload cache key without hashing the pixels.
177 pub fn id(&self) -> u64 {
178 self.data.id
179 }
180}
181
182impl PartialEq for Image {
183 fn eq(&self, other: &Self) -> bool {
184 self.data.id == other.data.id
185 }
186}
187
188impl From<Option<Image>> for Value<Option<Image>> {
189 fn from(value: Option<Image>) -> Value<Option<Image>> {
190 Value::Static(value)
191 }
192}
193
194impl std::fmt::Debug for Image {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 f.debug_struct("Image")
197 .field("id", &self.data.id)
198 .field("width", &self.data.width)
199 .field("height", &self.data.height)
200 .finish()
201 }
202}
203
204/// Checks that a `width` by `height` image has a pixel and that a buffer of
205/// `found` bytes holds exactly its RGBA8 pixels.
206fn check_size(width: u32, height: u32, found: usize) -> Result<(), ImageError> {
207 if width == 0 || height == 0 {
208 return Err(ImageError::ZeroSize { width, height });
209 }
210 let expected = u64::from(width)
211 .checked_mul(u64::from(height))
212 .and_then(|count| count.checked_mul(4))
213 .and_then(|bytes| usize::try_from(bytes).ok());
214 match expected {
215 Some(expected) if expected == found => Ok(()),
216 expected => Err(ImageError::PixelCount {
217 expected: expected.unwrap_or(usize::MAX),
218 found,
219 }),
220 }
221}
222
223/// Why an [`Image`] could not be built.
224#[derive(Debug, Clone, PartialEq, Eq)]
225#[non_exhaustive]
226pub enum ImageError {
227 /// The width or the height is zero, so the image has no pixel.
228 ZeroSize {
229 /// The width asked for, in pixels.
230 width: u32,
231 /// The height asked for, in pixels.
232 height: u32,
233 },
234 /// The pixel buffer's length does not match the width, height and
235 /// format.
236 PixelCount {
237 /// The length the width and height call for, in bytes, or
238 /// `usize::MAX` when that length does not fit in a `usize`.
239 expected: usize,
240 /// The length the buffer has, in bytes.
241 found: usize,
242 },
243}
244
245impl std::fmt::Display for ImageError {
246 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247 match self {
248 ImageError::ZeroSize { width, height } => {
249 write!(formatter, "an image cannot be {width}x{height} pixels")
250 }
251 ImageError::PixelCount { expected, found } => write!(
252 formatter,
253 "the pixel buffer holds {found} bytes, but the size calls for {expected}"
254 ),
255 }
256 }
257}
258
259impl std::error::Error for ImageError {}
260
261#[cfg(test)]
262#[path = "image_tests.rs"]
263mod tests;