vello_common/pixmap.rs
1// Copyright 2025 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! A simple pixmap type.
5
6use alloc::vec;
7use alloc::vec::Vec;
8use peniko::color::Rgba8;
9
10use crate::peniko::color::PremulRgba8;
11
12#[cfg(feature = "png")]
13extern crate std;
14
15/// A pixmap of premultiplied RGBA8 values backed by [`u8`][core::u8].
16#[derive(Debug, Clone)]
17pub struct Pixmap {
18 /// Width of the pixmap in pixels.
19 width: u16,
20 /// Height of the pixmap in pixels.
21 height: u16,
22 /// Buffer of the pixmap in RGBA8 format.
23 buf: Vec<PremulRgba8>,
24 /// Whether the pixmap may have non-opaque pixels.
25 ///
26 /// Note: This may become stale if pixels are modified via [`data_mut()`](Self::data_mut),
27 /// [`data_as_u8_slice_mut()`](Self::data_as_u8_slice_mut), or [`set_pixel()`](Self::set_pixel).
28 may_have_opacities: bool,
29}
30
31impl Pixmap {
32 /// Create a new pixmap with the given width and height in pixels.
33 ///
34 /// All pixels are initialized to transparent black.
35 pub fn new(width: u16, height: u16) -> Self {
36 let buf = vec![PremulRgba8::from_u32(0); width as usize * height as usize];
37 Self {
38 width,
39 height,
40 buf,
41 may_have_opacities: true,
42 }
43 }
44
45 /// Create a new pixmap with the given premultiplied RGBA8 data.
46 ///
47 /// The `data` vector must be of length `width * height` exactly.
48 ///
49 /// The pixels are in row-major order.
50 ///
51 /// This assumes the image may have transparent pixels. Use
52 /// [`from_parts_with_opacity`](Self::from_parts_with_opacity) if you already
53 /// know the opacity status to enable optimizations.
54 ///
55 /// # Panics
56 ///
57 /// Panics if the `data` vector is not of length `width * height`.
58 pub fn from_parts(data: Vec<PremulRgba8>, width: u16, height: u16) -> Self {
59 Self::from_parts_with_opacity(data, width, height, true)
60 }
61
62 /// Create a new pixmap with the given premultiplied RGBA8 data and precomputed opacity flag.
63 ///
64 /// The `data` vector must be of length `width * height` exactly.
65 ///
66 /// The pixels are in row-major order.
67 ///
68 /// Use this when you've already determined whether the data contains
69 /// non-opaque pixels to avoid redundant scanning.
70 ///
71 /// # Panics
72 ///
73 /// Panics if the `data` vector is not of length `width * height`.
74 pub fn from_parts_with_opacity(
75 data: Vec<PremulRgba8>,
76 width: u16,
77 height: u16,
78 may_have_opacities: bool,
79 ) -> Self {
80 assert_eq!(
81 data.len(),
82 usize::from(width) * usize::from(height),
83 "Expected `data` to have length of exactly `width * height`"
84 );
85 Self {
86 width,
87 height,
88 buf: data,
89 may_have_opacities,
90 }
91 }
92
93 /// Resizes the pixmap container to the given width and height; this does not resize the
94 /// contained image.
95 ///
96 /// If the pixmap buffer has to grow to fit the new size, those pixels are set to transparent
97 /// black. If the pixmap buffer is larger than required, the buffer is truncated and its
98 /// reserved capacity is unchanged.
99 pub fn resize(&mut self, width: u16, height: u16) {
100 let new_len = usize::from(width) * usize::from(height);
101 // If we're growing, new pixels are transparent black
102 if new_len > self.buf.len() {
103 self.may_have_opacities = true;
104 }
105 self.width = width;
106 self.height = height;
107 self.buf.resize(new_len, PremulRgba8::from_u32(0));
108 }
109
110 /// Shrink the capacity of the pixmap buffer to fit the pixmap's current size.
111 pub fn shrink_to_fit(&mut self) {
112 self.buf.shrink_to_fit();
113 }
114
115 /// The reserved capacity (in pixels) of this pixmap.
116 ///
117 /// When calling [`Pixmap::resize`] with a `width * height` smaller than this value, the pixmap
118 /// does not need to reallocate.
119 pub fn capacity(&self) -> usize {
120 self.buf.capacity()
121 }
122
123 /// Return the width of the pixmap.
124 pub fn width(&self) -> u16 {
125 self.width
126 }
127
128 /// Return the height of the pixmap.
129 pub fn height(&self) -> u16 {
130 self.height
131 }
132
133 /// Returns whether the pixmap may have non-opaque pixels.
134 ///
135 /// This value is computed at construction time. It may become stale if pixels are
136 /// modified directly via [`data_mut()`](Self::data_mut),
137 /// [`data_as_u8_slice_mut()`](Self::data_as_u8_slice_mut), or [`set_pixel()`](Self::set_pixel).
138 ///
139 /// Use [`set_may_have_opacities()`](Self::set_may_have_opacities) to manually update the flag,
140 /// or [`recompute_may_have_opacities()`](Self::recompute_may_have_opacities) to recalculate it
141 /// by scanning all pixels.
142 pub fn may_have_opacities(&self) -> bool {
143 self.may_have_opacities
144 }
145
146 /// Manually set the `may_have_opacities` flag.
147 ///
148 /// Use this after modifying pixels via [`data_mut()`](Self::data_mut) or
149 /// [`set_pixel()`](Self::set_pixel) when you know whether the image has
150 /// non-opaque pixels.
151 pub fn set_may_have_opacities(&mut self, may_have_opacities: bool) {
152 self.may_have_opacities = may_have_opacities;
153 }
154
155 /// Recalculate `may_have_opacities` by scanning all pixels.
156 ///
157 /// Use this after modifying pixels via [`data_mut()`](Self::data_mut) or
158 /// [`set_pixel()`](Self::set_pixel) when you need accurate opacity information.
159 pub fn recompute_may_have_opacities(&mut self) {
160 self.may_have_opacities = self.buf.iter().any(|pixel| pixel.a != 255);
161 }
162
163 /// Apply an alpha value to the whole pixmap.
164 pub fn multiply_alpha(&mut self, alpha: u8) {
165 #[expect(
166 clippy::cast_possible_truncation,
167 reason = "cannot overflow in this case"
168 )]
169 let multiply = |component| ((u16::from(alpha) * u16::from(component)) / 255) as u8;
170
171 for pixel in self.data_mut() {
172 *pixel = PremulRgba8 {
173 r: multiply(pixel.r),
174 g: multiply(pixel.g),
175 b: multiply(pixel.b),
176 a: multiply(pixel.a),
177 };
178 }
179
180 // If we applied a non-opaque alpha, the image now has opacities
181 if alpha != 255 {
182 self.may_have_opacities = true;
183 }
184 }
185
186 /// Create a pixmap from a PNG file.
187 #[cfg(feature = "png")]
188 pub fn from_png(data: impl std::io::Read) -> Result<Self, png::DecodingError> {
189 let mut decoder = png::Decoder::new(data);
190 decoder.set_transformations(
191 png::Transformations::normalize_to_color8() | png::Transformations::ALPHA,
192 );
193
194 let mut reader = decoder.read_info()?;
195 let mut pixmap = {
196 let info = reader.info();
197 let width: u16 = info
198 .width
199 .try_into()
200 .map_err(|_| png::DecodingError::LimitsExceeded)?;
201 let height: u16 = info
202 .height
203 .try_into()
204 .map_err(|_| png::DecodingError::LimitsExceeded)?;
205 Self::new(width, height)
206 };
207
208 // Note `reader.info()` returns the pre-transformation color type output, whereas
209 // `reader.output_color_type()` takes the transformation into account.
210 let (color_type, bit_depth) = reader.output_color_type();
211 debug_assert_eq!(
212 bit_depth,
213 png::BitDepth::Eight,
214 "normalize_to_color8 means the bit depth is always 8."
215 );
216
217 match color_type {
218 png::ColorType::Rgb | png::ColorType::Grayscale => {
219 unreachable!("We set a transformation to always convert to alpha")
220 }
221 png::ColorType::Indexed => {
222 unreachable!("Transformation should have expanded indexed images")
223 }
224 png::ColorType::Rgba => {
225 debug_assert_eq!(
226 pixmap.data_as_u8_slice().len(),
227 reader.output_buffer_size(),
228 "The pixmap buffer should have the same number of bytes as the image."
229 );
230 reader.next_frame(pixmap.data_as_u8_slice_mut())?;
231 }
232 png::ColorType::GrayscaleAlpha => {
233 debug_assert_eq!(
234 pixmap.data().len() * 2,
235 reader.output_buffer_size(),
236 "The pixmap buffer should have twice the number of bytes of the grayscale image."
237 );
238 let mut grayscale_data = vec![0; reader.output_buffer_size()];
239 reader.next_frame(&mut grayscale_data)?;
240
241 for (grayscale_pixel, pixmap_pixel) in
242 grayscale_data.chunks_exact(2).zip(pixmap.data_mut())
243 {
244 let [gray, alpha] = grayscale_pixel.try_into().unwrap();
245 *pixmap_pixel = PremulRgba8 {
246 r: gray,
247 g: gray,
248 b: gray,
249 a: alpha,
250 };
251 }
252 }
253 };
254
255 let mut may_have_opacities = false;
256 for pixel in pixmap.data_mut() {
257 let alpha = pixel.a;
258 if alpha != 255 {
259 may_have_opacities = true;
260 }
261 let alpha_u16 = u16::from(alpha);
262 #[expect(
263 clippy::cast_possible_truncation,
264 reason = "Overflow should be impossible."
265 )]
266 let premultiply = |e: u8| ((u16::from(e) * alpha_u16) / 255) as u8;
267 pixel.r = premultiply(pixel.r);
268 pixel.g = premultiply(pixel.g);
269 pixel.b = premultiply(pixel.b);
270 }
271 pixmap.may_have_opacities = may_have_opacities;
272
273 Ok(pixmap)
274 }
275
276 /// Return the current content of the pixmap as a PNG.
277 #[cfg(feature = "png")]
278 pub fn into_png(self) -> Result<Vec<u8>, png::EncodingError> {
279 let mut data = Vec::new();
280 let mut encoder = png::Encoder::new(&mut data, self.width as u32, self.height as u32);
281 encoder.set_color(png::ColorType::Rgba);
282 encoder.set_depth(png::BitDepth::Eight);
283 let mut writer = encoder.write_header()?;
284 writer.write_image_data(bytemuck::cast_slice(&self.take_unpremultiplied()))?;
285 writer.finish().map(|_| data)
286 }
287
288 /// Returns a reference to the underlying data as premultiplied RGBA8.
289 ///
290 /// The pixels are in row-major order.
291 pub fn data(&self) -> &[PremulRgba8] {
292 &self.buf
293 }
294
295 /// Returns a mutable reference to the underlying data as premultiplied RGBA8.
296 ///
297 /// The pixels are in row-major order.
298 pub fn data_mut(&mut self) -> &mut [PremulRgba8] {
299 &mut self.buf
300 }
301
302 /// Returns a reference to the underlying data as premultiplied RGBA8.
303 ///
304 /// The pixels are in row-major order. Each pixel consists of four bytes in the order
305 /// `[r, g, b, a]`.
306 pub fn data_as_u8_slice(&self) -> &[u8] {
307 bytemuck::cast_slice(&self.buf)
308 }
309
310 /// Returns a mutable reference to the underlying data as premultiplied RGBA8.
311 ///
312 /// The pixels are in row-major order. Each pixel consists of four bytes in the order
313 /// `[r, g, b, a]`.
314 pub fn data_as_u8_slice_mut(&mut self) -> &mut [u8] {
315 bytemuck::cast_slice_mut(&mut self.buf)
316 }
317
318 /// Sample a pixel from the pixmap.
319 ///
320 /// The pixel data is [premultiplied RGBA8][PremulRgba8].
321 #[inline(always)]
322 pub fn sample(&self, x: u16, y: u16) -> PremulRgba8 {
323 let idx = self.width as usize * y as usize + x as usize;
324 self.buf[idx]
325 }
326
327 /// Sample a pixel from a custom-calculated index. This index should be calculated assuming that
328 /// the data is stored in row-major order.
329 #[inline(always)]
330 pub fn sample_idx(&self, idx: u32) -> PremulRgba8 {
331 self.buf[idx as usize]
332 }
333
334 /// Set a pixel in the pixmap at the given coordinates.
335 ///
336 /// The pixel data should be [premultiplied RGBA8][PremulRgba8]. The coordinate system has
337 /// its origin at the top-left corner, with `x` increasing to the right and `y` increasing
338 /// downward.
339 #[inline(always)]
340 pub fn set_pixel(&mut self, x: u16, y: u16, pixel: PremulRgba8) {
341 let idx = self.width as usize * y as usize + x as usize;
342 self.buf[idx] = pixel;
343 }
344
345 /// Consume the pixmap, returning the data as the underlying [`Vec`] of premultiplied RGBA8.
346 ///
347 /// The pixels are in row-major order.
348 pub fn take(self) -> Vec<PremulRgba8> {
349 self.buf
350 }
351
352 /// Consume the pixmap, returning the data as (unpremultiplied) RGBA8.
353 ///
354 /// Not fast, but useful for saving to PNG etc.
355 ///
356 /// The pixels are in row-major order.
357 pub fn take_unpremultiplied(self) -> Vec<Rgba8> {
358 self.buf
359 .into_iter()
360 .map(|PremulRgba8 { r, g, b, a }| {
361 let alpha = 255.0 / f32::from(a);
362 if a != 0 {
363 #[expect(clippy::cast_possible_truncation, reason = "deliberate quantization")]
364 let unpremultiply = |component| (f32::from(component) * alpha + 0.5) as u8;
365 Rgba8 {
366 r: unpremultiply(r),
367 g: unpremultiply(g),
368 b: unpremultiply(b),
369 a,
370 }
371 } else {
372 Rgba8 { r, g, b, a }
373 }
374 })
375 .collect()
376 }
377}