Plug-ins: Open With passes an importer its parameters and adds the file to Open Recent

File › Open With asks for the parameters of an importer that declares
any, at their defaults, once the file is picked (dialog `pluginImport`),
and opens the file with the values set. `file.openWith`, `app.open` and
MCP's `open_file` take `params` and pass them to the importer;
`app.open` without them runs the importer on their defaults, with no
dialog. A file opened with Open With is added to Open Recent, as File ›
Open adds the files it opens.
This commit is contained in:
dotCooCoo
2026-11-08 05:07:08 -08:00
parent 36eec28617
commit 58f5789e9a
12 changed files with 164 additions and 27 deletions
+6 -4
View File
@@ -295,13 +295,15 @@ impl Headless {
Ok(out)
}
/// `app.open {path, importer?}`: any readable file as a new active document (`document.open`,
/// with the importer plug-in `importer` names).
/// `app.open {path, importer?, params?}`: any readable file as a new active document
/// (`document.open`, with the importer plug-in `importer` names and its parameters `params`).
pub fn open(&mut self, p: &Value) -> Result<Value, String> {
let path = s(p, "path").ok_or("missing `path`")?;
let mut params = json!({"path": path});
if let Some(importer) = p.get("importer").filter(|v| !v.is_null()) {
params["importer"] = importer.clone();
for k in ["importer", "params"] {
if let Some(v) = p.get(k).filter(|v| !v.is_null()) {
params[k] = v.clone();
}
}
let roots = self.roots.clone();
file_access::confine(roots.as_ref(), || self.session.execute("document.open", &params)).map_err(|e| e.to_string())
+27
View File
@@ -431,3 +431,30 @@ fn open_file_passes_the_importer_on() {
assert!(r.is_error && r.content[0]["text"].as_str().unwrap().contains("test.mcp.absent"), "{r:?}");
assert_eq!(h.session.documents().len(), 1);
}
/// `open_file` sends `app.open` the importer's `params` with the importer.
#[test]
fn open_file_passes_the_importer_s_params_on() {
struct Recording(Vec<(String, Value)>);
impl Backend for Recording {
fn call(&mut self, method: &str, params: Value) -> Result<Value, String> {
self.0.push((method.to_string(), params));
Ok(json!({}))
}
fn has_ui(&self) -> bool {
false
}
fn describe(&self) -> String {
"recording".into()
}
}
let tools = tool_definitions();
let open = tools.iter().find(|t| t["name"] == "open_file").unwrap();
assert_eq!(open["inputSchema"]["properties"]["params"]["type"], "object", "{open}");
let mut b = Recording(vec![]);
let args = json!({"path": "/in/a.tup", "importer": "plugin.test.mcp.params", "params": {"density": 7}});
assert!(!call_tool(&mut b, "open_file", &args).is_error);
let [(method, params)] = &b.0[..] else { panic!("one call: {:?}", b.0) };
assert_eq!(method, "app.open");
assert_eq!(params, &args);
}
+2 -1
View File
@@ -357,13 +357,14 @@ pub fn tool_definitions() -> Vec<Value> {
"open_file",
"Open file",
&format!(
"Open a file as a new, active document: .{} (see run_command document.formats). Templates (.ait, native templates) open as a new untitled document. The reply's `warnings` say what didn't come in as it was (an EPS shown as its preview image says why its PostScript couldn't be read). A file of an extension no built-in format reads opens with the installed importer plug-in that lists it; `importer` names the importer to use (File › Open With), also for an extension a built-in format reads.",
"Open a file as a new, active document: .{} (see run_command document.formats). Templates (.ait, native templates) open as a new untitled document. The reply's `warnings` say what didn't come in as it was (an EPS shown as its preview image says why its PostScript couldn't be read). A file of an extension no built-in format reads opens with the installed importer plug-in that lists it; `importer` names the importer to use (File › Open With), also for an extension a built-in format reads, and `params` gives that importer's parameters.",
OPEN_EXTS.join(", .")
),
obj(
json!({
"path": string("File path"),
"importer": string("An importer plug-in's format id, plugin.<id> (see run_command document.formats), which must list the file's extension (default: the built-in format that reads the file, else the first importer by id that lists its extension)"),
"params": {"type": "object", "description": "The importer plug-in's parameters (see run_command plugin.list; default: the defaults its manifest declares)"},
}),
&["path"],
),
+4 -2
View File
@@ -192,8 +192,10 @@ fn dispatch(b: &mut dyn Backend, name: &str, a: &Args) -> Result<ToolResult, Str
"open_file" => {
let path = absolute_path(b, req_str(a, "path")?)?;
let mut p = json!({"path": path});
if let Some(importer) = a.get("importer").filter(|v| !v.is_null()) {
p["importer"] = importer.clone();
for k in ["importer", "params"] {
if let Some(v) = a.get(k).filter(|v| !v.is_null()) {
p[k] = v.clone();
}
}
j(b.call("app.open", p)?)
}
+5 -1
View File
@@ -444,7 +444,11 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlReques
}
}
"app.open" => match p.get("importer").filter(|v| !v.is_null()) {
Some(importer) => wrap(app.run("file.openWith", json!({"path": s("path"), "importer": importer}))),
// The importer's parameters, else their defaults: automation gets no dialog asking for them.
Some(importer) => {
let params = p.get("params").filter(|v| !v.is_null()).cloned().unwrap_or_else(|| json!({}));
wrap(app.run("file.openWith", json!({"path": s("path"), "importer": importer, "params": params})))
}
None => wrap(app.run("file.open", json!({"path": s("path")}))),
},
"app.save" | "app.export" => {
+1
View File
@@ -390,6 +390,7 @@ registry! {
PrintPresets: [print_presets::KIND] => print_presets::SPEC,
Plugin: [plugin::KIND] => plugin::SPEC,
PluginExport: [plugin::EXPORT_KIND] => plugin::EXPORT_SPEC,
PluginImport: [plugin::IMPORT_KIND] => plugin::IMPORT_SPEC,
ScriptCheck: [script_check::KIND] => script_check::SPEC,
VectorHalftone: [halftone::KIND] => halftone::SPEC,
SimplifySlider: [simplify_slider::KIND] => simplify_slider::SPEC,
+31 -1
View File
@@ -9,7 +9,8 @@
//! it runs in the background ([`crate::plugin_runs`]). Exporters are formats of File › Export ›
//! Export As; one with parameters opens dialog `pluginExport` once the file is picked, and its
//! Export button writes the file. Importers open the files File › Open finds no built-in format
//! for, and File › Open With lists them, to open a file with one of them by name.
//! for, and File › Open With lists them, to open a file with one of them by name; one with
//! parameters opens dialog `pluginImport` once the file is picked, and its Open button opens it.
use std::sync::Arc;
@@ -211,3 +212,32 @@ fn export_confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String>
app.ui.dialog = None;
crate::io::export(app, format.as_deref(), path, &p)
}
/// The dialog kind of an importer's parameters, after Open With picked the file.
pub const IMPORT_KIND: &str = "pluginImport";
/// Headed by the plug-in's name, as it is; Open opens the file.
pub(super) const IMPORT_SPEC: DialogSpec = DialogSpec {
heading: |d| d.str("__label"),
body: export_body,
confirm: import_confirm,
ok: Some("Open"),
max_width: Some(420.0),
..DialogSpec::FORM
};
/// Open With with importer `plugin`, which has parameters: its fields at their defaults, and
/// `open`, Open With's own parameters (`path`, `importer`), kept for Open.
pub fn open_import(app: &mut VectorcraftApp, plugin: &Plugin, open: Value) {
let mut fields = fields_for(plugin.manifest());
fields.insert("__open".into(), open);
app.ui.dialog = Some(Dialog { kind: IMPORT_KIND.into(), fields });
}
/// Opens the file with Open With's parameters and the plug-in's, as `params`.
fn import_confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let mut p = d.fields.get("__open").cloned().filter(Value::is_object).ok_or("no file to open")?;
p["params"] = form::params(d);
app.ui.dialog = None;
crate::io::open_with(app, &p)
}
@@ -6,8 +6,9 @@
use serde_json::json;
use vectorcraft_engine::Session;
use vectorcraft_plugins::wat as tpl;
use vectorcraft_plugins::wat::{self as tpl, Given};
use super::tests_export::{frame, kind, set};
use super::*;
use crate::Services;
use crate::menus::Item;
@@ -64,10 +65,12 @@ fn open_with_lists_importers_and_opens_a_file_with_one() {
let svg = svg.to_string_lossy().into_owned();
crate::io::open_path(&mut app, &svg).unwrap();
assert_eq!(artboard(&app), (0.0, 0.0, 50.0, 40.0), "File › Open: the built-in SVG reader");
app.ui.recent_files.clear();
let r = app.run("file.openWith", json!({"importer": "plugin.test.ui.svgreader", "path": svg})).unwrap();
assert_eq!(r["format"], "plugin.test.ui.svgreader");
assert_eq!(artboard(&app), (10.0, 20.0, 110.0, 70.0), "Open With: the importer");
assert_eq!(app.session.documents().len(), 2);
assert_eq!(app.ui.recent_files, std::slice::from_ref(&svg), "Open With adds the file to Open Recent");
// `app.open {importer}` (MCP's open_file) opens as Open With does.
let (req, _rx) = crate::control::ControlRequest::new("app.open", json!({"path": svg, "importer": "plugin.test.ui.svgreader"}));
let crate::control::Outcome::Done(r) = crate::control::handle(&mut app, &egui::Context::default(), &req) else { panic!("not done") };
@@ -83,6 +86,55 @@ fn open_with_lists_importers_and_opens_a_file_with_one() {
let _ = std::fs::remove_dir_all(&dir);
}
/// Open With asks for an importer's parameters, at their defaults, once the file is picked, and
/// opens the file with the values set; given `params`, it asks nothing, and `app.open` without
/// them runs the importer on their defaults.
#[test]
fn open_with_asks_for_an_importer_s_parameters_and_passes_them_on() {
let dir = tmp_dir("params");
let mut app = app();
let params = r#"{"density":{"type":"number","min":0,"max":10,"default":4}}"#;
// RECT when the parameters hold density 7; the file, which isn't JSON, otherwise.
install(&mut app, &tpl::when_given(&tpl::importer("test.ui.params", r#"["tup"]"#, params), Given::Params, r#""density":7.0"#, RECT));
let file = dir.join("a.tup");
std::fs::write(&file, "not json").unwrap();
let file = file.to_string_lossy().into_owned();
app.run("file.openWith", json!({"importer": "plugin.test.ui.params", "path": file})).unwrap();
assert_eq!(kind(&app), Some(plugin::IMPORT_KIND));
let d = app.ui.dialog.as_ref().unwrap();
assert_eq!((d.str("__label"), d.fields.get("density")), ("Test test.ui.params".to_string(), Some(&json!(4.0))));
assert!(app.session.documents().is_empty(), "nothing opened before Open");
frame(&mut app);
set(&mut app, "density", json!(7));
confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
assert_eq!(app.session.documents().len(), 1);
assert_eq!(artboard(&app), (10.0, 20.0, 110.0, 70.0));
assert_eq!(app.ui.recent_files, std::slice::from_ref(&file));
let r = app.run("file.openWith", json!({"importer": "plugin.test.ui.params", "path": file, "params": {"density": 7}})).unwrap();
assert_eq!(r["format"], "plugin.test.ui.params");
assert!(app.ui.dialog.is_none());
assert_eq!(app.session.documents().len(), 2);
// `app.open` (MCP's open_file) passes `params` on, and asks nothing without them.
let control = |app: &mut VectorcraftApp, p: serde_json::Value| {
let (req, _rx) = crate::control::ControlRequest::new("app.open", p);
let crate::control::Outcome::Done(r) = crate::control::handle(app, &egui::Context::default(), &req) else { panic!("not done") };
r
};
let r = control(&mut app, json!({"path": file, "importer": "plugin.test.ui.params", "params": {"density": 7}}));
assert_eq!(r["result"]["format"], "plugin.test.ui.params", "{r}");
assert_eq!(app.session.documents().len(), 3);
let r = control(&mut app, json!({"path": file, "importer": "plugin.test.ui.params"}));
assert!(r.get("error").is_some(), "the defaults (density 4) don't open the file: {r}");
assert_ne!(kind(&app), Some(plugin::IMPORT_KIND));
assert_eq!(app.session.documents().len(), 3);
app.run("plugin.remove", json!({"id": "test.ui.params"})).unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
/// File › Open reads a file of an extension only an importer lists, and refuses one past the
/// importer's input limit before reading it, as Open With does.
#[test]
+21 -9
View File
@@ -260,9 +260,11 @@ pub fn open_dialog(app: &mut VectorcraftApp) -> Result<(), String> {
/// File › Open With: opens the file `p` names (`path`), else one picked among the files with the
/// extensions the importer plug-in `importer` lists, with that importer, also when a built-in
/// format reads the extension (`document.open {importer}`); a failure is reported as File › Open
/// reports it. The web's file picker can't carry the importer: there File › Open's picker opens,
/// and the file opens as File › Open opens it.
/// format reads the extension (`document.open {importer, params}`), and adds it to Open Recent; a
/// failure is reported as File › Open reports it. An importer with parameters, given no `params`,
/// asks for them first in dialog `pluginImport`, at their defaults. The web's file picker can't
/// carry the importer: there File › Open's picker opens, and the file opens as File › Open opens
/// it.
pub fn open_with(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let importer = p.get("importer").and_then(Value::as_str).ok_or("missing `importer` (see document.formats)")?;
let plugin = vectorcraft_plugins::import::named(importer).map_err(|e| e.to_string())?;
@@ -279,12 +281,22 @@ pub fn open_with(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
pick_open(app, &FilePick { filters, folder: open_dialog_folder(app), ..Default::default() })?
}
};
let opened = vectorcraft_engine::file_access::check_read(&path)
.and_then(|()| check_import_size(&path, true))
.and_then(|()| read(app, &path))
.and_then(|bytes| {
open_document(app, &path, &bytes, Some(path.clone()), &json!({ "importer": vectorcraft_plugins::export::format_id(plugin.id()) }))
});
let importer = vectorcraft_plugins::export::format_id(plugin.id());
let given = p.get("params").filter(|v| !v.is_null());
let opened = vectorcraft_engine::file_access::check_read(&path).and_then(|()| check_import_size(&path, true)).and_then(|()| {
if given.is_none() && !plugin.manifest().params.is_empty() {
dialogs::plugin::open_import(app, &plugin, json!({ "path": path, "importer": importer }));
return Ok(Value::Null);
}
let mut o = json!({ "importer": importer });
if let Some(params) = given {
o["params"] = params.clone();
}
let bytes = read(app, &path)?;
let r = open_document(app, &path, &bytes, Some(path.clone()), &o)?;
note_recent(app, &path);
Ok(r)
});
if let Err(e) = &opened {
report_open_error(app, &fileio::file_name(&path), e);
}
+4 -2
View File
@@ -107,7 +107,7 @@ the two can still win that race.
| `ui.render` | `{path?, scale?, artboard?, data?, traceViews?}` | render an artboard headlessly (PNG with the pixels of `app.export {format: "png", background: "white"}` at the same scale; `artboard`: its 0-based index, default the first; `traceViews: true`: Image Trace objects draw in their View, as on the canvas, instead of their tracing result): written to `path`, else (or with `data: true` as well) sent back as `pngBase64` |
| `ui.render` | `{path?, scale?, artboard?, data?}` | render an artboard headlessly (PNG with the pixels of `app.export {format: "png", background: "white"}` at the same scale; `artboard`: its 0-based index, default the first): written to `path`, else (or with `data: true` as well) sent back as `pngBase64` |
| `ui.resize` / `ui.focus` | | |
| `app.open` / `app.save` / `app.export` / `app.quit` | `{path, importer?}` / `{path?}` / `{path?, format?, artboard?, range?, scale?, …}` | `app.open` reads every format `document.open` reads (see `document.formats`) and returns its result (`{index, title, format, warnings, …}`; `warnings` say what didn't come in as it was, such as an EPS shown as its preview image and why its PostScript couldn't be read), or null when a dialog asks first or a library loads. With `importer` (an importer plug-in's format id, `plugin.<id>`) it runs `file.openWith`: the file opens with that importer, also when a built-in format reads its extension. `app.export` encodes through the engine's `document.export` (same options; the document keeps its path) and writes `path` through the host; without `path` it returns `{dataBase64, format, bytes}`, as headless mode does. `app.quit`, `file.close` and `file.closeAll` first open a `saveChanges` dialog for each modified document (they return `{"pending": "saveChanges"}`): `ui.dialog.confirm` saves, `ui.dialog.set {field: "discard", value: true}` then confirm discards, `ui.dialog.cancel` cancels the whole close or quit |
| `app.open` / `app.save` / `app.export` / `app.quit` | `{path, importer?, params?}` / `{path?}` / `{path?, format?, artboard?, range?, scale?, …}` | `app.open` reads every format `document.open` reads (see `document.formats`) and returns its result (`{index, title, format, warnings, …}`; `warnings` say what didn't come in as it was, such as an EPS shown as its preview image and why its PostScript couldn't be read), or null when a dialog asks first or a library loads. With `importer` (an importer plug-in's format id, `plugin.<id>`) it runs `file.openWith`: the file opens with that importer, also when a built-in format reads its extension, with the importer's parameters `params` (their defaults when omitted; no dialog asks for them). `app.export` encodes through the engine's `document.export` (same options; the document keeps its path) and writes `path` through the host; without `path` it returns `{dataBase64, format, bytes}`, as headless mode does. `app.quit`, `file.close` and `file.closeAll` first open a `saveChanges` dialog for each modified document (they return `{"pending": "saveChanges"}`): `ui.dialog.confirm` saves, `ui.dialog.set {field: "discard", value: true}` then confirm discards, `ui.dialog.cancel` cancels the whole close or quit |
| `app.export` with `useArtboards` | `{path, useArtboards: true, range? \| artboards?, …}` | Export As: PNG, JPEG and WebP write one file per chosen artboard (default all; SVG does so for a `range`), `<path stem>-<artboard>.<ext>`, and return `{path, files: [path…]}`; a PDF keeps them as pages of `path`. `useArtboards: false` covers the bounds of the visible art. The menu's Export As… is `file.exportAs`: the `exportAs` dialog (`format`, `useArtboards`, `all`, `range`), then a save dialog and the format's options: `pngOptions` / `jpgOptions` / `webpOptions`, `svgOptions`, or `savePdf` for a PDF with Use Artboards |
| `ui.dialog.*` on `gradientStop` | `{field: "color" \| "opacity" \| "location", value}` | double-clicking a stop on the Gradient tool's annotator opens its popover (fields `index`, `x`, `y`, `tab`); set `color` (hex), `opacity` or `location` (percentages) and `ui.dialog.confirm` to apply them to the selected stop. `gradient.selectStop {index}` picks the stop the annotator, the panels and Delete/←/→ (`ui.key`) act on |
| `engine.execute` `ui.savePdfDialog` | `{path?, preset?, range?, …document.exportPdf options}` | opens the Save PDF dialog (`file.export.pdf` with no params opens it too). Its fields are the `document.exportPdf` options: set `compatibility` with `ui.dialog.set {field: "compatibility", value: "1.5"}`, a whole section with `{field: "compression", value: {…}}`, the artboards with `__allArtboards: false` and `range`, the visible section with `__section` (General, Compression, Marks and Bleeds, Output, Advanced, Security, Summary). `ui.dialog.confirm` writes `path` (else asks for one), returns `{path, bytes, warnings}` and opens the file when `viewAfterSaving` is set |
@@ -213,7 +213,9 @@ after it), `__plugin` (the plug-in id) and `preview`. It previews `plugin.run` o
`ui.dialog.confirm` keeps the result as one undo step and `ui.dialog.cancel` rolls it back. Live-effect plug-ins open
the `effect` dialog (`effect.dialog {effect: "plugin.<id>"}`). An exporter plug-in with parameters opens the
`pluginExport` dialog once Export As has picked the file: one field per parameter, `__plugin`, and `__export` (the
export's own parameters); `ui.dialog.confirm` writes the file. `ui.installPlugin {path?}` installs a `.wasm` (without
export's own parameters); `ui.dialog.confirm` writes the file. An importer plug-in with parameters opens the
`pluginImport` dialog when `file.openWith` is given no `params`: one field per parameter, `__plugin`, and `__open`
(`path` and `importer`); `ui.dialog.confirm` opens the file. `ui.installPlugin {path?}` installs a `.wasm` (without
a path it asks for one); see [plugins.md](plugins.md).
Overprint Black: the Edit → Edit Colors → Overprint Black… menu item opens a `command` parameter dialog for
+6 -4
View File
@@ -443,7 +443,7 @@ opens the picker through `run_command` with the remote backend.
| `invoke_menu` | `{command, params?}` | Invokes a menu item by command id. Includes UI commands such as `view.*` and `window.*` in remote mode. |
| `open_panel` | `{panel}` | Remote only. `panel` is a panel id (`layers`, `swatches`, `colorGuide`, …, as `window.panel` takes) or its display label (`"Color Guide"`), in any case. |
| `screenshot` | `{path?, scale?, artboard?, window?, max_side?, traceViews?}` | Returns MCP image content (`image/png`, base64) plus a text block. Renders the artboard with the pixels of a PNG `export` with `background: "white"` at the same scale; `traceViews:true` draws Image Trace objects in the view set with `imageTrace.setView`, as in the app's window; `window:true` captures the app window (remote only), as `ui_screenshot` does, and `max_side` (1–4096, window captures only) scales the returned image down as described for `ui_screenshot`. With `path` the backend (the headless session or the app) writes the PNG there too; a window capture written there keeps its full size with `max_side`. |
| `open_file` | `{path, importer?}` | Opens any readable file as a new active document: `.vectorcraft`/`.drawcraft`, `.vctemplate`, `.svg`/`.svgz`, `.pdf`/`.ai`, `.ait`, `.eps`/`.epsf`, `.ps`, `.dxf`, `.emf`, `.wmf`, PNG (`.png`/`.pns`), JPEG, GIF, WebP, TIFF, BMP (`.bmp`/`.dib`/`.rle`), Targa (`.tga`/`.vda`/`.icb`/`.vst`), PCX, Pixar (`.pxr`), JPEG 2000 (`.jpf`/`.jpx`/`.jp2`/`.j2k`/`.j2c`/`.jpc`), PSD (`.psd`/`.pdd`)/PSB (an image opens as a document of its pixel size; a Photoshop file as its merged image). Templates (`.vctemplate`, `.ait`) open as a new untitled document. PDF, `.ai` and SVG files saved with Preserve Editing reopen as the document they carry. The reply is `document.open`'s: its `warnings` say what didn't come in as it was (an EPS whose PostScript can't be read opens as its preview, and the warning names the PostScript error, the operator and the procedure). A file of an extension only an installed importer plug-in lists opens with that importer ([Plug-ins](#plug-ins)); `importer: "plugin.<id>"` opens the file with the importer it names, which must list the file's extension, also one a built-in format reads (in the app, as File › Open With does). `run_command document.formats` lists the formats. |
| `open_file` | `{path, importer?, params?}` | Opens any readable file as a new active document: `.vectorcraft`/`.drawcraft`, `.vctemplate`, `.svg`/`.svgz`, `.pdf`/`.ai`, `.ait`, `.eps`/`.epsf`, `.ps`, `.dxf`, `.emf`, `.wmf`, PNG (`.png`/`.pns`), JPEG, GIF, WebP, TIFF, BMP (`.bmp`/`.dib`/`.rle`), Targa (`.tga`/`.vda`/`.icb`/`.vst`), PCX, Pixar (`.pxr`), JPEG 2000 (`.jpf`/`.jpx`/`.jp2`/`.j2k`/`.j2c`/`.jpc`), PSD (`.psd`/`.pdd`)/PSB (an image opens as a document of its pixel size; a Photoshop file as its merged image). Templates (`.vctemplate`, `.ait`) open as a new untitled document. PDF, `.ai` and SVG files saved with Preserve Editing reopen as the document they carry. The reply is `document.open`'s: its `warnings` say what didn't come in as it was (an EPS whose PostScript can't be read opens as its preview, and the warning names the PostScript error, the operator and the procedure). A file of an extension only an installed importer plug-in lists opens with that importer ([Plug-ins](#plug-ins)); `importer: "plugin.<id>"` opens the file with the importer it names, which must list the file's extension, also one a built-in format reads (in the app, as File › Open With does), and `params` gives that importer's parameters (their defaults when omitted). `run_command document.formats` lists the formats. |
| `save_file` | `{path?, compress?, version?, preview?, precision?, options?}` | Runs `document.save`: the document's own file in its own format (native `.vectorcraft` unless it was opened from or saved as SVG, PDF or a restorable `.ai`; then `warnings` say what that format loses). A path's extension picks the format (`.vectorcraft`, `.vctemplate`, `.pdf`, `.svg`, `.svgz`, `.ai`: a PDF carrying the native document, which reopens editable). For a native file, `compress: true` gzips it (default: the `useCompression` preference, Preferences › File Handling › Use Compression), `version` (1 to 4) writes it for an older VectorCraft (1 and 2 are never compressed, and `version: 2` with `compress: true` is an error), `preview: true` embeds a PNG of the first artboard, and `precision` (0 to 9) rounds path anchors and handles to that many decimal places for that save only (colors, transforms and type stay exact). `options` passes the format's save options as `run_command document.formats` lists them. When `compress`, `version`, `preview` or `precision` is given both at the top level and in `options` with different values, the call is an error (`` `compress: true` and `options.compress: false` disagree: give the option once ``) and nothing is written. The save drops each option the written format doesn't read. The reply's `warnings` then name each such option given (for example `` `compress` was not used: SVG files don't read it `` for an `.svg` path, and `` `options.preview` was not used: … `` for a key in `options`; a `.ai` file reads `compress` but not `version`, `preview` or `precision`). For a `.ai` file, these warnings name the kind of `.ai` written: a legacy .ai (version 7) for `aiVersion: "7"`, a non-PDF-compatible .ai for `pdfCompatible: false`, and a PDF-compatible .ai otherwise, including a save that reuses the options the document was last saved with. `run_command document.save` and the other save commands refuse any option given beside the path and in `options` with different values, and give the same warnings for a top-level `compress`, `version`, `preview` or `precision` and for each key in `options` that the format doesn't read. The headless session and the app receive the same `app.save` params, and the app is also sent `wait: true`: the call returns once the file is written, or with the error (such as a path outside the write root), even with Background Save on. When the save asks first (the question before replacing a file that opened with losses, or on macOS and Linux the file dialog for an untitled document without `path`), the reply is `{pending}` or `"cancelled"` as before, and the save that runs after the answer uses the Background Save preference. |
| `export` | `{path?, format?, scale?, artboard?, range?, selection?, outlineText?, options?}` | `svg`, `svgz`, `pdf`, `ai` (the PDF-compatible `.ai` that Save writes, which carries the document when every artboard is written in order), `eps`, `dxf`, `emf`, `wmf`, `png`, `jpg`, `webp`, `gif`, `png8` (an indexed `.png`), `tiff`, `bmp`, `tga`, `psd` (layered), `txt` (the document's text), `vectorcraft` or `template` (a native template). The tool's `format` enum and `document.formats` list them, generated from the engine's format table. When `format` is omitted, it comes from the path's extension. PDF and `.ai` write one page per artboard: all of them, or `artboard` (0-based) / `range` (`"1-3, 5"`, 1-based). SVG and SVGZ write one file per chosen artboard. EPS writes one file of the visible art; with `artboard`, `range`, `options.artboards` or `options: {"useArtboards": true}` it writes one file per chosen artboard, after a central file of the visible art when there are two or more, and `useArtboards: false` keeps the one file. Rasters, DXF, EMF and WMF write one artboard, or one file per chosen artboard with `options: {"useArtboards": true}`. Rasters also take `options: {"rect": [x, y, width, height]}` (page points, y down): that area of the page at the given `ppi`/`scale`, without an artboard and without changing the document; it is refused with `artboard`, `artboards`, `range` or `useArtboards`, with a non-positive or non-finite size, and for non-raster formats. `options` carries more format options (e.g. `{"quality": 80}` for JPEG). `selection: true` exports the selected objects cropped to their bounds (the reply adds their `bounds`, and reports `format` and the encoder's `warnings` as a whole-document export does); `outlineText: true` writes SVG text as paths. Template layers are left out, live effects are kept, and exporting `vectorcraft` never changes the document's path. Without `path` the bytes come back as `dataBase64`. Both backends run the same `document.export` call. |
| `add_text` | `{text, x?, y?, width?, height?, fit?, fitMinPercent?, path?, mode?, pathEffect?, size?, font?, color?, justify?, vertical?}` | Point type at (x, y) (`justify`: `left`, `center` or `right` starts, centres or ends each line on x); area type with `width`/`height` (`fit`: `none`, `autoHeight`, `shrinkText`, as `text.areaOptions`); or `path` + `mode` (`area`/`onPath`) to flow text in or along a path, with `pathEffect` (`rainbow`, `skew`, `3dRibbon`, `stairStep`, `gravity`); `justify` (also `auto`, `justifyAll`) aligns the paragraphs of every kind, as `text.setStyle` does later. `vertical: true` uses the existing vertical writing mode for every kind (default `false`, horizontal). Returns `{id}`, with `overflow: true` when the text doesn't fit its frame or path. |
@@ -6092,14 +6092,16 @@ id `plugin.<id>`: `effect.apply`, `effect.setParams` and `effect.expandAppearanc
`document.export {format: "plugin.<id>", params?, path?}` writes it, as does a `path` ending in one of its extensions
that no built-in format has. An importer adds a read-only file format `plugin.<id>` (listed by `document.formats`
with `read: true`, its extensions in `openExtensions`): `document.open {path | name, dataBase64, importer?:
"plugin.<id>", params?}` (and `open_file {path, importer?}`) opens a file whose extension no built-in format reads, by its content or its
"plugin.<id>", params?}` (and `open_file {path, importer?, params?}`) opens a file whose extension no built-in format reads, by its content or its
extension, with the installed importer that lists the extension (the one whose id sorts first, when several do).
`importer` names the importer to use, which must list the file's extension, also one a built-in format reads, such
as `svg`. The importer is given at most 128 MiB of the file; its objects become a new document whose artboard is
their bounds, Save then asks for a name, and `warnings` holds the importer's warnings. `plugin.run` of an importer is
an error that names `document.open`. In the app, File › Open With lists the installed importers, and
`file.openWith {importer, path?}` opens `path`, or a file picked among the importer's extensions, with one (on the web
it opens File › Open's picker). A failing plug-in returns an error and leaves the document as it was (an export writes
`file.openWith {importer, path?, params?}` opens `path`, or a file picked among the importer's extensions, with one (on
the web it opens File › Open's picker) and adds it to Open Recent; an importer with parameters, given no `params`,
first asks for them in the `pluginImport` dialog. `params` holds the importer's parameters (their defaults when
omitted). A failing plug-in returns an error and leaves the document as it was (an export writes
no file, and an import opens no document). A filter whose manifest asks for a heavy budget (minutes rather than a minute, `budget: "heavy"` in its
`plugin.inspect {path | dataBase64}` entry) installs only with `allowHeavy: true`: ask the user first. `plugin.run`
runs it where it is called, so the call may take minutes; `notifications/cancelled` stops it.
+4 -2
View File
@@ -60,8 +60,10 @@ heading and buttons in view. Exporters are formats of **File › Export › Expo
Artboards is greyed out (the plug-in writes the whole document); one with parameters asks for them in a dialog of the
same kind once the file is picked, and its Export button writes the file. Importers' files are listed under File ›
Open's "All readable files" and under a filter of their own, named after the importer; **File › Open With** lists the
installed importers by name (`file.openWith {"importer", "path"?}`), and each opens a file picked among its extensions
with that importer (on the web, File › Open's picker opens instead). **File › Open** of a `.wasm` file installs
installed importers by name (`file.openWith {"importer", "path"?, "params"?}`), and each opens a file picked among its
extensions with that importer (on the web, File › Open's picker opens instead) and adds it to Open Recent. An importer
with parameters, given no `params`, asks for them in a dialog of the same kind once the file is picked, and its Open
button opens the file; File › Open runs an importer with its parameters' defaults. **File › Open** of a `.wasm` file installs
a plug-in too (on the web this is how plug-ins get in). A heavy filter asks before it is installed, and runs in the
background ([below](#heavy-filters)). A plug-in that lists permissions asks for them before it is installed
([below](#permissions)).