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:
dotCooCoo
2026-10-18 12:34:41 +07:00
committed by bflatastic
parent 2643f57b1f
commit d23030ce2b
27 changed files with 116 additions and 15 deletions
+32 -10
View File
@@ -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
}