diff --git a/docs/plugins.md b/docs/plugins.md index d3af5fa06c..f616479167 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -1,9 +1,9 @@ # Plug-ins: the WebAssembly plug-in API (ABI v1) VectorCraft does not host native plug-ins built for other illustration apps: that needs unsafe FFI into -arbitrary machine code and cannot work in the browser build. Instead, plug-ins are **WebAssembly modules** -run in a sandbox. One `.wasm` file works on macOS, Windows, Linux and the web. The design follows PhotoCraft's -plug-in API (same sandbox, same manifest and parameter schema), extended for vector documents. +arbitrary machine code and cannot work in the browser build. Instead, plug-ins are **WebAssembly modules** run in a +sandbox. One `.wasm` file works on macOS, Windows, Linux and the web. The design follows PhotoCraft's plug-in API (same +sandbox, same manifest and parameter schema), extended for vector documents. There are four kinds of plug-in: @@ -20,8 +20,8 @@ There are four kinds of plug-in: paths and compound paths. File › Open lists their files, File › Open With lists the importers by name, and the host hands them the file's bytes. -This page is the contract for plug-in authors and the reference for the host (`crates/plugins`, the -`plugin.*` commands in `crates/engine/src/cmd/plugin.rs`). +This page is the contract for plug-in authors and the reference for the host (`crates/plugins`, the `plugin.*` commands +in `crates/engine/src/cmd/plugin.rs`). ## Using plug-ins @@ -44,14 +44,15 @@ An exporter adds a write-only format, `plugin.`, which `document.formats` li its extensions that no built-in format has; `selectedOnly` hands it the selection, as for the built-in formats. An importer adds a read-only format, `plugin.`, which `document.formats` lists with its `plugin` id. `openExtensions` -holds the extensions File › Open opens with the importer (the lists below describe which are left out). `document.open {"path"}` (or `{"name", "dataBase64"}`) opens a file with it when no built-in format -reads the file, by its content or by its extension, and the importer lists the file's extension; when several do, the -one whose id sorts first opens it. `document.open {"importer": "plugin.", "params"?: {…}}` names the importer, -which must list the file's extension, also one a built-in format reads (an importer that lists `svg` opens an `.svg` -only this way). A damaged file of a built-in format reports that format's error, and the file is not handed to an -importer. The importer's objects become a new document whose artboard is their bounds (at least 1 pt wide and high, -and within ±4,000,000 pt); the document keeps no path, so Save asks for a name. File › Place reads only the built-in formats, and a file only an importer reads opens as a new document when it is dropped on the canvas. These rules are -VectorCraft's own. +holds the extensions File › Open opens with the importer (the lists below describe which are left out). `document.open +{"path"}` (or `{"name", "dataBase64"}`) opens a file with it when no built-in format reads the file, by its content or +by its extension, and the importer lists the file's extension; when several do, the one whose id sorts first opens it. +`document.open {"importer": "plugin.", "params"?: {…}}` names the importer, which must list the file's extension, +also one a built-in format reads (an importer that lists `svg` opens an `.svg` only this way). A damaged file of a +built-in format reports that format's error, and the file is not handed to an importer. The importer's objects become a +new document whose artboard is their bounds (at least 1 pt wide and high, and within ±4,000,000 pt); the document keeps +no path, so Save asks for a name. File › Place reads only the built-in formats, and a file only an importer reads opens +as a new document when it is dropped on the canvas. These rules are VectorCraft's own. In the app, installed filters appear under **Object › Plug-ins**, next to **Install Plug-in…** and **Reload Plug-ins**; live effects under **Effect › Plug-ins** and in the Appearance panel's effect menu. A plug-in with @@ -60,25 +61,23 @@ of numbers and colors, labelled, explained and sorted into sections as its manif parameters it runs at once. Edit › Keyboard Shortcuts lists each installed filter under Object › Plug-ins, where it can be given a shortcut (`plugin:`); the shortcut stays saved while the plug-in is removed and applies again when it is reinstalled. Parameters too many for the window's height scroll inside the dialog, which keeps its -heading and buttons in view. Exporters are formats of **File › Export › Export As**, where Use -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 (other open dialogs, such as New -from Template and Load Styles, list only the files the built-in formats read). These lists leave out an importer's -extensions that a built-in format reads, and those File › Open installs, imports or loads as something else (`.wasm`, -`.kys`, `.jsx`, presets, libraries and swatch or graphic style libraries such as `.ase`). File › Open does not give -those files to the importer, and Open With does. **File › Open With** lists the -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. On the -desktop the importer runs off the UI thread, and a run that takes a while shows a progress window with Cancel, which -stops it. 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. Open Recent opens a file the way -File › Open does: it does not remember the importer or the parameters given to Open With. A built-in format reads the -file if one can, and otherwise the first importer by id that lists its extension reads it 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 +heading and buttons in view. Exporters are formats of **File › Export › Export As**, where Use 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 (other open dialogs, such as New from Template and Load Styles, +list only the files the built-in formats read). These lists leave out an importer's extensions that a built-in format +reads, and those File › Open installs, imports or loads as something else (`.wasm`, `.kys`, `.jsx`, presets, libraries +and swatch or graphic style libraries such as `.ase`). File › Open does not give those files to the importer, and Open +With does. **File › Open With** lists the 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. On the desktop the importer runs off the UI thread, and a run that takes a +while shows a progress window with Cancel, which stops it. 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. Open Recent opens a file the way File › Open does: it does not remember the importer or +the parameters given to Open With. A built-in format reads the file if one can, and otherwise the first importer by id +that lists its extension reads it 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)). ### Commands @@ -117,9 +116,9 @@ the object is drawn as if the effect weren't there, and the record is kept for w ## The sandbox -The host is [`wasmi`](https://github.com/wasmi-labs/wasmi), a pure-Rust WebAssembly interpreter -(MIT OR Apache-2.0). It builds for `wasm32-unknown-unknown` too, so plug-ins also run in the web build. Limits -(`vectorcraft_plugins::Limits`, defaults): +The host is [`wasmi`](https://github.com/wasmi-labs/wasmi), a pure-Rust WebAssembly interpreter (MIT OR Apache-2.0). It +builds for `wasm32-unknown-unknown` too, so plug-ins also run in the web build. Limits (`vectorcraft_plugins::Limits`, +defaults): | Limit | Default | On violation | |---|---|---| @@ -155,15 +154,15 @@ measured with the page's `performance.now()`. Some filters do heavy but legitimate work, such as routing a plotter's pen, packing shapes onto a sheet or running a simulation, which a minute and 50 M instructions don't cover. Such a filter declares `"budget": "heavy"` in its manifest (only a filter may: a live effect runs whenever the document is drawn, an exporter has an export's budget and -an importer an import's). Each of its runs gets 1 G instructions (20 times a light run's) plus the 4,000 per input byte, and 300 s -(`vectorcraft_plugins::{HEAVY_FUEL, HEAVY_WALL_TIME_MS}`). Its output can't add to the budget: the host learns how -much there is only once the run has ended. +an importer an import's). Each of its runs gets 1 G instructions (20 times a light run's) plus the 4,000 per input byte, +and 300 s (`vectorcraft_plugins::{HEAVY_FUEL, HEAVY_WALL_TIME_MS}`). Its output can't add to the budget: the host learns +how much there is only once the run has ended. - **Installing asks first.** Install Plug-in… and File › Open show what the filter asks for, and install it once the user clicks OK (see [Permissions](#permissions) for who can). `plugin.install` refuses it without `allowHeavy: true`, and refuses `allowHeavy` from a request except in a session started with `vectorcraft-cli --allow-plugin-grants` - (`plugin.inspect` shows the budget beforehand). A filter kept in the `Plug-ins` folder was agreed to and is installed again - at startup. The Additional Plug-ins Folder skips heavy filters with a note in its report, since loading a folder + (`plugin.inspect` shows the budget beforehand). A filter kept in the `Plug-ins` folder was agreed to and is installed + again at startup. The Additional Plug-ins Folder skips heavy filters with a note in its report, since loading a folder asks nobody. - **It runs in the background.** Its dialog has no Preview. OK (or the menu item, without parameters) runs it on a thread of its own while editing goes on, shown in the status bar and, after a moment, in a progress window with @@ -203,17 +202,15 @@ capability model proposed for PhotoCraft plug-ins `state`, which only its commands use. A filter may list any of its three, and `commands` with a `commandAllowlist`. Any kind may add `progress`, `settings` and `cache`, which no kind implies. An unknown name, a name listed twice, a name that doesn't apply to the kind, more than 16 names, `progress`, `settings`, `cache` or `commands` with an `api` - below 2, or - one of the names kept for a later plug-in API - (`ui.panel`, `ui.tool`) is refused when the module is installed, and the error names it. + below 2, or one of the names kept for a later plug-in API (`ui.panel`, `ui.tool`) is refused when the module is + installed, and the error names it. - **Installing asks first.** Install Plug-in… and File › Open show the plug-in's name, id, version, the SHA-256 of the file and each permission in plain words, and install it once the user clicks OK, if the file still has the - SHA-256 shown; Cancel installs nothing. The - names shown have control and bidirectional characters escaped (`\u{202E}`) and are cut to 64 characters. - Only a person can press its OK: `ui.dialog.confirm`, MCP's `answer_dialog` and injected input (`ui.key`, `ui.click`) - are refused and the dialog stays open. Enter confirms it only when OK has the keyboard focus. `ui.dialog.set` can't - change what it installs. OK stays disabled until the question has been on screen in the focused window for half a - second (VectorCraft's own value, from the delay the security guidance at + SHA-256 shown; Cancel installs nothing. The names shown have control and bidirectional characters escaped (`\u{202E}`) + and are cut to 64 characters. Only a person can press its OK: `ui.dialog.confirm`, MCP's `answer_dialog` and injected + input (`ui.key`, `ui.click`) are refused and the dialog stays open. Enter confirms it only when OK has the keyboard + focus. `ui.dialog.set` can't change what it installs. OK stays disabled until the question has been on screen in the + focused window for half a second (VectorCraft's own value, from the delay the security guidance at https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/security/security-considerations-for-browser-ui.md gives for dialog boxes), so a click, Enter or Space, or a press begun before then, installs nothing. The half second starts over for every new question, even one that reads the same, and when the window gets the focus back. Cancel @@ -280,23 +277,22 @@ capability model proposed for PhotoCraft plug-ins does not count for any session in the same process that uses the same `Plug-ins` folder, by any path to it, until `grants.json` can be written for that plug-in, as when the plug-in is granted again. Another process on the same folder, for example `vectorcraft-cli` beside the app, still reads `grants.json` as it is written. A grant for the - same id kept in another `Plug-ins` folder counts. In Safe Mode, and for - a stored module that the user disabled, + same id kept in another `Plug-ins` folder counts. In Safe Mode, and for a stored module that the user disabled, VectorCraft does not read the module, and `grants.json` keeps the grant for its id. The Additional Plug-ins Folder and `plugin.reload` do not change the `Plug-ins` folder: when they leave a module that lists no permissions under an id, the process forgets its own grant for that id, and a grant kept in `grants.json` stays with the stored module it covers. An entry of `grants.json` that this version can't use (a permission it doesn't know, an invalid id) is skipped and stays in the file until its plug-in is granted again, replaced by a module that lists no permissions, or removed. When such an entry can't be removed from the file, `plugin.install` and `plugin.remove` treat it as a grant - that can't be removed. A `grants.json` that is not a JSON list is never overwritten, and the file is never written larger than - 1 MiB, the most that is read. When a `Plug-ins` folder is in use, only the grants kept - in it count for `plugin.install`, `plugin.inspect`, `plugin.reload` and the Additional Plug-ins Folder. At startup, - a stored plug-in is skipped when no grant covers its bytes and the permissions it lists. The Additional Plug-ins - Folder and `plugin.reload` install only plug-ins that such a grant covers, and report the others. A module without a + that can't be removed. A `grants.json` that is not a JSON list is never overwritten, and the file is never written + larger than 1 MiB, the most that is read. When a `Plug-ins` folder is in use, only the grants kept in it count for + `plugin.install`, `plugin.inspect`, `plugin.reload` and the Additional Plug-ins Folder. At startup, a stored plug-in + is skipped when no grant covers its bytes and the permissions it lists. The Additional Plug-ins Folder and + `plugin.reload` install only plug-ins that such a grant covers, and report the others. A module without a `permissions` list, or with an empty one, needs no grant. -- The host enforces the grant on a filter run and a command run: without `doc.read` the filter is given no objects; without `state` its - `documentData` is `null` and an output that sets it is refused; without `doc.write` an output that changes the - document is refused. A refused output leaves the document as it was. +- The host enforces the grant on a filter run and a command run: without `doc.read` the filter is given no objects; + without `state` its `documentData` is `null` and an output that sets it is refused; without `doc.write` an output that + changes the document is refused. A refused output leaves the document as it was. - `api` is the plug-in API level the manifest is written for: `1` (the default) or `2`, which adds the [host imports](#host-imports-api-level-2), a filter's `inputs` ([reading type, images and symbols](#reading-type-images-and-symbols)) and a live effect's [`paintOutput`](#painted-live-effects). A higher one @@ -307,12 +303,11 @@ capability model proposed for PhotoCraft plug-ins - Parameter declarations are unchanged by permissions (`params`, below). Every run gets a **fresh instance**, so no state survives between calls except what a plug-in writes to its -[settings and cache](#settings-and-cache). Traps, exhausted budgets, malformed -modules, bad manifests, out-of-range pointers, negative return codes and invalid output all become errors, and the -document is left untouched: a filter's output is checked completely before one edit applies it. A live effect that -fails draws the object's geometry unchanged; `plugin.info` reports why (`lastError`). A run that succeeds clears the -error of a failed run. A check that finds every swatch and pattern a painted result names clears only the error of a -refused swatch or pattern. +[settings and cache](#settings-and-cache). Traps, exhausted budgets, malformed modules, bad manifests, out-of-range +pointers, negative return codes and invalid output all become errors, and the document is left untouched: a filter's +output is checked completely before one edit applies it. A live effect that fails draws the object's geometry unchanged; +`plugin.info` reports why (`lastError`). A run that succeeds clears the error of a failed run. A check that finds every +swatch and pattern a painted result names clears only the error of a refused swatch or pattern. wasmi is built with its *portable* (loop) dispatcher: the default tail-call dispatcher relies on LLVM sibling-call optimisation, and when that doesn't happen a long-running module overflows the host stack, which aborts @@ -365,12 +360,12 @@ still returns `1`. `vc_progress`, `vc_cancelled` and `vc_log` need `progress`; t filter that lists `progress` runs in the background as a heavy one does, under its light budget, when it runs at once from Object › Plug-ins or its shortcut, or when OK runs it with Preview off; with Preview on, its dialog keeps the previewed run. A filter that also lists a `commandAllowlist` runs where it is called, since its command calls need - the document. A plug-in that checks `vc_cancelled` can - stop early, and the host discards what it returns after the Cancel or after its time budget ran out; one that - ignores it is still stopped by the instruction and time budgets. With a known total, a report is passed on only when - it reaches another tenth of a percent; with an unknown total, each change of `done` is passed on. After 2,000 reports - in a run, one change in 1,000 is passed on, and so is the first report after the total becomes known or unknown. A - live effect's progress is not shown, since it is drawn as part of something else. + the document. A plug-in that checks `vc_cancelled` can stop early, and the host discards what it returns after the + Cancel or after its time budget ran out; one that ignores it is still stopped by the instruction and time budgets. + With a known total, a report is passed on only when it reaches another tenth of a percent; with an unknown total, each + change of `done` is passed on. After 2,000 reports in a run, one change in 1,000 is passed on, and so is the first + report after the total becomes known or unknown. A live effect's progress is not shown, since it is drawn as part of + something else. - Log lines go to the app's log (target `vectorcraft_plugins`) as `: `, with control and bidirectional characters escaped. The whole text must be UTF-8. A line shows its first 1,024 bytes, as at most 1,024 characters with the escapes, and ends with `…` when it is cut. A run writes at most 200 lines and 32 KiB of shown @@ -405,8 +400,7 @@ or modified time differs from the last read or write. A get answered from memory set to the value the key already has writes nothing, and neither does a delete of a key that has no value. A delete of the last setting removes the file and counts no written bytes. Every other successful set or delete writes the file before the call returns. A cache call reads or writes the whole entry in memory in one step, up to 64 MiB, and -the run waits for it. -A live effect runs while the art draws, so keep the entries it uses small. +the run waits for it. A live effect runs while the art draws, so keep the entries it uses small. - A key is 1 to 64 characters from `A-Z`, `a-z`, `0-9`, `_` and `-`, and is not a Windows device name (`CON`, `PRN`, `AUX`, `NUL`, `COM0` to `COM9`, `LPT0` to `LPT9`, in any case). Keys differ in case: `tile` and `Tile` are two. @@ -416,28 +410,27 @@ A live effect runs while the art draws, so keep the entries it uses small. also fails with `-4` when the `cache` folder holds more than 8,192 items; clearing the cache removes every file in it. - The desktop app and `vectorcraft-cli` keep them in a folder named from the id under `storage/` in the `Plug-ins` - folder (ids that differ only in case get two folders): `settings.json` and a - `cache` folder with one file per entry, named by the host. A settings file that is too large or doesn't parse is - moved to `settings.unreadable.json`, replacing an earlier one there, and the settings start empty. A link in that - folder is refused (`-5`). Writes go to a temporary file that then replaces the old one; a temporary file a failed - write leaves in the `cache` folder does not count toward the caps, and clearing the cache removes it. + folder (ids that differ only in case get two folders): `settings.json` and a `cache` folder with one file per entry, + named by the host. A settings file that is too large or doesn't parse is moved to `settings.unreadable.json`, + replacing an earlier one there, and the settings start empty. A link in that folder is refused (`-5`). Writes go to a + temporary file that then replaces the old one; a temporary file a failed write leaves in the `cache` folder does not + count toward the caps, and clearing the cache removes it. - A plug-in reaches only the storage of its own id. Replacing it with another module of the same id - (`plugin.install` with `replace: true`, after the user agreed) keeps the storage; `plugin.remove` deletes it, and a run of the plug-in still going gets `-5` from every storage call after that. - `plugin.remove` first renames the folder, adding `.removed` to its name (or `.removing` while a `.removed` folder - holds settings an earlier removal kept). When that fails, `plugin.remove` fails and the plug-in stays installed with - its storage as it was. Before VectorCraft deletes a renamed folder, it writes an empty file beside it with `.delete` - added to the folder's name, and removes that file once the folder is gone. When that file can't be written, the - folder is not deleted and is treated like any renamed folder without a `.delete` file (see below). Files that can't be deleted stay in the - renamed folder with the `.delete` file beside it, where no plug-in reaches them. A later `plugin.remove` tries again - to delete them when a plug-in with that id is installed or a storage folder for the id exists, and that deletion can - fail too. For an id that isn't installed and has only such a folder, `plugin.remove` does not delete it and fails as - not found. When `plugin.remove` can't remove the grant and can't rename the folder back either, its error names the - renamed folder. The next load, install or `plugin.remove` of the plug-in renames a renamed folder that has no - `.delete` file back while the plug-in has no folder of its own; when it has one, that folder is used and the renamed - folder stays until `plugin.remove` deletes both. - `plugin.remove` with the id of a plug-in that isn't installed deletes the storage kept for that id, when there is - any. - `plugin.clearCache` empties the cache and leaves the settings, and `plugin.list` and + (`plugin.install` with `replace: true`, after the user agreed) keeps the storage; `plugin.remove` deletes it, and a + run of the plug-in still going gets `-5` from every storage call after that. `plugin.remove` first renames the folder, + adding `.removed` to its name (or `.removing` while a `.removed` folder holds settings an earlier removal kept). When + that fails, `plugin.remove` fails and the plug-in stays installed with its storage as it was. Before VectorCraft + deletes a renamed folder, it writes an empty file beside it with `.delete` added to the folder's name, and removes + that file once the folder is gone. When that file can't be written, the folder is not deleted and is treated like any + renamed folder without a `.delete` file (see below). Files that can't be deleted stay in the renamed folder with the + `.delete` file beside it, where no plug-in reaches them. A later `plugin.remove` tries again to delete them when a + plug-in with that id is installed or a storage folder for the id exists, and that deletion can fail too. For an id + that isn't installed and has only such a folder, `plugin.remove` does not delete it and fails as not found. When + `plugin.remove` can't remove the grant and can't rename the folder back either, its error names the renamed folder. + The next load, install or `plugin.remove` of the plug-in renames a renamed folder that has no `.delete` file back + while the plug-in has no folder of its own; when it has one, that folder is used and the renamed folder stays until + `plugin.remove` deletes both. `plugin.remove` with the id of a plug-in that isn't installed deletes the storage kept + for that id, when there is any. `plugin.clearCache` empties the cache and leaves the settings, and `plugin.list` and `plugin.info` give the cache's size as `cacheBytes`. - Without a `Plug-ins` folder (the web build, and embedders that attach none) the storage calls return `-5`. @@ -458,8 +451,9 @@ has the id, `-6` when a plug-in may not call the command, `-7` when the command `-8` when the command ran and failed, `-9` when a limit below is passed, and `-11` when nothing can be called (from the module's start function, in a run that isn't a filter's run on a document, or after a call in the run failed). -- Every code also fails the run, whatever the plug-in does next, and the error names the call and why it failed. The run's calls and its output are one undo step, named after the plug-in or its command. A run that fails, - traps, runs out of budget or is canceled leaves every document, its selection and its history as they were. +- Every code also fails the run, whatever the plug-in does next, and the error names the call and why it failed. The + run's calls and its output are one undo step, named after the plug-in or its command. A run that fails, traps, runs + out of budget or is canceled leaves every document, its selection and its history as they were. - Each call runs on the document the run is for, with its params checked as any request's are, and sees what the calls before it changed: a `select.all` followed by `object.move` moves everything. The filter's own input is the one it was given when the run started; an edit still in progress (typing, or a shape being drawn) is kept as its own undo @@ -540,18 +534,17 @@ the module's start function, in a run that isn't a filter's run on a document, o no empty part between dots. A malformed list is refused when the module is installed, and the error names the id. - `params`: in dialog order. Names are `[A-Za-z0-9_]`, not starting with `_`, and not `id`, `ids`, `params` or `preview`. The host validates the user's values before calling: missing values take the default, numbers are - clamped to `min..max` (ints rounded), a wrong type or an unknown choice is an error. Numeric defaults - are also clamped to their range, including the implicit zero default of an `int` parameter. Besides `number`, - `int`, `bool` and `choice` there are `string` (`{"type": "string", "default": "", "maxLength": 256}`: a text field; - longer text is cut to `maxLength`, at most 4,096 characters) and `numberList` (`{"type": "numberList", "min": 0, - "max": 360, "default": [45, 135], "minItems": 1, "maxItems": 16}`: numbers typed separated by commas or spaces, - each clamped to `min..max`; a list of too few or too many is an error, and `maxItems` is at most 256) and `color` - (`{"type": "color", "default": "#3a7bd5"}`: a color button that opens a color picker; the default, black when left - out, and a value given to `plugin.run` may be a color object as paints hold one or a hex string, and the plug-in - always gets a color object, its components clamped, such as `{"model": "rgb", "r": 0.23, "g": 0.48, "b": 0.84}`; a - hex string and the picker's choice are display colors, given as the working RGB color that shows as them, so in a - working RGB space other than sRGB the numbers differ from the hex). Hosts from before `color` refuse a manifest that - uses it. A plug-in declares up to 256 + clamped to `min..max` (ints rounded), a wrong type or an unknown choice is an error. Numeric defaults are also clamped + to their range, including the implicit zero default of an `int` parameter. Besides `number`, `int`, `bool` and + `choice` there are `string` (`{"type": "string", "default": "", "maxLength": 256}`: a text field; longer text is cut + to `maxLength`, at most 4,096 characters) and `numberList` (`{"type": "numberList", "min": 0, "max": 360, "default": + [45, 135], "minItems": 1, "maxItems": 16}`: numbers typed separated by commas or spaces, each clamped to `min..max`; a + list of too few or too many is an error, and `maxItems` is at most 256) and `color` (`{"type": "color", "default": + "#3a7bd5"}`: a color button that opens a color picker; the default, black when left out, and a value given to + `plugin.run` may be a color object as paints hold one or a hex string, and the plug-in always gets a color object, its + components clamped, such as `{"model": "rgb", "r": 0.23, "g": 0.48, "b": 0.84}`; a hex string and the picker's choice + are display colors, given as the working RGB color that shows as them, so in a working RGB space other than sRGB the + numbers differ from the hex). Hosts from before `color` refuse a manifest that uses it. A plug-in declares up to 256 parameters, at most 64 of them outside groups. Each parameter may also say how dialogs show it. Hosts from before these keys ignore them, so a plug-in that @@ -576,17 +569,18 @@ parameters uses `label`, `unit`, `help` and `visibleWhen`, and lists the paramet order (bottom first): the selected ones and those inside selected groups and layers; hidden, locked, guide (compound guides too) and clipping paths are left out, and so are other kinds of object (type, images, symbols, live blends…), which a filter never touches unless its manifest lists them in `inputs` ([reading type, images and -symbols](#reading-type-images-and-symbols)). With nothing selected the list is empty (a generator then adds its own objects). A live effect gets one -object: its geometry (`type`, `path`, `fillRule`, `bounds`) and its `fills` and `strokes`, as a filter gets them (hosts -from before these give geometry only). The paints are read only: an effect may draw as wide as a stroke or use a -fill's colour to decide its shape, but it changes only the geometry, unless it declares `paintOutput` ([painted live -effects](#painted-live-effects)). +symbols](#reading-type-images-and-symbols)). With nothing selected the list is empty (a generator then adds its own +objects). A live effect gets one object: its geometry (`type`, `path`, `fillRule`, `bounds`) and its `fills` and +`strokes`, as a filter gets them (hosts from before these give geometry only). The paints are read only: an effect may +draw as wide as a stroke or use a fill's colour to decide its shape, but it changes only the geometry, unless it +declares `paintOutput` ([painted live effects](#painted-live-effects)). An **exporter** gets something else: the native `.vectorcraft` document JSON without indentation, optional preview/PDF -renditions or save-preference metadata (the format is documented in [`crates/format/src/lib.rs`](../crates/format/src/lib.rs)). That is every layer, object, -appearance and effect record, artboard, swatch and symbol, so an exporter reads whatever the document holds; it checks -`version`, since a later host may write a newer one. Images of 8 KiB or more are listed in `images` with their MIME -type and `blob` but without their data, which follows the JSON in a saved file; smaller ones keep their base64 `data`. +renditions or save-preference metadata (the format is documented in +[`crates/format/src/lib.rs`](../crates/format/src/lib.rs)). That is every layer, object, appearance and effect record, +artboard, swatch and symbol, so an exporter reads whatever the document holds; it checks `version`, since a later host +may write a newer one. Images of 8 KiB or more are listed in `images` with their MIME type and `blob` but without their +data, which follows the JSON in a saved file; smaller ones keep their base64 `data`. An **importer** gets the bytes of the file being opened, as they are (not JSON), at most 128 MiB of them. @@ -615,12 +609,11 @@ An **importer** gets the bytes of the file being opened, as they are (not JSON), (absent: no handle), `kind` `"Smooth"` when the handles stay collinear. Coordinates are points, y down. - `fills`, `strokes`: the object's fills and strokes in paint order (bottom first); `null` is no paint. A stroke has its `paint`, `width` in points, `join` (`"miter"`, `"round"` or `"bevel"`), `cap` (`"butt"`, `"round"` or `"square"`) - and `miterLimit`. Paints use the - native file format's form: `{"type": "solid", "color", "swatch"?, "tint"?}`, `{"type": "gradient", "gradient": - {"kind": "Linear"|"Radial"|"Freeform", "stops": [{offset, color, opacity, midpoint}], "dither"?, "method"?: "Perceptual"}, "geom"?, "angle", - "freeform"?}`, `{"type": "pattern", "pattern", "xf"?}`. Colours are `{"model": "rgb", r, g, b}`, `{"model": "cmyk", - c, m, y, k}`, `{"model": "gray", k}` (ink: 0 white, 1 black) or `{"model": "lab", l, a, b}`; components 0–1 except - Lab's. + and `miterLimit`. Paints use the native file format's form: `{"type": "solid", "color", "swatch"?, "tint"?}`, + `{"type": "gradient", "gradient": {"kind": "Linear"|"Radial"|"Freeform", "stops": [{offset, color, opacity, + midpoint}], "dither"?, "method"?: "Perceptual"}, "geom"?, "angle", "freeform"?}`, `{"type": "pattern", "pattern", + "xf"?}`. Colours are `{"model": "rgb", r, g, b}`, `{"model": "cmyk", c, m, y, k}`, `{"model": "gray", k}` (ink: 0 + white, 1 black) or `{"model": "lab", l, a, b}`; components 0–1 except Lab's. - `effects` (filters): the object's own effects in order, as its effect records: `id` (as `effect.list` lists it, or `plugin.` for a live-effect plug-in), `params` and `visible`. Its fills' and strokes' own effects aren't listed. - `bounds`: `[x0, y0, x1, y1]`, the geometric bounds (read only). @@ -631,9 +624,9 @@ A filter whose manifest lists `inputs` also gets the selected objects of those k and under the same rules (hidden and locked ones, objects that clip a clip group, alone or inside a group that clips it, and anything inside a symbol, a placed document or a live object are left out). They are never turned into outlines, rectangles or the symbol's art. Each has `id`, `type`, `name` (when it has one), `bounds` (`null` until the -object has been laid out or drawn) and -`transform`, `[a, b, c, d, e, f]` mapping its own space (text space, pixel space, the symbol's art) into the document. -In this example the character and paragraph styles are cut short; a filter gets every attribute: +object has been laid out or drawn) and `transform`, `[a, b, c, d, e, f]` mapping its own space (text space, pixel space, +the symbol's art) into the document. In this example the character and paragraph styles are cut short; a filter gets +every attribute: ```json {"objects": [ @@ -662,13 +655,13 @@ In this example the character and paragraph styles are cut short; a filter gets `pixelsOmitted: false`. A CMYK image's pixels are its inks through the working CMYK profile, as sRGB. The pixels of all the images in one input take at most 16 MiB in base64, and an image the host starts to decode takes its size from those 16 MiB even when decoding fails. A larger image, one whose pixels don't fit what the images before - it in paint order left, and a linked image (`linked: true`) have - `pixelsOmitted: true` and no pixels; the host checks an image's size before it decodes the image. An image of any - size also has `pixelsOmitted: true` when its data is missing from the document, when its width or height is 0, when - its data cannot be decoded (including a non-CMYK image whose decoder enforces the host's 64 MiB memory limit and - needs more, and a TIFF the host rejects as too large), or when the dimensions in its data differ from `pixelSize`. - A plug-in checks `pixelsOmitted` before it reads `pixelsBase64`, which is present only when `pixelsOmitted` is `false`. The host never - reads a linked image's file for a filter and never gives its path. + it in paint order left, and a linked image (`linked: true`) have `pixelsOmitted: true` and no pixels; the host checks + an image's size before it decodes the image. An image of any size also has `pixelsOmitted: true` when its data is + missing from the document, when its width or height is 0, when its data cannot be decoded (including a non-CMYK image + whose decoder enforces the host's 64 MiB memory limit and needs more, and a TIFF the host rejects as too large), or + when the dimensions in its data differ from `pixelSize`. A plug-in checks `pixelsOmitted` before it reads + `pixelsBase64`, which is present only when `pixelsOmitted` is `false`. The host never reads a linked image's file for + a filter and never gives its path. - A symbol instance (`"symbolInstance"`): `symbolId` is the name of the symbol it shows, with `missing: true` when the document has no symbol of that name. `overrides` is `{}`. @@ -699,16 +692,16 @@ toward the 64 MiB output cap. ``` `mode` is `"filter"`, `"effect"`, `"export"`, `"import"` or `"command"` (a [command](#commands) run, whose -`command` is the command's id); `bounds` the selection's geometric bounds (a live effect: the reference -box of the effect, the object's bounds or those of the previous effect's result); `artboard` (filters) the artboard -under the selection's centre, else the first; `documentData` (filters) what the plug-in last kept with the document -(`null` until it keeps something; see Output). A live effect's `item` says where it sits: `"object"` (one of the -object's effects), or `{"fill": i}` / `{"stroke": i}` (in the own effects of the object's `i`th fill or stroke, -counted from the bottom as in `fills` and `strokes`). An export's `_context` is `{"mode": "export", "extension": "plt", -"name": "Poster"}` instead: the extension of the file being written (one the exporter lists) and its name without the -extension (the document's name when the export is not given a path), which some formats keep in their header. An -import's `_context` is `{"mode": "import", "extension": "plt", "name": "Poster"}`: the extension of the file being -opened and its name without the extension. +`command` is the command's id); `bounds` the selection's geometric bounds (a live effect: the reference box of the +effect, the object's bounds or those of the previous effect's result); `artboard` (filters) the artboard under the +selection's centre, else the first; `documentData` (filters) what the plug-in last kept with the document (`null` until +it keeps something; see Output). A live effect's `item` says where it sits: `"object"` (one of the object's effects), or +`{"fill": i}` / `{"stroke": i}` (in the own effects of the object's `i`th fill or stroke, counted from the bottom as in +`fills` and `strokes`). An export's `_context` is `{"mode": "export", "extension": "plt", "name": "Poster"}` instead: +the extension of the file being written (one the exporter lists) and its name without the extension (the document's name +when the export is not given a path), which some formats keep in their header. An import's `_context` is `{"mode": +"import", "extension": "plt", "name": "Poster"}`: the extension of the file being opened and its name without the +extension. ### Output @@ -785,17 +778,17 @@ is a piece drawn by itself, in the order returned, with paints of its own: path or compound path is hit-tested on its source path and appearance. On an object with no geometry effect of its own, Pathfinder effects and compound shapes use the object's path for an effect on a fill or stroke, as they do for every other effect on a fill or stroke; on an object that has one, they also use the pieces of a painted effect on a - fill or stroke. A 3D effect later in the stack uses the - object's input path when the pieces name a swatch or pattern. Each piece counts against the budget of the - object's geometry effects as well as its anchors, so all the painted effects in a stack make at most 100,000 pieces - together; a run whose pieces would pass the budget draws its input as it was. + fill or stroke. A 3D effect later in the stack uses the object's input path when the pieces name a swatch or pattern. + Each piece counts against the budget of the object's geometry effects as well as its anchors, so all the painted + effects in a stack make at most 100,000 pieces together; a run whose pieces would pass the budget draws its input as + it was. - The result is checked whole before any of it is drawn: the limits of a filter's output (100,000 objects, 4,000,000 anchors, 256 fills and 256 strokes per piece, 256 stops per gradient), finite numbers in range with nothing clamped (color components, tints, offsets, opacities, midpoints 0.13 to 0.87, stops in order of `offset`, stroke - widths 0 to 1,000 pt, miter limits 1 to 500), and every swatch a solid paint, gradient or stop links to and every pattern named in - the document. One fault refuses the whole result: that step of the stack draws its input as it was, the effect - record stays, and `plugin.info`'s `lastError` names the plug-in and the field, for example - `Test Tone: plug-in output: objects[1].strokes[0].width: …`. + widths 0 to 1,000 pt, miter limits 1 to 500), and every swatch a solid paint, gradient or stop links to and every + pattern named in the document. One fault refuses the whole result: that step of the stack draws its input as it was, + the effect record stays, and `plugin.info`'s `lastError` names the plug-in and the field, for example `Test Tone: + plug-in output: objects[1].strokes[0].width: …`. - The pieces are never written into the document while it is drawn, measured, previewed, saved or exported. PDF and SVG export draw them as the canvas does, from a copy of the document. Object › Expand Appearance writes each piece as an ordinary path or compound path with its fills and strokes, as one undo step. Object › Expand of gradient and pattern @@ -823,12 +816,13 @@ crates/plugins/example/build.sh # cargo build --release --target wasm32-unk The script also refreshes `crates/plugins/tests/fixtures/desaturate.wasm`, the test fixture built from that source. The crate is not a workspace member. Any language that targets core WebAssembly works (C, Zig, AssemblyScript, -WAT…), as long as the module imports nothing but the host imports: in Rust, `std` on `wasm32-unknown-unknown` is fine as long as it never -touches WASI. `vectorcraft_plugins::wat::module` is the skeleton of a plug-in in WebAssembly text, which the tests -build their plug-ins from; `wat::echo_exporter` is an exporter whose file is its input or its parameters (with a -base64 encoder written out in WebAssembly text), which the tests read back what the host hands a plug-in with, and -`wat::when_given` a plug-in whose output depends on whether its input or parameters hold a given text, which shows in a -live effect's geometry. `wat::reporting` and `wat::cooperating` build level 2 plug-ins that import the host functions. +WAT…), as long as the module imports nothing but the host imports: in Rust, `std` on `wasm32-unknown-unknown` is fine as +long as it never touches WASI. `vectorcraft_plugins::wat::module` is the skeleton of a plug-in in WebAssembly text, +which the tests build their plug-ins from; `wat::echo_exporter` is an exporter whose file is its input or its parameters +(with a base64 encoder written out in WebAssembly text), which the tests read back what the host hands a plug-in with, +and `wat::when_given` a plug-in whose output depends on whether its input or parameters hold a given text, which shows +in a live effect's geometry. `wat::reporting` and `wat::cooperating` build level 2 plug-ins that import the host +functions. ## Performance