Plug-ins: filters at API level 2 read type, images and symbol instances

A filter manifest at `api` 2 may list `inputs` (`type`, `image`,
`symbolInstance`). Such a filter is also given the selected objects of
those kinds, read only: type with its runs and paragraph attributes,
images with their pixel size, color mode and, when embedded and small
enough, their RGBA pixels in base64 (never a linked file's pixels or
path), and symbol instances with their symbol and transform.

The output must return each read-only object as {"id": ...} alone or
exactly as given. A change, an omission or a duplicate is refused
before the edit, so the document and undo history stay as they were,
including for output handed in from a heavy run. A filter without
`inputs` gets the same input bytes as before.

A manifest that lists `inputs` below `api` 2, outside a filter, with an
unknown or repeated kind, or with a permission list that lacks
`doc.read` does not install.

Part of #4333
This commit is contained in:
dotCooCoo
2026-11-07 23:34:20 -08:00
parent 0a702cc0a5
commit 68609fb43b
36 changed files with 956 additions and 35 deletions
+57 -5
View File
@@ -145,7 +145,10 @@ capability model proposed for PhotoCraft plug-ins
`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 higher one is refused. `plugin.list` and `plugin.info` give it as `api`.
imports](#host-imports-api-level-2) and a filter's `inputs` ([reading type, images and
symbols](#reading-type-images-and-symbols)). A higher one is refused. `plugin.list` and `plugin.info` give it as
`api`. `inputs` is not a permission: the objects it asks for are read under `doc.read`, which a filter that lists
`permissions` and `inputs` must list.
- Parameter declarations are unchanged by permissions (`params`, below).
Every run gets a **fresh instance**, so no state survives between calls. Traps, exhausted budgets, malformed
@@ -237,6 +240,11 @@ still returns `1`.
filters](#heavy-filters)). Hosts from before it ignore the key, so there the filter gets a light run's budget.
- `api`: the plug-in API level, `1` when left out. `permissions`: what the plug-in may do, a list of names
([permissions](#permissions)); without it, what its kind implies.
- `inputs` (filters at `api` 2 only): the kinds of object besides paths and compound paths the filter reads, any of
`"type"`, `"image"` and `"symbolInstance"`, each at most once (`"inputs": ["type", "image"]`). Left out or `[]`, the
filter gets paths and compound paths alone, as before. An unknown, repeated or wrongly typed entry, `inputs` on
another kind, below `api` 2, or with `permissions` that leave out `doc.read`, is refused when the plug-in is
installed, and so is `api` 2 on hosts from before it.
- `extensions` (exporters only, and required): 1–8 file extensions, lower-case letters and digits without the dot
(`["plt"]`); the first is the one Export As gives the file. A built-in format keeps an extension both list (`svg`):
`document.export` then writes the exporter's format only when asked for by its format id.
@@ -276,8 +284,9 @@ parameters uses `label`, `unit`, `help` and `visibleWhen`, and lists the paramet
`input` is UTF-8 JSON: `{"objects": [...]}`. An object filter gets the selected paths and compound paths in paint
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. With nothing selected the list is empty (a generator then adds its own objects). A live effect gets one
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.
@@ -323,6 +332,49 @@ type and `blob` but without their data, which follows the JSON in a saved file;
`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).
### Reading type, images and symbols
A filter whose manifest lists `inputs` also gets the selected objects of those kinds, in paint order among the paths
and under the same rules (hidden and locked ones, type that clips a clip group 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:
```json
{"objects": [
{"id": 17, "type": "type", "bounds": [10, 38, 52, 52], "transform": [1, 0, 0, 1, 10, 50],
"text": {"kind": "point", "vertical": false,
"runs": [{"text": "Hello\nthere", "style": {"font_family": "Source Sans 3", "size": 12}}],
"paragraphs": [{"justify": "Left"}, {"justify": "Center"}]},
"truncated": false},
{"id": 18, "type": "image", "bounds": [0, 0, 4, 2], "transform": [2, 0, 0, 2, 0, 0], "pixelSize": [2, 1],
"colorMode": "rgb", "linked": false, "pixelsOmitted": false, "pixelsBase64": "AAAegGQAHoA="},
{"id": 19, "type": "symbolInstance", "bounds": [100, 0, 110, 10], "transform": [1, 0, 0, 1, 100, 0],
"symbolId": "Star", "overrides": {}}
]}
```
- Type (`"type"`): `text.kind` is `"point"`, `"area"` or `"path"`; `runs` are its text runs with their character
attributes as the native file stores them (`inline` when a run is an inline graphic), and `paragraphs` holds one
paragraph style per paragraph (the paragraphs are split at `\n`). No font data or glyph outlines are given. Type
whose runs hold more than 256 KiB of UTF-8 text, or more than 4,096 runs or paragraphs, comes with `truncated:
true` and empty `runs` and `paragraphs`.
- An image (`"image"`): `pixelSize` is its width and height in pixels; `colorMode` is `"rgb"`, `"cmyk"`, `"gray"` or
`"unknown"`, read from the image itself. An embedded image whose pixels take at most 1 MiB in base64 (443 × 443
pixels fit) has them in `pixelsBase64`: straight RGBA, 8 bits a channel, row by row from the top, and
`pixelsOmitted: false`. A larger one, and a linked image (`linked: true`), has `pixelsOmitted: true` and no pixels.
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 `{}`.
These objects are read only. The output must list each of them exactly once, as `{"id": 17}` alone or exactly as it
was given (numbers compare by value, so `1` and `1.0` are the same). A changed or added field, one left out, an object
listed twice or left out, and a new object with one of these types are refused, and the document and undo history
stay as they were: `objects[1] (id 17): read-only field 'text.runs[0].text' changed`, `objects (id 17): a read-only
object was left out`. New objects listed after one of them go just above it. Their text and pixels count towards the
input's half of the memory cap, and an echo of them towards the 64 MiB output cap.
### Parameters
`params` points at `params_len` bytes of UTF-8 JSON: the validated parameters, plus `_context`:
@@ -420,6 +472,6 @@ per change of their input, parameters or paints (results are cached), not per fr
- Tool plug-ins (pointer events on the canvas, a guide line or rectangle, geometry back), panel plug-ins, importers
(plug-in formats that open files).
- Live effects that change paint (they reshape geometry; an object filter recolours), and filters on type, images or
symbols.
- Live effects that change paint (they reshape geometry; an object filter recolours), and filters that change type,
images or symbol instances (a filter at `api` 2 reads them, see [`inputs`](#reading-type-images-and-symbols)).
- Apply Last Effect and Last Effect work for plug-in effects; there is no "last filter" for object filters yet.