@grida/refig 0.0.3 → 0.0.4
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 +93 -17
- package/dist/browser.d.mts +63 -5
- package/dist/browser.mjs +1 -1
- package/dist/{chunk-YI7PIULZ.mjs → chunk-INJ5F2RK.mjs} +521 -42
- package/dist/cli.mjs +2541 -14
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ Render Figma documents to **PNG, JPEG, WebP, PDF, and SVG** in **Node.js (no bro
|
|
|
6
6
|
|
|
7
7
|
Use a `.fig` export (offline) or a Figma REST API file JSON response (`GET /v1/files/:key`), pick a node ID, and get pixels.
|
|
8
8
|
|
|
9
|
+
Refig aims to render designs as faithfully as possible to the original. See [Known limitations](#known-limitations) for current exceptions.
|
|
10
|
+
|
|
9
11
|
## Features (checklist)
|
|
10
12
|
|
|
11
13
|
- [x] Render from **`.fig` files** (offline / no API calls)
|
|
@@ -14,6 +16,7 @@ Use a `.fig` export (offline) or a Figma REST API file JSON response (`GET /v1/f
|
|
|
14
16
|
- [x] **CLI** (`refig`) and **library API** (`FigmaDocument`, `FigmaRenderer`)
|
|
15
17
|
- [x] **Node.js** + **browser** entrypoints (`@grida/refig`, `@grida/refig/browser`)
|
|
16
18
|
- [x] IMAGE fills supported via **embedded `.fig` images** or a **local `images/` directory** for REST JSON
|
|
19
|
+
- [x] **Bring-your-own-font** — supply custom font files for designs that use non-default typefaces
|
|
17
20
|
- [x] Batch export with **`--export-all`** (renders nodes with Figma export presets)
|
|
18
21
|
- [x] WASM + Skia-backed renderer via `@grida/canvas-wasm`
|
|
19
22
|
|
|
@@ -105,6 +108,57 @@ const { data } = await renderer.render("<node-id>", { format: "png" });
|
|
|
105
108
|
renderer.dispose();
|
|
106
109
|
```
|
|
107
110
|
|
|
111
|
+
### Render with custom fonts
|
|
112
|
+
|
|
113
|
+
Unlike images, fonts do not have a Figma API. Use **`listFontFamilies()`** to see which font families are used in your file (or a scoped node), then load those fonts and pass them to the renderer:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
117
|
+
import { FigmaDocument, FigmaRenderer } from "@grida/refig";
|
|
118
|
+
|
|
119
|
+
const doc = FigmaDocument.fromFile("path/to/file.fig");
|
|
120
|
+
|
|
121
|
+
// 1. Discover font families used (omit rootNodeId for full document)
|
|
122
|
+
const fontFamilies = doc.listFontFamilies("<node-id>"); // e.g. ["Inter", "Caveat", "Roboto"]
|
|
123
|
+
|
|
124
|
+
// 2. Load your custom fonts (local FS, CDN, asset service, etc.)
|
|
125
|
+
// Skip Figma defaults (Inter, Noto Sans KR/JP/SC, etc.) — the renderer loads those.
|
|
126
|
+
const fonts: Record<string, Uint8Array> = {};
|
|
127
|
+
for (const family of fontFamilies) {
|
|
128
|
+
if (
|
|
129
|
+
family === "Inter" ||
|
|
130
|
+
family.startsWith("Noto Sans") ||
|
|
131
|
+
family === "Noto Color Emoji"
|
|
132
|
+
)
|
|
133
|
+
continue;
|
|
134
|
+
fonts[family] = new Uint8Array(readFileSync(`./fonts/${family}.ttf`)); // adjust path to your font file structure
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 3. Render
|
|
138
|
+
const renderer = new FigmaRenderer(doc, { fonts });
|
|
139
|
+
const { data } = await renderer.render("<node-id>", { format: "png" });
|
|
140
|
+
writeFileSync("out.png", data);
|
|
141
|
+
renderer.dispose();
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**CLI:** Use `--fonts <dir>` to pass a directory of TTF/OTF files (scanned recursively). Fonts are inferred from the name table; multiple files per family are grouped automatically. Use `--skip-default-fonts` to avoid loading Figma defaults (useful to verify custom font rendering):
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
refig ./figma-response.json --fonts ./my-fonts --node "1:23" --out out.png
|
|
148
|
+
refig ./doc.json --fonts ./my-fonts --node "1:23" --out out.png --skip-default-fonts
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
With a project directory, place fonts in `fonts/` next to `document.json` (and optionally `images/`); refig auto-discovers them:
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
refig ./my-figma-export --node "1:23" --format png
|
|
155
|
+
# Expects my-figma-export/document.json and, if present, my-figma-export/fonts/
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Load **all** font files that match each family (variable or static) so the renderer can pick the right one for each text style, just like the original design. For multiple files per family (e.g. Regular, Bold, Italic), pass an array: `fonts: { "MyFamily": [regularBytes, boldBytes, italicBytes] }`.
|
|
159
|
+
|
|
160
|
+
If the design uses **locally-installed fonts** (fonts the designer had on their machine), loading those from your OS may require extra scripts or tooling to locate and extract the font files. We do not provide such tooling.
|
|
161
|
+
|
|
108
162
|
## Quick start (Browser)
|
|
109
163
|
|
|
110
164
|
```ts
|
|
@@ -143,14 +197,21 @@ new FigmaDocument(json: Record<string, unknown>)
|
|
|
143
197
|
// From a file path (Node only — @grida/refig)
|
|
144
198
|
FigmaDocument.fromFile("path/to/file.fig") // .fig binary
|
|
145
199
|
FigmaDocument.fromFile("path/to/doc.json") // REST API JSON
|
|
200
|
+
|
|
201
|
+
// Font families used in the document (for bring-your-own-font)
|
|
202
|
+
document.listFontFamilies(rootNodeId?: string): string[]
|
|
203
|
+
// — rootNodeId: optional; scope to that node's subtree, or omit for full document
|
|
204
|
+
// — returns unique family names; load all font files that match each family (VF or static)
|
|
146
205
|
```
|
|
147
206
|
|
|
148
207
|
### `FigmaRenderer`
|
|
149
208
|
|
|
150
209
|
```ts
|
|
151
210
|
const renderer = new FigmaRenderer(document: FigmaDocument, options?: {
|
|
152
|
-
useEmbeddedFonts?: boolean;
|
|
211
|
+
useEmbeddedFonts?: boolean; // default: true
|
|
212
|
+
loadFigmaDefaultFonts?: boolean; // default: true — Inter, Noto Sans KR/JP/SC, etc.
|
|
153
213
|
images?: Record<string, Uint8Array>; // image ref → bytes; used for REST API IMAGE fills
|
|
214
|
+
fonts?: Record<string, Uint8Array | Uint8Array[]>; // font family → bytes (TTF/OTF); one or more files per family
|
|
154
215
|
});
|
|
155
216
|
|
|
156
217
|
const result = await renderer.render(nodeId: string, {
|
|
@@ -206,9 +267,10 @@ pnpm dlx @grida/refig <input> --export-all
|
|
|
206
267
|
- A **file**: path to a `.fig` file or a JSON file (Figma REST API response).
|
|
207
268
|
- A **directory**: path to a folder that contains:
|
|
208
269
|
- **`document.json`** — the REST API response (required),
|
|
209
|
-
- **`images/`** — directory of image assets (optional; used for REST API IMAGE fills)
|
|
270
|
+
- **`images/`** — directory of image assets (optional; used for REST API IMAGE fills),
|
|
271
|
+
- **`fonts/`** — directory of font files TTF/OTF (optional; scanned recursively).
|
|
210
272
|
|
|
211
|
-
Using a directory avoids passing the document and
|
|
273
|
+
Using a directory avoids passing the document, images, and fonts separately.
|
|
212
274
|
|
|
213
275
|
```sh
|
|
214
276
|
# Single node (default)
|
|
@@ -242,6 +304,10 @@ refig ./my-figma-export --node "1:23" --format png
|
|
|
242
304
|
# Explicit images directory (when not using a project directory)
|
|
243
305
|
refig ./figma-response.json --images ./downloaded-images --node "1:23" --format png
|
|
244
306
|
|
|
307
|
+
# Custom fonts (when design uses non-default typefaces)
|
|
308
|
+
refig ./figma-response.json --fonts ./my-fonts --node "1:23" --out out.png
|
|
309
|
+
refig ./doc.json --fonts ./fonts --node "1:23" --out out.png --skip-default-fonts
|
|
310
|
+
|
|
245
311
|
# Export all: render every node that has export settings (see below)
|
|
246
312
|
refig ./figma-response.json --export-all
|
|
247
313
|
refig ./design.fig --export-all
|
|
@@ -298,17 +364,19 @@ With **`--export-all`**, refig walks the document and renders every node that ha
|
|
|
298
364
|
|
|
299
365
|
### Flags
|
|
300
366
|
|
|
301
|
-
| Flag
|
|
302
|
-
|
|
|
303
|
-
| `<input>`
|
|
304
|
-
| `--images <dir>`
|
|
305
|
-
| `--
|
|
306
|
-
| `--
|
|
307
|
-
| `--
|
|
308
|
-
| `--
|
|
309
|
-
| `--
|
|
310
|
-
| `--
|
|
311
|
-
| `--
|
|
367
|
+
| Flag | Required | Default | Description |
|
|
368
|
+
| ---------------------- | -------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
369
|
+
| `<input>` | yes | | Path to `.fig`, JSON file, or directory containing `document.json` (and optionally `images/`, `fonts/`) |
|
|
370
|
+
| `--images <dir>` | no | | Directory of image assets for REST document (ignored if `<input>` is a dir with `images/`) |
|
|
371
|
+
| `--fonts <dir>` | no | | Directory of font files (TTF/OTF) for custom fonts (ignored if `<input>` is a dir with `fonts/`) |
|
|
372
|
+
| `--node <id>` | yes\* | | Figma node ID to render (\*omit when using `--export-all`) |
|
|
373
|
+
| `--out <path>` | no | OS temp dir when omitted | Output file path (single node) or output directory (`--export-all`). When omitted, writes to the OS temp directory (valid with `--export-all` or with both `--format` and `--node`). |
|
|
374
|
+
| `--export-all` | no | | Export every node with exportSettings (REST JSON or .fig); `--out` is a directory |
|
|
375
|
+
| `--format <fmt>` | no | inferred from `--out` extension | `png`, `jpeg`, `webp`, `pdf`, `svg` (single-node only; required when `--out` is omitted) |
|
|
376
|
+
| `--width <px>` | no | `1024` | Viewport width (single-node only) |
|
|
377
|
+
| `--height <px>` | no | `1024` | Viewport height (single-node only) |
|
|
378
|
+
| `--scale <n>` | no | `1` | Raster scale factor (single-node only) |
|
|
379
|
+
| `--skip-default-fonts` | no | | Do not load Figma default fonts (Inter, Noto Sans, etc.); use only custom fonts from `--fonts` or `fonts/` |
|
|
312
380
|
|
|
313
381
|
## Architecture
|
|
314
382
|
|
|
@@ -340,12 +408,18 @@ REST JSON ───┘
|
|
|
340
408
|
|
|
341
409
|
- **`--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:
|
|
342
410
|
`refig ./figma-response.json --images ./downloaded-images --node "1:23" --format png`
|
|
343
|
-
- **Directory input** — Pass a single directory that contains **`document.json`** (REST response) and optionally **`images/`**. No need to pass `--images` separately:
|
|
411
|
+
- **Directory input** — Pass a single directory that contains **`document.json`** (REST response) and optionally **`images/`** and **`fonts/`**. No need to pass `--images` or `--fonts` separately:
|
|
344
412
|
`refig ./my-figma-export --node "1:23" --format png`
|
|
345
|
-
(expects `my-figma-export/document.json` and, if present, `my-figma-export/images/`.)
|
|
413
|
+
(expects `my-figma-export/document.json` and, if present, `my-figma-export/images/` and `my-figma-export/fonts/`.)
|
|
346
414
|
|
|
347
415
|
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.
|
|
348
416
|
|
|
417
|
+
## Known limitations
|
|
418
|
+
|
|
419
|
+
- **Rich text** — Text with mixed styles (e.g. bold and italic in the same paragraph) is not yet supported.
|
|
420
|
+
- **Image transformation** — Complex image transforms from Figma designs are not yet properly aligned. Known issue; will fix.
|
|
421
|
+
- **Emoji** — Rendered with Noto Color Emoji instead of Figma's platform emoji (Apple Color Emoji / Segoe UI Emoji). Output differs by design.
|
|
422
|
+
|
|
349
423
|
## Not planned
|
|
350
424
|
|
|
351
425
|
- **Figma API fetching / auth** — bring your own tokens and HTTP client
|
|
@@ -371,7 +445,9 @@ Yes. Import from `@grida/refig/browser`. The core renderer uses `@grida/canvas-w
|
|
|
371
445
|
|
|
372
446
|
### What about fonts?
|
|
373
447
|
|
|
374
|
-
The WASM runtime ships with embedded fallback fonts (Geist / Geist Mono). **`loadFigmaDefaultFonts`** is enabled by default: the renderer loads the Figma default font set (Inter, Noto Sans KR/JP/SC, and optionally Noto Sans TC/HK and Noto Color Emoji) from CDN and registers them as fallbacks before the first render, so mixed-script and CJK text avoid tofu. Set **`loadFigmaDefaultFonts: false`** to disable (e.g. to avoid network or use only embedded fonts).
|
|
448
|
+
The WASM runtime ships with embedded fallback fonts (Geist / Geist Mono). **`loadFigmaDefaultFonts`** is enabled by default: the renderer loads the Figma default font set (Inter, Noto Sans KR/JP/SC, and optionally Noto Sans TC/HK and Noto Color Emoji) from CDN and registers them as fallbacks before the first render, so mixed-script and CJK text avoid tofu. Set **`loadFigmaDefaultFonts: false`** to disable (e.g. to avoid network or use only embedded fonts).
|
|
449
|
+
|
|
450
|
+
**Custom fonts** (e.g. Caveat, Roboto, brand typefaces) use the bring-your-own-font flow: call **`document.listFontFamilies(rootNodeId?)`** to see which families are used, load those fonts yourself, then pass **`fonts: Record<string, Uint8Array>`** to `FigmaRenderer`. See [Render with custom fonts](#render-with-custom-fonts).
|
|
375
451
|
|
|
376
452
|
## Contributing
|
|
377
453
|
|
package/dist/browser.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ExportSetting } from '@figma/rest-api-spec';
|
|
2
|
+
import { iofigma } from '@grida/io-figma';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* @grida/refig — shared core (no node:fs, no DOM)
|
|
@@ -28,6 +29,14 @@ interface RefigRendererOptions {
|
|
|
28
29
|
* Ref must match document references (e.g. 40-char hex for .fig, Figma image fill hash for REST).
|
|
29
30
|
*/
|
|
30
31
|
images?: Record<string, Uint8Array>;
|
|
32
|
+
/**
|
|
33
|
+
* Custom fonts keyed by family name. Use the family name returned by
|
|
34
|
+
* `listFontFamilies()` (e.g. from Figma's `style.font_family`). Pass one or
|
|
35
|
+
* more font files per family; the canvas resolves the correct face per
|
|
36
|
+
* text style. Skip Figma defaults (Inter, Noto Sans KR/JP/SC, etc.) —
|
|
37
|
+
* those are loaded by `loadFigmaDefaultFonts`.
|
|
38
|
+
*/
|
|
39
|
+
fonts?: Record<string, Uint8Array | Uint8Array[]>;
|
|
31
40
|
}
|
|
32
41
|
interface RefigRenderOptions {
|
|
33
42
|
format: RefigRenderFormat;
|
|
@@ -44,21 +53,61 @@ interface RefigRenderResult {
|
|
|
44
53
|
height: number;
|
|
45
54
|
}
|
|
46
55
|
type FigmaJsonDocument = Record<string, unknown>;
|
|
56
|
+
type FigFileDocument = ReturnType<typeof iofigma.kiwi.parseFile>;
|
|
47
57
|
declare class FigmaDocument {
|
|
48
58
|
readonly sourceType: "fig-file" | "rest-api-json";
|
|
49
59
|
/**
|
|
50
|
-
* For "fig-file" this is the raw bytes
|
|
60
|
+
* For "fig-file" this is the raw bytes (Uint8Array) or a pre-parsed FigFileDocument
|
|
61
|
+
* (e.g. from parseFileFromStream for large ZIP files).
|
|
51
62
|
* For "rest-api-json" this is the parsed REST API document JSON.
|
|
52
63
|
*/
|
|
53
|
-
readonly payload: Uint8Array | FigmaJsonDocument;
|
|
64
|
+
readonly payload: Uint8Array | FigmaJsonDocument | FigFileDocument;
|
|
65
|
+
/** Cached parsed FigFile (.fig only). */
|
|
66
|
+
private _figFile?;
|
|
67
|
+
/**
|
|
68
|
+
* Cache of ResolvedScene per rootNodeId. REST with images is not cached.
|
|
69
|
+
* Bounded to avoid unbounded memory growth in --export-all flows.
|
|
70
|
+
*/
|
|
71
|
+
private _sceneCache;
|
|
72
|
+
/** Max cached scenes; evicts LRU when exceeded. */
|
|
73
|
+
private static readonly _MAX_SCENE_CACHE;
|
|
54
74
|
/**
|
|
55
|
-
* @param input Raw `.fig` bytes (Uint8Array)
|
|
56
|
-
*
|
|
75
|
+
* @param input Raw `.fig` bytes (Uint8Array), a pre-parsed FigFileDocument
|
|
76
|
+
* (e.g. from parseFileFromStream for large files), or REST JSON.
|
|
57
77
|
*
|
|
58
78
|
* For file-path convenience in Node, use `FigmaDocument.fromFile()` from
|
|
59
79
|
* the `@grida/refig` entrypoint.
|
|
60
80
|
*/
|
|
61
|
-
constructor(input: Uint8Array | FigmaJsonDocument);
|
|
81
|
+
constructor(input: Uint8Array | FigmaJsonDocument | FigFileDocument);
|
|
82
|
+
/**
|
|
83
|
+
* Resolve document to Grida IR (scene JSON + images to register).
|
|
84
|
+
* On-demand, cached when deterministic (no images for REST).
|
|
85
|
+
* Cache is bounded (LRU eviction) to avoid OOM in --export-all flows.
|
|
86
|
+
* @internal
|
|
87
|
+
*/
|
|
88
|
+
_resolve(rootNodeId?: string, images?: Record<string, Uint8Array>): ResolvedScene;
|
|
89
|
+
private _sceneCacheGet;
|
|
90
|
+
private _sceneCacheSet;
|
|
91
|
+
/** @internal */
|
|
92
|
+
private _restToScene;
|
|
93
|
+
/** @internal */
|
|
94
|
+
private _figToScene;
|
|
95
|
+
/**
|
|
96
|
+
* Returns the list of font family names used in this document.
|
|
97
|
+
*
|
|
98
|
+
* The result is family names only — no weights, PostScript names, or other metadata.
|
|
99
|
+
* That is intentional: in practice, you can load all TTF/OTF files for each family
|
|
100
|
+
* (variable or static) and pass them to the renderer; it will resolve the correct face
|
|
101
|
+
* per text style. We prioritize simple usage and accurate font selection over
|
|
102
|
+
* performance or resource-optimized patterns.
|
|
103
|
+
*
|
|
104
|
+
* When rootNodeId is omitted, traverses all pages so fonts from every page are included.
|
|
105
|
+
*
|
|
106
|
+
* @param rootNodeId — Optional. When provided, scope to that node's subtree. Omit for the full document (all pages).
|
|
107
|
+
* @returns Unique font family names (e.g. `["Inter", "Caveat", "Roboto"]`).
|
|
108
|
+
*/
|
|
109
|
+
listFontFamilies(rootNodeId?: string): string[];
|
|
110
|
+
private _collectFontFamiliesFromSceneJson;
|
|
62
111
|
}
|
|
63
112
|
declare function resolveMimeType(format: RefigRenderFormat): string;
|
|
64
113
|
type RestNode = Record<string, unknown> & {
|
|
@@ -81,6 +130,15 @@ declare function collectExportsFromDocument(json: FigmaJsonDocument): ExportItem
|
|
|
81
130
|
* Constraint SCALE → scale; WIDTH/HEIGHT → width/height from constraint value and node aspect ratio.
|
|
82
131
|
*/
|
|
83
132
|
declare function exportSettingToRenderOptions(node: RestNode, setting: ExportSetting): RefigRenderOptions;
|
|
133
|
+
/**
|
|
134
|
+
* Resolved scene: Grida IR ready for canvas load.
|
|
135
|
+
* @internal
|
|
136
|
+
*/
|
|
137
|
+
interface ResolvedScene {
|
|
138
|
+
sceneJson: string;
|
|
139
|
+
images: Record<string, Uint8Array>;
|
|
140
|
+
imageRefsUsed?: string[];
|
|
141
|
+
}
|
|
84
142
|
declare class FigmaRenderer {
|
|
85
143
|
readonly document: FigmaDocument;
|
|
86
144
|
readonly options: RefigRendererOptions;
|