1//! COLRv1 glyphs, which swash can't paint: skrifa walks the paint graph and this paints it
2//! on the CPU, in premultiplied sRGB as browsers blend it, into an image the atlas takes as
3//! any colour glyph.
4
5use skrifa::{
6 FontRef, GlyphId, MetadataProvider,
7 color::{Brush, ColorGlyphFormat, ColorPainter, ColorStop, CompositeMode, Extend, Transform},
8 instance::{LocationRef, Size},
9 outline::{DrawSettings, OutlinePen},
10 raw::TableProvider,
11 raw::types::{BoundingBox, F2Dot14},
12};
13use swash::{
14 scale::image::{Content, Image},
15 zeno::{Command, Mask, Placement, Vector},
16};
17
18/// The colour image of glyph `glyph` at `size` pixels per em, moved right and up by
19/// `offset` pixels; `None` where the font has no COLRv1 paint for it.
20pub(super) fn paint(
21 data: &[u8],
22 index: u32,
23 glyph: u16,
24 size: f32,
25 coords: &[i16],
26 offset: [f32; 2],
27) -> Option<Image> {
28 let font = FontRef::from_index(data, index).ok()?;
29 let glyph = GlyphId::from(glyph);
30 let color = font
31 .color_glyphs()
32 .get_with_format(glyph, ColorGlyphFormat::ColrV1)?;
33 let coords: Vec<F2Dot14> = coords.iter().map(|&c| F2Dot14::from_bits(c)).collect();
34 let location = LocationRef::new(&coords);
35 let bounds = color
36 .bounding_box(location, Size::new(size))
37 .or_else(|| font.metrics(Size::new(size), location).bounds)?;
38 let left = (bounds.x_min + offset[0]).floor();
39 let top = (bounds.y_max + offset[1]).ceil();
40 let width = ((bounds.x_max + offset[0]).ceil() - left) as usize;
41 let height = (top - (bounds.y_min + offset[1]).floor()) as usize;
42 if width == 0 || height == 0 || width * height > 1 << 22 {
43 return None;
44 }
45 let scale = size / f32::from(font.head().ok()?.units_per_em());
46 let palette = font
47 .color_palettes()
48 .get(0)
49 .map(|palette| {
50 palette
51 .colors()
52 .iter()
53 .map(|c| [c.red, c.green, c.blue, c.alpha].map(|v| f32::from(v) / 255.0))
54 .collect()
55 })
56 .unwrap_or_default();
57 let mut painter = Painter {
58 font: &font,
59 location,
60 palette,
61 width,
62 height,
63 transforms: vec![Transform {
64 xx: scale,
65 yy: -scale,
66 dx: offset[0] - left,
67 dy: top - offset[1],
68 ..Transform::default()
69 }],
70 clips: vec![vec![1.0; width * height]],
71 layers: vec![(vec![[0.0; 4]; width * height], CompositeMode::SrcOver)],
72 };
73 color.paint(location, &mut painter).ok()?;
74 let (pixels, _) = painter.layers.pop()?;
75 Some(Image {
76 content: Content::Color,
77 placement: Placement {
78 left: left as i32,
79 top: top as i32,
80 width: width as u32,
81 height: height as u32,
82 },
83 data: pixels
84 .iter()
85 .flat_map(|&[r, g, b, a]| {
86 let straight = |v: f32| match a > 0.0 {
87 true => ((v / a).clamp(0.0, 1.0) * 255.0).round() as u8,
88 false => 0,
89 };
90 [
91 straight(r),
92 straight(g),
93 straight(b),
94 (a * 255.0).round() as u8,
95 ]
96 })
97 .collect(),
98 ..Image::default()
99 })
100}
101
102/// Paints into layers of premultiplied pixels; transforms map font units to pixels, y down.
103struct Painter<'a> {
104 font: &'a FontRef<'a>,
105 location: LocationRef<'a>,
106 palette: Vec<[f32; 4]>,
107 width: usize,
108 height: usize,
109 transforms: Vec<Transform>,
110 /// Coverage, each the intersection of those below it.
111 clips: Vec<Vec<f32>>,
112 layers: Vec<(Vec<[f32; 4]>, CompositeMode)>,
113}
114
115impl Painter<'_> {
116 fn transform(&self) -> Transform {
117 *self.transforms.last().expect("the pixel transform stays")
118 }
119
120 /// Narrows the clip to the path `commands` describes, in pixels.
121 fn clip(&mut self, commands: &[Command]) {
122 let mut coverage = vec![0; self.width * self.height];
123 Mask::new(commands)
124 .size(self.width as u32, self.height as u32)
125 .render_into(&mut coverage, None);
126 let clip = self.clips.last().expect("the whole image stays a clip");
127 let clip = clip
128 .iter()
129 .zip(coverage)
130 .map(|(outer, inner)| outer * f32::from(inner) / 255.0)
131 .collect();
132 self.clips.push(clip);
133 }
134
135 /// The premultiplied colour of palette entry `index` at `alpha`; 0xFFFF, the text's
136 /// colour, is black, as colour glyphs aren't tinted.
137 fn color(&self, index: u16, alpha: f32) -> [f32; 4] {
138 let [r, g, b, a] = match index {
139 0xFFFF => [0.0, 0.0, 0.0, 1.0],
140 _ => self
141 .palette
142 .get(usize::from(index))
143 .copied()
144 .unwrap_or_default(),
145 };
146 let a = a * alpha;
147 [r * a, g * a, b * a, a]
148 }
149}
150
151impl ColorPainter for Painter<'_> {
152 fn push_transform(&mut self, transform: Transform) {
153 self.transforms.push(self.transform() * transform);
154 }
155
156 fn pop_transform(&mut self) {
157 if self.transforms.len() > 1 {
158 self.transforms.pop();
159 }
160 }
161
162 fn push_clip_glyph(&mut self, glyph: GlyphId) {
163 let mut pen = Pen {
164 transform: self.transform(),
165 commands: Vec::new(),
166 };
167 if let Some(outline) = self.font.outline_glyphs().get(glyph) {
168 let _ = outline.draw(
169 DrawSettings::unhinted(Size::unscaled(), self.location),
170 &mut pen,
171 );
172 }
173 self.clip(&pen.commands);
174 }
175
176 fn push_clip_box(&mut self, clip: BoundingBox<f32>) {
177 let mut pen = Pen {
178 transform: self.transform(),
179 commands: Vec::new(),
180 };
181 pen.move_to(clip.x_min, clip.y_min);
182 pen.line_to(clip.x_max, clip.y_min);
183 pen.line_to(clip.x_max, clip.y_max);
184 pen.line_to(clip.x_min, clip.y_max);
185 pen.close();
186 self.clip(&pen.commands);
187 }
188
189 fn pop_clip(&mut self) {
190 if self.clips.len() > 1 {
191 self.clips.pop();
192 }
193 }
194
195 fn fill(&mut self, brush: Brush<'_>) {
196 let shade = Shade::new(self, brush);
197 let inverse = invert(self.transform());
198 let clip = self.clips.last().expect("the whole image stays a clip");
199 let (layer, _) = self.layers.last_mut().expect("the image stays a layer");
200 for (i, (pixel, &coverage)) in layer.iter_mut().zip(clip).enumerate() {
201 if coverage <= 0.0 {
202 continue;
203 }
204 let (x, y) = ((i % self.width) as f32 + 0.5, (i / self.width) as f32 + 0.5);
205 let Some(source) = shade.at(inverse.transform(x, y)) else {
206 continue;
207 };
208 let behind = 1.0 - source[3] * coverage;
209 *pixel = std::array::from_fn(|c| source[c] * coverage + pixel[c] * behind);
210 }
211 }
212
213 fn push_layer(&mut self, mode: CompositeMode) {
214 self.layers
215 .push((vec![[0.0; 4]; self.width * self.height], mode));
216 }
217
218 fn pop_layer(&mut self) {
219 if self.layers.len() < 2 {
220 return;
221 }
222 let (source, mode) = self.layers.pop().expect("checked above");
223 let (backdrop, _) = self.layers.last_mut().expect("checked above");
224 for (b, s) in backdrop.iter_mut().zip(source) {
225 *b = composite(mode, s, *b);
226 }
227 }
228}
229
230/// `source` over `backdrop` in `mode`, both premultiplied.
231fn composite(mode: CompositeMode, s: [f32; 4], b: [f32; 4]) -> [f32; 4] {
232 use CompositeMode::*;
233 let (sa, ba) = (s[3], b[3]);
234 let porter_duff = |fs: f32, fb: f32| std::array::from_fn(|c| s[c] * fs + b[c] * fb);
235 // W3C Compositing: a separable blend of unpremultiplied channels, then source-over.
236 let blend = |f: fn(f32, f32) -> f32| {
237 let mut out: [f32; 4] = std::array::from_fn(|c| {
238 let mixed = match sa > 0.0 && ba > 0.0 {
239 true => sa * ba * f(s[c] / sa, b[c] / ba),
240 false => 0.0,
241 };
242 s[c] * (1.0 - ba) + b[c] * (1.0 - sa) + mixed
243 });
244 out[3] = sa + ba - sa * ba;
245 out
246 };
247 match mode {
248 Clear => [0.0; 4],
249 Src => s,
250 Dest => b,
251 DestOver => porter_duff(1.0 - ba, 1.0),
252 SrcIn => porter_duff(ba, 0.0),
253 DestIn => porter_duff(0.0, sa),
254 SrcOut => porter_duff(1.0 - ba, 0.0),
255 DestOut => porter_duff(0.0, 1.0 - sa),
256 SrcAtop => porter_duff(ba, 1.0 - sa),
257 DestAtop => porter_duff(1.0 - ba, sa),
258 Xor => porter_duff(1.0 - ba, 1.0 - sa),
259 Plus => std::array::from_fn(|c| (s[c] + b[c]).min(1.0)),
260 Screen => blend(|s, b| s + b - s * b),
261 Multiply => blend(|s, b| s * b),
262 Darken => blend(f32::min),
263 Lighten => blend(f32::max),
264 Difference => blend(|s, b| (s - b).abs()),
265 Exclusion => blend(|s, b| s + b - 2.0 * s * b),
266 Overlay => blend(|s, b| hard_light(b, s)),
267 HardLight => blend(hard_light),
268 SoftLight => blend(|s, b| {
269 if s <= 0.5 {
270 b - (1.0 - 2.0 * s) * b * (1.0 - b)
271 } else {
272 let d = match b <= 0.25 {
273 true => ((16.0 * b - 12.0) * b + 4.0) * b,
274 false => b.sqrt(),
275 };
276 b + (2.0 * s - 1.0) * (d - b)
277 }
278 }),
279 ColorDodge => blend(|s, b| match s < 1.0 {
280 true => (b / (1.0 - s)).min(1.0),
281 false => f32::from(b > 0.0),
282 }),
283 ColorBurn => blend(|s, b| match s > 0.0 {
284 true => 1.0 - ((1.0 - b) / s).min(1.0),
285 false => f32::from(b >= 1.0),
286 }),
287 // The HSL modes, rare in emoji, and unknown modes paint as source-over.
288 _ => porter_duff(1.0, 1.0 - sa),
289 }
290}
291
292fn hard_light(s: f32, b: f32) -> f32 {
293 match s <= 0.5 {
294 true => b * 2.0 * s,
295 false => {
296 let s = 2.0 * s - 1.0;
297 b + s - b * s
298 }
299 }
300}
301
302fn invert(m: Transform) -> Transform {
303 let det = m.xx * m.yy - m.xy * m.yx;
304 if det.abs() < f32::EPSILON {
305 return Transform {
306 xx: 0.0,
307 yy: 0.0,
308 ..Transform::default()
309 };
310 }
311 let (xx, xy, yx, yy) = (m.yy / det, -m.xy / det, -m.yx / det, m.xx / det);
312 Transform {
313 xx,
314 yx,
315 xy,
316 yy,
317 dx: -(xx * m.dx + xy * m.dy),
318 dy: -(yx * m.dx + yy * m.dy),
319 }
320}
321
322/// A brush resolved against the palette, evaluated at points in its own space.
323enum Shade {
324 Solid([f32; 4]),
325 Linear {
326 p0: [f32; 2],
327 along: [f32; 2],
328 line: Line,
329 },
330 Radial {
331 c0: [f32; 2],
332 r0: f32,
333 cd: [f32; 2],
334 dr: f32,
335 line: Line,
336 },
337 Sweep {
338 c0: [f32; 2],
339 start: f32,
340 span: f32,
341 line: Line,
342 },
343}
344
345impl Shade {
346 fn new(painter: &Painter, brush: Brush<'_>) -> Self {
347 let line = |stops: &[ColorStop], extend| Line {
348 stops: {
349 let mut stops: Vec<(f32, [f32; 4])> = stops
350 .iter()
351 .map(|s| (s.offset, painter.color(s.palette_index, s.alpha)))
352 .collect();
353 stops.sort_by(|a, b| a.0.total_cmp(&b.0));
354 stops
355 },
356 extend,
357 };
358 match brush {
359 Brush::Solid {
360 palette_index,
361 alpha,
362 } => Shade::Solid(painter.color(palette_index, alpha)),
363 Brush::LinearGradient {
364 p0,
365 p1,
366 color_stops,
367 extend,
368 } => {
369 let d = [p1.x - p0.x, p1.y - p0.y];
370 let length = d[0] * d[0] + d[1] * d[1];
371 Shade::Linear {
372 p0: [p0.x, p0.y],
373 along: match length > 0.0 {
374 true => d.map(|v| v / length),
375 false => [0.0; 2],
376 },
377 line: line(color_stops, extend),
378 }
379 }
380 Brush::RadialGradient {
381 c0,
382 r0,
383 c1,
384 r1,
385 color_stops,
386 extend,
387 } => Shade::Radial {
388 c0: [c0.x, c0.y],
389 r0,
390 cd: [c1.x - c0.x, c1.y - c0.y],
391 dr: r1 - r0,
392 line: line(color_stops, extend),
393 },
394 Brush::SweepGradient {
395 c0,
396 start_angle,
397 end_angle,
398 color_stops,
399 extend,
400 } => Shade::Sweep {
401 c0: [c0.x, c0.y],
402 start: start_angle,
403 span: end_angle - start_angle,
404 line: line(color_stops, extend),
405 },
406 }
407 }
408
409 fn at(&self, (x, y): (f32, f32)) -> Option<[f32; 4]> {
410 match self {
411 Shade::Solid(color) => Some(*color),
412 Shade::Linear { p0, along, line } => {
413 Some(line.at((x - p0[0]) * along[0] + (y - p0[1]) * along[1]))
414 }
415 Shade::Radial {
416 c0,
417 r0,
418 cd,
419 dr,
420 line,
421 } => {
422 // The greatest t whose circle, of radius r0 + t·dr ≥ 0, passes through the
423 // point: a two-point conical gradient, as Skia and the spec draw it.
424 let p = [x - c0[0], y - c0[1]];
425 let a = cd[0] * cd[0] + cd[1] * cd[1] - dr * dr;
426 let b = p[0] * cd[0] + p[1] * cd[1] + r0 * dr;
427 let c = p[0] * p[0] + p[1] * p[1] - r0 * r0;
428 let radius = |t: f32| r0 + t * dr >= 0.0;
429 let t = if a.abs() < 1e-6 {
430 Some(c / (2.0 * b)).filter(|t| t.is_finite() && radius(*t))
431 } else {
432 let discriminant = b * b - a * c;
433 (discriminant >= 0.0)
434 .then(|| {
435 let root = discriminant.sqrt();
436 let (t1, t2) = ((b + root) / a, (b - root) / a);
437 let (high, low) = (t1.max(t2), t1.min(t2));
438 [high, low].into_iter().find(|t| radius(*t))
439 })
440 .flatten()
441 };
442 t.map(|t| line.at(t))
443 }
444 Shade::Sweep {
445 c0,
446 start,
447 span,
448 line,
449 } => {
450 let angle = (y - c0[1]).atan2(x - c0[0]).to_degrees().rem_euclid(360.0);
451 (*span != 0.0).then(|| line.at((angle - start) / span))
452 }
453 }
454 }
455}
456
457/// A colour line: stops by offset, extended past 0 and 1 by `extend`.
458struct Line {
459 stops: Vec<(f32, [f32; 4])>,
460 extend: Extend,
461}
462
463impl Line {
464 fn at(&self, t: f32) -> [f32; 4] {
465 let t = match self.extend {
466 Extend::Repeat => t.rem_euclid(1.0),
467 Extend::Reflect => 1.0 - (t.rem_euclid(2.0) - 1.0).abs(),
468 _ => t,
469 };
470 let Some(first) = self.stops.first() else {
471 return [0.0; 4];
472 };
473 if t <= first.0 {
474 return first.1;
475 }
476 for pair in self.stops.windows(2) {
477 let ((t0, c0), (t1, c1)) = (pair[0], pair[1]);
478 if t <= t1 {
479 let f = match t1 > t0 {
480 true => (t - t0) / (t1 - t0),
481 false => 1.0,
482 };
483 return std::array::from_fn(|c| c0[c] + (c1[c] - c0[c]) * f);
484 }
485 }
486 self.stops.last().expect("checked above").1
487 }
488}
489
490/// Collects an outline as pixel-space path commands.
491struct Pen {
492 transform: Transform,
493 commands: Vec<Command>,
494}
495
496impl Pen {
497 fn point(&self, x: f32, y: f32) -> Vector {
498 let (x, y) = self.transform.transform(x, y);
499 Vector::new(x, y)
500 }
501}
502
503impl OutlinePen for Pen {
504 fn move_to(&mut self, x: f32, y: f32) {
505 self.commands.push(Command::MoveTo(self.point(x, y)));
506 }
507
508 fn line_to(&mut self, x: f32, y: f32) {
509 self.commands.push(Command::LineTo(self.point(x, y)));
510 }
511
512 fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
513 self.commands
514 .push(Command::QuadTo(self.point(cx0, cy0), self.point(x, y)));
515 }
516
517 fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
518 self.commands.push(Command::CurveTo(
519 self.point(cx0, cy0),
520 self.point(cx1, cy1),
521 self.point(x, y),
522 ));
523 }
524
525 fn close(&mut self) {
526 self.commands.push(Command::Close);
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 #[test]
533 fn colrv1_glyphs_paint_their_gradients_in_colour() {
534 // Noto Color Emoji's 😀 alone (assets/NotoColorEmoji-OFL.txt).
535 let font = include_bytes!("../../assets/NotoColorEmoji-Grinning.ttf");
536 let image = super::paint(font, 0, 1, 64.0, &[], [0.0, 0.0]).expect("a COLRv1 glyph");
537 assert_eq!(image.content, swash::scale::image::Content::Color);
538 let p = image.placement;
539 let pixel = |x: u32, y: u32| {
540 let i = ((y * p.width + x) * 4) as usize;
541 [0, 1, 2, 3].map(|c| image.data[i + c])
542 };
543 // The face's radial gradient: yellow, opaque, lighter above than below.
544 let [r, g, b, a] = pixel(p.width / 2, p.height / 6);
545 assert!(
546 a == 255 && r > 240 && g > 180 && b < 100,
547 "{:?}",
548 [r, g, b, a]
549 );
550 assert!(pixel(p.width / 2, p.height / 6)[1] > pixel(p.width / 8, p.height * 3 / 5)[1]);
551 assert_eq!(pixel(0, 0)[3], 0, "outside the face is clear");
552 }
553}