Implement the penpot_add_svg tool with svg-raw fallback and conversion reporting #210

Closed
opened 2026-09-14 23:13:57 +00:00 by david · 1 comment
Owner

Summary

Implement penpot_add_svg: take an SVG (file path or inline string), convert it with the SVG→shapes converter, stage the resulting native shapes plus svg-raw fallbacks for anything unsupported, and report back exactly what was converted and what was not.

Background

Depends on: #209

The converter produces descriptors; this step makes them real staged changes and gives the agent honest feedback. Two behaviours matter beyond the plumbing:

  1. Colour→library matching. Where an SVG fill colour matches a colour asset already in the target file (or a linked library), the shape should use the asset reference (fillColorRefId/fillColorRefFile) rather than a literal hex. That is what keeps imported artwork editable within the design system instead of dragging in a parallel palette. Matching is by resolved hex value (case-insensitive), and ambiguity (two assets with the same hex) must be reported, not guessed.
  2. Honest reporting. The result must state which nodes converted, which fell back to svg-raw and why. An agent that gets a silent drop produces a design that looks wrong for reasons nobody can see; an agent that gets a reason can fix the input or tell the user.

svg-raw shapes are a legitimate outcome, not a failure: they keep unsupported artwork visible and editable-as-an-object, which is strictly better than losing it.

Documentation Required

A separate process downloads these into the listed folders before this issue is implemented. Check the folders for the actual reference material before starting.

docs/reference/penpot-api/

docs/reference/svg/

docs/reference/pi-coding-agent/ and docs/reference/typebox/

Implementation Details

Create extensions/penpot/src/tools/svg.ts and register the tool.

Parameters:

  • file_id (string, required).
  • path (string, optional) — path to an .svg file to read.
  • svg (string, optional) — inline SVG markup. Exactly one of path/svg must be provided; a runtime check with a clear error.
  • x, y (numbers, optional, default 0) — placement offset for the root of the imported artwork.
  • parent_id (string, optional) — the frame to place into; defaults to the page root.
  • page_id (string, optional).
  • scale (number, optional, default 1) — uniform scale applied at import, so an icon can be placed at a usable size.
  • name_prefix (string, optional) — prefix for generated layer names (defaults to the file basename or svg).

Behaviour:

  1. Read/resolve the markup; refuse a missing/empty file and non-SVG input with a clear message.
  2. Convert (convertSvg) before staging anything: a parse error must leave the changeset untouched and report the offending offset/snippet.
  3. Match fills against the file's colour assets (own + linked libraries) by hex; on a match, emit fillColorRefId/fillColorRefFile; on a duplicate-hex ambiguity, prefer the literal colour and report the ambiguity in the result. Provide a link_colors boolean (default true) to disable matching entirely.
  4. Emit add-obj changes for every converted shape and every svg-raw fallback, in parent-first order, with geometry offset by x/y and scaled by scale.
  5. Return: a text report listing counts by type, converted vs fallback, the distinct fallback reasons, the colour-matching outcome (matched / ambiguous / literal), plus details carrying the staged shape ids, the skipped list with reasons and the report object.
  6. Do not write; penpot_commit applies everything atomically.

Acceptance Criteria

  • Exactly one of path/svg is required, and giving neither or both produces a clear error.
  • A malformed SVG produces a parse error with the offending position and stages nothing.
  • Converted nodes become native shapes; unsupported nodes become svg-raw shapes carrying the original markup.
  • The result text reports converted vs fallback counts and the distinct fallback reasons, and never omits a skipped node.
  • SVG fills whose hex matches a file colour asset are emitted as asset references (fillColorRefId/fillColorRefFile); the match is case-insensitive on hex.
  • An ambiguous hex (two assets with the same colour) is reported and the literal colour used, rather than an arbitrary asset being chosen.
  • link_colors: false disables matching and leaves literal hex fills.
  • x/y offset and scale are applied to every emitted shape, with geometry and selrect consistent.
  • Layer names use path/svg id/name attributes where present, prefixed as requested.
  • Nothing is written before commit; the unit test asserts no update-file call.

Test Plan

node --test extensions/penpot/src/tools/svg.test.ts

Live validation (requires PENPOT_URL/PENPOT_TOKEN, scratch file with library colours):

  1. Authorise the scratch file; stage a frame and import a simple icon SVG via path; commit.
  2. In the Penpot UI confirm the icon is a set of native shapes (rects/paths), selectable and individually editable, with the expected layer names.
  3. Re-import the same SVG with a colour equal to one of the file's library colours; confirm in the UI that the fill shows as a linked swatch.
  4. Import an SVG that uses a gradient, a filter and an A-command path; confirm those nodes landed as svg-raw objects (visible on the canvas) and that the tool's report lists each fallback reason.
  5. Import a malformed SVG and confirm the error names the position and that the file's revn is unchanged.
