Merge pull request #6872 from dotCooCoo/docs/plugins-safe-mode-wrap

Docs: docs/plugins.md paragraphs wrap at the file's width
This commit is contained in:
BFlat
2026-11-12 21:03:53 +07:00
committed by GitHub
+151 -157
View File
@@ -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.<id>`, 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.<id>`, 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.<id>", "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.<id>", "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:<id>`); 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 `<plug-in id>: <text>`, 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.<id>` 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