@grida/refig 0.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Grida
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,333 @@
1
+ # `@grida/refig`
2
+
3
+ > **re**nder **fig**ma — headless Figma renderer in the spirit of [`resvg`](https://github.com/nicolo-ribaudo/resvg-js)
4
+
5
+ Render Figma documents to **PNG, JPEG, WebP, PDF, and SVG** without Figma Desktop and without a browser.
6
+
7
+ Pass a `.fig` file or a Figma REST API JSON response, pick a node, get pixels.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ pnpm add @grida/refig
13
+ ```
14
+
15
+ ## Entrypoints
16
+
17
+ | Import | Environment | Notes |
18
+ | ---------------------- | ----------- | ------------------------------------------------------------------------- |
19
+ | `@grida/refig` | **Node.js** | Default. Includes `fs` helpers for reading `.fig` / JSON files from disk. |
20
+ | `@grida/refig/browser` | **Browser** | No `node:fs` dependency. Accepts `Uint8Array` and JSON objects only. |
21
+
22
+ Both entrypoints export the same core API (`FigmaDocument`, `FigmaRenderer`, types). The only difference is that the Node entrypoint adds a convenience `FigmaDocument.fromFile(path)` static method.
23
+
24
+ ## Quick start (Node)
25
+
26
+ ### Render from a `.fig` file
27
+
28
+ ```ts
29
+ import { readFileSync, writeFileSync } from "node:fs";
30
+ import { FigmaDocument, FigmaRenderer } from "@grida/refig";
31
+
32
+ const doc = FigmaDocument.fromFile("path/to/file.fig");
33
+ const renderer = new FigmaRenderer(doc);
34
+
35
+ const { data } = await renderer.render("<node-id>", {
36
+ format: "png",
37
+ width: 1024,
38
+ height: 1024,
39
+ scale: 2,
40
+ });
41
+
42
+ writeFileSync("out.png", data);
43
+ renderer.dispose();
44
+ ```
45
+
46
+ ### Render from Figma REST API JSON
47
+
48
+ ```ts
49
+ import { readFileSync, writeFileSync } from "node:fs";
50
+ import { FigmaDocument, FigmaRenderer } from "@grida/refig";
51
+
52
+ // GET /v1/files/:key — fetched by your own client
53
+ const json = JSON.parse(readFileSync("figma-response.json", "utf-8"));
54
+
55
+ const renderer = new FigmaRenderer(new FigmaDocument(json));
56
+
57
+ const { data } = await renderer.render("<node-id>", {
58
+ format: "svg",
59
+ });
60
+
61
+ writeFileSync("out.svg", data);
62
+ renderer.dispose();
63
+ ```
64
+
65
+ > Fetching / authentication is intentionally out of scope. Provide the document data from your own API layer.
66
+
67
+ ### Render from REST JSON with custom images
68
+
69
+ When your document has IMAGE fills, pass image bytes keyed by the Figma image ref (hash):
70
+
71
+ ```ts
72
+ import { readFileSync, readdirSync } from "node:fs";
73
+ import path from "node:path";
74
+ import { FigmaDocument, FigmaRenderer } from "@grida/refig";
75
+
76
+ const json = JSON.parse(readFileSync("figma-response.json", "utf-8"));
77
+ const imagesDir = "./downloaded-images";
78
+ const images: Record<string, Uint8Array> = {};
79
+ for (const file of readdirSync(imagesDir)) {
80
+ const ref = path.basename(file).replace(/\.[^.]+$/, "");
81
+ images[ref] = new Uint8Array(readFileSync(path.join(imagesDir, file)));
82
+ }
83
+
84
+ const renderer = new FigmaRenderer(new FigmaDocument(json), { images });
85
+ const { data } = await renderer.render("<node-id>", { format: "png" });
86
+ // ...
87
+ renderer.dispose();
88
+ ```
89
+
90
+ ## Quick start (Browser)
91
+
92
+ ```ts
93
+ import { FigmaDocument, FigmaRenderer } from "@grida/refig/browser";
94
+
95
+ // Uint8Array from a File input, fetch(), or drag-and-drop
96
+ const figBytes: Uint8Array = await file
97
+ .arrayBuffer()
98
+ .then((b) => new Uint8Array(b));
99
+
100
+ const renderer = new FigmaRenderer(new FigmaDocument(figBytes));
101
+
102
+ const { data } = await renderer.render("<node-id>", {
103
+ format: "png",
104
+ width: 512,
105
+ height: 512,
106
+ });
107
+
108
+ // data is a Uint8Array — display it, upload it, etc.
109
+ const blob = new Blob([data], { type: "image/png" });
110
+ renderer.dispose();
111
+ ```
112
+
113
+ ## API
114
+
115
+ ### `FigmaDocument`
116
+
117
+ ```ts
118
+ // From raw .fig bytes (Node + Browser)
119
+ new FigmaDocument(figBytes: Uint8Array)
120
+
121
+ // From Figma REST API JSON (Node + Browser)
122
+ new FigmaDocument(json: Record<string, unknown>)
123
+
124
+ // From a file path (Node only — @grida/refig)
125
+ FigmaDocument.fromFile("path/to/file.fig") // .fig binary
126
+ FigmaDocument.fromFile("path/to/doc.json") // REST API JSON
127
+ ```
128
+
129
+ ### `FigmaRenderer`
130
+
131
+ ```ts
132
+ const renderer = new FigmaRenderer(document: FigmaDocument, options?: {
133
+ useEmbeddedFonts?: boolean; // default: true
134
+ images?: Record<string, Uint8Array>; // image ref → bytes; used for REST API IMAGE fills
135
+ });
136
+
137
+ const result = await renderer.render(nodeId: string, {
138
+ format: "png" | "jpeg" | "webp" | "pdf" | "svg";
139
+ width?: number; // default: 1024
140
+ height?: number; // default: 1024
141
+ scale?: number; // default: 1
142
+ });
143
+
144
+ // result.data — Uint8Array (encoded image / document bytes)
145
+ // result.format
146
+ // result.mimeType
147
+ // result.nodeId
148
+ // result.width
149
+ // result.height
150
+
151
+ renderer.dispose(); // release WASM resources
152
+ ```
153
+
154
+ ### `RefigRenderResult`
155
+
156
+ ```ts
157
+ interface RefigRenderResult {
158
+ data: Uint8Array;
159
+ format: "png" | "jpeg" | "webp" | "pdf" | "svg";
160
+ mimeType: string;
161
+ nodeId: string;
162
+ width: number;
163
+ height: number;
164
+ }
165
+ ```
166
+
167
+ ## CLI
168
+
169
+ ### Install
170
+
171
+ ```sh
172
+ pnpm add -g @grida/refig
173
+ ```
174
+
175
+ ### Usage
176
+
177
+ **`<input>`** can be:
178
+
179
+ - A **file**: path to a `.fig` file or a JSON file (Figma REST API response).
180
+ - A **directory**: path to a folder that contains:
181
+ - **`document.json`** — the REST API response (required),
182
+ - **`images/`** — directory of image assets (optional; used for REST API IMAGE fills).
183
+
184
+ Using a directory avoids passing the document and images separately.
185
+
186
+ ```sh
187
+ # Single node (default)
188
+ refig <input> --node <node-id> --out <path> [options]
189
+
190
+ # With images directory (REST JSON only; IMAGE fills rendered from local files)
191
+ refig <input> --images <dir> --node <node-id> --out <path>
192
+
193
+ # Directory input: document.json + images/ under one folder
194
+ refig ./my-figma-export --node "1:23" --out ./out.png
195
+
196
+ # Export all nodes that have exportSettings (REST JSON or .fig)
197
+ refig <input> --export-all --out <output-dir>
198
+ ```
199
+
200
+ ### Examples
201
+
202
+ ```sh
203
+ # Render a node from a .fig file
204
+ refig ./design.fig --node "1:23" --out ./out.png
205
+
206
+ # Render from REST API JSON
207
+ refig ./figma-response.json --node "1:23" --out ./out.svg
208
+
209
+ # Directory with document.json (and optionally images/): one path instead of response + --images
210
+ refig ./my-figma-export --node "1:23" --out ./out.png
211
+ # (my-figma-export/document.json, my-figma-export/images/)
212
+
213
+ # Explicit images directory (when not using a project directory)
214
+ refig ./figma-response.json --images ./downloaded-images --node "1:23" --out ./out.png
215
+
216
+ # Export all: render every node that has export settings (see below)
217
+ refig ./figma-response.json --export-all --out ./exports
218
+ refig ./design.fig --export-all --out ./exports
219
+
220
+ # Scale 2x, custom dimensions
221
+ refig ./design.fig --node "1:23" --out ./out.png --width 512 --height 512 --scale 2
222
+
223
+ # No-install (CI one-liner)
224
+ pnpm dlx @grida/refig ./design.fig --node "1:23" --out ./out.png
225
+ ```
226
+
227
+ ### Export all (`--export-all`)
228
+
229
+ With **`--export-all`**, refig walks the document and renders every node that has [Figma export settings](https://www.figma.com/developers/api#exportsetting-type) — one file per (node, setting), using that setting’s format, suffix, and constraint. Both **REST API JSON** (e.g. `GET /v1/files/:key`) and **`.fig` files** are supported when the file includes export settings.
230
+
231
+ **When it’s useful:** You choose in Figma exactly what to export: select a node, click **Export +** in the right panel, add one or more presets (e.g. PNG @2x, SVG). Add exports on as many nodes as you want. Then run the CLI with `--export-all` and the path to your REST JSON; refig renders all of those with the same config, without long or repeated `--node` / `--format` / `--scale` options. Same idea for testing the renderer: add export presets on the nodes you care about in Figma, run `refig … --export-all --out ./out`, and compare outputs.
232
+
233
+ **REST API note:** The Figma REST API (`GET /v1/files/:key`) does not include `exportSettings` for SECTION nodes, even when those sections have export presets in Figma. FRAME, COMPONENT, INSTANCE, etc. correctly include them. As a result, `--export-all` on REST JSON will not discover SECTION exports; use a `.fig` file input if you need to export nodes that are sections. See [figma/rest-api-spec#87](https://github.com/figma/rest-api-spec/issues/87).
234
+
235
+ ### Flags
236
+
237
+ | Flag | Required | Default | Description |
238
+ | ---------------- | -------- | ------------------------------- | --------------------------------------------------------------------------------------------- |
239
+ | `<input>` | yes | | Path to `.fig`, JSON file, or directory containing `document.json` (and optionally `images/`) |
240
+ | `--images <dir>` | no | | Directory of image assets for REST document (ignored if `<input>` is a dir with `images/`) |
241
+ | `--node <id>` | yes\* | | Figma node ID to render (\*omit when using `--export-all`) |
242
+ | `--out <path>` | yes | | Output file path (single node) or output directory (`--export-all`) |
243
+ | `--export-all` | no | | Export every node with exportSettings (REST JSON or .fig); `--out` is a directory |
244
+ | `--format <fmt>` | no | inferred from `--out` extension | `png`, `jpeg`, `webp`, `pdf`, `svg` (single-node only) |
245
+ | `--width <px>` | no | `1024` | Viewport width (single-node only) |
246
+ | `--height <px>` | no | `1024` | Viewport height (single-node only) |
247
+ | `--scale <n>` | no | `1` | Raster scale factor (single-node only) |
248
+
249
+ ## Architecture
250
+
251
+ ```
252
+ Input Conversion Rendering
253
+ ───── ────────── ─────────
254
+ .fig bytes ──┐
255
+ ├──→ @grida/io-figma ──→ Grida IR ──→ @grida/canvas-wasm ──→ PNG/JPEG/WebP/PDF/SVG
256
+ REST JSON ───┘
257
+ ```
258
+
259
+ - **`@grida/io-figma`** converts Figma data (`.fig` Kiwi binary or REST API JSON) into Grida's intermediate representation
260
+ - **`@grida/canvas-wasm`** renders the IR via Skia (raster backend for headless, WebGL for browser)
261
+ - **`@grida/refig`** ties them together behind a simple `render(nodeId, options)` call
262
+
263
+ ## Images
264
+
265
+ **`.fig` input** — Image fills used in the design are stored inside the `.fig` file. No extra step is required; refig uses them when rendering.
266
+
267
+ **REST API input** — The file JSON does not contain image bytes; it references image fills by hash. To render with correct bitmaps you must supply the image assets:
268
+
269
+ 1. **Fetch image fills** — Call `GET /v1/files/:key/images` (Figma REST API). This returns the list of **image fills** used in the file (i.e. which bitmap images are used as fills), not “export node as image.” The response includes a mapping of image hash → URL (signed) for each fill.
270
+
271
+ 2. **Download and pass an images directory (recommended)** — Download each image from the returned URLs and save them under a directory using the `<hash>.<ext>` naming (e.g. `a1b2c3d4....png`). Pass that directory to refig as the **images directory**. We recommend this because the URLs from the API are **signed and expire**; downloading once and reusing the files avoids expiry and keeps rendering repeatable (e.g. in CI or offline).
272
+
273
+ **API** — `FigmaRenderer` accepts an optional **`images`** option: `Record<string, Uint8Array>` (image ref → bytes). Supply image assets when using REST document input; IMAGE fills will render using these bytes. Refs must match the Figma image fill hashes in the document.
274
+
275
+ **CLI** — You can pass images in two ways:
276
+
277
+ - **`--images <dir>`** — Explicit images directory. Files are keyed by filename without extension (e.g. `a1b2c3d4.png` → ref `a1b2c3d4`). Use when the document is a separate file:
278
+ `refig ./figma-response.json --images ./downloaded-images --node "1:23" --out ./out.png`
279
+ - **Directory input** — Pass a single directory that contains **`document.json`** (REST response) and optionally **`images/`**. No need to pass `--images` separately:
280
+ `refig ./my-figma-export --node "1:23" --out ./out.png`
281
+ (expects `my-figma-export/document.json` and, if present, `my-figma-export/images/`.)
282
+
283
+ For **`.fig`** input, images are embedded in the file; no extra images directory is needed. For **REST** input, use `--images` or a project directory with `images/` to render IMAGE fills correctly.
284
+
285
+ ## Features
286
+
287
+ - **Multiple output formats** — `png`, `jpeg`, `webp`, `pdf`, `svg`
288
+ - **`.fig` file input** — render from exported `.fig` without API calls
289
+ - **REST API JSON input** — render from document JSON you already have
290
+ - **CI-friendly** — headless, deterministic, no browser required
291
+ - **Browser-compatible** — `@grida/refig/browser` works in any modern browser
292
+ - **WASM-powered** — Skia-backed rendering for pixel-accurate output
293
+
294
+ ## Not planned
295
+
296
+ - **Figma API fetching / auth** — bring your own tokens and HTTP client
297
+ - **Design-to-code** — this renders pixels, not HTML/CSS/Flutter
298
+ - **Authoring / editing** — read + render only
299
+
300
+ ## FAQ
301
+
302
+ ### Why not just use the Figma Images API?
303
+
304
+ If you have API access, the Images API is usually simplest. This package is for when you need:
305
+
306
+ - Offline / air-gapped rendering
307
+ - Deterministic output in CI without network calls
308
+ - Custom viewport sizes or scale factors
309
+ - Rendering from `.fig` files without API access
310
+ - High-throughput or random access where the API is too slow or rate-limited (e.g. low Figma tier)
311
+ - Avoiding Figma access token lifecycle (refresh, storage, rotation)
312
+
313
+ ### Does this work in the browser?
314
+
315
+ Yes. Import from `@grida/refig/browser`. The core renderer uses `@grida/canvas-wasm` which supports both Node (raster) and browser (WebGL) backends.
316
+
317
+ ### What about fonts?
318
+
319
+ The WASM runtime ships with embedded fallback fonts. Custom font loading (Google Fonts, local directories) is planned but not yet available.
320
+
321
+ ## Contributing
322
+
323
+ From the package root:
324
+
325
+ 1. Install dependencies and build: `pnpm install && pnpm build`
326
+ 2. Link the package so the `refig` CLI is available: `pnpm link --global`
327
+ 3. Run the `refig` command from anywhere to test (e.g. `refig ./fixture.json --node "1:1" --out ./out.png`)
328
+
329
+ To unlink: `pnpm unlink --global`.
330
+
331
+ ## License
332
+
333
+ See [`LICENSE`](./LICENSE).
@@ -0,0 +1,94 @@
1
+ import { ExportSetting } from '@figma/rest-api-spec';
2
+
3
+ /**
4
+ * @grida/refig — shared core (no node:fs, no DOM)
5
+ *
6
+ * This module is environment-agnostic. Both the Node and browser entrypoints
7
+ * re-export everything from here.
8
+ */
9
+
10
+ type RefigRenderFormat = "png" | "jpeg" | "webp" | "pdf" | "svg";
11
+ interface RefigRendererOptions {
12
+ /**
13
+ * When enabled, the renderer loads the embedded default fonts.
14
+ * @default true
15
+ */
16
+ useEmbeddedFonts?: boolean;
17
+ /**
18
+ * Map of Figma image ref (hash) to image bytes.
19
+ * Used for REST API and .fig input so IMAGE fills render correctly.
20
+ * Ref must match document references (e.g. 40-char hex for .fig, Figma image fill hash for REST).
21
+ */
22
+ images?: Record<string, Uint8Array>;
23
+ }
24
+ interface RefigRenderOptions {
25
+ format: RefigRenderFormat;
26
+ width?: number;
27
+ height?: number;
28
+ scale?: number;
29
+ }
30
+ interface RefigRenderResult {
31
+ data: Uint8Array;
32
+ format: RefigRenderFormat;
33
+ mimeType: string;
34
+ nodeId: string;
35
+ width: number;
36
+ height: number;
37
+ }
38
+ type FigmaJsonDocument = Record<string, unknown>;
39
+ declare class FigmaDocument {
40
+ readonly sourceType: "fig-file" | "rest-api-json";
41
+ /**
42
+ * For "fig-file" this is the raw bytes of the .fig file.
43
+ * For "rest-api-json" this is the parsed REST API document JSON.
44
+ */
45
+ readonly payload: Uint8Array | FigmaJsonDocument;
46
+ /**
47
+ * @param input Raw `.fig` bytes (Uint8Array) or a parsed Figma REST API
48
+ * document JSON object.
49
+ *
50
+ * For file-path convenience in Node, use `FigmaDocument.fromFile()` from
51
+ * the `@grida/refig` entrypoint.
52
+ */
53
+ constructor(input: Uint8Array | FigmaJsonDocument);
54
+ }
55
+ declare function resolveMimeType(format: RefigRenderFormat): string;
56
+ type RestNode = Record<string, unknown> & {
57
+ id?: string;
58
+ children?: RestNode[];
59
+ };
60
+ /** One export to perform: a node and one of its Figma export settings. */
61
+ interface ExportItem {
62
+ nodeId: string;
63
+ node: RestNode;
64
+ setting: ExportSetting;
65
+ }
66
+ /**
67
+ * Walk the REST document and collect every (node, exportSetting) for nodes that have exportSettings.
68
+ * Follows Figma HasExportSettingsTrait; one ExportItem per setting per node.
69
+ */
70
+ declare function collectExportsFromDocument(json: FigmaJsonDocument): ExportItem[];
71
+ /**
72
+ * Map a Figma ExportSetting and node bounds to RefigRenderOptions.
73
+ * Constraint SCALE → scale; WIDTH/HEIGHT → width/height from constraint value and node aspect ratio.
74
+ */
75
+ declare function exportSettingToRenderOptions(node: RestNode, setting: ExportSetting): RefigRenderOptions;
76
+ declare class FigmaRenderer {
77
+ readonly document: FigmaDocument;
78
+ readonly options: RefigRendererOptions;
79
+ private _canvas;
80
+ private _sceneLoaded;
81
+ /** When set, scene is built with this node as root (REST only). Cleared when nodeId changes. */
82
+ private _requestedNodeId;
83
+ constructor(document: FigmaDocument | FigmaJsonDocument, options?: RefigRendererOptions);
84
+ private ensureCanvas;
85
+ private loadScene;
86
+ render(nodeId: string, renderOptions: RefigRenderOptions): Promise<RefigRenderResult>;
87
+ /**
88
+ * Release the underlying WASM canvas.
89
+ * After calling this the renderer must not be used again.
90
+ */
91
+ dispose(): void;
92
+ }
93
+
94
+ export { type ExportItem, FigmaDocument, FigmaRenderer, type RefigRenderFormat, type RefigRenderOptions, type RefigRenderResult, type RefigRendererOptions, collectExportsFromDocument, FigmaRenderer as default, exportSettingToRenderOptions, resolveMimeType };
@@ -0,0 +1,15 @@
1
+ import {
2
+ FigmaDocument,
3
+ FigmaRenderer,
4
+ collectExportsFromDocument,
5
+ exportSettingToRenderOptions,
6
+ resolveMimeType
7
+ } from "./chunk-DAHUXARL.mjs";
8
+ export {
9
+ FigmaDocument,
10
+ FigmaRenderer,
11
+ collectExportsFromDocument,
12
+ FigmaRenderer as default,
13
+ exportSettingToRenderOptions,
14
+ resolveMimeType
15
+ };