Files
vectorcraft/crates/render/src/renderer/text.rs
T
dotCooCoo 1bfd916d63 Type: Strong type in a translucent brushed path keeps the path's opacity
Crisp and Strong reshape each edge pixel by the type's coverage, drawn
in black. Type in the art of a scatter brush on an undashed path drew
that coverage with the path's opacity folded in, so Strong made fully
covered pixels opaque at 50% path opacity. The coverage is now drawn
at full opacity; the pixels keep the folded opacity.

The evidence entry for the Anti-aliasing Method notes that scores and
hours are unchanged.
2026-11-08 13:21:48 -08:00

715 lines
38 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::{
Affine, AppearanceItem, Arc, BezPath, BlendMode, FillRule, Frame, MAX_INLINE_DEPTH, MISSING_INLINE_LOGGED, Node, NodeKind, PlacedEntry,
PlacedKey, Rect, RenderContext, Renderer, SUBSTITUTED, Shape, TextGeom, TextObject, blend_mode, blob_pixmap, depends_on_document, fx, holds_type,
image_mip, ink, kurbo, lru, peniko, place, proof, stain_bits, text_geom, text_geom_snapped, vello_cpu,
};
use super::{AntiAlias, Pixmap, single_threaded_context};
use crate::paint;
use crate::{MAX_RASTER_SIDE, TypeAntiAlias, TypeEdgeEntry};
impl Renderer {
pub(super) fn draw_text(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, t: &TextObject) {
let t = &*f.doc.inline_resolved(t);
// Type drawn as `fx_owner` (the node `draw_arc` is drawing, or type art that `fx_arts`
// keeps, see `draw_fx_art`) takes its glyph geometry from `texts`, as on the fast path,
// except in frames that snap type to pixels.
let owner = self.fx_owner.as_ref().filter(|o| std::ptr::eq(&***o, n)).cloned();
let (g, kept) = match (f.text_snap(t), owner) {
(None, Some(a)) => self.text_geom_of(&a, t),
(snap, _) => (Arc::new(text_geom_snapped(t, snap)), false),
};
self.draw_text_geom(ctx, f, n, t, &g, kept);
}
/// Draw the inline graphics of type `t` (laid out in `g`): each symbol's art through the
/// normal node path, placed by the layout (inside the text's own transparency group, so its
/// opacity and blend mode apply). A missing symbol draws nothing.
fn draw_inlines(&mut self, ctx: &mut RenderContext, f: &Frame, t: &TextObject, g: &TextGeom) {
if g.inlines.is_empty() && !t.runs.iter().any(|r| r.inline.is_some()) {
return;
}
if self.inline_depth >= MAX_INLINE_DEPTH {
return;
}
for r in t.runs.iter().filter_map(|r| r.inline.as_ref()) {
if !f.doc.symbols.iter().any(|s| s.name == r.symbol) && !MISSING_INLINE_LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) {
log::warn!("inline graphic shows symbol {:?}, which the document doesn't have: it draws nothing", r.symbol);
}
}
self.inline_depth += 1;
for ig in &g.inlines {
let Some(art) = t.runs.get(ig.run).and_then(|r| r.inline.as_ref()) else { continue };
let Some(sym) = f.doc.symbols.iter().find(|s| s.name == art.symbol) else { continue };
// Strokes scale with the art (it is sized to the type, as in the SVG `<use>`).
let (node, kept) = self.placed_art(&sym.art, t.xf * ig.xf * f.doc.symbol_natural_xf(&art.symbol), true, None);
self.draw_placed_art(ctx, f, &node, kept);
}
self.inline_depth -= 1;
}
/// Symbol art `art` as an instance or an inline graphic draws it ([`place`]), and whether it is
/// kept. Art that is a group, a layer or a live object without an appearance of its own is kept
/// between frames under the art's `Arc`, `xf`, `scale_strokes` and the stain of instance
/// `inst`: it and its members keep their `Arc`s from frame to frame, and the caches keyed by a
/// node's address (geometry, stroke outlines, glyphs, live-effect geometry, evaluated live
/// objects) hit for them. Other art is placed again in every frame, and so is art that holds
/// symbol instances, type with inline graphics, images, placed documents or raster effects
/// ([`depends_on_document`]), and art that the cache has no room for. Art that holds type is
/// placed again when the fonts change.
pub(super) fn placed_art(&mut self, art: &Arc<Node>, xf: Affine, scale_strokes: bool, inst: Option<&Node>) -> (Arc<Node>, bool) {
let kept_kind = matches!(art.kind, NodeKind::Group { .. } | NodeKind::Layer { .. }) || vectorcraft_doc::live::is_live(art);
// An instance with appearance changes of its own isn't kept: the key doesn't tell them apart.
let overridden = inst.is_some_and(|i| !i.extras().symbol_overrides.is_empty());
if !kept_kind || overridden || fx::has_object_fx(art) {
return (place(art, xf, scale_strokes, inst), false);
}
let stain = stain_bits(inst.and_then(vectorcraft_brush::stain));
let key = (Arc::as_ptr(art) as usize, xf.as_coeffs().map(f64::to_bits), scale_strokes, stain);
if let Some(e) = self.placed_arts.get(&key, self.stamp)
&& std::ptr::eq(e.art.as_ptr(), Arc::as_ptr(art))
&& e.fonts.is_none_or(|g| g == vectorcraft_text::FontDb::global().generation())
&& e.slice.is_none()
{
return match &e.placed {
Some(placed) => (placed.clone(), true),
None => (place(art, xf, scale_strokes, inst), false),
};
}
let placed = place(art, xf, scale_strokes, inst);
let inserted = self.keep_placed(key, art, &placed, None);
(placed, inserted)
}
/// Symbol art `art` placed for instance `inst` (placed by `xf`) through 9-slice map `map`
/// ([`vectorcraft_doc::NineSliceMap::place`], stained by the instance), and whether it is kept,
/// when the frame's symbol budget (`used`, see [`vectorcraft_doc::charge_symbol_art`]) has room
/// for it. The art is kept between frames as [`Self::placed_art`] keeps it, under the same key
/// and only with the same map. Kept art is charged by its own object count, the art of its
/// opacity masks included ([`Node::count_with_masks`]), and is not placed
/// again; other art is charged by [`vectorcraft_doc::NineSliceMap::most_placed`] before it is
/// placed.
pub(super) fn sliced_art(
&mut self,
doc: &vectorcraft_doc::Document,
art: &Arc<Node>,
map: &vectorcraft_doc::NineSliceMap,
xf: Affine,
inst: &Node,
used: &mut u64,
) -> Option<(Arc<Node>, bool)> {
let kept_kind = matches!(art.kind, NodeKind::Group { .. } | NodeKind::Layer { .. }) || vectorcraft_doc::live::is_live(art);
let stain = stain_bits(vectorcraft_brush::stain(inst));
let key = (Arc::as_ptr(art) as usize, xf.as_coeffs().map(f64::to_bits), false, stain);
// As in `placed_art`, an instance with appearance changes of its own isn't kept.
let overridden = !inst.extras().symbol_overrides.is_empty();
let mut keeps = kept_kind && !overridden && !fx::has_object_fx(art);
if keeps
&& let Some(e) = self.placed_arts.get(&key, self.stamp)
&& std::ptr::eq(e.art.as_ptr(), Arc::as_ptr(art))
&& e.fonts.is_none_or(|g| g == vectorcraft_text::FontDb::global().generation())
&& e.slice.as_ref() == Some(map)
{
match e.placed.clone() {
Some(placed) => {
let count = placed.count_with_masks() as u64;
return vectorcraft_doc::charge_symbol_art(used, count, || ((placed, true), count));
}
None => keeps = false,
}
}
let stained = crate::brush_fx::instance_art(art, inst);
let most = map.most_placed(&stained, doc);
let placed = vectorcraft_doc::charge_symbol_art(used, most, || {
let placed = Arc::new(map.place(&stained, doc));
let count = placed.count_with_masks() as u64;
(placed, count)
})?;
let kept = keeps && self.keep_placed(key, art, &placed, Some(*map));
Some((placed, kept))
}
/// Keep `placed`, symbol art `art` as placed under `key` (through 9-slice map `slice`, when it
/// has one), unless `art` depends on the document ([`depends_on_document`]): then the entry
/// records that it is placed again in every frame. Whether `placed` is kept.
fn keep_placed(&mut self, key: PlacedKey, art: &Arc<Node>, placed: &Arc<Node>, slice: Option<vectorcraft_doc::NineSliceMap>) -> bool {
let kept = (!depends_on_document(art)).then(|| placed.clone());
let keeps = kept.is_some();
let fonts = (keeps && holds_type(art)).then(|| vectorcraft_text::FontDb::global().generation());
// The bytes count the key and the entry, the art's allocation that the weak reference
// keeps, and the kept art.
let bytes = size_of::<(PlacedKey, PlacedEntry, Node)>() + kept.as_deref().map_or(0, lru::node_bytes);
let inserted = self.placed_arts.insert(key, PlacedEntry { art: Arc::downgrade(art), placed: kept, fonts, slice }, bytes, self.stamp);
keeps && inserted
}
/// Cached glyph geometry for a text node (keyed by Arc identity like paths).
/// Text `t` that is not the node's own, such as the copy whose inline graphics
/// `Document::inline_resolved` sized again, is laid out without the cache. Also returns
/// whether the geometry is kept in the cache.
pub(super) fn text_geom_of(&mut self, a: &Arc<Node>, t: &TextObject) -> (Arc<TextGeom>, bool) {
if !matches!(&a.kind, NodeKind::Text(own) if std::ptr::eq(&**own, t)) {
return (Arc::new(text_geom(t)), false);
}
let fonts = vectorcraft_text::FontDb::global().generation();
if fonts != self.text_fonts {
self.texts.clear();
self.text_ids.clear();
self.char_strokes.clear();
self.text_fonts = fonts;
}
let key = Arc::as_ptr(a) as usize;
if let Some((_, g)) = self.texts.get_if(&key, self.stamp, |(node, _)| Arc::ptr_eq(node, a)) {
return (g.clone(), true);
}
if let Some(&previous) = self.text_ids.get(&a.id)
&& let Some(g) = self
.texts
.get_if(&previous, self.stamp, |(node, _)| {
let NodeKind::Text(old) = &node.kind else { return false };
// Shrink Text to Fit searches document-point sizes, so a scale change can
// alter layout. Translation leaves the glyphs in text space unchanged.
if old.xf.as_coeffs()[..4] != t.xf.as_coeffs()[..4] {
return false;
}
let mut comparable = (**old).clone();
comparable.xf = t.xf;
comparable == *t
})
.map(|(_, g)| g.clone())
{
// Kept under this node from here on: the next frames find it without comparing.
self.texts.remove(&previous);
let kept = self.keep_text(a, g.clone());
return (g, kept);
}
let g = Arc::new(text_geom(t));
let kept = self.keep_text(a, g.clone());
(g, kept)
}
/// Keep glyph geometry `g` of text node `a`, as the latest of its id. Whether it was kept.
fn keep_text(&mut self, a: &Arc<Node>, g: Arc<TextGeom>) -> bool {
let key = Arc::as_ptr(a) as usize;
let bytes = g.bytes();
let kept = self.texts.insert(key, (a.clone(), g), bytes, self.stamp);
if kept {
self.text_ids.insert(a.id, key);
} else {
self.text_ids.remove(&a.id);
}
kept
}
/// Draw type `n` (`t`, laid out in `g`). The outlines of its character strokes are cached
/// when `g` is kept in `texts` (`kept`).
pub(super) fn draw_text_geom(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, t: &TextObject, g: &Arc<TextGeom>, kept: bool) {
let xf = f.view * t.xf;
if f.opts.outline {
let mut p = g.all.clone();
p.apply_affine(xf);
ctx.set_transform(Affine::IDENTITY);
ctx.set_stroke(kurbo::Stroke::new(self.hairline));
ctx.set_paint(peniko::Color::BLACK);
ctx.stroke_path(&p);
// Colour bitmap glyphs: their boxes, as images show in outline mode.
for (_, image, to_text) in &g.images {
let mut b = image.rect().to_path(0.1);
b.apply_affine(t.xf * *to_text);
self.hairline(ctx, f, &b, [0, 0, 0, 255]);
}
// COLR glyphs: their layers' outlines.
for (_, art) in &g.colours {
self.draw_colour_glyph(ctx, f, art, t.xf);
}
self.draw_inlines(ctx, f, t, g);
return;
}
if t.anti_alias != TypeAntiAlias::Sharp && self.draw_type_edges(ctx, f, n, t, g, kept) {
return;
}
let tb = t.xf.transform_rect_bbox(g.bounds);
// Overprinting characters darken per draw, like overprinting fills and strokes
// ([`proof`]); the object blend mode is Normal here or applied by an enclosing layer.
let overprints = proof::overprints(f.opts);
let overprint = |ctx: &mut RenderContext, on: bool| {
if overprints {
ctx.set_blend_mode(if on { blend_mode(vectorcraft_color::BlendMode::Darken) } else { BlendMode::default() });
}
};
// The object's own fills and strokes paint the whole outline: those below the Characters
// row under the characters' own fill and stroke, the others over them.
let (below, above) = n.appearance.split_contents();
let all = (!n.appearance.items.is_empty()).then(|| {
let mut all = g.all.clone();
all.apply_affine(t.xf);
all
});
if let Some(all) = &all {
self.draw_text_items(ctx, f, n, below, all, tb);
}
if f.opts.highlight_substitutions {
for (on, path) in [
(f.opts.highlight_substituted_fonts, Some(&g.substituted_fonts)),
(f.doc.setup.highlight_substituted_glyphs, Some(&g.substituted_glyphs)),
// Measured only when shown: it lays the text out again.
(f.opts.highlight_alternate_glyphs, f.opts.highlight_alternate_glyphs.then(|| g.alternate_glyphs(t))),
] {
let Some(path) = path else { continue };
if on && !path.elements().is_empty() {
ctx.set_transform(xf);
ctx.set_fill_rule(peniko::Fill::NonZero);
ctx.set_paint(SUBSTITUTED);
ctx.fill_path(path);
}
}
}
for (i, run) in t.runs.iter().enumerate() {
// Colour bitmap glyphs paint themselves: the run's fill and stroke don't.
for (_, image, to_text) in g.images.iter().filter(|(r, ..)| *r == i) {
overprint(ctx, false);
self.draw_glyph_image(ctx, f, image, t.xf * *to_text);
}
for (_, art) in g.colours.iter().filter(|(r, _)| *r == i) {
self.draw_colour_glyph(ctx, f, art, t.xf);
}
let path = g.run(i);
let path = path.as_ref();
if path.elements().is_empty() {
continue;
}
ctx.set_transform(xf);
ctx.set_fill_rule(peniko::Fill::NonZero);
if paint::set_fill_paint(ctx, &run.style.fill, g.bounds, f) {
self.fold_alpha(ctx, f.ink, &run.style.fill);
overprint(ctx, run.style.overprint_fill);
ctx.fill_path(path);
}
if run.style.has_stroke() {
// Character strokes are drawn in text space, with their cap, join and dashes.
overprint(ctx, run.style.overprint_stroke);
let chars = kept.then_some((g, i));
self.draw_stroke(ctx, f, t.xf, path, FillRule::NonZero, &run.style.stroke_layer(), chars, g.bounds);
}
}
overprint(ctx, false);
self.draw_inlines(ctx, f, t, g);
if let Some(all) = &all {
self.draw_text_items(ctx, f, n, above, all, tb);
}
}
/// Draw type `t` of node `n` (laid out in `g`) with its anti-aliasing method, None, Crisp or
/// Strong, on the pixels of the output ([`Frame::output`]): the type is drawn offscreen on those
/// pixels and placed back pixel for pixel. With None it is drawn with hard edges, so a pixel
/// takes the type's color where the type covers at least half of it. With Crisp and Strong each
/// group of its paints ([`type_paints`]: the object's own fills and strokes one at a time, and
/// the characters' fills and strokes) is drawn on its own as Sharp draws it, and again in
/// opaque black, which gives the share `c` of each pixel the group covers; the pixel's color and
/// opacity are then scaled by [`edge_coverage`] of `c` divided by `c`, so the opacity of the
/// paints is kept, and the groups are composited in the order they are drawn. A pixel that
/// every group covers wholly or not at all is the pixel Sharp draws. Where the output's pixels would number more than the frame's there
/// and more than [`fx::PIXEL_BUDGET`], or be more than [`MAX_RASTER_SIDE`] a side (a frame
/// drawn coarser than the output), the frame's own pixels are used.
///
/// The pixels are kept (`type_edges`). In a later frame they are placed without drawing the
/// type offscreen while the object, the output's zoom and rotation, the method and edge
/// smoothing and what the type reads ([`Self::raster_inputs`]) are unchanged, the output has
/// moved by whole pixels, and the kept pixels hold every pixel the frame needs. After a pan,
/// the kept pixels are placed moved by it; after a pan that brings more of the type into view,
/// the type is drawn offscreen again. The outlines of its character strokes are cached when
/// `g` is kept in `texts` (`geom_kept`).
///
/// Returns false, having drawn nothing, for type that is drawn as Sharp draws it: type with
/// inline graphics, type with color glyphs (bitmap or COLR), type whose own fills or strokes
/// include a brush stroke or a blend mode other than Normal, type that overprints while
/// Overprint Preview is on, and type that shows a substitution highlight on screen. Drawn
/// offscreen, those would blend with nothing or take the method on art that is not the
/// characters'. It also returns false for type whose offscreen drawing would be more than
/// [`MAX_RASTER_SIDE`] pixels a side or more than [`fx::PIXEL_BUDGET`] pixels in all.
fn draw_type_edges(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, t: &TextObject, g: &Arc<TextGeom>, geom_kept: bool) -> bool {
if proof::overprints(f.opts) && t.runs.iter().any(|r| r.style.overprint_fill || r.style.overprint_stroke) {
return false;
}
let normal = vectorcraft_color::BlendMode::Normal;
let brush = |i: &AppearanceItem| matches!(i, AppearanceItem::Stroke(st) if st.brush.is_some());
if t.runs.iter().any(|r| r.inline.is_some()) || n.appearance.items.iter().any(|i| i.visible() && (i.blend() != normal || brush(i))) {
return false;
}
// Color glyphs paint their own pixels and layers, whose opacity is not the characters' coverage.
if !g.images.is_empty() || !g.colours.is_empty() {
return false;
}
if f.opts.highlight_substitutions {
let shows = |on: bool, path: &BezPath| on && !path.elements().is_empty();
if shows(f.opts.highlight_substituted_fonts, &g.substituted_fonts)
|| shows(f.doc.setup.highlight_substituted_glyphs, &g.substituted_glyphs)
// Measured only when shown: it lays the text out again.
|| (f.opts.highlight_alternate_glyphs && !g.alternate_glyphs(t).elements().is_empty())
{
return false;
}
}
// Where the type draws: its glyphs, grown by the farthest its strokes reach, besides its
// bounds (an area type frame's, or a point type estimate before it is laid out).
let glyphs = type_reach(n, t, g);
let reach = fx::cull_bounds(f.symbols, n).map_or(glyphs, |b| b.union(glyphs));
// This frame's pixels to the output's: a scale and an offset (a supersampled export's
// factor, an offscreen layer's crop).
let grid = f.output * f.view.inverse();
let [sx, _, _, sy, _, _] = grid.as_coeffs();
let reach = reach.inflate(2.0 * f.px / sx.abs().min(1.0), 2.0 * f.px / sy.abs().min(1.0));
let on = f.view.transform_rect_bbox(reach).intersect(Rect::new(0.0, 0.0, f64::from(ctx.width()), f64::from(ctx.height())));
if !on.is_finite() {
return false;
}
if on.width() <= 0.0 || on.height() <= 0.0 {
return true;
}
let grid = edge_grid(on, grid);
let output = grid * f.view;
let [sx, _, _, sy, _, _] = grid.as_coeffs();
let on = grid.transform_rect_bbox(on);
// Whole pixels of the output.
let (x0, y0) = (on.x0.floor(), on.y0.floor());
let (Ok(w), Ok(h)) = (u16::try_from((on.x1 - x0).ceil() as u64), u16::try_from((on.y1 - y0).ceil() as u64)) else { return false };
// (vello_cpu panics drawing near `u16::MAX` pixels a side.) Past the pixel budget the type
// is drawn as Sharp draws it.
if u32::from(w) > MAX_RASTER_SIDE || u32::from(h) > MAX_RASTER_SIDE || f64::from(w) * f64::from(h) > fx::PIXEL_BUDGET {
return false;
}
let view = Affine::translate((-x0, -y0)) * output;
let visible = view.inverse().transform_rect_bbox(Rect::new(0.0, 0.0, f64::from(w), f64::from(h)));
let px = 1.0 / view.determinant().abs().sqrt().max(1e-12);
let frame = Frame { mt: false, view, visible, px, output: view, ..*f };
let threshold = match t.anti_alias {
TypeAntiAlias::None => AntiAlias::None.threshold(),
// The frame's pixels are smaller than the output's in a supersampled export, whose art
// is smooth on the output's pixels (Art Optimized).
_ if sx.abs() < 0.999 || sy.abs() < 0.999 => None,
_ => f.opts.anti_alias.threshold(),
};
// Pixels kept from an earlier frame, for the type of the node itself (not a copy of its
// text): drawn on output pixels of the same size, rotation and phase, holding those needed.
let own = matches!(&n.kind, NodeKind::Text(o) if std::ptr::eq(&**o, t));
let key = (std::ptr::from_ref(n) as usize, f.ink);
let linear = fx::linear_key(output);
let shift = output.translation();
let settings = (t.anti_alias, threshold, f.opts.anti_alias, f.opts.precise, self.alpha.to_bits());
let inputs = own.then(|| self.raster_inputs(f, n, fx::Reads::of(n)));
let whole = |v: f64| (v - v.round()).abs() < 1e-6;
let kept = inputs.as_ref().and_then(|inputs| {
let e = self.type_edges.get(&key, self.stamp)?;
let d = shift - e.shift;
let (ox, oy) = (e.origin.0 + d.x.round(), e.origin.1 + d.y.round());
let (iw, ih) = (f64::from(e.image.width()), f64::from(e.image.height()));
let holds = ox <= x0 && oy <= y0 && ox + iw >= x0 + f64::from(w) && oy + ih >= y0 + f64::from(h);
let fits = e.linear == linear && e.settings == settings && whole(d.x) && whole(d.y) && holds && e.inputs == *inputs && e.node == *n;
fits.then(|| (e.image.clone(), ox, oy))
});
let (image, ox, oy) = match kept {
Some(kept) => kept,
None => {
let pm = self.draw_type_pixels(&frame, n, t, g, geom_kept, (w, h), threshold);
self.stats.type_edges += 1;
let image = Arc::new(pm);
if let Some(inputs) = inputs {
let bytes = usize::from(w) * usize::from(h) * 4 + lru::node_bytes(n);
let e = TypeEdgeEntry { node: n.clone(), linear, settings, inputs, shift, origin: (x0, y0), image: image.clone() };
self.type_edges.insert(key, e, bytes, self.stamp);
}
(image, x0, y0)
}
};
ctx.set_transform(grid.inverse() * Affine::translate((ox, oy)));
// The pixmap maps onto the rect pixel for pixel, each one on a pixel of the output.
ctx.reset_paint_transform();
let sampler = peniko::ImageSampler { quality: peniko::ImageQuality::Low, ..Default::default() };
let (iw, ih) = (f64::from(image.width()), f64::from(image.height()));
ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(image), sampler });
ctx.fill_rect(&Rect::new(0.0, 0.0, iw, ih));
ctx.set_transform(Affine::IDENTITY);
true
}
/// The pixels of type `t` of node `n` (laid out in `g`) with its anti-aliasing method, drawn
/// offscreen in `frame` on `w` × `h` pixels with edge threshold `threshold`
/// ([`Self::draw_type_edges`]). The outlines of its character strokes are cached when `g` is
/// kept in `texts` (`kept`).
#[allow(clippy::too_many_arguments)]
fn draw_type_pixels(
&mut self,
frame: &Frame,
n: &Node,
t: &TextObject,
g: &Arc<TextGeom>,
kept: bool,
(w, h): (u16, u16),
threshold: Option<u8>,
) -> Pixmap {
let draw = |r: &mut Self, frame: &Frame, n: &Node, t: &TextObject| {
let mut off = single_threaded_context(w, h);
off.set_aliasing_threshold(threshold);
r.inside_layer(|r| r.draw_text_geom(&mut off, frame, n, t, g, kept));
off.flush();
let mut pm = Pixmap::new(w, h);
off.render_with(&mut pm, &mut r.resources, r.raster);
pm
};
let sharp = TextObject { anti_alias: TypeAntiAlias::Sharp, ..t.clone() };
if t.anti_alias == TypeAntiAlias::None {
return draw(self, frame, n, &sharp);
}
let black = |p: &mut vectorcraft_color::Paint| {
if !p.is_none() {
*p = vectorcraft_color::Paint::solid(vectorcraft_color::Color::BLACK);
}
};
// A group of paints drawn on its own, its pixels taking the method from its own coverage,
// and that coverage.
let reshaped = |r: &mut Self, group: Option<&[TypePaint]>| {
let (pn, pt) = match group {
Some(keep) => type_paint_only(n, &sharp, keep),
None => (n.clone(), sharp.clone()),
};
let mut pm = draw(r, frame, &pn, &pt);
let (mut inked, mut ink) = (pn, pt);
for r in &mut ink.runs {
black(&mut r.style.fill);
black(&mut r.style.stroke);
}
for item in &mut inked.appearance.items {
black(item.paint_mut());
match item {
AppearanceItem::Fill(fl) => fl.opacity = 1.0,
AppearanceItem::Stroke(st) => st.opacity = 1.0,
}
}
// The coverage leaves out the opacity folded in from the object the type is drawn in
// (a translucent brushed path), which the pixels already take.
let folded = std::mem::replace(&mut r.alpha, 1.0);
let covered = draw(r, frame, &inked, &ink);
r.alpha = folded;
for (p, c) in pm.data_mut().iter_mut().zip(covered.data()) {
*p = edge_pixel(*p, c.a, t.anti_alias);
}
(pm, covered)
};
let groups: Vec<Option<Vec<TypePaint>>> = match type_paints(n, &sharp, g, self.alpha >= 1.0) {
Some(groups) => groups.into_iter().map(Some).collect(),
None => vec![None],
};
if groups.len() <= 1 {
return groups.first().map_or_else(|| Pixmap::new(w, h), |group| reshaped(self, group.as_deref()).0);
}
// Where every group covers a pixel wholly or not at all, the pixel is the type's as Sharp
// draws it. Elsewhere it is the groups composited in the order they are drawn, source over.
let mut pm = draw(self, frame, n, &sharp);
let mut sum = vec![[0.0f32; 4]; usize::from(w) * usize::from(h)];
let mut edge = vec![false; sum.len()];
for group in &groups {
let (layer, covered) = reshaped(self, group.as_deref());
for (((s, e), p), c) in sum.iter_mut().zip(&mut edge).zip(layer.data()).zip(covered.data()) {
*e |= c.a != 0 && c.a != 255;
let keep = 1.0 - f32::from(p.a) / 255.0;
for (v, c) in s.iter_mut().zip([p.r, p.g, p.b, p.a]) {
*v = f32::from(c) + *v * keep;
}
}
}
for ((p, s), e) in pm.data_mut().iter_mut().zip(&sum).zip(&edge) {
if *e {
let [red, green, blue, a] = s.map(|v| v.round().clamp(0.0, 255.0) as u8);
*p = vello_cpu::color::PremulRgba8 { r: red.min(a), g: green.min(a), b: blue.min(a), a };
}
}
pm
}
/// Paint some of type `n`'s own fills and strokes (`items`) on its glyph outlines `all`
/// (document space; `tb` their bounds). Fills composite with their own opacity and blend mode;
/// strokes take every stroke option, as on paths.
fn draw_text_items(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, items: &[AppearanceItem], all: &BezPath, tb: Rect) {
for item in items.iter().filter(|i| i.visible() && !i.paint().is_none()) {
match item {
AppearanceItem::Fill(fl) => {
let layered = fl.opacity < 1.0 || fl.blend != vectorcraft_color::BlendMode::Normal;
if layered {
ctx.set_transform(Affine::IDENTITY);
ctx.push_layer(None, Some(blend_mode(fl.blend)), Some(fl.opacity), None, None);
}
ctx.set_transform(f.view);
if paint::set_fill_paint(ctx, &fl.paint, tb, f) {
ctx.set_fill_rule(peniko::Fill::NonZero);
ctx.fill_path(all);
}
if layered {
ctx.pop_layer();
}
}
AppearanceItem::Stroke(st) if st.width > 0.0 => {
if st.brush.is_none() || !self.draw_brush(ctx, f, n, all, st) {
self.draw_stroke(ctx, f, Affine::IDENTITY, all, FillRule::NonZero, st, None, tb);
}
}
AppearanceItem::Stroke(_) => {}
}
}
}
/// Draw COLR glyph `art` (its layers in text space) through `xf` (text space → document), as
/// art: kept between frames as symbol art is ([`Self::placed_art`]).
fn draw_colour_glyph(&mut self, ctx: &mut RenderContext, f: &Frame, art: &Arc<Node>, xf: Affine) {
let (node, kept) = self.placed_art(art, xf, true, None);
let outer = std::mem::replace(&mut self.made_again, !kept);
self.draw_node(ctx, f, &node, true);
self.made_again = outer;
}
/// Draw colour bitmap glyph `image` through `xf` (its pixels → document space), its pixels
/// decoded once and kept with the images' pixels.
fn draw_glyph_image(&mut self, ctx: &mut RenderContext, f: &Frame, image: &vectorcraft_text::GlyphImage, xf: Affine) {
let Some((key, pm)) = blob_pixmap(&image.blob, true) else { return };
// Shrunk before it is inked, as images are (`draw_image_in`).
let unit = f.view * xf * Affine::scale_non_uniform(image.width as f64, image.height as f64);
let (key, pm) = image_mip(key, pm, unit);
let (key, pm) = ink::ink_image(&key, &pm, f.ink, Some(&image.blob), true);
let pm = match f.plates {
Some(lut) => proof::plate_image(lut, &key, &pm, Some(&image.blob), true).1,
None => pm,
};
let sx = image.width as f64 / pm.width().max(1) as f64;
let sy = image.height as f64 / pm.height().max(1) as f64;
let quality = if f.opts.smooth_images { peniko::ImageQuality::Medium } else { peniko::ImageQuality::Low };
ctx.set_paint(vello_cpu::Image {
image: vello_cpu::ImageSource::Pixmap(pm),
sampler: peniko::ImageSampler { quality, ..Default::default() },
});
ctx.set_transform(f.view * xf);
ctx.set_paint_transform(Affine::scale_non_uniform(sx, sy));
ctx.fill_rect(&image.rect());
ctx.reset_paint_transform();
}
}
/// How much of a pixel (out of 255) type that covers `c` of it shows with anti-aliasing method
/// `m`: `c` with Sharp; all of it from half coverage (127) up, else none, with None; `2c - 0.5`
/// with Crisp; and `2c` with Strong; each kept between 0 and all.
pub(crate) fn edge_coverage(m: TypeAntiAlias, c: u8) -> u8 {
let c = i32::from(c);
let shown = match m {
TypeAntiAlias::None => {
if c >= 127 {
255
} else {
0
}
}
TypeAntiAlias::Sharp => c,
TypeAntiAlias::Crisp => 2 * c - 128,
TypeAntiAlias::Strong => 2 * c,
};
shown.clamp(0, 255) as u8
}
/// Where (document space) type `t` of node `n`, laid out in `g`, paints: its glyphs grown by the
/// farthest the characters' strokes reach in text space, transformed as the type is, then grown by
/// the farthest the object's own strokes reach in document space. A stroke's reach takes in its
/// miter spikes and square caps ([`StrokeLayer::reach`]).
pub(crate) fn type_reach(n: &Node, t: &TextObject, g: &TextGeom) -> Rect {
let chars = t.runs.iter().filter(|r| r.style.has_stroke()).map(|r| r.style.stroke_layer().reach()).fold(0.0, f64::max);
let own = n.appearance.outset();
t.xf.transform_rect_bbox(g.bounds.inflate(chars, chars)).inflate(own, own)
}
/// The map from a frame's pixels to the pixels [`Renderer::draw_type_edges`] draws type on, for
/// the region `on` of the frame's pixels: `grid`, which maps them to the output's, unless the
/// output's pixels there would number more than the frame's and more than [`fx::PIXEL_BUDGET`],
/// or be more than [`MAX_RASTER_SIDE`] a side, and then the frame's own pixels (the identity).
pub(crate) fn edge_grid(on: Rect, grid: Affine) -> Affine {
let area = |r: Rect| r.width() * r.height();
let (out, side) = (grid.transform_rect_bbox(on), f64::from(MAX_RASTER_SIDE));
if area(out) > area(on).max(fx::PIXEL_BUDGET) || out.width() > side || out.height() > side { Affine::IDENTITY } else { grid }
}
/// One paint of type: one of the object's own fills and strokes (its index), or the fill or the
/// stroke of a run (its index).
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
enum TypePaint {
Item(usize),
Fill(usize),
Stroke(usize),
}
/// Most groups of paints [`Renderer::draw_type_pixels`] draws one at a time ([`type_paints`]).
const MAX_TYPE_PAINTS: usize = 8;
/// The paints of type `t` of node `n` (laid out in `g`) in groups that
/// [`Renderer::draw_type_pixels`] draws one at a time, in the order they are drawn: the object's
/// own fills and strokes below the characters, the runs' fills and strokes, then the object's own
/// fills and strokes above the characters. Each of the object's own fills and strokes is a group,
/// and so is each fill and stroke of a run, except that fills and strokes of runs one after
/// another in solid colors are one group when colors are drawn opaque (`opaque`). A run's stroke
/// starts a new group rather than join its own fill's, so the stroke's edges inside the characters
/// take the method too. Paints that draw nothing are left out. `None` for type with more than
/// [`MAX_TYPE_PAINTS`] groups, which is drawn as one.
fn type_paints(n: &Node, t: &TextObject, g: &TextGeom, opaque: bool) -> Option<Vec<Vec<TypePaint>>> {
let shown = |i: &AppearanceItem| i.visible() && !i.paint().is_none() && !matches!(i, AppearanceItem::Stroke(st) if st.width <= 0.0);
let items = &n.appearance.items;
let k = n.appearance.contents_at();
let mut groups: Vec<Vec<TypePaint>> = (0..k).filter(|j| shown(&items[*j])).map(|j| vec![TypePaint::Item(j)]).collect();
// Whether the last group holds run paints in opaque solid colors, which the next such paint joins.
let mut joins = false;
for (i, run) in t.runs.iter().enumerate() {
if g.run(i).elements().is_empty() {
continue;
}
let fill = (!run.style.fill.is_none()).then_some((TypePaint::Fill(i), &run.style.fill));
let stroke = run.style.has_stroke().then_some((TypePaint::Stroke(i), &run.style.stroke));
for (p, paint) in [fill, stroke].into_iter().flatten() {
let solid = opaque && matches!(paint, vectorcraft_color::Paint::Solid { .. });
// A run's fill and stroke overlap along every outline, so they never share a group.
let own_fill = matches!(p, TypePaint::Stroke(_)) && groups.last().is_some_and(|last| last.contains(&TypePaint::Fill(i)));
match groups.last_mut() {
Some(last) if joins && solid && !own_fill => last.push(p),
_ => groups.push(vec![p]),
}
joins = solid;
}
}
groups.extend((k..items.len()).filter(|j| shown(&items[*j])).map(|j| vec![TypePaint::Item(j)]));
(groups.len() <= MAX_TYPE_PAINTS).then_some(groups)
}
/// Node `n` and type `t` with every paint but those in `keep` taken away.
fn type_paint_only(n: &Node, t: &TextObject, keep: &[TypePaint]) -> (Node, TextObject) {
let keep: std::collections::HashSet<TypePaint> = keep.iter().copied().collect();
let (mut n, mut t) = (n.clone(), t.clone());
for (j, item) in n.appearance.items.iter_mut().enumerate() {
if !keep.contains(&TypePaint::Item(j)) {
*item.paint_mut() = vectorcraft_color::Paint::None;
}
}
for (i, run) in t.runs.iter_mut().enumerate() {
if !keep.contains(&TypePaint::Fill(i)) {
run.style.fill = vectorcraft_color::Paint::None;
}
if !keep.contains(&TypePaint::Stroke(i)) {
run.style.stroke = vectorcraft_color::Paint::None;
}
}
(n, t)
}
/// Pixel `p` of type the anti-aliasing method `m` draws, where the type covers `c` (out of 255) of
/// the pixel: its color and opacity scaled by [`edge_coverage`] of `c` divided by `c`. A pixel the
/// type covers wholly or not at all is kept.
fn edge_pixel(p: vello_cpu::color::PremulRgba8, c: u8, m: TypeAntiAlias) -> vello_cpu::color::PremulRgba8 {
if c == 0 || c == 255 {
return p;
}
let (to, c) = (u32::from(edge_coverage(m, c)), u32::from(c));
let scale = |v: u8| ((u32::from(v) * to + c / 2) / c).min(255) as u8;
let a = scale(p.a);
vello_cpu::color::PremulRgba8 { r: scale(p.r).min(a), g: scale(p.g).min(a), b: scale(p.b).min(a), a }
}