@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 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; // default: true
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 images separately.
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 | Required | Default | Description |
302
- | ---------------- | -------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
303
- | `<input>` | yes | | Path to `.fig`, JSON file, or directory containing `document.json` (and optionally `images/`) |
304
- | `--images <dir>` | no | | Directory of image assets for REST document (ignored if `<input>` is a dir with `images/`) |
305
- | `--node <id>` | yes\* | | Figma node ID to render (\*omit when using `--export-all`) |
306
- | `--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`). |
307
- | `--export-all` | no | | Export every node with exportSettings (REST JSON or .fig); `--out` is a directory |
308
- | `--format <fmt>` | no | inferred from `--out` extension | `png`, `jpeg`, `webp`, `pdf`, `svg` (single-node only; required when `--out` is omitted) |
309
- | `--width <px>` | no | `1024` | Viewport width (single-node only) |
310
- | `--height <px>` | no | `1024` | Viewport height (single-node only) |
311
- | `--scale <n>` | no | `1` | Raster scale factor (single-node only) |
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). Custom or other Google Fonts are **not** loaded by the renderer; the user is responsible for fetching font bytes and registering them with the canvas if needed.
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
 
@@ -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 of the .fig file.
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) or a parsed Figma REST API
56
- * document JSON object.
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;
package/dist/browser.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  collectExportsFromDocument,
5
5
  exportSettingToRenderOptions,
6
6
  resolveMimeType
7
- } from "./chunk-YI7PIULZ.mjs";
7
+ } from "./chunk-INJ5F2RK.mjs";
8
8
  export {
9
9
  FigmaDocument,
10
10
  FigmaRenderer,