//! COLR colour glyphs (v0 layers, v1 paint graphs): read into a tree of document nodes in font //! units (y down, as outlines are), which the renderer, the exporters and Create Outlines draw and //! keep as they do any art. A glyph whose paints VectorCraft has no form for is drawn into an image //! instead, as bitmap glyphs are ([`GlyphImage`]); [`Tree::unsupported`] names what made it one. use std::sync::Arc; use kurbo::{Affine, BezPath, Point, Rect, Shape, Vec2}; use skrifa::color::{Brush, ColorGlyph, ColorGlyphCollection, ColorPainter, ColorPalettes, ColorStop, CompositeMode, Extend, Transform}; use skrifa::instance::{LocationRef, Size}; use skrifa::outline::DrawSettings; use skrifa::raw::TableProvider; use skrifa::raw::types::BoundingBox; use skrifa::{GlyphId, MetadataProvider}; use vectorcraft_doc::color::{BlendMode, Color, Gradient, GradientGeom, GradientKind, GradientPaint, GradientSpread, GradientStop, Paint}; use vectorcraft_doc::{Appearance, FillLayer, ImageBlob, Node, NodeId, NodeKind}; use vectorcraft_geom::PathData; use vello_cpu::peniko; use crate::GlyphImage; use crate::fontdb::FlipPen; /// The most nodes a colour glyph's tree may have: past it, a font's glyph draws its outline. const MAX_COLR_NODES: usize = 4096; /// The most path elements the clips and fills of a colour glyph's tree may hold in all: layers /// that repeat one large outline each hold a copy of it. const MAX_COLR_POINTS: usize = 1 << 20; /// The deepest the groups of a colour glyph may nest. const MAX_COLR_DEPTH: usize = 64; /// The pixels per em an image of a colour glyph is drawn at. const FALLBACK_PPEM: f64 = 256.0; /// The largest side of such an image, in pixels. const MAX_FALLBACK_SIDE: f64 = 1024.0; /// The id of the layers' nodes: none, as the art exporters make on the fly has (Create Outlines /// gives them ids of the document's). const NO_ID: NodeId = NodeId(u64::MAX); /// Font units y up → y down. const FLIP: Affine = Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, 0.0]); /// A colour glyph as VectorCraft draws it. #[derive(Clone)] pub(crate) enum ColourContent { /// Its layers as document nodes in font units (y down): a group. Art(Arc), /// Its picture, for paints VectorCraft has no form for. Image(Arc), } /// Does `font` have COLR colour glyphs? pub(crate) fn has_colour_layers(font: &skrifa::FontRef<'_>) -> bool { font.colr().is_ok() } /// Does glyph `gid` of `font` have COLR layers? pub(crate) fn has_colour_glyph(font: &skrifa::FontRef<'_>, gid: u32) -> bool { ColorGlyphCollection::new(font).get(GlyphId::new(gid)).is_some() } /// Glyph `gid`'s colour content in `font` at `location`, palette 0's colours and `foreground` for /// palette index 0xFFFF, and whether it used `foreground`; `None` when it isn't a colour glyph, or /// its graph doesn't read (a cycle, too deep, too many layers). pub(crate) fn colour_glyph( font: &skrifa::FontRef<'_>, location: LocationRef<'_>, upem: f64, gid: u32, foreground: Color, ) -> (Option, bool) { let Some(glyph) = ColorGlyphCollection::new(font).get(GlyphId::new(gid)) else { return (None, false) }; let mut tree = Tree::new(font, location, foreground); if glyph.paint(location, &mut tree).is_err() { return (None, false); } let used = tree.foreground_used; if tree.unsupported.is_some() { // What it paints, y up, for a glyph without a clip box. let painted = tree.painted.map(|b| FLIP.transform_rect_bbox(b)); return (raster(&glyph, font, location, upem, foreground, painted).map(|i| ColourContent::Image(Arc::new(i))), used); } (tree.finish().map(ColourContent::Art), used) } /// Palette 0's colours with their alpha. fn palette(font: &skrifa::FontRef<'_>) -> Vec<(Color, f32)> { let palettes = ColorPalettes::new(font); let Some(palette) = palettes.get(0) else { return vec![] }; palette.colors().iter().map(|c| (Color::rgb8(c.red, c.green, c.blue), f32::from(c.alpha) / 255.0)).collect() } /// The colour and alpha of palette entry `index` (0xFFFF: `foreground`), times `alpha`; `None` /// for an entry the palette doesn't have. fn colour(palette: &[(Color, f32)], foreground: Color, index: u16, alpha: f32) -> Option<(Color, f32)> { let (c, a) = if index == 0xFFFF { (foreground, 1.0) } else { *palette.get(usize::from(index))? }; Some((c, (a * alpha).clamp(0.0, 1.0))) } fn affine(t: &Transform) -> Affine { Affine::new([t.xx, t.yx, t.xy, t.yy, t.dx, t.dy].map(f64::from)) } fn point(p: skrifa::raw::types::Point) -> Point { Point::new(f64::from(p.x), f64::from(p.y)) } fn box_path(b: &BoundingBox) -> BezPath { Rect::new(f64::from(b.x_min), f64::from(b.y_min), f64::from(b.x_max), f64::from(b.y_max)).to_path(0.1) } /// Glyph `gid`'s outline in font units, y up (the font database's pen draws y down, as text /// space has it). fn outline(font: &skrifa::FontRef<'_>, location: LocationRef<'_>, gid: GlyphId) -> BezPath { let mut pen = FlipPen(BezPath::new()); if let Some(g) = font.outline_glyphs().get(gid) { let _ = g.draw(DrawSettings::unhinted(Size::unscaled(), location), &mut pen); } let mut path = pen.0; path.apply_affine(FLIP); path } /// A group being built: its clipping path, or the blend mode of a composite's layer. struct Open { clip: Option, blend: Option, children: Vec>, /// A fill filled the clip's whole area: alone, it is the clip filled. flooded: bool, } /// A [`ColorPainter`] building a colour glyph's tree. struct Tree<'a> { font: &'a skrifa::FontRef<'a>, location: LocationRef<'a>, palette: Vec<(Color, f32)>, foreground: Color, /// Font units y up → the tree's space (y down), with the paint graph's transforms. transforms: Vec, groups: Vec, nodes: usize, /// The path elements of the clips and fills so far. points: usize, /// What VectorCraft has no form for, the first one met: the glyph becomes an image. unsupported: Option<&'static str>, /// The graph is past [`MAX_COLR_NODES`] or [`MAX_COLR_DEPTH`]. too_large: bool, /// The box of what it paints, in the tree's space. painted: Option, /// It paints with the foreground colour (palette index 0xFFFF). foreground_used: bool, } impl<'a> Tree<'a> { fn new(font: &'a skrifa::FontRef<'a>, location: LocationRef<'a>, foreground: Color) -> Self { let root = Open { clip: None, blend: Some(BlendMode::Normal), children: vec![], flooded: false }; Tree { font, location, palette: palette(font), foreground, transforms: vec![FLIP], groups: vec![root], nodes: 0, points: 0, unsupported: None, too_large: false, painted: None, foreground_used: false, } } /// Glyph `gid`'s outline in the current transform; empty once the glyph is over budget, so /// the layers that follow cost nothing (a compact layer list can name one big outline often). fn layer_outline(&mut self, gid: GlyphId) -> BezPath { if self.too_large { return BezPath::new(); } let mut path = outline(self.font, self.location, gid); path.apply_affine(self.transform()); path } fn transform(&self) -> Affine { self.transforms.last().copied().unwrap_or(FLIP) } fn add(&mut self, node: Node) { self.nodes += 1; self.too_large |= self.nodes > MAX_COLR_NODES; if let Some(g) = self.groups.last_mut().filter(|_| !self.too_large) { g.children.push(Arc::new(node)); } } /// Count `path` against [`MAX_COLR_POINTS`]: whether it may be kept. fn charge(&mut self, path: &BezPath) -> bool { self.points = self.points.saturating_add(path.elements().len()); self.too_large |= self.points > MAX_COLR_POINTS; !self.too_large } fn open(&mut self, group: Open) { self.too_large |= self.groups.len() >= MAX_COLR_DEPTH; self.groups.push(group); } /// Closes the innermost group into its parent: a clip with only the fill it clips is that /// filled path; an empty group goes. fn close(&mut self) { // The root stays open until `finish`. if self.groups.len() < 2 { return; } let Some(g) = self.groups.pop() else { return }; if g.children.is_empty() { return; } let mut children = g.children; if let Some(clip) = g.clip { if let ([only], true) = (children.as_slice(), g.flooded) { let only = only.clone(); return self.add_arc(only); } let mut path = Node::path(NO_ID, PathData::from_bezpath(&clip), Appearance::default()); if let NodeKind::Path { clipping, .. } = &mut path.kind { *clipping = true; } children.insert(0, Arc::new(path)); let mut group = Node::group(NO_ID, children); if let NodeKind::Group { clip, .. } = &mut group.kind { *clip = true; } return self.add(group); } let mut group = Node::group(NO_ID, children); group.isolate = true; group.blend = g.blend.unwrap_or(BlendMode::Normal); self.add(group); } fn add_arc(&mut self, node: Arc) { if let Some(g) = self.groups.last_mut() { g.children.push(node); } } /// The tree: the glyph as a group. It needs no isolation of its own: blend modes only come /// with composites, whose groups are isolated, so they never reach the art behind the type. fn finish(mut self) -> Option> { if self.too_large { return None; } while self.groups.len() > 1 { self.close(); } let root = self.groups.pop()?; if root.children.is_empty() { return None; } Some(Arc::new(Node::group(NO_ID, root.children))) } /// `brush` as a VectorCraft paint and opacity, in the tree's space; `None` with /// [`Self::unsupported`] set when it has no form there. fn paint(&mut self, brush: &Brush<'_>) -> Option<(Paint, f32)> { self.foreground_used |= match brush { Brush::Solid { palette_index, .. } => *palette_index == 0xFFFF, Brush::LinearGradient { color_stops, .. } | Brush::RadialGradient { color_stops, .. } | Brush::SweepGradient { color_stops, .. } => { color_stops.iter().any(|s| s.palette_index == 0xFFFF) } }; let xf = self.transform(); let stops = |stops: &[ColorStop], tree: &Self| -> Vec { stops .iter() .filter_map(|s| { let (color, opacity) = colour(&tree.palette, tree.foreground, s.palette_index, s.alpha)?; Some(GradientStop { opacity, ..GradientStop::new(s.offset, color) }) }) .collect() }; let gradient = |kind, stops: Vec, spread, geom| { // COLR repeats the interval from the first stop to the last, and one run of the // stops spans the gradient's vector. let mut paint = GradientPaint::new(Gradient { kind, stops, spread, ..Gradient::default() }); paint.geom = Some(geom); Paint::Gradient(Box::new(paint)) }; match brush { Brush::Solid { palette_index, alpha } => { let (c, a) = colour(&self.palette, self.foreground, *palette_index, *alpha)?; Some((Paint::solid(c), a)) } Brush::LinearGradient { p0, p1, color_stops, extend } => { let spread = spread(*extend); let (p0, p1) = (point(*p0), point(*p1)); let stops = stops(color_stops, self); // Without a stop, the others would repeat over a different interval. if !spread.is_pad() && stops.len() < color_stops.len() { self.unsupported.get_or_insert("a repeating or reflecting gradient with a stop missing from the palette"); return None; } let (stops, first, last) = normalized(stops)?; // Stops past 0 and 1 move the ends instead. let mut geom = GradientGeom { start: p0.lerp(p1, f64::from(first)), end: p0.lerp(p1, f64::from(last)), aspect: 1.0, focal: None }; geom.transform(xf, GradientKind::Linear); Some((gradient(GradientKind::Linear, stops, spread, geom), 1.0)) } Brush::RadialGradient { c0, r0, c1, r1, color_stops, extend } => { let spread = spread(*extend); let line = stops(color_stops, self); // Without a stop, the others would repeat over a different interval. if !spread.is_pad() && line.len() < color_stops.len() { self.unsupported.get_or_insert("a repeating or reflecting gradient with a stop missing from the palette"); return None; } let (c0, r0, c1, r1, mut stops) = non_negative_radii(point(*c0), f64::from(*r0), point(*c1), f64::from(*r1), line, *extend)?; let concentric = (c0 - c1).hypot() < 1e-6; if (r0 > 1e-6 && !concentric) || r1 <= r0 || (c0 - c1).hypot() >= r1 { self.unsupported.get_or_insert("a radial gradient from a circle"); return None; } if stops.iter().any(|s| !(0.0..=1.0).contains(&s.offset)) { self.unsupported.get_or_insert("a radial gradient with stops past its circles"); return None; } if !spread.is_pad() { // A run of the stops is the whole way from the center to the circle: one that // starts at a circle (a first stop past the center comes as one) repeats over // less of it. The normalized ends can be a rounding error off 0 and 1. let (lo, hi) = stops.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), s| (lo.min(s.offset), hi.max(s.offset))); if r0.abs() > 1e-6 || lo.abs() > 1e-4 || (hi - 1.0).abs() > 1e-4 { self.unsupported.get_or_insert("a repeating or reflecting radial gradient that doesn't start at its center"); return None; } } // A start circle around the centre starts the stops further out. for s in &mut stops { s.offset = ((r0 + f64::from(s.offset) * (r1 - r0)) / r1) as f32; } let mut geom = GradientGeom { start: c1, end: c1 + Vec2::new(r1, 0.0), aspect: 1.0, focal: (!concentric).then_some(c0) }; geom.transform(xf, GradientKind::Radial); Some((gradient(GradientKind::Radial, stops, spread, geom), 1.0)) } Brush::SweepGradient { .. } => { self.unsupported.get_or_insert("a sweep gradient"); None } } } /// `path` (in the tree's space) filled with `brush` (in the current transform's). fn fill_path(&mut self, path: &BezPath, brush: &Brush<'_>) { if !self.charge(path) { return; } let b = path.bounding_box(); self.painted = Some(self.painted.map_or(b, |p| p.union(b))); let Some((paint, opacity)) = self.paint(brush) else { return }; let fill = vectorcraft_doc::AppearanceItem::Fill(FillLayer { opacity, ..FillLayer::new(paint) }); self.add(Node::path(NO_ID, PathData::from_bezpath(path), Appearance { items: vec![fill], ..Appearance::default() })); } } /// A COLR extend mode as the gradient's spread; an unknown one pads, as the COLR specification /// says. fn spread(extend: Extend) -> GradientSpread { match extend { Extend::Repeat => GradientSpread::Repeat, Extend::Reflect => GradientSpread::Reflect, _ => GradientSpread::Pad, } } /// `stops` sorted, from 0 to 1 (as the gradient's ends then move), and where the first and last /// were; `None` without stops. fn normalized(mut stops: Vec) -> Option<(Vec, f32, f32)> { stops.sort_by(|a, b| a.offset.total_cmp(&b.offset)); let (first, last) = (stops.first()?.offset, stops.last()?.offset); let span = last - first; for s in &mut stops { s.offset = if span > 0.0 { (s.offset - first) / span } else { 0.0 }; } Some((stops, first, if span > 0.0 { last } else { first + 1.0 })) } /// A radial gradient's circles (`c0`, `r0`) and (`c1`, `r1`) and color line `stops` (0 and 1 at /// the circles) recast so that neither circle has a negative radius, as normalizing a color line /// to 0..1 can give. Circles of a negative radius are not drawn, only those on the side of the /// circle of radius 0 (the tip of the cone) where the radius is positive. With pad, the stops on /// the other side become one stop at the tip with the color there; with repeat and reflect, the /// circles move along by whole periods of the color line to the drawn side /// (https://learn.microsoft.com/en-us/typography/opentype/spec/colr, Radial gradients). `None` /// when no circle is drawn. fn non_negative_radii( c0: Point, r0: f64, c1: Point, r1: f64, mut stops: Vec, extend: Extend, ) -> Option<(Point, f64, Point, f64, Vec)> { if (r0 >= 0.0 && r1 >= 0.0) || stops.is_empty() { return Some((c0, r0, c1, r1, stops)); } if r0 == r1 { return None; } stops.sort_by(|a, b| a.offset.total_cmp(&b.offset)); let tip = r0 / (r0 - r1); let grows = r1 > r0; let (from, to, stops) = match extend { Extend::Repeat | Extend::Reflect => { // A reflected color line repeats every 2: once forward, then back. let (period, stops) = if extend == Extend::Reflect { let forward = stops.iter().map(|s| GradientStop { offset: s.offset / 2.0, ..s.clone() }); let back = stops.iter().rev().map(|s| GradientStop { offset: 1.0 - s.offset / 2.0, ..s.clone() }); (2.0, forward.chain(back).collect()) } else { (1.0, stops) }; let from = if grows { (tip / period).ceil() * period } else { (tip / period).floor() * period - period }; (from, from + period, stops) } _ => { // The color line runs from 0 to 1, and further where its stops do. let first = stops.first().map_or(0.0, |s| f64::from(s.offset).min(0.0)); let last = stops.last().map_or(1.0, |s| f64::from(s.offset).max(1.0)); let (from, to) = match (grows, tip) { (true, t) if t < last => (t, last), (true, t) => (t, t + 1.0), (false, t) if t > first => (first, t), (false, t) => (t - 1.0, t), }; let line = Gradient { stops, ..Gradient::default() }; // The color at `w`, as a stop at `offset` of the new line. let at = |w: f64, offset: f32| { let (color, opacity) = line.sample(w as f32); GradientStop { opacity, ..GradientStop::new(offset, color) } }; // Stops at the tip stay on the drawn side of its stop, so a sharp change of color there // keeps the color on that side. let mut kept: Vec = line .stops .iter() .filter(|s| (from - 1e-6..=to + 1e-6).contains(&f64::from(s.offset))) .map(|s| GradientStop { offset: ((f64::from(s.offset) - from) / (to - from)).clamp(0.0, 1.0) as f32, ..s.clone() }) .collect(); if grows { kept.insert(0, at(tip, 0.0)); if kept.last().is_none_or(|s| s.offset < 1.0) { kept.push(at(to, 1.0)); } } else { kept.push(at(tip, 1.0)); if kept.first().is_none_or(|s| s.offset > 0.0) { kept.insert(0, at(from, 0.0)); } } (from, to, kept) } }; let circle = |w: f64| (c0.lerp(c1, w), (r0 + w * (r1 - r0)).max(0.0)); let ((c0, r0), (c1, r1)) = (circle(from), circle(to)); Some((c0, r0, c1, r1, stops)) } impl ColorPainter for Tree<'_> { fn push_transform(&mut self, transform: Transform) { let t = self.transform() * affine(&transform); self.transforms.push(t); } fn pop_transform(&mut self) { if self.transforms.len() > 1 { self.transforms.pop(); } } fn push_clip_glyph(&mut self, glyph_id: GlyphId) { let clip = self.layer_outline(glyph_id); self.charge(&clip); self.open(Open { clip: Some(clip), blend: None, children: vec![], flooded: false }); } fn push_clip_box(&mut self, clip_box: BoundingBox) { let mut clip = box_path(&clip_box); clip.apply_affine(self.transform()); self.open(Open { clip: Some(clip), blend: None, children: vec![], flooded: false }); } fn pop_clip(&mut self) { self.close(); } /// A fill fills the innermost clip's shape (a layer between them stays around it). fn fill(&mut self, brush: Brush<'_>) { let Some(at) = self.groups.iter().rposition(|g| g.clip.is_some()) else { self.unsupported.get_or_insert("a fill outside any glyph"); return; }; let Some(clip) = self.groups.get(at).and_then(|g| g.clip.clone()) else { return }; if at + 1 == self.groups.len() && let Some(g) = self.groups.last_mut() { g.flooded = g.children.is_empty(); } self.fill_path(&clip, &brush); } /// The glyph's outline filled, the brush moved by `brush_transform` (the glyph stays). fn fill_glyph(&mut self, glyph_id: GlyphId, brush_transform: Option, brush: Brush<'_>) { let path = self.layer_outline(glyph_id); let brush_xf = self.transform() * brush_transform.as_ref().map_or(Affine::IDENTITY, affine); self.transforms.push(brush_xf); self.fill_path(&path, &brush); self.transforms.pop(); } fn push_layer(&mut self, mode: CompositeMode) { let blend = blend_mode(mode); if blend.is_none() { self.unsupported.get_or_insert("a compositing mode other than blending"); } self.open(Open { clip: None, blend, children: vec![], flooded: false }); } fn pop_layer(&mut self) { self.close(); } } /// COLRv1's blend composite modes as VectorCraft's (`SrcOver` is Normal); `None` for the /// Porter-Duff modes, which it has no form for. fn blend_mode(mode: CompositeMode) -> Option { Some(match mode { CompositeMode::SrcOver => BlendMode::Normal, CompositeMode::Screen => BlendMode::Screen, CompositeMode::Overlay => BlendMode::Overlay, CompositeMode::Darken => BlendMode::Darken, CompositeMode::Lighten => BlendMode::Lighten, CompositeMode::ColorDodge => BlendMode::ColorDodge, CompositeMode::ColorBurn => BlendMode::ColorBurn, CompositeMode::HardLight => BlendMode::HardLight, CompositeMode::SoftLight => BlendMode::SoftLight, CompositeMode::Difference => BlendMode::Difference, CompositeMode::Exclusion => BlendMode::Exclusion, CompositeMode::Multiply => BlendMode::Multiply, CompositeMode::HslHue => BlendMode::Hue, CompositeMode::HslSaturation => BlendMode::Saturation, CompositeMode::HslColor => BlendMode::Color, CompositeMode::HslLuminosity => BlendMode::Luminosity, _ => return None, }) } /// Colour glyph `glyph` drawn into an image at [`FALLBACK_PPEM`], placed in font units (y down) /// as bitmap glyphs are, in its clip box or else the box of what it `painted` (y up); `None` when /// it paints nothing or doesn't draw. fn raster( glyph: &ColorGlyph<'_>, font: &skrifa::FontRef<'_>, location: LocationRef<'_>, upem: f64, foreground: Color, painted: Option, ) -> Option { let b = glyph.bounding_box(location, Size::unscaled()).map(|b| Rect::new(b.x_min.into(), b.y_min.into(), b.x_max.into(), b.y_max.into())); // Without a clip box, the box of what it paints (`painted`, y up). let b = b.or(painted).filter(|b| b.area() > 0.0)?; let s = (FALLBACK_PPEM / upem.max(1.0)).min(MAX_FALLBACK_SIDE / b.width().max(b.height())); if !(s.is_finite() && s > 0.0) { return None; } let (w, h) = ((b.width() * s).ceil() as u16, (b.height() * s).ceil() as u16); if w == 0 || h == 0 { return None; } // Font units y up → pixels. let to_pixels = Affine::new([s, 0.0, 0.0, -s, -b.x0 * s, b.y1 * s]); let mut ctx = vello_cpu::RenderContext::new(w, h); let mut painter = Pixels { ctx: &mut ctx, font, location, palette: palette(font), foreground, transforms: vec![to_pixels], width: w, height: h }; glyph.paint(location, &mut painter).ok()?; ctx.flush(); let mut pm = vello_cpu::Pixmap::new(w, h); ctx.render(&mut pm, &mut vello_cpu::Resources::new()); let png = pm.into_png().ok()?; let xf = Affine::new([1.0 / s, 0.0, 0.0, 1.0 / s, b.x0, -b.y1]); Some(GlyphImage { blob: ImageBlob::png(png), width: u32::from(w), height: u32::from(h), xf }) } /// A [`ColorPainter`] drawing a colour glyph with vello_cpu. struct Pixels<'a, 'c> { ctx: &'c mut vello_cpu::RenderContext, font: &'a skrifa::FontRef<'a>, location: LocationRef<'a>, palette: Vec<(Color, f32)>, foreground: Color, transforms: Vec, width: u16, height: u16, } impl Pixels<'_, '_> { fn transform(&self) -> Affine { self.transforms.last().copied().unwrap_or_default() } fn peniko_colour(&self, index: u16, alpha: f32) -> peniko::Color { let (c, a) = colour(&self.palette, self.foreground, index, alpha).unwrap_or((Color::BLACK, 0.0)); let [r, g, b, a] = c.to_rgba8(a); peniko::Color::from_rgba8(r, g, b, a) } fn clip(&mut self, path: &BezPath) { self.ctx.set_transform(self.transform()); self.ctx.push_clip_layer(path); } } impl ColorPainter for Pixels<'_, '_> { fn push_transform(&mut self, transform: Transform) { let t = self.transform() * affine(&transform); self.transforms.push(t); } fn pop_transform(&mut self) { if self.transforms.len() > 1 { self.transforms.pop(); } } fn push_clip_glyph(&mut self, glyph_id: GlyphId) { let path = outline(self.font, self.location, glyph_id); self.clip(&path); } fn push_clip_box(&mut self, clip_box: BoundingBox) { self.clip(&box_path(&clip_box)); } fn pop_clip(&mut self) { self.ctx.pop_layer(); } fn fill(&mut self, brush: Brush<'_>) { let stops = |stops: &[ColorStop]| -> Vec { stops.iter().map(|s| peniko::ColorStop::from((s.offset, self.peniko_colour(s.palette_index, s.alpha)))).collect() }; let extend = |e: Extend| match e { Extend::Repeat => peniko::Extend::Repeat, Extend::Reflect => peniko::Extend::Reflect, _ => peniko::Extend::Pad, }; let paint: peniko::Brush = match brush { Brush::Solid { palette_index, alpha } => peniko::Brush::Solid(self.peniko_colour(palette_index, alpha)), Brush::LinearGradient { p0, p1, color_stops, extend: e } => { peniko::Gradient::new_linear(point(p0), point(p1)).with_stops(stops(color_stops).as_slice()).with_extend(extend(e)).into() } Brush::RadialGradient { c0, r0, c1, r1, color_stops, extend: e } => { let line = color_stops .iter() .map(|s| { let (c, a) = colour(&self.palette, self.foreground, s.palette_index, s.alpha).unwrap_or((Color::BLACK, 0.0)); GradientStop { opacity: a, ..GradientStop::new(s.offset, c) } }) .collect(); let Some((c0, r0, c1, r1, line)) = non_negative_radii(point(c0), f64::from(r0), point(c1), f64::from(r1), line, e) else { return }; let stops: Vec = line .iter() .map(|s| { let [r, g, b, a] = s.color.to_rgba8(s.opacity); peniko::ColorStop::from((s.offset, peniko::Color::from_rgba8(r, g, b, a))) }) .collect(); peniko::Gradient::new_two_point_radial(c0, r0 as f32, c1, r1 as f32).with_stops(stops.as_slice()).with_extend(extend(e)).into() } Brush::SweepGradient { c0, start_angle, end_angle, color_stops, extend: e } => { peniko::Gradient::new_sweep(point(c0), start_angle.to_radians(), end_angle.to_radians()) .with_stops(stops(color_stops).as_slice()) .with_extend(extend(e)) .into() } }; // Everything the image shows, in the paint's space. let xf = self.transform(); let all = xf.inverse().transform_rect_bbox(Rect::new(0.0, 0.0, f64::from(self.width), f64::from(self.height))); self.ctx.set_transform(xf); match paint { peniko::Brush::Solid(c) => self.ctx.set_paint(c), peniko::Brush::Gradient(g) => self.ctx.set_paint(g), peniko::Brush::Image(_) => return, } self.ctx.fill_rect(&all); } fn push_layer(&mut self, mode: CompositeMode) { use peniko::{BlendMode as Mode, Compose, Mix}; let (mix, compose) = match mode { CompositeMode::Clear => (Mix::Normal, Compose::Clear), CompositeMode::Src => (Mix::Normal, Compose::Copy), CompositeMode::Dest => (Mix::Normal, Compose::Dest), CompositeMode::DestOver => (Mix::Normal, Compose::DestOver), CompositeMode::SrcIn => (Mix::Normal, Compose::SrcIn), CompositeMode::DestIn => (Mix::Normal, Compose::DestIn), CompositeMode::SrcOut => (Mix::Normal, Compose::SrcOut), CompositeMode::DestOut => (Mix::Normal, Compose::DestOut), CompositeMode::SrcAtop => (Mix::Normal, Compose::SrcAtop), CompositeMode::DestAtop => (Mix::Normal, Compose::DestAtop), CompositeMode::Xor => (Mix::Normal, Compose::Xor), CompositeMode::Plus => (Mix::Normal, Compose::Plus), CompositeMode::Screen => (Mix::Screen, Compose::SrcOver), CompositeMode::Overlay => (Mix::Overlay, Compose::SrcOver), CompositeMode::Darken => (Mix::Darken, Compose::SrcOver), CompositeMode::Lighten => (Mix::Lighten, Compose::SrcOver), CompositeMode::ColorDodge => (Mix::ColorDodge, Compose::SrcOver), CompositeMode::ColorBurn => (Mix::ColorBurn, Compose::SrcOver), CompositeMode::HardLight => (Mix::HardLight, Compose::SrcOver), CompositeMode::SoftLight => (Mix::SoftLight, Compose::SrcOver), CompositeMode::Difference => (Mix::Difference, Compose::SrcOver), CompositeMode::Exclusion => (Mix::Exclusion, Compose::SrcOver), CompositeMode::Multiply => (Mix::Multiply, Compose::SrcOver), CompositeMode::HslHue => (Mix::Hue, Compose::SrcOver), CompositeMode::HslSaturation => (Mix::Saturation, Compose::SrcOver), CompositeMode::HslColor => (Mix::Color, Compose::SrcOver), CompositeMode::HslLuminosity => (Mix::Luminosity, Compose::SrcOver), _ => (Mix::Normal, Compose::SrcOver), }; self.ctx.push_layer(None, Some(Mode::new(mix, compose)), None, None, None); } fn pop_layer(&mut self) { self.ctx.pop_layer(); } }