From c101576df3ea962e6f0bf950d96bbb0a569ebfcc Mon Sep 17 00:00:00 2001 From: Don Omondi <282319807+mockydevs@users.noreply.github.com> Date: Sun, 1 Nov 2026 05:30:27 +0300 Subject: [PATCH] Fix #4920: one file per copy Each copy is saved with images embedded and artboards together, so a document last saved with linked SVG images writes no image files beside its copy; any other file a save reports is reserved for the copies still to come. --- crates/engine/src/cmd/addons/document.rs | 16 +++++++++++-- .../engine/src/cmd/addons/document/tests.rs | 23 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/addons/document.rs b/crates/engine/src/cmd/addons/document.rs index faa02efc2..04609d3e6 100644 --- a/crates/engine/src/cmd/addons/document.rs +++ b/crates/engine/src/cmd/addons/document.rs @@ -36,7 +36,7 @@ pub fn specs() -> Vec { "Export All Open Documents", ["Object", "Plug-ins", "Document Tools"], None, - "{folder, format?: vectorcraft|pdf|svg|svgz|ai|eps (default: pdf)} write a copy of every open document, in tab order, to `folder` as File › Save a Copy writes it, each named after its document (.; -2, -3… added when that name is taken, so no file is replaced): every document keeps its path, title and modified state, and no undo step is made. A document that can't be written is listed in `failed` and the others are still written; the active document stays active → {files: [{title, path, warnings}], failed: [{title, error}], format}", + "{folder, format?: vectorcraft|pdf|svg|svgz|ai|eps (default: pdf)} write a copy of every open document, in tab order, to `folder` as File › Save a Copy writes it, each named after its document (.; -2, -3… added when that name is taken, so no file is replaced), one file each (images embedded, artboards together): every document keeps its path, title and modified state, and no undo step is made. A document that can't be written is listed in `failed` and the others are still written; the active document stays active → {files: [{title, path, warnings}], failed: [{title, error}], format}", has_doc, export_all_open ), @@ -117,9 +117,21 @@ fn export_all_open(s: &mut Session, p: &Value) -> Result { failed.push(json!({ "title": title, "error": "every numbered name for the copy is taken" })); continue; }; - match fileio::save_document(s, SaveMode::Copy, &json!({ "path": path, "format": format.id })) { + // One file per document: the remembered options of a document saved before in this format + // (linked SVG images, separate artboard files) would write others beside it. Each format + // reads only the options it has. + let options = json!({ "images": "embed", "separateArtboards": false }); + match fileio::save_document(s, SaveMode::Copy, &json!({ "path": path, "format": format.id, "options": options })) { Ok(r) => { written.push(path.clone()); + // Any other file the save wrote is taken for the copies still to come. + for key in ["files", "linked"] { + for f in r.get(key).and_then(Value::as_array).into_iter().flatten() { + if let Some(f) = f.as_str().or_else(|| f.get("path").and_then(Value::as_str)) { + written.push(f.to_string()); + } + } + } files.push(json!({ "title": title, "path": r.get("path").cloned().unwrap_or(json!(path)), "warnings": r.get("warnings").cloned().unwrap_or(json!([])) })); } Err(e) => failed.push(json!({ "title": title, "error": e.to_string() })), diff --git a/crates/engine/src/cmd/addons/document/tests.rs b/crates/engine/src/cmd/addons/document/tests.rs index aef604eb3..e1b5d59bf 100644 --- a/crates/engine/src/cmd/addons/document/tests.rs +++ b/crates/engine/src/cmd/addons/document/tests.rs @@ -217,3 +217,26 @@ fn export_all_open_documents_writes_each_and_reports_failures() { assert_eq!(s.active_index(), Some(0)); let _ = std::fs::remove_dir_all(&dir); } + +/// An SVG document last saved with linked images exports as one SVG with its images embedded: no +/// image file is written beside the copies (#4920 review). +#[test] +fn export_all_open_documents_writes_one_file_each() { + let dir = std::env::temp_dir().join(format!("vc-export-all-one-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(dir.join("out")).unwrap(); + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 100, "height": 100, "name": "pic"})).unwrap(); + let mut png = vec![]; + image::RgbaImage::from_pixel(4, 4, image::Rgba([1, 2, 3, 255])).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap(); + s.execute("file.place", &json!({"name": "a.png", "dataBase64": vectorcraft_format::base64_encode(&png)})).unwrap(); + s.execute("document.save", &json!({"path": dir.join("pic.svg"), "format": "svg", "options": {"images": "link"}})).unwrap(); + let saved: Vec<_> = std::fs::read_dir(&dir).unwrap().filter_map(|e| e.ok()).filter(|e| e.path().is_file()).collect(); + assert!(saved.len() > 1, "the linked save wrote its image beside it: {saved:?}"); + let out = dir.join("out"); + let r = s.execute("addon.document.exportAllOpen", &json!({"folder": out, "format": "svg"})).unwrap(); + let names: Vec = std::fs::read_dir(&out).unwrap().map(|e| e.unwrap().file_name().to_string_lossy().into_owned()).collect(); + assert_eq!(names, ["pic.svg"], "{r}"); + assert!(std::fs::read_to_string(out.join("pic.svg")).unwrap().contains("data:image/png;base64,"), "the image is embedded"); + let _ = std::fs::remove_dir_all(&dir); +}