mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
SVG: type written as outlines stacks colour glyphs under their run's paint
As on the canvas: each run's bitmap, COLR and SVG glyphs come before the run's path, so a plain character set over a colour one stays on top.
This commit is contained in:
+26
-23
@@ -1920,11 +1920,11 @@ impl Writer<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Text as glyph outlines: one compound path per run, painted like the run (gradients span
|
/// Text as glyph outlines: one compound path per run, painted like the run (gradients span
|
||||||
/// the whole text, as on the canvas).
|
/// the whole text), over its colour glyphs, as on the canvas.
|
||||||
fn text_outlines(&mut self, n: &Node, t: &TextObject) {
|
fn text_outlines(&mut self, n: &Node, t: &TextObject) {
|
||||||
let db = vectorcraft_text::FontDb::global();
|
let db = vectorcraft_text::FontDb::global();
|
||||||
let lay = vectorcraft_text::layout(db, t);
|
let lay = vectorcraft_text::layout(db, t);
|
||||||
let mut runs: Vec<(usize, kurbo::BezPath)> = vec![];
|
let mut runs = vec![kurbo::BezPath::new(); t.runs.len()];
|
||||||
// Underline and strikethrough bars join their run's outlines; ruby glyphs are painted
|
// Underline and strikethrough bars join their run's outlines; ruby glyphs are painted
|
||||||
// with the run of the text they annotate.
|
// with the run of the text they annotate.
|
||||||
let bars = vectorcraft_text::decorations(&lay, db, t);
|
let bars = vectorcraft_text::decorations(&lay, db, t);
|
||||||
@@ -1935,9 +1935,8 @@ impl Writer<'_> {
|
|||||||
.chain(lay.ruby.iter().map(|g| (g.run, &g.outline)))
|
.chain(lay.ruby.iter().map(|g| (g.run, &g.outline)))
|
||||||
.chain(bars.iter().map(|(r, b)| (*r, b)))
|
.chain(bars.iter().map(|(r, b)| (*r, b)))
|
||||||
{
|
{
|
||||||
match runs.iter_mut().find(|(r, _)| *r == run) {
|
if let Some(bp) = runs.get_mut(run) {
|
||||||
Some((_, bp)) => bp.extend(outline.iter()),
|
bp.extend(outline.iter());
|
||||||
None => runs.push((run, outline.clone())),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let id = self.id_attr(n);
|
let id = self.id_attr(n);
|
||||||
@@ -1947,29 +1946,33 @@ impl Writer<'_> {
|
|||||||
// The glyphs stay in text space; the text's transform joins the document's meanwhile.
|
// The glyphs stay in text space; the text's transform joins the document's meanwhile.
|
||||||
let saved = self.xf;
|
let saved = self.xf;
|
||||||
self.xf = saved * t.xf;
|
self.xf = saved * t.xf;
|
||||||
for (r, bp) in runs {
|
for (i, (bp, run)) in runs.iter().zip(&t.runs).enumerate() {
|
||||||
let Some(run) = t.runs.get(r) else { continue };
|
// Colour glyphs paint themselves, under their run's paint: bitmap glyphs (emoji) as
|
||||||
let pd = PathData::from_bezpath(&bp);
|
// their images, COLR and SVG glyphs as their art.
|
||||||
|
for g in lay.glyphs.iter().filter(|g| g.run == i) {
|
||||||
|
if let Some((image, to_text)) = g.placed_image() {
|
||||||
|
let m = self.matrix(self.xf * to_text);
|
||||||
|
let href = self.blob_href(&image.blob);
|
||||||
|
self.line(&format!(
|
||||||
|
"<image width=\"{}\" height=\"{}\" transform=\"{m}\" preserveAspectRatio=\"none\" xlink:href=\"{}\"/>",
|
||||||
|
image.width,
|
||||||
|
image.height,
|
||||||
|
xml_escape(&href)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(art) = g.placed_colour(Affine::IDENTITY) {
|
||||||
|
self.node(&art);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bp.elements().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let pd = PathData::from_bezpath(bp);
|
||||||
// An unnamed id: the pieces carry no id attribute (the group has it).
|
// An unnamed id: the pieces carry no id attribute (the group has it).
|
||||||
let glyphs = Node::path(NodeId(u64::MAX), pd.clone(), run.style.appearance());
|
let glyphs = Node::path(NodeId(u64::MAX), pd.clone(), run.style.appearance());
|
||||||
let d = self.path_d(&pd, self.xf);
|
let d = self.path_d(&pd, self.xf);
|
||||||
self.shape(&glyphs, &d, &[&pd], FillRule::NonZero, Some(lay.bounds));
|
self.shape(&glyphs, &d, &[&pd], FillRule::NonZero, Some(lay.bounds));
|
||||||
}
|
}
|
||||||
// Colour bitmap glyphs (emoji): their images, which the runs' paint doesn't paint.
|
|
||||||
for (image, to_text) in lay.glyphs.iter().filter_map(|g| g.placed_image()) {
|
|
||||||
let m = self.matrix(self.xf * to_text);
|
|
||||||
let href = self.blob_href(&image.blob);
|
|
||||||
self.line(&format!(
|
|
||||||
"<image width=\"{}\" height=\"{}\" transform=\"{m}\" preserveAspectRatio=\"none\" xlink:href=\"{}\"/>",
|
|
||||||
image.width,
|
|
||||||
image.height,
|
|
||||||
xml_escape(&href)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
// COLR glyphs: their layers, as art.
|
|
||||||
for art in lay.glyphs.iter().filter_map(|g| g.placed_colour(Affine::IDENTITY)) {
|
|
||||||
self.node(&art);
|
|
||||||
}
|
|
||||||
self.xf = saved;
|
self.xf = saved;
|
||||||
self.depth -= 1;
|
self.depth -= 1;
|
||||||
self.line("</g>");
|
self.line("</g>");
|
||||||
|
|||||||
@@ -43,14 +43,14 @@ fn embedded_fonts_leave_bitmap_glyphs_to_the_viewer() {
|
|||||||
assert!(out.svg.contains(&format!("{COLOUR_CHAR}F")), "the character stays text");
|
assert!(out.svg.contains(&format!("{COLOUR_CHAR}F")), "the character stays text");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `ch` in the COLR test font (v1 or v0), 24 pt, filled blue.
|
/// `text` in the COLR test font (v1 or v0), 24 pt, filled blue.
|
||||||
fn colr_doc(v1: bool, ch: char) -> Document {
|
fn colr_doc(v1: bool, text: impl std::fmt::Display) -> Document {
|
||||||
use vectorcraft_text::test_fonts::{colr_family, colr_font};
|
use vectorcraft_text::test_fonts::{colr_family, colr_font};
|
||||||
vectorcraft_text::FontDb::global().add_font(colr_font(v1).unwrap());
|
vectorcraft_text::FontDb::global().add_font(colr_font(v1).unwrap());
|
||||||
let mut d = Document::new(200.0, 100.0);
|
let mut d = Document::new(200.0, 100.0);
|
||||||
let fill = vectorcraft_doc::color::Paint::solid(vectorcraft_doc::color::Color::rgb8(0, 0, 255));
|
let fill = vectorcraft_doc::color::Paint::solid(vectorcraft_doc::color::Color::rgb8(0, 0, 255));
|
||||||
let st = CharStyle { font_family: colr_family(v1).into(), size: 24.0, fill, ..CharStyle::default() };
|
let st = CharStyle { font_family: colr_family(v1).into(), size: 24.0, fill, ..CharStyle::default() };
|
||||||
let n = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(10.0, 50.0), &ch.to_string(), st))));
|
let n = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(10.0, 50.0), &text.to_string(), st))));
|
||||||
let l = d.layers[0].id;
|
let l = d.layers[0].id;
|
||||||
d.insert(Some(l), 0, n).unwrap();
|
d.insert(Some(l), 0, n).unwrap();
|
||||||
d
|
d
|
||||||
@@ -82,3 +82,15 @@ fn embedded_fonts_leave_colr_glyphs_to_the_viewer() {
|
|||||||
assert!(out.warnings.iter().any(|w| w.contains("emoji")), "{:?}", out.warnings);
|
assert!(out.warnings.iter().any(|w| w.contains("emoji")), "{:?}", out.warnings);
|
||||||
assert!(out.svg.contains(COLOUR_CHAR), "the character stays text");
|
assert!(out.svg.contains(COLOUR_CHAR), "the character stays text");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Outlined text stacks as the canvas paints type: a run's colour glyphs under its paint, so a
|
||||||
|
/// plain character set over a colour one stays on top.
|
||||||
|
#[test]
|
||||||
|
fn outlined_colour_glyphs_stay_under_their_runs_paint() {
|
||||||
|
let outlined = ExportOptions { outline_text: true, ..Default::default() };
|
||||||
|
let svg = export_full(&doc(), &outlined, None).svg;
|
||||||
|
assert!(svg.find("<image ").unwrap() < svg.find("<path").unwrap(), "the emoji, then the run: {svg}");
|
||||||
|
let svg = export_full(&colr_doc(false, format!("{COLOUR_CHAR}F")), &outlined, None).svg;
|
||||||
|
let first = &svg[svg.find("<path").unwrap()..];
|
||||||
|
assert!(first[..first.find('>').unwrap()].contains("f00"), "the red layer first: {svg}");
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user