From 439d32fcf8a477d727158056dd3e4e89efcd4f79 Mon Sep 17 00:00:00 2001
From: Taha-Mahmoodi <85902429+Taha-Mahmoodi@users.noreply.github.com>
Date: Thu, 22 Oct 2026 15:48:52 +0430
Subject: [PATCH] Fix #3544: an SVG of one artboard on its own has the
artboard's name as its title
The SVG writer always wrote the document's title in
(crates/svg/src/export.rs), so every file of Export As with Use
Artboards, Export for Screens or an `artboard`/`range` export was titled
after the document ("Icons"), whatever its artboard was called. The
writer's `artboard` option can't tell those apart from Save: Save writes
the first artboard too, standing for the whole document.
ExportOptions has a new `artboard_title` (set by the caller, as
`artboard` is, not deserialized): the file is that one artboard, exported
on its own, and its is the artboard's name (XML-escaped as
before), or the document's title when the artboard has no name. The
engine's SVG encoder (fileio/svg.rs) sets it exactly when the export
names its artboards, which is also when a file holds only that
artboard's art (#550): Export As per artboard, document.export /
serialize with `artboard` or `range`, MCP export, and Export for Screens'
artboards, assets, layers and selections (each written as an artboard
named after it). Save and Save As keep the document's title. Export for
Screens' Full Document draws the art bounds as one artboard, which is now
named after the document, so its SVG keeps the document's title too.
docs/file-format-parity.md's SVG row says so.
Tests: title_control_chars::an_artboard_exported_alone_is_titled_after_it
(crates/svg: two named artboards, the second named with markup;
exporting either alone gives its escaped name, the default and an
artboard without artboard_title give the document's title, an unnamed
artboard falls back to it) and
tests_svgenc::an_svg_of_one_artboard_is_titled_after_it (the engine:
`artboard` 0 and 1, useArtboards range all, Save's SVG, Export for
Screens per artboard, an asset and Full Document). Both fail without the
change (the title was "Icons") and pass with it.
---
crates/engine/src/cmd/fileio/screens.rs | 6 ++--
crates/engine/src/cmd/fileio/svg.rs | 5 +--
crates/engine/src/cmd/fileio/tests_svgenc.rs | 36 ++++++++++++++++++++
crates/svg/src/export.rs | 7 ++--
crates/svg/src/lib.rs | 8 +++++
crates/svg/tests/title_control_chars.rs | 28 +++++++++++++++
docs/file-format-parity.md | 2 +-
7 files changed, 85 insertions(+), 7 deletions(-)
diff --git a/crates/engine/src/cmd/fileio/screens.rs b/crates/engine/src/cmd/fileio/screens.rs
index 0da52a9a4..4c3816ebd 100644
--- a/crates/engine/src/cmd/fileio/screens.rs
+++ b/crates/engine/src/cmd/fileio/screens.rs
@@ -800,10 +800,12 @@ fn items(
};
if o.full_document {
// The bounds of all visible art, as the one artboard of the files that aren't PDFs (a PDF
- // row writes every artboard of the document itself).
+ // row writes every artboard of the document itself). It is named after the document: an
+ // SVG of one artboard takes the artboard's name as its title (#3544), and this one stands
+ // for the whole document.
let (whole, region) = if crop {
let bounds = vectorcraft_render::encode::art_bounds(doc).ok_or_else(|| bad(C, "nothing to export: the document has no visible art"))?;
- (Arc::new(single_artboard(doc, bounds, "Document")), bounds)
+ (Arc::new(single_artboard(doc, bounds, &doc.title)), bounds)
} else {
(doc.clone(), Rect::ZERO)
};
diff --git a/crates/engine/src/cmd/fileio/svg.rs b/crates/engine/src/cmd/fileio/svg.rs
index 8f028934a..0651f0039 100644
--- a/crates/engine/src/cmd/fileio/svg.rs
+++ b/crates/engine/src/cmd/fileio/svg.rs
@@ -179,9 +179,10 @@ pub(super) fn encode(doc: &Document, p: &Value, compressed: bool) -> Result`; Save's SVG and Export for Screens' Full
+/// Document keep the document's.
+#[test]
+fn an_svg_of_one_artboard_is_titled_after_it() {
+ let mut s = Session::new();
+ s.execute("file.new", &json!({"name": "Icons", "width": 24, "height": 24, "artboards": 2})).unwrap();
+ for (index, name) in [(0, "arrow-left"), (1, "arrow-right")] {
+ s.execute("artboard.setProps", &json!({"index": index, "name": name})).unwrap();
+ }
+ let square = s.execute("shape.rectangle", &json!({"x": 4, "y": 4, "width": 16, "height": 16})).unwrap()["id"].clone();
+ let title = |svg: &str| svg.lines().find_map(|l| l.trim().strip_prefix("")?.strip_suffix("")).map(str::to_string);
+ let serialize = |s: &mut Session, p: serde_json::Value| s.execute("document.serialize", &p).unwrap()["text"].as_str().unwrap().to_string();
+ assert_eq!(title(&serialize(&mut s, json!({"format": "svg", "artboard": 1}))).as_deref(), Some("arrow-right"));
+ assert_eq!(title(&serialize(&mut s, json!({"format": "svg", "artboard": 0}))).as_deref(), Some("arrow-left"));
+ assert_eq!(title(&serialize(&mut s, json!({"format": "svg"}))).as_deref(), Some("Icons"), "Save's SVG");
+ let all = s.execute("document.serialize", &json!({"format": "svg", "useArtboards": true, "range": "all"})).unwrap();
+ let titles: Vec<_> = all["files"].as_array().unwrap().iter().map(|f| title(f["text"].as_str().unwrap())).collect();
+ assert_eq!(titles, [Some("arrow-left".into()), Some("arrow-right".into())]);
+ // Export for Screens: each artboard, an asset, and the Full Document.
+ let screens = |s: &mut Session, p: serde_json::Value| -> Vec