mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 11:17:01 -06:00
Export: a .ait export names file.saveAsTemplate, and the CLI's convert writes the template
document.export, document.serialize, document.exportSelection and Export for Screens rows answered "PDF-compatible .ait template files can be opened but not written" for a .ait, although file.saveAsTemplate writes them. The CLI's convert and run --export call document.export, so vectorcraft-cli convert in.pdf out.ait failed with that error. The error now reads "PDF-compatible .ait template files are written by file.saveAsTemplate", with a row in the 17 catalogs that translate status messages and in bg, da, id and nl. convert and run --export write a .ait with file.saveAsTemplate, which writes every artboard with the .ai options. convert passes --replace-lossy and its .ai flags on, and refuses --artboard and --range for a .ait. Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -78,8 +78,8 @@ USAGE:
|
||||
|
||||
vectorcraft-cli run [--in FILE] [--cmd ID [--params JSON]]... [--export FILE]... [--scale N]
|
||||
Headless batch: open FILE (any readable format) or start a new document, run commands in
|
||||
order, export each FILE in the format its extension picks (see Writable formats). Prints one
|
||||
JSON result per step.
|
||||
order, export each FILE in the format its extension picks (see Writable formats; a .ait FILE
|
||||
is a template, written as file.saveAsTemplate writes it). Prints one JSON result per step.
|
||||
|
||||
vectorcraft-cli commands
|
||||
Print the command catalogue as JSON.
|
||||
@@ -88,6 +88,8 @@ USAGE:
|
||||
Open IN (any readable format) and export OUT in the format its extension picks (see Writable
|
||||
formats). --artboard is 0-based, --range 1-based (\"1-3,5\"); a PDF gets every artboard
|
||||
unless one of them is given, EPS the bounds of the art, the other formats the first artboard.
|
||||
A .ait OUT is a template of every artboard, written as file.saveAsTemplate writes it (not with
|
||||
--artboard or --range).
|
||||
Live effects are kept, and hidden layers and objects (written hidden in SVG and PSD), with
|
||||
SVG's data-* attributes; --outline-text writes SVG text as paths, --decimals sets SVG's coordinate
|
||||
precision (1–7, default 3). OUT may not be IN when reading
|
||||
@@ -259,6 +261,10 @@ fn convert(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
let [input, output] = <[String; 2]>::try_from(files).map_err(|_| "convert needs IN and OUT")?;
|
||||
let template = is_template(&output);
|
||||
if template && (artboard.is_some() || range.is_some()) {
|
||||
return Err("a .ait template holds every artboard: --artboard and --range don't apply".into());
|
||||
}
|
||||
let mut h = Headless::new();
|
||||
let opened = if editing_data {
|
||||
let params = json!({"path": input, "editingData": "always"});
|
||||
@@ -266,16 +272,22 @@ fn convert(args: &[String]) -> Result<(), String> {
|
||||
} else {
|
||||
h.call("app.open", json!({"path": input})).map_err(|e| format!("open {input}: {e}"))?
|
||||
};
|
||||
// A conversion keeps hidden layers and objects, written hidden, where the format can (SVG,
|
||||
// PSD).
|
||||
let mut params = json!({"path": output, "scale": scale, "artboard": artboard, "range": range, "outlineText": outline_text, "hiddenLayers": true, "acknowledgeLoss": replace_lossy});
|
||||
if let Some(decimals) = decimals {
|
||||
params["decimals"] = json!(decimals);
|
||||
}
|
||||
let (command, mut params) = if template {
|
||||
// Save as Template writes every artboard, with the .ai options.
|
||||
("file.saveAsTemplate", json!({"path": output, "format": "ait", "outlineText": outline_text, "acknowledgeLoss": replace_lossy}))
|
||||
} else {
|
||||
// A conversion keeps hidden layers and objects, written hidden, where the format can (SVG,
|
||||
// PSD).
|
||||
let mut params = json!({"path": output, "scale": scale, "artboard": artboard, "range": range, "outlineText": outline_text, "hiddenLayers": true, "acknowledgeLoss": replace_lossy});
|
||||
if let Some(decimals) = decimals {
|
||||
params["decimals"] = json!(decimals);
|
||||
}
|
||||
("document.export", params)
|
||||
};
|
||||
for (key, value) in legacy {
|
||||
params[key] = json!(value);
|
||||
}
|
||||
let mut r = h.call("engine.execute", json!({"command": "document.export", "params": params})).map_err(|e| format!("export {output}: {e}"))?;
|
||||
let mut r = h.call("engine.execute", json!({"command": command, "params": params})).map_err(|e| format!("export {output}: {e}"))?;
|
||||
// What reading the input left out (a missing linked file, an unsupported filter…) matters as
|
||||
// much as what writing the output did.
|
||||
if let (Some(w), Some(out)) = (opened.get("warnings").filter(|w| w.as_array().is_some_and(|a| !a.is_empty())), r.as_object_mut()) {
|
||||
@@ -285,6 +297,11 @@ fn convert(args: &[String]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Is `path` a `.ait` template? `file.saveAsTemplate` writes it; exports don't.
|
||||
fn is_template(path: &str) -> bool {
|
||||
vectorcraft_engine::cmd::fileio::format_for_name(path).is_some_and(|f| f.id == "ait")
|
||||
}
|
||||
|
||||
fn info(args: &[String]) -> Result<(), String> {
|
||||
let file = args.first().ok_or("info needs a FILE")?;
|
||||
let mut h = Headless::new();
|
||||
@@ -371,7 +388,12 @@ fn run(args: &[String]) -> Result<(), String> {
|
||||
r
|
||||
}
|
||||
Step::Export(path) => {
|
||||
let r = h.call("app.export", json!({"path": path, "scale": scale})).map_err(|e| format!("export {path}: {e}"))?;
|
||||
let r = if is_template(&path) {
|
||||
h.call("engine.execute", json!({"command": "file.saveAsTemplate", "params": {"path": path, "format": "ait"}}))
|
||||
} else {
|
||||
h.call("app.export", json!({"path": path, "scale": scale}))
|
||||
};
|
||||
let r = r.map_err(|e| format!("export {path}: {e}"))?;
|
||||
emit(json!({"step": "export", "result": r}))?;
|
||||
r
|
||||
}
|
||||
|
||||
@@ -179,3 +179,28 @@ fn convert_writes_legacy_ai_with_its_options() {
|
||||
assert!(!bad.status.success());
|
||||
assert!(String::from_utf8_lossy(&bad.stderr).contains("hiddenObjects"), "{}", String::from_utf8_lossy(&bad.stderr));
|
||||
}
|
||||
|
||||
/// convert and `run --export` write a `.ait` as Save as Template does: a PDF-compatible template of
|
||||
/// every artboard that opens as a new untitled document.
|
||||
#[test]
|
||||
fn convert_and_run_write_ait_templates() {
|
||||
let pdf = tmp("template-in.pdf");
|
||||
make(&pdf, 120, 80);
|
||||
let (converted, exported) = (tmp("converted.ait"), tmp("exported.ait"));
|
||||
let r: Value = serde_json::from_str(&ok(&["convert", &pdf, &converted])).unwrap();
|
||||
assert_eq!((&r["format"], &r["path"]), (&Value::from("ait"), &Value::from(converted.as_str())), "{r}");
|
||||
let r: Value = serde_json::from_str(ok(&["run", "--in", &pdf, "--export", &exported]).lines().nth(1).unwrap()).unwrap();
|
||||
assert_eq!(r["result"]["format"], "ait", "{r}");
|
||||
for ait in [&converted, &exported] {
|
||||
assert!(std::fs::read(ait).unwrap().starts_with(b"%PDF"), "{ait}");
|
||||
let opened: Value = serde_json::from_str(ok(&["run", "--in", ait]).lines().next().unwrap()).unwrap();
|
||||
let opened = &opened["result"];
|
||||
assert_eq!((&opened["format"], &opened["restored"]), (&Value::from("ait"), &Value::from(true)), "{opened}");
|
||||
assert!(opened["title"].as_str().is_some_and(|t| t.starts_with("Untitled-")), "{opened}");
|
||||
assert_eq!(info(ait)["artboards"].as_array().unwrap().len(), 2, "{ait}");
|
||||
}
|
||||
// A template holds every artboard: one can't be picked.
|
||||
let one = Command::new(BIN).args(["convert", &pdf, &tmp("one.ait"), "--artboard", "0"]).output().unwrap();
|
||||
assert!(!one.status.success());
|
||||
assert!(String::from_utf8_lossy(&one.stderr).contains("every artboard"), "{}", String::from_utf8_lossy(&one.stderr));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user