From 166afce3605f28018c0761e7ba8a7b705df230bd Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 24 Oct 2026 04:33:32 +0700 Subject: [PATCH] SVG export: type with small capitals, superscript, subscript or tab leaders is written as outlines (#3869) --- crates/engine/src/cmd/fileio/tests_svg.rs | 24 +++++++++++++++++++++++ crates/svg/src/export.rs | 8 ++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/fileio/tests_svg.rs b/crates/engine/src/cmd/fileio/tests_svg.rs index b9b90cc00..61a96bd5e 100644 --- a/crates/engine/src/cmd/fileio/tests_svg.rs +++ b/crates/engine/src/cmd/fileio/tests_svg.rs @@ -180,6 +180,30 @@ fn reopened_preserve_editing_svg_saves_as_the_full_document() { let _ = std::fs::remove_dir_all(dir); } +/// #3869: small capitals, superscript and subscript, and tab leaders, which SVG text can't draw, +/// are written as outlines (as vertical type and ruby are) rather than as plain text at full size; +/// type without them stays text. +#[test] +fn type_svg_text_cant_draw_is_written_as_outlines() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 500, "height": 200})).unwrap(); + s.execute("text.create", &json!({"x": 50, "y": 100, "text": "Small caps x2 H2O", "size": 30})).unwrap(); + let plain = svg(&mut s, json!({})); + assert!(plain.contains(" { let resolved = doc.inline_resolved(t); let t = &*resolved; let inline = t.runs.iter().any(|r| r.inline.is_some()); - // Ruby is written as outlines with its base text: a reader's can't place it. - let outlined = self.opts.outline_text || t.vertical || !t.ruby.is_empty() || (inline && matches!(t.kind, TextKind::OnPath { .. })); + // Ruby is written as outlines with its base text: a reader's can't place it. So are + // small capitals, superscript and subscript and tab leaders, which SVG text doesn't draw (#3869). + let unwritable = t.runs.iter().any(|r| r.style.small_caps.is_some() || r.style.position != vectorcraft_doc::CharPosition::Normal) + || std::iter::once(&t.para).chain(&t.paras).any(|p| p.tabs.iter().any(|tab| !tab.leader.is_empty())); + let outlined = + self.opts.outline_text || t.vertical || !t.ruby.is_empty() || (inline && matches!(t.kind, TextKind::OnPath { .. })) || unwritable; let chars = |w: &mut Self, n: &Node| if outlined { w.text_outlines(n, t) } else { w.text(n, t) }; let painted = n.appearance.items.iter().any(|i| i.visible() && !i.paint().is_none()); // Live type with an inside or outside character stroke writes that stroke apart from the