mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 21:57:12 -06:00
Merge pull request #5055 from mockydevs/fix/export-all-one-file-4920
Export All Open Documents: one file per copy (#4920 follow-up)
This commit is contained in:
@@ -36,7 +36,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"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 (<name>.<ext>; -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 (<name>.<ext>; -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<Value> {
|
||||
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<Value> {
|
||||
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,9 +131,18 @@ fn export_all_open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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 })) {
|
||||
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());
|
||||
// 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() })),
|
||||
|
||||
@@ -217,3 +217,62 @@ 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<String> = 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);
|
||||
}
|
||||
|
||||
/// 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<String> =
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user