Type: colour glyphs go with type that effects reshape and Expand Appearance

COLR and SVG glyphs, which have no outlines, were left out wherever type was taken as its outlines: geometry effects, Expand Appearance, EMF/WMF export.
This commit is contained in:
Jürg Lehni
2026-10-24 02:06:11 +07:00
committed by bflatastic
parent 863a954cd2
commit c8fe9193d9
5 changed files with 99 additions and 26 deletions
+38 -24
View File
@@ -13,7 +13,7 @@
use std::sync::Arc;
use vectorcraft_doc::{Appearance, AppearanceItem, Effect, Node, NodeKind};
use vectorcraft_doc::{Appearance, AppearanceItem, Effect, Node, NodeId, NodeKind};
use vectorcraft_geom::{Affine, BezPath, FillRule, PathData, Rect, Shape, shapes};
use crate::{Budget, GeomContext, apply_one, is_geometry, merged_params};
@@ -140,12 +140,13 @@ pub fn image_paint(n: &Node) -> Vec<(BezPath, FillRule, AppearanceItem)> {
/// The glyph outlines of type `t` in text space with its underline and strikethrough bars: one
/// path per run, and all of them as one.
pub(crate) fn glyph_outlines(t: &vectorcraft_doc::TextObject) -> (Vec<kurbo::BezPath>, kurbo::BezPath) {
let (runs, all, _) = laid_out(t);
let (runs, all, ..) = laid_out(t);
(runs, all)
}
/// [`glyph_outlines`] and the bounds of `t`'s layout in text space.
fn laid_out(t: &vectorcraft_doc::TextObject) -> (Vec<kurbo::BezPath>, kurbo::BezPath, Rect) {
/// [`glyph_outlines`], the bounds of `t`'s layout in text space, and the art of its colour glyphs
/// (COLR layers, SVG documents), which have no outlines, placed in the document, with their runs.
fn laid_out(t: &vectorcraft_doc::TextObject) -> (Vec<kurbo::BezPath>, kurbo::BezPath, Rect, Vec<(usize, Node)>) {
let db = vectorcraft_text::FontDb::global();
let layout = vectorcraft_text::layout(db, t);
let mut runs = vec![kurbo::BezPath::new(); t.runs.len()];
@@ -163,34 +164,36 @@ fn laid_out(t: &vectorcraft_doc::TextObject) -> (Vec<kurbo::BezPath>, kurbo::Bez
}
all.extend(bar.iter());
}
(runs, all, layout.bounds)
let colours = layout.glyphs.iter().filter_map(|g| Some((g.run, g.placed_colour(t.xf)?))).collect();
(runs, all, layout.bounds, colours)
}
/// Type as glyph outlines in document space: one path per run with the run's fill and stroke,
/// and (when the object has its own fills/strokes) the whole outline with the object's items below
/// the characters under them and the others over them, as the renderer paints them. Each run's
/// path is placed by the type's transform, which also scales its stroke weights and dashes and
/// transforms its pattern tiles; its unplaced gradients are first fixed to the layout's bounds in
/// text space. The object's own unplaced gradients are fixed to the bounding box of those bounds
/// in the document. The group is marked as type's outlines
/// its colour glyphs' art, and (when the object has its own fills/strokes) the whole outline with
/// the object's items below the characters under them and the others over them, as the renderer
/// paints them. Each run's path is placed by the type's transform, which also scales its stroke
/// weights and dashes and transforms its pattern tiles; its unplaced gradients are first fixed to
/// the layout's bounds in text space. The object's own unplaced gradients are fixed to the
/// bounding box of those bounds in the document. The group is marked as type's outlines
/// ([`vectorcraft_doc::NodeExtras::type_outlines`]).
pub fn outline_text(n: &Node) -> Option<Node> {
let NodeKind::Text(t) = &n.kind else { return None };
let (runs, all, bounds) = laid_out(t);
let (runs, all, bounds, colours) = laid_out(t);
let path = |bp: &kurbo::BezPath| PathData::from_bezpath(bp).transformed(t.xf);
let scaling = vectorcraft_doc::Scaling { strokes: true, patterns: true, ..Default::default() };
let mut children: Vec<Arc<Node>> = runs
.iter()
.zip(&t.runs)
.filter(|(bp, _)| !bp.elements().is_empty())
.map(|(bp, run)| {
let mut ap = run.style.appearance();
ap.pin_gradients(bounds);
let mut piece = Node::path(n.id, PathData::from_bezpath(bp), ap);
piece.transform(t.xf, scaling);
Arc::new(piece)
})
.collect();
let mut children: Vec<Arc<Node>> = vec![];
for (i, (bp, run)) in runs.iter().zip(&t.runs).enumerate() {
// Colour glyphs paint themselves, under their run's fill, as the renderer paints them.
children.extend(colours.iter().filter(|(r, _)| *r == i).map(|(_, art)| Arc::new(with_id(art, n.id))));
if bp.elements().is_empty() {
continue;
}
let mut ap = run.style.appearance();
ap.pin_gradients(bounds);
let mut piece = Node::path(n.id, PathData::from_bezpath(bp), ap);
piece.transform(t.xf, scaling);
children.push(Arc::new(piece));
}
if !n.appearance.items.is_empty() && !all.elements().is_empty() {
let (below, above) = n.appearance.split_contents();
let tb = t.xf.transform_rect_bbox(bounds);
@@ -211,6 +214,17 @@ pub fn outline_text(n: &Node) -> Option<Node> {
Some(g)
}
/// `art` with `id` for it and all its members, as the other pieces of outlined type have the type's
/// (Expand Appearance gives them ids of their own).
fn with_id(art: &Node, id: NodeId) -> Node {
let mut art = art.clone();
art.id = id;
for c in art.children_mut().into_iter().flatten() {
*c = Arc::new(with_id(c, id));
}
art
}
/// `n` as plain art that geometry effects can reshape, without its transparency and object
/// effects: type outlined ([`outline_text`]), live objects evaluated (type inside them outlined),
/// a symbol instance as `symbol_art` (the symbol's art, stained as the caller draws it) placed by
+19
View File
@@ -132,3 +132,22 @@ fn pdf_draws_an_svg_glyphs_art_once_when_its_font_cant_be_embedded() {
let red = vectorcraft_color::Paint::solid(vectorcraft_color::Color::rgb8(255, 0, 0));
assert_eq!(fills_of(&pdf).iter().filter(|(p, _)| *p == red).count(), 1);
}
/// Expand Appearance of type with an effect of its own (drawn reshaped as its outlines) keeps its
/// colour glyphs, as their art.
#[test]
fn expand_appearance_keeps_the_colour_glyphs_of_type_with_effects() {
use vectorcraft_text::test_fonts::{COLOUR_CHAR, colr_family, colr_font};
vectorcraft_text::FontDb::global().add_font(colr_font(true).unwrap());
let mut s = session();
let r = s.execute("text.create", &json!({"x": 10, "y": 50, "text": format!("{COLOUR_CHAR}F"), "size": 32, "font": colr_family(true)})).unwrap();
let t = NodeId(r["id"].as_u64().unwrap());
s.execute("effect.apply", &json!({"ids": [t.0], "effect": "distort.transform", "params": {"moveH": 5}})).unwrap();
let r = ids(&s.execute("effect.expandAppearance", &json!({})).unwrap());
let mut gradients = 0;
for id in r {
node(&s, id).walk(&mut |n| gradients += usize::from(matches!(n.appearance.fill_paint(), vectorcraft_color::Paint::Gradient(_))));
}
assert!(gradients > 0, "the colour glyph's gradient layers");
vectorcraft_testkit::invariants::check_document(&s.doc().unwrap().doc).unwrap();
}
+34
View File
@@ -87,3 +87,37 @@ fn a_sweep_glyph_draws_its_image() {
assert!(ink(&img, |p| p[0] > 180 && p[1] < 60 && p[2] < 120).is_some(), "red where the sweep starts");
assert!(ink(&img, magenta).is_none());
}
/// Type with a geometry effect is drawn reshaped as its outlines: its colour glyphs go with them,
/// as their art (here moved 40 pt by a Transform effect).
#[test]
fn colour_glyphs_go_with_type_that_a_geometry_effect_reshapes() {
let _adding = crate::tests_fontchange::adding_fonts();
vectorcraft_text::FontDb::global().add_font(colr_font(false).unwrap());
let mut d = Document::new(160.0, 160.0);
let st = CharStyle { font_family: colr_family(false).into(), size: 100.0, ..Default::default() };
let mut n = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(20.0, 120.0), &COLOUR_CHAR.to_string(), st))));
let moved = vectorcraft_doc::Effect { id: "distort.transform".into(), params: serde_json::json!({"moveH": 40}), visible: true };
let mut appearance = (*n.appearance).clone();
appearance.effects.push(moved);
n.appearance = appearance.into();
let l = d.layers[0].id;
d.insert(Some(l), 0, n).unwrap();
let img = Renderer::new().render(&d, 160, 160, Affine::IDENTITY, &RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() });
let red = ink(&img, |p| p == [255, 0, 0, 255]).expect("the `O` layer, in red");
assert!(red.0 >= 58, "moved right with the type: {red:?}");
}
/// Type's outlines stack as the canvas paints type: a run's colour glyphs under its fill, so a
/// plain character set over a colour one stays on top.
#[test]
fn outlined_colour_glyphs_stay_under_their_runs_fill() {
let _adding = crate::tests_fontchange::adding_fonts();
vectorcraft_text::FontDb::global().add_font(colr_font(false).unwrap());
let mut d = Document::new(160.0, 160.0);
let st = CharStyle { font_family: colr_family(false).into(), size: 100.0, ..Default::default() };
let n = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(20.0, 120.0), &format!("{COLOUR_CHAR}F"), st))));
let outlined = vectorcraft_effects::outline_text(&n).unwrap();
let kinds: Vec<bool> = outlined.children().unwrap().iter().map(|c| matches!(c.kind, NodeKind::Group { .. })).collect();
assert_eq!(kinds, [true, false], "the colour glyph's group, then the run's path");
}
+5 -1
View File
File diff suppressed because one or more lines are too long
+3 -1
View File
File diff suppressed because one or more lines are too long