openfig-cli 0.3.35 → 0.3.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -55
- package/lib/rasterizer/svg-builder.mjs +18 -32
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,86 +4,68 @@
|
|
|
4
4
|
|
|
5
5
|
Open tools for Figma files.
|
|
6
6
|
|
|
7
|
-
Parse, inspect, and
|
|
7
|
+
Parse, inspect, render, and modify `.deck` and `.fig` files without the Figma application.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Install
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g openfig-cli
|
|
13
|
+
```
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
Node 18+. No build step. Pure ESM.
|
|
16
|
+
|
|
17
|
+
## File Format Support
|
|
14
18
|
|
|
15
|
-
| Product | Extension |
|
|
16
|
-
|
|
17
|
-
| Figma Slides | `.deck` | ✅ |
|
|
18
|
-
| Figma Design | `.fig` | ✅
|
|
19
|
-
| Figma Jam (whiteboard) | `.jam` | ❌ not yet |
|
|
20
|
-
| Figma Buzz | `.buzz` | ❌ not yet |
|
|
21
|
-
| Figma Sites | `.site` | ❌ not yet |
|
|
22
|
-
| Figma Make | `.make` | ❌ not yet |
|
|
19
|
+
| Product | Extension | Read | Render | Modify |
|
|
20
|
+
|---------|-----------|------|--------|--------|
|
|
21
|
+
| Figma Slides | `.deck` | ✅ | ✅ PNG / PDF | ✅ |
|
|
22
|
+
| Figma Design | `.fig` | ✅ | ✅ PNG / PDF | — |
|
|
23
23
|
|
|
24
24
|
## Render Quality
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
**≥99% SSIM** against Figma reference exports across all test cases:
|
|
27
27
|
|
|
28
28
|
| Test suite | Visual results |
|
|
29
29
|
|------------|----------------|
|
|
30
30
|
| `.deck` slides | [render-report-deck.html](https://rcoenen.github.io/OpenFig/test/rasterizer/reports/openfig-render-report-deck.html) |
|
|
31
31
|
| `.fig` design frames | [render-report-fig.html](https://rcoenen.github.io/OpenFig/test/rasterizer/reports/openfig-render-report-fig.html) |
|
|
32
32
|
|
|
33
|
-
##
|
|
34
|
-
|
|
35
|
-
Figma Slides lets you download presentations as `.deck` files and re-upload them. This is the **native** round-trip format. Exporting to `.pptx` is lossy — vectors get rasterized, fonts fall back to system defaults, layout breaks. By staying in `.deck`, you preserve everything exactly as Figma renders it.
|
|
36
|
-
|
|
37
|
-
OpenFig makes this round-trip programmable. Download a `.deck`, modify it, re-upload. Everything stays native.
|
|
38
|
-
|
|
39
|
-
Plug in Claude Cowork or any coding agent and you have an AI that can read and edit Figma presentations end-to-end — without ever opening the Figma UI.
|
|
40
|
-
|
|
41
|
-
## Use Cases
|
|
42
|
-
|
|
43
|
-
- **AI agent for presentations** — let an LLM rewrite copy, insert images, and produce a ready-to-upload `.deck`
|
|
44
|
-
- **Batch-produce branded decks** — start from a template, feed in data per client/project, get pixel-perfect slides out
|
|
45
|
-
- **Inspect and audit** — understand the internal structure of any `.deck` file
|
|
46
|
-
- **Automate** text and image placement across dozens of slides in seconds
|
|
47
|
-
|
|
48
|
-
## Install
|
|
33
|
+
## CLI
|
|
49
34
|
|
|
50
35
|
```bash
|
|
51
|
-
|
|
36
|
+
# Read & inspect (works on .deck and .fig)
|
|
37
|
+
openfig inspect deck.deck # node hierarchy tree
|
|
38
|
+
openfig list-text deck.deck # all text and image content per slide
|
|
39
|
+
openfig list-overrides deck.deck # editable override keys per symbol
|
|
40
|
+
|
|
41
|
+
# Render (works on .deck and .fig)
|
|
42
|
+
openfig export deck.deck # export slides/frames as PNG
|
|
43
|
+
openfig pdf deck.deck # export as multi-page PDF
|
|
44
|
+
|
|
45
|
+
# Modify (.deck only)
|
|
46
|
+
openfig update-text deck.deck -o out.deck --slide <id> --set "key=value"
|
|
47
|
+
openfig insert-image deck.deck -o out.deck --slide <id> --key <nodeId> --image <path>
|
|
48
|
+
openfig clone-slide deck.deck -o out.deck --template <id|name> --name <name> [--set key=value ...]
|
|
49
|
+
openfig remove-slide deck.deck -o out.deck --slide <id>
|
|
50
|
+
openfig roundtrip in.deck out.deck # decode + re-encode validation
|
|
52
51
|
```
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
## File Format Support
|
|
57
|
-
|
|
58
|
-
All CLI commands work on both `.deck` (Figma Slides) and `.fig` (Figma Design) files. Pass either format wherever a file path is expected.
|
|
53
|
+
> Full CLI reference: [docs/cli.md](docs/cli.md)
|
|
59
54
|
|
|
60
|
-
##
|
|
55
|
+
## Why native `.deck`?
|
|
61
56
|
|
|
62
|
-
|
|
63
|
-
openfig inspect my-presentation.deck # node hierarchy
|
|
64
|
-
openfig list-text my-presentation.deck # all text + images per slide
|
|
65
|
-
openfig list-overrides my-presentation.deck # editable fields per symbol
|
|
66
|
-
```
|
|
57
|
+
Figma Slides lets you download and re-upload `.deck` files losslessly. Exporting to `.pptx` is lossy — vectors rasterize, fonts fall back, layout breaks. OpenFig makes this native round-trip programmable: download, modify, re-upload.
|
|
67
58
|
|
|
68
|
-
|
|
59
|
+
Plug in Claude Cowork or any coding agent and you have an AI that can read and edit Figma presentations end-to-end — without opening the Figma UI.
|
|
69
60
|
|
|
70
|
-
##
|
|
61
|
+
## Agentic / MCP Integration
|
|
71
62
|
|
|
72
63
|
> Install guide, MCP workflows, and template states: [docs/agentic/claude-cowork.md](docs/agentic/claude-cowork.md)
|
|
73
64
|
|
|
74
|
-
##
|
|
75
|
-
|
|
76
|
-
```javascript
|
|
77
|
-
import { Deck } from 'openfig';
|
|
78
|
-
|
|
79
|
-
const deck = await Deck.open('template.deck');
|
|
80
|
-
const slide = deck.slides[0];
|
|
81
|
-
slide.addText('Hello world', { style: 'Title' });
|
|
82
|
-
await deck.save('output.deck');
|
|
83
|
-
```
|
|
65
|
+
## Docs
|
|
84
66
|
|
|
85
|
-
|
|
|
86
|
-
|
|
67
|
+
| | |
|
|
68
|
+
|---|---|
|
|
87
69
|
| MCP / Claude workflows | [docs/mcp.md](docs/mcp.md) |
|
|
88
70
|
| High-level API | [docs/api-spec.md](docs/api-spec.md) |
|
|
89
71
|
| Low-level FigDeck API | [docs/library.md](docs/library.md) |
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { readFileSync } from 'fs';
|
|
23
23
|
import { join } from 'path';
|
|
24
|
+
import { extractRenderableGradientFill, resolveGradientGeometry } from 'openfig-core';
|
|
24
25
|
import { hashToHex } from '../core/image-helpers.mjs';
|
|
25
26
|
import { nid } from '../core/node-helpers.mjs';
|
|
26
27
|
|
|
@@ -56,49 +57,34 @@ function resolveFill(fillPaints) {
|
|
|
56
57
|
* w, h = element dimensions in pixels (needed for userSpaceOnUse coordinates).
|
|
57
58
|
* Returns { defs: string, fill: string } where fill is 'url(#grad-N)'. */
|
|
58
59
|
function resolveGradientSvg(paint, w, h) {
|
|
60
|
+
const gradient = extractRenderableGradientFill([paint]);
|
|
61
|
+
if (!gradient) return null;
|
|
62
|
+
const geometry = resolveGradientGeometry(gradient, w, h);
|
|
63
|
+
if (!geometry) return null;
|
|
64
|
+
|
|
59
65
|
const id = `grad-${++_svgIdSeq}`;
|
|
60
|
-
const stops =
|
|
66
|
+
const stops = gradient.stops.map(s => {
|
|
61
67
|
const color = cssColor(s.color ?? {});
|
|
62
68
|
return `<stop offset="${s.position}" stop-color="${color}"/>`;
|
|
63
69
|
}).join('');
|
|
64
|
-
const opacityAttr =
|
|
65
|
-
|
|
66
|
-
// Figma's paint.transform maps from NODE space to GRADIENT space.
|
|
67
|
-
// We need the inverse: gradient space → node normalized space → pixels.
|
|
68
|
-
const t = paint.transform ?? {};
|
|
69
|
-
const ga = t.m00 ?? 1, gc = t.m01 ?? 0, ge = t.m02 ?? 0;
|
|
70
|
-
const gb = t.m10 ?? 0, gd = t.m11 ?? 1, gf = t.m12 ?? 0;
|
|
71
|
-
const det = ga * gd - gb * gc;
|
|
72
|
-
// Inverse affine: paint.transform maps node→gradient; we need gradient→node
|
|
73
|
-
const ia = gd / det, ic = -gc / det, ie = (gc * gf - gd * ge) / det;
|
|
74
|
-
const ib = -gb / det, iid = ga / det, iif = (gb * ge - ga * gf) / det;
|
|
75
|
-
const tx = (gx, gy) => (ia * gx + ic * gy + ie) * w;
|
|
76
|
-
const ty = (gx, gy) => (ib * gx + iid * gy + iif) * h;
|
|
70
|
+
const opacityAttr = gradient.opacity !== 1 ? ` opacity="${gradient.opacity}"` : '';
|
|
77
71
|
const f = v => +v.toFixed(2);
|
|
78
72
|
|
|
79
|
-
if (
|
|
80
|
-
// Linear gradient line: (0, 0.5) → (1, 0.5) in gradient space
|
|
81
|
-
return {
|
|
82
|
-
defs: `<linearGradient id="${id}" x1="${f(tx(0,0.5))}" y1="${f(ty(0,0.5))}" x2="${f(tx(1,0.5))}" y2="${f(ty(1,0.5))}" gradientUnits="userSpaceOnUse">${stops}</linearGradient>`,
|
|
83
|
-
fill: `url(#${id})`,
|
|
84
|
-
opacityAttr,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
if (paint.type === 'GRADIENT_RADIAL') {
|
|
88
|
-
// Radial gradient: center (0.5, 0.5), radius mapped through transform
|
|
89
|
-
const cx = f(tx(0.5, 0.5)), cy = f(ty(0.5, 0.5));
|
|
90
|
-
// Radius along the gradient's x-axis: distance from center to (1, 0.5)
|
|
91
|
-
const rx = f(Math.hypot(tx(1, 0.5) - tx(0.5, 0.5), ty(1, 0.5) - ty(0.5, 0.5)));
|
|
92
|
-
const ry = f(Math.hypot(tx(0.5, 1) - tx(0.5, 0.5), ty(0.5, 1) - ty(0.5, 0.5)));
|
|
93
|
-
// Rotation angle from the transform
|
|
94
|
-
const angle = f(Math.atan2(ty(1, 0.5) - ty(0.5, 0.5), tx(1, 0.5) - tx(0.5, 0.5)) * 180 / Math.PI);
|
|
73
|
+
if (geometry.type === 'linear') {
|
|
95
74
|
return {
|
|
96
|
-
defs: `<
|
|
75
|
+
defs: `<linearGradient id="${id}" x1="${f(geometry.start.x)}" y1="${f(geometry.start.y)}" x2="${f(geometry.end.x)}" y2="${f(geometry.end.y)}" gradientUnits="userSpaceOnUse">${stops}</linearGradient>`,
|
|
97
76
|
fill: `url(#${id})`,
|
|
98
77
|
opacityAttr,
|
|
99
78
|
};
|
|
100
79
|
}
|
|
101
|
-
|
|
80
|
+
const cx = f(geometry.center.x), cy = f(geometry.center.y);
|
|
81
|
+
const rx = f(geometry.radiusX), ry = f(geometry.radiusY);
|
|
82
|
+
const angle = f(geometry.angle * 180 / Math.PI);
|
|
83
|
+
return {
|
|
84
|
+
defs: `<radialGradient id="${id}" cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}" gradientUnits="userSpaceOnUse" gradientTransform="rotate(${angle},${cx},${cy})">${stops}</radialGradient>`,
|
|
85
|
+
fill: `url(#${id})`,
|
|
86
|
+
opacityAttr,
|
|
87
|
+
};
|
|
102
88
|
}
|
|
103
89
|
|
|
104
90
|
function appendDefs(defs, extra) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openfig-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.38",
|
|
4
4
|
"description": "OpenFig — Open-source tools for Figma file parsing and rendering",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
46
46
|
"@resvg/resvg-wasm": "^2.6.2",
|
|
47
47
|
"kiwi-schema": "^0.5.0",
|
|
48
|
-
"openfig-core": "^0.3.
|
|
48
|
+
"openfig-core": "^0.3.5",
|
|
49
49
|
"pako": "^2.1.0",
|
|
50
50
|
"pdf-lib": "^1.17.1",
|
|
51
51
|
"sharp": "^0.34.5",
|