Skip to main content

vello_common/
paint.rs

1// Copyright 2025 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Types for paints.
5
6use crate::TextureId;
7use crate::geometry::RectU16;
8use crate::pixmap::Pixmap;
9use alloc::sync::Arc;
10pub use peniko::Color;
11use peniko::{
12    Gradient,
13    color::{AlphaColor, PremulRgba8, Srgb},
14};
15
16/// A paint that needs to be resolved via its index.
17// In the future, we might add additional flags, that's why we have
18// this thin wrapper around u32, so we can change the underlying
19// representation without breaking the API.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct IndexedPaint(u32);
22
23impl IndexedPaint {
24    /// Create a new indexed paint from an index.
25    pub fn new(index: usize) -> Self {
26        Self(u32::try_from(index).expect("exceeded the maximum number of paints"))
27    }
28
29    /// Return the index of the paint.
30    pub fn index(&self) -> usize {
31        usize::try_from(self.0).unwrap()
32    }
33}
34
35/// A paint used internally by a rendering frontend to store how a draw should be painted.
36/// There are only two types of paint:
37///
38/// 1) Simple solid colors, which are stored in premultiplied representation so that
39///    the renderer doesn't have to recompute it.
40/// 2) Indexed paints, which can represent any arbitrary, more complex paint that is
41///    determined by the frontend. The intended way of using this is to store a vector
42///    of paints and store its index inside `IndexedPaint`.
43#[derive(Debug, Clone, PartialEq)]
44pub enum Paint {
45    /// A premultiplied RGBA8 color.
46    Solid(PremulColor),
47    /// A paint that needs to be resolved via an index.
48    Indexed(IndexedPaint),
49}
50
51impl From<AlphaColor<Srgb>> for Paint {
52    fn from(value: AlphaColor<Srgb>) -> Self {
53        Self::Solid(PremulColor::from_alpha_color(value))
54    }
55}
56
57/// Opaque image handle
58#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
59pub struct ImageId(u32);
60
61impl ImageId {
62    // TODO: make this private in future
63    /// Create a new image id from a u32.
64    pub fn new(value: u32) -> Self {
65        Self(value)
66    }
67
68    /// Return the image id as a u32.
69    pub fn as_u32(&self) -> u32 {
70        self.0
71    }
72}
73
74/// Bitmap source used by `Image`.
75#[derive(Debug, Clone)]
76pub enum ImageSource {
77    /// Pixmap pixels travel with the scene packet.
78    Pixmap(Arc<Pixmap>),
79    // TODO: Explore whether we can merge opaque ID and external texture in some form?
80    /// Pixmap pixels were registered earlier; this is just a handle.
81    OpaqueId {
82        /// The image handle.
83        id: ImageId,
84        /// Whether the image may contain non-opaque pixels.
85        may_have_transparency: bool,
86    },
87    /// An externally owned texture supplied to the renderer at render time.
88    ExternalTexture {
89        /// Opaque external texture handle.
90        id: TextureId,
91        /// Source region to sample from in texel coordinates.
92        source_region: RectU16,
93        /// Whether the source region may contain non-opaque pixels.
94        may_have_transparency: bool,
95    },
96}
97
98impl ImageSource {
99    /// Create an [`ImageSource`] from a pre-registered image handle.
100    ///
101    /// Conservatively assumes the image may have non-opaque pixels.
102    /// Use [`Self::opaque_id_with_transparency_hint`] when you know the image is fully opaque.
103    pub fn opaque_id(id: ImageId) -> Self {
104        Self::OpaqueId {
105            id,
106            may_have_transparency: true,
107        }
108    }
109
110    /// Create an [`ImageSource`] from a pre-registered image handle,
111    /// with an explicit hint about whether the image may have non-opaque pixels.
112    pub fn opaque_id_with_transparency_hint(id: ImageId, may_have_transparency: bool) -> Self {
113        Self::OpaqueId {
114            id,
115            may_have_transparency,
116        }
117    }
118
119    /// Create an image source backed by a texture supplied to the renderer at render time.
120    ///
121    /// # Panics
122    ///
123    /// Panics if `source_region` is empty.
124    pub fn external_texture(
125        texture_id: TextureId,
126        source_region: RectU16,
127        may_have_transparency: bool,
128    ) -> Self {
129        assert!(
130            !source_region.is_empty(),
131            "external texture source regions must not be empty"
132        );
133
134        Self::ExternalTexture {
135            id: texture_id,
136            source_region,
137            may_have_transparency,
138        }
139    }
140
141    /// Returns whether this image source may contain non-opaque pixels.
142    pub fn may_have_transparency(&self) -> bool {
143        match self {
144            Self::Pixmap(p) => p.may_have_transparency(),
145            Self::OpaqueId {
146                may_have_transparency,
147                ..
148            }
149            | Self::ExternalTexture {
150                may_have_transparency,
151                ..
152            } => *may_have_transparency,
153        }
154    }
155
156    /// Convert a [`peniko::ImageData`] to an [`ImageSource`].
157    ///
158    /// This is a somewhat lossy conversion, as the image data data is transformed to
159    /// [premultiplied RGBA8](`PremulRgba8`).
160    ///
161    /// # Panics
162    ///
163    /// This panics if `image` has a `width` or `height` greater than `u16::MAX`.
164    pub fn from_peniko_image_data(image: &peniko::ImageData) -> Self {
165        // TODO: how do we deal with `peniko::ImageFormat` growing? See also
166        // <https://github.com/linebender/vello/pull/996#discussion_r2080510863>.
167        let do_alpha_multiply = image.alpha_type != peniko::ImageAlphaType::AlphaPremultiplied;
168
169        assert!(
170            image.width <= u16::MAX as u32 && image.height <= u16::MAX as u32,
171            "The image is too big. Its width and height can be no larger than {} pixels.",
172            u16::MAX,
173        );
174        let width = image.width.try_into().unwrap();
175        let height = image.height.try_into().unwrap();
176
177        // TODO: SIMD
178        let mut may_have_transparency = false;
179        #[expect(clippy::cast_possible_truncation, reason = "This cannot overflow.")]
180        let pixels = image
181            .data
182            .data()
183            .chunks_exact(4)
184            .map(|pixel| {
185                let rgba: [u8; 4] = match image.format {
186                    peniko::ImageFormat::Rgba8 => pixel.try_into().unwrap(),
187                    peniko::ImageFormat::Bgra8 => [pixel[2], pixel[1], pixel[0], pixel[3]],
188                    format => unimplemented!("Unsupported image format: {format:?}"),
189                };
190                may_have_transparency |= rgba[3] != 255;
191                let alpha = u16::from(rgba[3]);
192                let multiply = |component| ((alpha * u16::from(component)) / 255) as u8;
193                if do_alpha_multiply {
194                    PremulRgba8 {
195                        r: multiply(rgba[0]),
196                        g: multiply(rgba[1]),
197                        b: multiply(rgba[2]),
198                        a: rgba[3],
199                    }
200                } else {
201                    PremulRgba8 {
202                        r: rgba[0],
203                        g: rgba[1],
204                        b: rgba[2],
205                        a: rgba[3],
206                    }
207                }
208            })
209            .collect();
210        let pixmap = Pixmap::from_parts_with_opacity(pixels, width, height, may_have_transparency);
211
212        Self::Pixmap(Arc::new(pixmap))
213    }
214}
215
216/// An image.
217pub type Image = peniko::ImageBrush<ImageSource>;
218
219/// Trait for resolving opaque image IDs to pixmaps at rasterization time.
220///
221/// This allows delaying the resolution of `ImageSource::OpaqueId` until the
222/// image is actually needed during rasterization, enabling patterns like
223/// dynamic sprite atlases where the image data may be updated between
224/// encoding and rendering.
225pub trait ImageResolver: Send + Sync {
226    /// Resolve an `ImageId` to its pixmap data.
227    ///
228    /// This method may be called repeatedly (dozens or even hundreds of times
229    /// per frame) and should therefore be very fast.
230    ///
231    /// Returns `None` if the image ID is not found in the registry.
232    fn resolve(&self, id: ImageId) -> Option<Arc<Pixmap>>;
233}
234
235/// A no-op image resolver that always returns `None`.
236#[derive(Debug, Clone, Copy, Default)]
237pub struct NoOpImageResolver;
238
239impl ImageResolver for NoOpImageResolver {
240    fn resolve(&self, _id: ImageId) -> Option<Arc<Pixmap>> {
241        None
242    }
243}
244
245/// A premultiplied color.
246#[derive(Debug, Clone, PartialEq, Copy)]
247pub struct PremulColor {
248    premul_u8: PremulRgba8,
249    premul_f32: peniko::color::PremulColor<Srgb>,
250}
251
252impl PremulColor {
253    /// Create a new premultiplied color.
254    pub fn from_alpha_color(color: AlphaColor<Srgb>) -> Self {
255        Self::from_premul_color(color.premultiply())
256    }
257
258    /// Create a new premultiplied color from `peniko::PremulColor`.
259    pub fn from_premul_color(color: peniko::color::PremulColor<Srgb>) -> Self {
260        Self {
261            premul_u8: color.to_rgba8(),
262            premul_f32: color,
263        }
264    }
265
266    /// Return the color as a premultiplied RGBA8 color.
267    pub fn as_premul_rgba8(&self) -> PremulRgba8 {
268        self.premul_u8
269    }
270
271    /// Return the color as a premultiplied RGBAF32 color.
272    pub fn as_premul_f32(&self) -> peniko::color::PremulColor<Srgb> {
273        self.premul_f32
274    }
275
276    /// Return whether the color is opaque (i.e. doesn't have transparency).
277    pub fn is_opaque(&self) -> bool {
278        self.premul_f32.components[3] == 1.0
279    }
280}
281
282/// How tint color is applied to an image.
283#[derive(Copy, Clone, Debug, PartialEq, Eq)]
284#[repr(u8)]
285pub enum TintMode {
286    /// Alpha-mask tinting: `tint_premul * source.alpha`.
287    ///
288    /// The source image's alpha channel is used as a coverage mask,
289    /// and the result is filled with the premultiplied tint color.
290    /// This is the standard approach for glyph / monochrome image tinting.
291    AlphaMask = 0,
292    /// Component-wise multiply: `source * tint`.
293    ///
294    /// Each channel of the source pixel is multiplied by the corresponding
295    /// channel of the tint color. This works well for full-color images.
296    Multiply = 1,
297}
298
299impl TintMode {
300    /// Return the discriminant as a `u32`.
301    pub fn as_u32(self) -> u32 {
302        self as u32
303    }
304}
305
306/// A tint applied to image paints.
307#[derive(Copy, Clone, Debug, PartialEq)]
308pub struct Tint {
309    /// The tint color.
310    pub color: Color,
311    /// How the tint is applied.
312    pub mode: TintMode,
313}
314
315/// A kind of paint that can be used for filling and stroking shapes.
316pub type PaintType = peniko::Brush<Image, Gradient>;
317
318#[cfg(test)]
319mod tests {
320    use super::ImageSource;
321    use alloc::sync::Arc;
322
323    fn image_data(pixels: &[u8], alpha_type: peniko::ImageAlphaType) -> peniko::ImageData {
324        peniko::ImageData {
325            data: peniko::Blob::new(Arc::new(pixels.to_vec())),
326            format: peniko::ImageFormat::Rgba8,
327            alpha_type,
328            width: (pixels.len() / 4) as u32,
329            height: 1,
330        }
331    }
332
333    #[test]
334    fn from_peniko_image_data_computes_transparency_hint() {
335        for alpha_type in [
336            peniko::ImageAlphaType::Alpha,
337            peniko::ImageAlphaType::AlphaPremultiplied,
338        ] {
339            let opaque = image_data(&[10, 20, 30, 255, 40, 50, 60, 255], alpha_type);
340            assert!(!ImageSource::from_peniko_image_data(&opaque).may_have_transparency());
341
342            let translucent = image_data(&[10, 20, 30, 255, 40, 50, 60, 128], alpha_type);
343            assert!(ImageSource::from_peniko_image_data(&translucent).may_have_transparency());
344        }
345    }
346}