Skip to main content

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;