mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-10 20:29:33 -06:00
M6: add bounded offline Penpot import and export
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
# Penpot import and export
|
||||
|
||||
FigCraft reads and writes Penpot 2.x ZIP archives (`penpot/export-files`, version 1)
|
||||
in Rust. Import and export run offline on desktop, CLI, MCP and WASM.
|
||||
|
||||
## Open and save
|
||||
|
||||
- **File → Open…** opens a `.penpot` as a separate unsaved document. Dropping it
|
||||
onto the canvas does the same. The original archive is not overwritten.
|
||||
- **File → Import from file…** adds its pages to the current document.
|
||||
- **Save / Save as…** writes native `.figcraft` files.
|
||||
- **Export** with format `penpot` writes an archive for a Penpot team.
|
||||
|
||||
Engine commands use the same importer and exporter:
|
||||
|
||||
```json
|
||||
{"command":"file.open","params":{"path":"example.penpot"}}
|
||||
{"command":"file.import","params":{"path":"example.penpot"}}
|
||||
{"command":"file.export","params":{"format":"penpot","path":"out.penpot"}}
|
||||
```
|
||||
|
||||
Without a path, `file.export` returns base64 `data`. Unsupported conversions are
|
||||
reported in `warnings`; the CLI prints them as notes.
|
||||
|
||||
```sh
|
||||
figcraft-cli info example.penpot --json
|
||||
figcraft-cli convert example.penpot editable.figcraft
|
||||
figcraft-cli export editable.figcraft -o out.penpot --format penpot
|
||||
```
|
||||
|
||||
## Supported content
|
||||
|
||||
| Area | Support |
|
||||
|---|---|
|
||||
| Structure | Pages, boards, groups, masks, nested frames, editable shapes and vectors |
|
||||
| Appearance | Solid, gradient and image fills; strokes, shadows, blur, opacity, blending and transforms |
|
||||
| Text | Paragraphs, styled runs, alignment, sizing, decorations and original font references |
|
||||
| Layout | Constraints, guides, display grids and supported flex/grid layout with fixed, content-sized and fractional tracks |
|
||||
| Components | Local main components, linked copies, overrides and variant sets; unresolved external copies remain independent with a warning |
|
||||
| Tokens | Supported scalar tokens, aliases, theme modes, expressions and matching property bindings; literal compound typography/shadow tokens can become native styles |
|
||||
| Prototypes | Flows, supported pointer/delay triggers, navigation, open/close overlays, back and links, with supported transitions |
|
||||
|
||||
Penpot source ids and selected source properties remain in layer metadata.
|
||||
Fingerprints let export reuse unchanged text, layout, strokes, interactions and
|
||||
tokens instead of regenerating them. Edited supported values are generated from
|
||||
FigCraft's model. New layers receive deterministic UUIDs.
|
||||
|
||||
## Fonts
|
||||
|
||||
Embedded TTF/OTF/TTC/WOFF signatures are detected without decoding their payloads.
|
||||
The current document model does not retain font binaries; import reports this
|
||||
limitation. Install licensed fonts locally when needed. Native saves retain the
|
||||
original family names and bounded provider metadata, rather than the binaries.
|
||||
|
||||
Provider references never trigger downloads. `file.fontReport` reports availability
|
||||
and distinguishes Google provider references from custom library references whose
|
||||
original binary cannot be verified. Missing fonts use local fallback while their
|
||||
original family names remain editable.
|
||||
|
||||
Penpot export writes font references. Install the referenced fonts in the
|
||||
destination Penpot team.
|
||||
|
||||
## Limits
|
||||
|
||||
- Penpot 1.x binary exports are unsupported; re-export from Penpot 2.x.
|
||||
- Malformed archives, oversized members and excessive nesting/layer counts are
|
||||
rejected or bounded. Unsupported features produce import/export warnings.
|
||||
- ZIP output is limited to 1 GiB of bytes actually read, with at most 16 MiB per
|
||||
JSON member and 128 MiB of shape JSON per page. All JSON members share a
|
||||
conservative four-million-value/key budget, checked before allocating parsed
|
||||
trees. Dense arrays cannot bypass it with small compressed or serialized sizes.
|
||||
- Token expressions share nesting and work budgets across aliases and theme
|
||||
modes. Grid-cell membership is indexed once per parent.
|
||||
- Raw SVG uses FigCraft's SVG importer where possible and retains source metadata.
|
||||
Complex masks and unsupported geometry may remain placeholders with warnings.
|
||||
- Penpot export approximates unsupported paints/effects, custom dashes and
|
||||
transitions; unsupported actions and triggers are reported. Slices are omitted.
|
||||
- Regenerated text has no Penpot `positionData`; Penpot lays it out again.
|
||||
- An imported `tokens.json` is preserved when token changes cannot be regenerated;
|
||||
the exporter reports that limitation.
|
||||
- Reverse flow, stretch alignment, wrapped-line space-around/space-evenly,
|
||||
negative padding, item margins/self-alignment and percentage tracks need richer
|
||||
model support. Their containers keep saved positions with warnings. Bounded
|
||||
source metadata survives native saves and unchanged Penpot re-export.
|
||||
- Toggle-overlay has no equivalent in the current model. It is skipped with a
|
||||
warning, while unchanged source interactions remain available on re-export.
|
||||
- Embedded font binaries are not retained by native saves or Penpot re-export.
|
||||
|
||||
## Verification
|
||||
|
||||
Regression tests cover supported editable content, unsupported-layout position
|
||||
and source retention, offline font references, toggle-overlay warnings and the
|
||||
archive/JSON/token budgets. These checks do not establish live Penpot server or
|
||||
native macOS/Linux compatibility.
|
||||
|
||||
```sh
|
||||
cargo xtask ci
|
||||
cargo test -p figcraft-format penpot
|
||||
cargo test -p figcraft-engine penpot
|
||||
```
|
||||
|
||||
Optional local samples are not distributed or required:
|
||||
|
||||
```sh
|
||||
FIGCRAFT_PENPOT_SAMPLE=/path/to/example.penpot cargo test -p figcraft-format penpot
|
||||
FIGCRAFT_PENPOT_SAMPLES=/path/to/folder cargo test -p figcraft-engine penpot_export_tests
|
||||
```
|
||||
+10
-2
@@ -49,7 +49,7 @@ page 1 "Screens": 380 nodes, 4 top-level
|
||||
…
|
||||
```
|
||||
|
||||
### `export <file> -o <out> [--format png|jpg|svg|webp|pdf|fig] [--scale N] [--page NAME] [--node ID|NAME] [--bleed LENGTH] [--crop-marks] [--bleed-marks] [--cmyk]`
|
||||
### `export <file> -o <out> [--format png|jpg|svg|webp|pdf|fig|penpot] [--scale N] [--page NAME] [--node ID|NAME] [--bleed LENGTH] [--crop-marks] [--bleed-marks] [--cmyk]`
|
||||
|
||||
- `<out>` is a directory: one file per top-level frame of the page (default the first page), named
|
||||
after the frame (`Home.png`, `Settings.png`…); with no frames, one file per top-level layer.
|
||||
@@ -65,6 +65,13 @@ page 1 "Screens": 380 nodes, 4 top-level
|
||||
documents and small test files, in other software; other apps may not open every file. What was
|
||||
left out (comments, Dev Mode data, some bindings and settings…) is printed as `note:` lines;
|
||||
`docs/file-format-parity.md` lists what was checked and what is unchecked.
|
||||
- `--format penpot` (or an `<out>` ending in `.penpot`) writes the whole file as a Penpot 2.x
|
||||
archive (`manifest.json` plus one JSON per file, page and shape) that Penpot imports into a team:
|
||||
pages, boards, groups, rectangles, ellipses, paths, booleans and text, fills, strokes, shadows and
|
||||
blurs, flex and grid layout, components and variants, design tokens and prototype interactions.
|
||||
Layers opened from a `.penpot` keep their Penpot ids, and unchanged text, layouts, interactions
|
||||
and tokens are written back from the original. What Penpot cannot represent is printed as
|
||||
`note:` lines.
|
||||
- The format comes from `--format`, else `<out>`'s extension, else png. `--scale` (default 1, max 64)
|
||||
applies to raster formats.
|
||||
- SVG/PDF exports with visible descendants more than 256 levels below an exported root fail with
|
||||
@@ -96,7 +103,8 @@ By extension. Inputs: `.figcraft`, `.json` (FigCraft or REST API file JSON), `.f
|
||||
shapes, text, gradients, masks and effects; `info` lists what it left out as `note:` lines), `.png/.jpg/.webp/.gif/.tif/.bmp`
|
||||
(an image layer). Outputs: `.figcraft`, `.json` (FigCraft JSON, pretty-printed), `.svg`,
|
||||
`.png/.jpg/.webp` (the current page rendered at 1x with its background), `.pdf` (the current page's
|
||||
frames, one page each), `.fig` (the whole file, experimental, as `export --format fig`).
|
||||
frames, one page each), `.fig` (the whole file, experimental, as `export --format fig`), `.penpot`
|
||||
(the whole file as a Penpot 2.x archive, as `export --format penpot`).
|
||||
|
||||
### `render <file> -o <out.png> [--scale N] [--page NAME]`
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (the Fonts table's missing-font row describes the Missing fonts window; `.figcraft` carries the file kind (boards); `.fig` import: resized instances and variable modes by asset key; atomic saves; auto-save; SVG export clip of nested frames; SVG/PDF export depth refusal (#208); `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; `.fig` export: written files checked by opening them in other software; experimental `.fig` export; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; embedded image colour profiles; `.fig` import, best effort; animated GIF fills and TIFF images; last major: created, formats from the installed Figma's Info.plist and help center, ours from source; REST import keeps slot layers and instances' slot content (#792); REST import reads grid track sizes (#465); `.fig` import fidelity from the community batches, budgets for large files, the import report, a `.fig` copy from Save as, paste from another design app; instance layers' size and resizing and Hug on import (#731, #732)) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (M6: offline Penpot import/export with existing model and bounded parsing) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
|
||||
Every format Figma reads or writes, with FigCraft's support. Sources: `Info.plist` of
|
||||
`/Applications/Figma.app` (document types only; nothing inside the bundle's code was read), Figma's
|
||||
@@ -17,6 +17,7 @@ and prototype interactions are still unchecked there.
|
||||
|
||||
| Format | Figma | FigCraft read | FigCraft write | Fidelity / testing | Hours |
|
||||
|---|---|---|---|---|---|
|
||||
| `.penpot` (Penpot 2.x ZIP export) | — | **yes, offline** (open, import, drop, CLI, MCP) | **yes, offline** (export, CLI convert) | Editable layers, supported flex/grid layout, components, tokens and prototypes using the existing model. Unsupported layouts retain saved positions and bounded metadata; toggle-overlay and embedded font binaries are reported. Font references remain editable; no downloads. Synthetic regression coverage; live Penpot and cross-platform QA remain open. See [Penpot reference](PENPOT_IMPORT.md). | further live Penpot and cross-platform QA |
|
||||
| `.fig` (Figma Design, `com.figma.document`, `application/x-figma`) | native (cloud; "Save local copy") | **yes, best effort** (`file.open`, `file.import`, drop, CLI, MCP) | **experimental** ([below](#export-to-fig-experimental)) | `.fig` files open (best effort): the zip, its deflate/zstd chunks and the kiwi schema are decoded generically and mapped by field name onto the REST importer, so pages, the layer tree, geometry, paints (styles resolved), strokes, effects, text with styled runs, vectors as editable vector networks (points, curved segments, filled areas with their winding, per-point corner radius and handle mirroring, end caps; outlines when the network is missing or damaged), auto layout (grid included; a flow that records no primary-axis sizing hugs on that axis, an instance that records no auto layout has its main's), constraints, ellipse arcs, boolean operations, components, sets (with their descriptions), instances (expanded from their main with overrides, derived layout and property values; the size and resizing their layers override stay overrides), styles, variables with modes and bindings, prototyping (set-variable, set-variable-mode and conditional actions included) and image fills from the archive come across; sticky notes, shapes with text, connectors, tables, code blocks, slides and similar layers come in as the closest frames, text layers and vectors, with a warning; anything else is a warning. Since 2026-10-17 (community batches #727, #728): an instance's own identity and the cached library components a file carries, overrides applied by key and order, local text values and instance overrides over a shared text style, grid track sizes (fixed, content-sized, fill), slot content, labels of connectors saved with their drawn path (a static snapshot; the ends aren't live), skip ink, paragraph options and hanging lists, list start numbers, vertical trim, per-point stroke joins, editable text on a path, image adjustments and images carried in the message. Budgets come from the file's own size (one value per compressed byte, 20M–128M, lower ceilings on 32-bit targets; the layer cap follows), so a 15.4 MB synthetic file with 300,000 layers opens whole; the file's bytes stay out of the command journal; an import report lists what was left out or changed once the file opens. Not read: elbowed or curved connectors without a saved path (drawn straight) and labels of connectors without a saved snapshot, table cells beyond their own layers, filled areas with their own fills, variables bound to fields the editor can't bind, video fills. Tested on fixtures generated in code (`crates/format/src/fig/tests.rs`, `tail_tests.rs` and the per-feature `*_tests.rs` beside them, fuzzed) and by reading back what the export writes; no real-world files yet | read tail 5–10, write: checks against real files 5–15 |
|
||||
| `.jam` (whiteboards), `.deck` (Slides) | native to other Figma products | no | no | `.jam`: not read or written (boards are FigCraft files, see [boards-parity](boards-parity.md)); `.deck` out of scope | — |
|
||||
| REST API file JSON (`GET /v1/files/:key`) + local variables JSON + `images/` folder | the API's output | **yes** (`file.open`, `file.import`, CLI) | no | pages, layer tree, geometry, paints, strokes, effects, text runs, vectors, auto layout (sizing modes left out read as hug, as documented; grid track sizes from `gridColumnsSizing` / `gridRowsSizing`: px, fr, auto, `minmax(0, 1fr)`, `repeat()`), constraints, grids, components, sets, instances with overrides (sizes and resizing included), component properties, slots (`SLOT` layers bound to their slot property; an instance's slot keeps its content as the instance's own unless its property value is the main's default content; slot limits and preferred components are warnings), styles, prototyping (incl. conditionals and variable modes), variables with modes, aliases, scopes and bindings, image adjustments (the same tint sign as `.fig`); the import report after opening; warnings for what's skipped (video triggers and actions, modifier keys). Unit tests in `crates/format/src/rest/*_tests.rs` | — |
|
||||
@@ -165,6 +166,7 @@ each.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-25 | minor | M6: offline Penpot 2.x open/import/export through the editor, CLI and MCP using the existing document model. Unsupported layout settings retain saved positions and bounded source metadata with warnings; font binaries and toggle-overlay are reported, and provider fonts are never downloaded. Shared ZIP/JSON/token budgets and bounded stroke dash expansion guard untrusted input. Figma parity numbers unchanged. |
|
||||
| 2026-10-24 | patch | Fonts: the missing-font row said "warnings only (REST import)"; it now names the Missing fonts window (#752), `text.missingFonts` and the `missing font` lines of `figcraft-cli info`. Replacing a missing font is still missing. Numbers unchanged |
|
||||
| 2026-10-24 | minor | Slices and contents only (Figma's Ignore overlapping layers): with it on, a slice inside a frame or group exports only that frame or group and the layers in it, and its Export preview shows the same; with it off, or for a slice on the page or in a section, the slice exports everything in its area as before |
|
||||
| 2026-10-24 | minor | Boards (M12): `.figcraft` saves the file kind (`kind: "board"`, absent for design files) and each board object's `board` metadata; the `.jam` row points at boards |
|
||||
|
||||
Reference in New Issue
Block a user