M6: SVG import fidelity (#90)

SVG import comes in editable and looking right: groups named from their
ids with transforms baked into the layers (rotated, scaled and skewed
groups, viewBox scaling and <use> offsets used to be lost); rectangles,
ellipses and lines recognised and kept as those shapes; gradients with
exact handles, reflect/repeat and focal points; patterns as image fills;
clip paths and masks as vector, luminance and alpha masks; shadow and
blur filter chains as drop shadows, inner shadows and layer blurs; blend
modes; text as text layers (it used to vanish); images and nested SVG
images. What can't be represented is reported as import warnings
(file.import / file.open warnings, figcraft-cli info notes). Linked
files are never read, and documents nested too deep for the parser are
refused instead of crashing.

Pasted SVG is recognised after XML declarations, comments and doctypes
and lands centred in the view (file.import center). SVG export writes
vector masks as clip paths and alpha/luminance masks as the mask layer
drawn, so masks survive a round trip.

Tests: a corpus of 21 synthetic SVG files with hand-worked expected
pixels (158 probes), structure checks and export round trips; unit tests
for shape recognition, paints, filters, structure, mask export; hostile
input and byte-mangled corpus fuzzing; engine file command tests.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
BFlat
2026-10-10 20:36:55 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 4904303fc2
commit e27f2751d4
45 changed files with 3391 additions and 267 deletions
+21 -1
View File
@@ -135,7 +135,7 @@ Or in a project's `.mcp.json`:
| `select {ids}` | Replace the selection (`[]` deselects). |
| `export {ids, format, scale \| size, path?}` | png, jpg, svg, webp, pdf; `size` is `2x`, `512w` or `512h`. PDF: frames one page each (sized in points), anything else together on one page; `run_command` `file.exportFramesPdf {page?, path?}` writes every top-level frame of a page. Without `path`, images come back as MCP image content, SVG as text. Layers' own export settings: `run_command` with `export.add` / `export.update {index, size?, suffix?, format?}` / `export.remove` and `file.exportLayers {ids?, dir?}` (every layer once per setting, named `<layer><suffix>.<format>`). |
| `screenshot {ids?, scale?, maxSize?, window?}` | PNG of the page (with background) or of layers, scaled to fit `maxSize` (default 1600). `window: true` captures the app window when attached. |
| `open_file {path, variables?}` / `save_file {path?}` | Open `.figcraft`, `.json` (FigCraft JSON or REST API file JSON with its variables, see below), `.svg` or an image; save as `.figcraft`. |
| `open_file {path, variables?}` / `save_file {path?}` | Open `.figcraft`, `.json` (FigCraft JSON or REST API file JSON with its variables, see below), `.svg` (see Importing SVG below) or an image; save as `.figcraft`. |
| `undo` / `redo` | Step through history. |
| `pointer {kind, x, y, to?, steps?, tool?, shift, alt, cmd}` | Mouse gestures in page coordinates with the current tool: `click`, `double`, `drag`, `down`, `move`, `up`, `hover`. |
| `key {key, shift, alt, cmd}` | A shortcut or key as if typed on the canvas (every default shortcut, tool keys, Escape, Delete, arrows, number keys for opacity; while editing text, the text shortcuts and then text editing keys). Keys are names like `A`, `0`, `Enter`, `ArrowLeft`, `=`, `/`, or egui's (`Plus`, `Questionmark`); `cmd` is Cmd on macOS and Ctrl elsewhere. |
@@ -352,6 +352,26 @@ click or drag across regions with `pointer` to merge, add `alt` to remove; `shap
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"run_command","arguments":{"command":"shapeBuilder.merge","params":{"ids":[2,3],"points":[[120,80],[160,80]]}}}}
```
### Importing SVG
`open_file {path}` opens an SVG as a new document; `run_command {command: "file.import", params:
{path | data + name, x?, y?, center?}}` adds it to the current page as a frame (top-left at `x, y`,
or centred there with `center: true`, as pasting does). It comes in editable: groups named from
their `id`, transforms baked into the layers, `<rect>`/`<circle>`/`<ellipse>`/`<line>` (and paths
tracing those outlines) as rectangles with their corner radius, ellipses and lines, everything else
as vectors; strokes with their width, dashes, caps, joins and miter limit; linear and radial
gradients with their exact handles (`gradientTransform`, `reflect`/`repeat` unrolled, a focal
point as the centre); patterns as image fills (fill, fit, crop or tile); clip paths as vector masks
and masks as luminance or alpha masks; shadow and blur filters (`feDropShadow`, the offset / blur /
flood / composite chains, colour-matrix shadows, inner shadows, `feMorphology` spread) as drop
shadows, inner shadows and layer blurs; opacity and `mix-blend-mode`; text as text layers (fonts
the SVG names when installed, anchors, letter spacing, styled `<tspan>` runs, stacked lines as one
layer); images as image fills, and SVG images imported in place. CSS, `<use>`/`<symbol>`, units
and `viewBox`/`preserveAspectRatio` are resolved. The result's `warnings` list what was left out
(filters that aren't shadows or blurs, rotated repeating patterns, a clip path's own clip path).
Linked files are never read, only data URIs; documents nested more than 512 elements deep are
refused.
### Importing REST API file JSON
The JSON returned by the public REST API's file endpoint (`GET /v1/files/:key`, best with