Skip to main content

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}