diff --git a/crates/engine/src/cmd/addons/document.rs b/crates/engine/src/cmd/addons/document.rs index 04609d3e6..30548b80a 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), 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}", + "{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 self-contained file each with all of its art (images embedded or included, every artboard; no remembered save options apply): 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 ), @@ -98,6 +98,20 @@ fn save_timestamped_copy(s: &mut Session, p: &Value) -> Result { fileio::save_document(s, SaveMode::Copy, &json!({ "path": path, "format": format.id })) } +/// The save options of every copy, given in full so that no document's remembered options (linked +/// SVG images, a PDF page range, separate artboard files) apply: one self-contained file per +/// document with all of its art. Each format reads only the options it has, and `range` is left +/// out of SVG, where it would pick artboards over `useArtboards`. +fn copy_options(format: &str) -> Value { + match format { + "svg" | "svgz" => json!({ "images": "embed", "useArtboards": false }), + "pdf" => json!({ "range": "all" }), + "eps" => json!({ "includeLinkedFiles": true, "useArtboards": false }), + "ai" | "ait" => json!({ "range": "all", "includeLinked": true, "separateArtboards": false }), + _ => json!({ "includeLinked": true, "separateArtboards": false }), + } +} + fn export_all_open(s: &mut Session, p: &Value) -> Result { const C: &str = "addon.document.exportAllOpen"; let format = fileio::save_format(Some(str_param(p, "format").unwrap_or("pdf")), None).map_err(|e| bad(C, e))?; @@ -117,10 +131,7 @@ 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; }; - // 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 }); + let options = copy_options(format.id); match fileio::save_document(s, SaveMode::Copy, &json!({ "path": path, "format": format.id, "options": options })) { Ok(r) => { written.push(path.clone()); diff --git a/crates/engine/src/cmd/addons/document/tests.rs b/crates/engine/src/cmd/addons/document/tests.rs index e1b5d59bf..562a389e8 100644 --- a/crates/engine/src/cmd/addons/document/tests.rs +++ b/crates/engine/src/cmd/addons/document/tests.rs @@ -240,3 +240,39 @@ fn export_all_open_documents_writes_one_file_each() { 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); } + +/// A PDF document last saved with a page range exports with every page, and an SVG document with +/// two artboards exports as one SVG holding the art of both (#4920 review). +#[test] +fn export_all_open_documents_keeps_every_page_and_artboard() { + let dir = std::env::temp_dir().join(format!("vc-export-all-pages-{}", 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, "artboards": 2, "name": "pages"})).unwrap(); + s.execute("document.save", &json!({"path": dir.join("pages.pdf"), "format": "pdf", "options": {"range": "1"}})).unwrap(); + let pages = |b: &[u8]| { + String::from_utf8_lossy(b).matches("/Type /Page\n").count() + + String::from_utf8_lossy(b).matches("/Type/Page/").count() + + String::from_utf8_lossy(b).matches("/Type /Page/").count() + + String::from_utf8_lossy(b).matches("/Type /Page ").count() + }; + let first = pages(&std::fs::read(dir.join("pages.pdf")).unwrap()); + let out = dir.join("out"); + s.execute("addon.document.exportAllOpen", &json!({"folder": out, "format": "pdf"})).unwrap(); + let copy = pages(&std::fs::read(out.join("pages.pdf")).unwrap()); + assert!(copy > first, "the copy has every page: {first} saved, {copy} exported"); + // SVG: one file with the art on both artboards. + let a = s.doc().unwrap().doc.artboards[1].rect; + s.execute("shape.rectangle", &json!({"x": a.x0 + 10.0, "y": a.y0 + 10.0, "width": 20, "height": 20})).unwrap(); + s.execute("addon.document.exportAllOpen", &json!({"folder": out, "format": "svg"})).unwrap(); + let svgs: Vec = + std::fs::read_dir(&out).unwrap().map(|e| e.unwrap().file_name().to_string_lossy().into_owned()).filter(|n| n.ends_with(".svg")).collect(); + assert_eq!(svgs, ["pages.svg"]); + let svg = std::fs::read_to_string(out.join("pages.svg")).unwrap(); + // Not the first artboard alone (its 100 × 100 box at the origin): the art's own bounds. + let view = svg.split("viewBox=\"").nth(1).and_then(|v| v.split('"').next()).unwrap(); + let w: f64 = view.split_whitespace().nth(2).unwrap().parse().unwrap(); + assert!(w > 19.0 && w < 30.0, "the SVG shows the art (the square on the second artboard), not the first artboard: viewBox {view}"); + let _ = std::fs::remove_dir_all(&dir); +}