## Summary Implement `penpot_add_svg`: take an SVG (file path or inline string), convert it with the SVG→shapes converter, stage the resulting native shapes plus `svg-raw` fallbacks for anything unsupported, and report back exactly what was converted and what was not. ## Background **Depends on:** #209 The converter produces descriptors; this step makes them real staged changes and gives the agent honest feedback. Two behaviours matter beyond the plumbing: 1. **Colour→library matching.** Where an SVG fill colour matches a colour asset already in the target file (or a linked library), the shape should use the asset reference (`fillColorRefId`/`fillColorRefFile`) rather than a literal hex. That is what keeps imported artwork editable within the design system instead of dragging in a parallel palette. Matching is by resolved hex value (case-insensitive), and ambiguity (two assets with the same hex) must be reported, not guessed. 2. **Honest reporting.** The result must state which nodes converted, which fell back to `svg-raw` and **why**. An agent that gets a silent drop produces a design that looks wrong for reasons nobody can see; an agent that gets a reason can fix the input or tell the user. `svg-raw` shapes are a legitimate outcome, not a failure: they keep unsupported artwork visible and editable-as-an-object, which is strictly better than losing it. ## Documentation Required A separate process downloads these into the listed folders before this issue is implemented. Check the folders for the actual reference material before starting. **`docs/reference/penpot-api/`** - https://raw.githubusercontent.com/penpot/penpot/2.17.2/common/src/app/common/types/shape.cljc — the `svg-raw` shape variant and what it needs (its raw content plus the standard geometry fields). Confirm the field that carries the markup on this version. - https://raw.githubusercontent.com/penpot/penpot/2.17.2/common/src/app/common/types/path.cljc — path shape `content`, for the converted `path` nodes. - https://raw.githubusercontent.com/penpot/penpot/2.17.2/common/src/app/common/types/fills.cljc — fill records, including the asset-reference fields used for colour matching. - `<PENPOT_URL>/api/main/doc/openapi.json` — `update-file`/`add-obj`, and the exact `svg-raw` payload the server will validate. - https://help.penpot.app/user-guide/ — how imported artwork appears and behaves in the Layers panel/Toolbox, to word the tool description accurately. **`docs/reference/svg/`** - https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element — the element set the converter claims to support (so the tool description states the same subset). - https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/fill and .../stroke — the colour/`none` semantics being matched against library assets. **`docs/reference/pi-coding-agent/`** and **`docs/reference/typebox/`** - https://pi.dev/docs/latest/extensions — tool registration, tool `description` wording, and structured `details` for the conversion report. - https://github.com/sinclairzx81/typebox — string/boolean parameter schemas. ## Implementation Details Create `extensions/penpot/src/tools/svg.ts` and register the tool. Parameters: - `file_id` (string, **required**). - `path` (string, optional) — path to an `.svg` file to read. - `svg` (string, optional) — inline SVG markup. Exactly one of `path`/`svg` must be provided; a runtime check with a clear error. - `x`, `y` (numbers, optional, default 0) — placement offset for the root of the imported artwork. - `parent_id` (string, optional) — the frame to place into; defaults to the page root. - `page_id` (string, optional). - `scale` (number, optional, default 1) — uniform scale applied at import, so an icon can be placed at a usable size. - `name_prefix` (string, optional) — prefix for generated layer names (defaults to the file basename or `svg`). Behaviour: 1. Read/resolve the markup; refuse a missing/empty file and non-SVG input with a clear message. 2. Convert (`convertSvg`) **before** staging anything: a parse error must leave the changeset untouched and report the offending offset/snippet. 3. Match fills against the file's colour assets (own + linked libraries) by hex; on a match, emit `fillColorRefId`/`fillColorRefFile`; on a duplicate-hex ambiguity, prefer the literal colour and report the ambiguity in the result. Provide a `link_colors` boolean (default `true`) to disable matching entirely. 4. Emit `add-obj` changes for every converted shape and every `svg-raw` fallback, in parent-first order, with geometry offset by `x`/`y` and scaled by `scale`. 5. Return: a text report listing counts by type, converted vs fallback, the distinct fallback reasons, the colour-matching outcome (matched / ambiguous / literal), plus `details` carrying the staged shape ids, the skipped list with reasons and the report object. 6. Do not write; `penpot_commit` applies everything atomically. ## Acceptance Criteria - [ ] Exactly one of `path`/`svg` is required, and giving neither or both produces a clear error. - [ ] A malformed SVG produces a parse error with the offending position and stages **nothing**. - [ ] Converted nodes become native shapes; unsupported nodes become `svg-raw` shapes carrying the original markup. - [ ] The result text reports converted vs fallback counts and the distinct fallback reasons, and never omits a skipped node. - [ ] SVG fills whose hex matches a file colour asset are emitted as asset references (`fillColorRefId`/`fillColorRefFile`); the match is case-insensitive on hex. - [ ] An ambiguous hex (two assets with the same colour) is reported and the literal colour used, rather than an arbitrary asset being chosen. - [ ] `link_colors: false` disables matching and leaves literal hex fills. - [ ] `x`/`y` offset and `scale` are applied to every emitted shape, with geometry and `selrect` consistent. - [ ] Layer names use `path`/`svg` `id`/`name` attributes where present, prefixed as requested. - [ ] Nothing is written before commit; the unit test asserts no `update-file` call. ## Test Plan ```bash node --test extensions/penpot/src/tools/svg.test.ts ``` Live validation (requires `PENPOT_URL`/`PENPOT_TOKEN`, scratch file with library colours): 1. Authorise the scratch file; stage a frame and import a simple icon SVG via `path`; commit. 2. In the Penpot UI confirm the icon is a set of native shapes (rects/paths), selectable and individually editable, with the expected layer names. 3. Re-import the same SVG with a colour equal to one of the file's library colours; confirm in the UI that the fill shows as a **linked** swatch. 4. Import an SVG that uses a gradient, a filter and an `A`-command path; confirm those nodes landed as `svg-raw` objects (visible on the canvas) and that the tool's report lists each fallback reason. 5. Import a malformed SVG and confirm the error names the position and that the file's `revn` is unchanged.
david closed this issue 2026-09-15 06:53:16 +00:00
Author
Owner

pi-loop opened and merged a pull request for this issue: #243

pi-loop opened and merged a pull request for this issue: https://git.excelera.net/david/pi-extensions-and-skills/pulls/243
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
david/pi-extensions-and-skills#210
No description provided.