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