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