Refuse incomplete SVG/PDF exports beyond the depth limit (#218)

SVG and PDF exports check the visible subtree's depth before bounds or drawing and refuse, with an actionable message, a document nested past 256 levels instead of silently leaving the deep layers out; nothing partial is written, and an inner layer can still be exported. The CLI and MCP report the same error. Fixes #208.

Also: on Windows, figcraft and figcraft-cli link with an 8 MB main-thread stack (Windows gives 1 MB where Linux and macOS give 8), so an export at the depth limit no longer overflows it.

Co-authored-by: bflatastic <bflatastic@users.noreply.github.com>
This commit is contained in:
YTwsy
2026-10-14 05:23:28 +07:00
committed by GitHub
co-authored by bflatastic
parent e8d7dc24f2
commit 33dcb1aef6
20 changed files with 441 additions and 17 deletions
+29 -2
View File
@@ -102,8 +102,8 @@ pub fn write_doc(doc: &Document, page: NodeId, out: &str) -> CliResult<()> {
let v = figcraft_format::to_json(doc).map_err(|e| CliError::Failed(e.to_string()))?;
serde_json::to_vec_pretty(&v).map_err(|e| CliError::Failed(e.to_string()))?
}
"svg" => figcraft_svg::export_svg(&doc.in_srgb(), &children(doc, page), &Default::default())
.ok_or_else(|| CliError::Failed("the page is empty: nothing to export".into()))?
"svg" => figcraft_svg::try_export_svg(&doc.in_srgb(), &children(doc, page), &Default::default())
.map_err(|e| CliError::Failed(e.to_string()))?
.into_bytes(),
"png" | "jpg" | "jpeg" | "webp" => encode(doc, &render_page(doc, page, 1.0)?, &e)?,
"pdf" => page_pdf(doc, page, &PrintSettings::default())?,
@@ -379,4 +379,31 @@ mod tests {
assert_eq!(sanitize("../.."), "_");
assert_eq!(sanitize(""), "layer");
}
#[test]
fn export_depth_errors_preserve_conversion_destinations() {
let mut doc = Document::new("export depth");
let page = doc.first_page().unwrap();
let root = doc.create(page, figcraft_doc::NodeKind::Frame, "root", 0.0, 0.0, 32.0, 32.0).unwrap();
let mut parent = root;
for _ in 1..257 {
parent = doc.create(parent, figcraft_doc::NodeKind::Group, "group", 0.0, 0.0, 32.0, 32.0).unwrap();
}
doc.create(parent, figcraft_doc::NodeKind::Rectangle, "leaf", 0.0, 0.0, 8.0, 8.0).unwrap();
let stamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
let dir = std::env::temp_dir().join(format!("figcraft-cli-export-depth-{}-{stamp}", std::process::id()));
std::fs::create_dir(&dir).unwrap();
for ext in ["svg", "pdf"] {
let path = dir.join(format!("previous.{ext}"));
std::fs::write(&path, b"previous export").unwrap();
let CliError::Failed(error) = write_doc(&doc, page, path.to_str().unwrap()).unwrap_err() else {
panic!("depth overflow must be an operation failure");
};
assert!(error.contains("maximum export depth of 256"), "{ext}: {error}");
assert!(error.contains("select an inner layer"), "{ext}: {error}");
assert_eq!(std::fs::read(&path).unwrap(), b"previous export");
}
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 2);
std::fs::remove_dir_all(dir).unwrap();
}
}