@grida/refig 0.0.0 → 0.0.1
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 +63 -15
- package/dist/browser.d.mts +8 -0
- package/dist/browser.mjs +1 -1
- package/dist/{chunk-DAHUXARL.mjs → chunk-PK5L35ID.mjs} +171 -8
- package/dist/cli.mjs +20 -5
- package/dist/index.mjs +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
# `@grida/refig`
|
|
2
2
|
|
|
3
|
-
> **re**nder **fig**ma — headless Figma renderer in the spirit of [`resvg`](https://github.com/nicolo-ribaudo/resvg-js)
|
|
3
|
+
> **re**nder **fig**ma — headless Figma renderer (Node.js + browser) in the spirit of [`resvg-js`](https://github.com/nicolo-ribaudo/resvg-js)
|
|
4
4
|
|
|
5
|
-
Render Figma documents to **PNG, JPEG, WebP, PDF, and SVG**
|
|
5
|
+
Render Figma documents to **PNG, JPEG, WebP, PDF, and SVG** in **Node.js (no browser required)** or directly in the **browser**.
|
|
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
|
+
|
|
9
|
+
## Features (checklist)
|
|
10
|
+
|
|
11
|
+
- [x] Render from **`.fig` files** (offline / no API calls)
|
|
12
|
+
- [x] Render from **Figma REST API JSON** (bring your own auth + HTTP client)
|
|
13
|
+
- [x] Output formats: **PNG, JPEG, WebP, PDF, SVG**
|
|
14
|
+
- [x] **CLI** (`refig`) and **library API** (`FigmaDocument`, `FigmaRenderer`)
|
|
15
|
+
- [x] **Node.js** + **browser** entrypoints (`@grida/refig`, `@grida/refig/browser`)
|
|
16
|
+
- [x] IMAGE fills supported via **embedded `.fig` images** or a **local `images/` directory** for REST JSON
|
|
17
|
+
- [x] Batch export with **`--export-all`** (renders nodes with Figma export presets)
|
|
18
|
+
- [x] WASM + Skia-backed renderer via `@grida/canvas-wasm`
|
|
19
|
+
|
|
20
|
+
## Use cases
|
|
21
|
+
|
|
22
|
+
- Export assets in CI (deterministic, no network calls required)
|
|
23
|
+
- Generate thumbnails / previews from `.fig` or REST JSON
|
|
24
|
+
- Offline / air-gapped rendering from `.fig` exports
|
|
25
|
+
- In-browser previews with `@grida/refig/browser`
|
|
8
26
|
|
|
9
27
|
## Install
|
|
10
28
|
|
|
@@ -26,7 +44,7 @@ Both entrypoints export the same core API (`FigmaDocument`, `FigmaRenderer`, typ
|
|
|
26
44
|
### Render from a `.fig` file
|
|
27
45
|
|
|
28
46
|
```ts
|
|
29
|
-
import {
|
|
47
|
+
import { writeFileSync } from "node:fs";
|
|
30
48
|
import { FigmaDocument, FigmaRenderer } from "@grida/refig";
|
|
31
49
|
|
|
32
50
|
const doc = FigmaDocument.fromFile("path/to/file.fig");
|
|
@@ -92,6 +110,7 @@ renderer.dispose();
|
|
|
92
110
|
```ts
|
|
93
111
|
import { FigmaDocument, FigmaRenderer } from "@grida/refig/browser";
|
|
94
112
|
|
|
113
|
+
// `file` is a File from <input type="file">, drag-and-drop, etc.
|
|
95
114
|
// Uint8Array from a File input, fetch(), or drag-and-drop
|
|
96
115
|
const figBytes: Uint8Array = await file
|
|
97
116
|
.arrayBuffer()
|
|
@@ -172,6 +191,13 @@ interface RefigRenderResult {
|
|
|
172
191
|
pnpm add -g @grida/refig
|
|
173
192
|
```
|
|
174
193
|
|
|
194
|
+
Or run without installing:
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
npx @grida/refig <input> --node <node-id> --out <path>
|
|
198
|
+
pnpm dlx @grida/refig <input> --node <node-id> --out <path>
|
|
199
|
+
```
|
|
200
|
+
|
|
175
201
|
### Usage
|
|
176
202
|
|
|
177
203
|
**`<input>`** can be:
|
|
@@ -220,10 +246,41 @@ refig ./design.fig --export-all --out ./exports
|
|
|
220
246
|
# Scale 2x, custom dimensions
|
|
221
247
|
refig ./design.fig --node "1:23" --out ./out.png --width 512 --height 512 --scale 2
|
|
222
248
|
|
|
223
|
-
# No-install (
|
|
249
|
+
# No-install (run without installing)
|
|
250
|
+
npx @grida/refig ./design.fig --node "1:23" --out ./out.png
|
|
224
251
|
pnpm dlx @grida/refig ./design.fig --node "1:23" --out ./out.png
|
|
225
252
|
```
|
|
226
253
|
|
|
254
|
+
### Quick test via `figma_archive.py` (REST API → `document.json` + `images/`)
|
|
255
|
+
|
|
256
|
+
If you want an end-to-end test from a real Figma file using the REST API, you can generate a local “project directory” that refig can consume directly.
|
|
257
|
+
|
|
258
|
+
1. Archive a Figma file (stdlib-only Python script):
|
|
259
|
+
|
|
260
|
+
- Script: [`figma_archive.py` (gist)](https://gist.github.com/softmarshmallow/27ad65dfa5babc2c67b41740f1f05791)
|
|
261
|
+
- (For repo contributors, it’s also in this monorepo at `.tools/figma_archive.py`.)
|
|
262
|
+
- Save the script locally as `figma_archive.py`, then run:
|
|
263
|
+
|
|
264
|
+
```sh
|
|
265
|
+
# File key is the "<key>" part of `https://www.figma.com/file/<key>/...`
|
|
266
|
+
python3 figma_archive.py --x-figma-token "<token>" --filekey "<key>" --archive-dir ./my-figma-export
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
This writes:
|
|
270
|
+
|
|
271
|
+
- `./my-figma-export/document.json` (with `geometry=paths`)
|
|
272
|
+
- `./my-figma-export/images/<ref>.<ext>` (image fills downloaded from `/v1/files/:key/images`)
|
|
273
|
+
|
|
274
|
+
2. Render using the directory as `<input>`:
|
|
275
|
+
|
|
276
|
+
```sh
|
|
277
|
+
# Single node
|
|
278
|
+
refig ./my-figma-export --node "1:23" --out ./out.png
|
|
279
|
+
|
|
280
|
+
# Or export everything with Figma export presets
|
|
281
|
+
refig ./my-figma-export --export-all --out ./exports
|
|
282
|
+
```
|
|
283
|
+
|
|
227
284
|
### Export all (`--export-all`)
|
|
228
285
|
|
|
229
286
|
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.
|
|
@@ -282,15 +339,6 @@ REST JSON ───┘
|
|
|
282
339
|
|
|
283
340
|
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
341
|
|
|
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
342
|
## Not planned
|
|
295
343
|
|
|
296
344
|
- **Figma API fetching / auth** — bring your own tokens and HTTP client
|
|
@@ -316,7 +364,7 @@ Yes. Import from `@grida/refig/browser`. The core renderer uses `@grida/canvas-w
|
|
|
316
364
|
|
|
317
365
|
### What about fonts?
|
|
318
366
|
|
|
319
|
-
The WASM runtime ships with embedded fallback fonts.
|
|
367
|
+
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.
|
|
320
368
|
|
|
321
369
|
## Contributing
|
|
322
370
|
|
package/dist/browser.d.mts
CHANGED
|
@@ -14,6 +14,14 @@ interface RefigRendererOptions {
|
|
|
14
14
|
* @default true
|
|
15
15
|
*/
|
|
16
16
|
useEmbeddedFonts?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* When true (default), the renderer ensures Figma default fonts (Inter, Noto Sans KR/JP/SC, etc.)
|
|
19
|
+
* are loaded from CDN and registered with the canvas before any scene is loaded.
|
|
20
|
+
* Reduces tofu for mixed-script and CJK text. Set to false to skip (e.g. to avoid network or use only embedded fonts).
|
|
21
|
+
* Custom fonts remain the user's responsibility.
|
|
22
|
+
* @default true
|
|
23
|
+
*/
|
|
24
|
+
loadFigmaDefaultFonts?: boolean;
|
|
17
25
|
/**
|
|
18
26
|
* Map of Figma image ref (hash) to image bytes.
|
|
19
27
|
* Used for REST API and .fig input so IMAGE fills render correctly.
|
package/dist/browser.mjs
CHANGED
|
@@ -13393,6 +13393,61 @@ var iofigma;
|
|
|
13393
13393
|
...map2.blendModeMap,
|
|
13394
13394
|
PASS_THROUGH: "pass-through"
|
|
13395
13395
|
};
|
|
13396
|
+
function normalizeRestVectorNetworkToIR(rest) {
|
|
13397
|
+
if (!Array.isArray(rest.vertices) || !Array.isArray(rest.segments) || !Array.isArray(rest.regions)) {
|
|
13398
|
+
return null;
|
|
13399
|
+
}
|
|
13400
|
+
const vertexCount = rest.vertices.length;
|
|
13401
|
+
const segmentCount = rest.segments.length;
|
|
13402
|
+
for (const seg of rest.segments) {
|
|
13403
|
+
if (typeof seg.start !== "number" || typeof seg.end !== "number" || seg.start < 0 || seg.start >= vertexCount || seg.end < 0 || seg.end >= vertexCount || !seg.startTangent || !seg.endTangent) {
|
|
13404
|
+
return null;
|
|
13405
|
+
}
|
|
13406
|
+
}
|
|
13407
|
+
for (const region of rest.regions) {
|
|
13408
|
+
if (!Array.isArray(region.loops)) return null;
|
|
13409
|
+
for (const loop of region.loops) {
|
|
13410
|
+
if (!Array.isArray(loop)) return null;
|
|
13411
|
+
for (const segIdx of loop) {
|
|
13412
|
+
if (typeof segIdx !== "number" || segIdx < 0 || segIdx >= segmentCount) {
|
|
13413
|
+
return null;
|
|
13414
|
+
}
|
|
13415
|
+
}
|
|
13416
|
+
}
|
|
13417
|
+
}
|
|
13418
|
+
const vertices = rest.vertices.map(
|
|
13419
|
+
(v) => ({
|
|
13420
|
+
x: v.position?.x ?? 0,
|
|
13421
|
+
y: v.position?.y ?? 0,
|
|
13422
|
+
styleID: 0
|
|
13423
|
+
})
|
|
13424
|
+
);
|
|
13425
|
+
const segments = rest.segments.map(
|
|
13426
|
+
(seg) => ({
|
|
13427
|
+
styleID: 0,
|
|
13428
|
+
start: {
|
|
13429
|
+
vertex: seg.start,
|
|
13430
|
+
dx: seg.startTangent.x,
|
|
13431
|
+
dy: seg.startTangent.y
|
|
13432
|
+
},
|
|
13433
|
+
end: {
|
|
13434
|
+
vertex: seg.end,
|
|
13435
|
+
dx: seg.endTangent.x,
|
|
13436
|
+
dy: seg.endTangent.y
|
|
13437
|
+
}
|
|
13438
|
+
})
|
|
13439
|
+
);
|
|
13440
|
+
const regions = rest.regions.map(
|
|
13441
|
+
(region) => {
|
|
13442
|
+
const wr = region.windingRule?.toUpperCase?.();
|
|
13443
|
+
const windingRule = wr === "EVENODD" || wr === "ODD" ? "ODD" : "NONZERO";
|
|
13444
|
+
const loops = region.loops.map((loop) => ({ segments: loop }));
|
|
13445
|
+
return { styleID: 0, windingRule, loops };
|
|
13446
|
+
}
|
|
13447
|
+
);
|
|
13448
|
+
return { vertices, segments, regions };
|
|
13449
|
+
}
|
|
13450
|
+
map2.normalizeRestVectorNetworkToIR = normalizeRestVectorNetworkToIR;
|
|
13396
13451
|
})(map = restful2.map || (restful2.map = {}));
|
|
13397
13452
|
let factory;
|
|
13398
13453
|
((factory2) => {
|
|
@@ -13699,6 +13754,7 @@ var iofigma;
|
|
|
13699
13754
|
try {
|
|
13700
13755
|
const vectorNetwork = index_default2.fromSVGPathData(pathData);
|
|
13701
13756
|
const bbox = index_default2.getBBox(vectorNetwork);
|
|
13757
|
+
const strokeAsFill = options.strokeAsFill === true;
|
|
13702
13758
|
return {
|
|
13703
13759
|
id: childId,
|
|
13704
13760
|
...base_node_trait({
|
|
@@ -13716,12 +13772,25 @@ var iofigma;
|
|
|
13716
13772
|
],
|
|
13717
13773
|
size: { x: bbox.width, y: bbox.height }
|
|
13718
13774
|
}),
|
|
13719
|
-
...
|
|
13720
|
-
|
|
13721
|
-
|
|
13722
|
-
|
|
13723
|
-
|
|
13724
|
-
|
|
13775
|
+
...strokeAsFill ? {
|
|
13776
|
+
...fills_trait(
|
|
13777
|
+
parentNode.strokes ?? [],
|
|
13778
|
+
context,
|
|
13779
|
+
imageRefsUsed
|
|
13780
|
+
),
|
|
13781
|
+
...stroke_trait(
|
|
13782
|
+
{ strokes: [], strokeWeight: 0 },
|
|
13783
|
+
context,
|
|
13784
|
+
imageRefsUsed
|
|
13785
|
+
)
|
|
13786
|
+
} : {
|
|
13787
|
+
...options.useFill ? fills_trait(parentNode.fills, context, imageRefsUsed) : {},
|
|
13788
|
+
...options.useStroke ? stroke_trait(parentNode, context, imageRefsUsed) : stroke_trait(
|
|
13789
|
+
{ strokes: [], strokeWeight: 0 },
|
|
13790
|
+
context,
|
|
13791
|
+
imageRefsUsed
|
|
13792
|
+
)
|
|
13793
|
+
},
|
|
13725
13794
|
..."effects" in parentNode && parentNode.effects ? effects_trait(parentNode.effects) : effects_trait(void 0),
|
|
13726
13795
|
type: "vector",
|
|
13727
13796
|
vector_network: vectorNetwork,
|
|
@@ -13770,7 +13839,7 @@ var iofigma;
|
|
|
13770
13839
|
node2,
|
|
13771
13840
|
childId,
|
|
13772
13841
|
name,
|
|
13773
|
-
{ useFill: false, useStroke: true }
|
|
13842
|
+
{ useFill: false, useStroke: false, strokeAsFill: true }
|
|
13774
13843
|
);
|
|
13775
13844
|
if (childNode) {
|
|
13776
13845
|
nodes[childId] = childNode;
|
|
@@ -14013,6 +14082,37 @@ var iofigma;
|
|
|
14013
14082
|
case "REGULAR_POLYGON":
|
|
14014
14083
|
case "STAR":
|
|
14015
14084
|
case "VECTOR": {
|
|
14085
|
+
const useRestVectorNetwork = context.disable_volatile_apis !== true && "vectorNetwork" in node && node.vectorNetwork != null;
|
|
14086
|
+
if (useRestVectorNetwork) {
|
|
14087
|
+
try {
|
|
14088
|
+
const ir = restful2.map.normalizeRestVectorNetworkToIR(
|
|
14089
|
+
node.vectorNetwork
|
|
14090
|
+
);
|
|
14091
|
+
if (ir) {
|
|
14092
|
+
const gridaVectorNetwork = {
|
|
14093
|
+
vertices: ir.vertices.map((v) => [v.x, v.y]),
|
|
14094
|
+
segments: ir.segments.map((seg) => ({
|
|
14095
|
+
a: seg.start.vertex,
|
|
14096
|
+
b: seg.end.vertex,
|
|
14097
|
+
ta: [seg.start.dx, seg.start.dy],
|
|
14098
|
+
tb: [seg.end.dx, seg.end.dy]
|
|
14099
|
+
}))
|
|
14100
|
+
};
|
|
14101
|
+
return {
|
|
14102
|
+
id: gridaId,
|
|
14103
|
+
...base_node_trait(node),
|
|
14104
|
+
...positioning_trait(node),
|
|
14105
|
+
...fills_trait(node.fills, context, imageRefsUsed),
|
|
14106
|
+
...stroke_trait(node, context, imageRefsUsed),
|
|
14107
|
+
...corner_radius_trait(node),
|
|
14108
|
+
...effects_trait(node.effects),
|
|
14109
|
+
type: "vector",
|
|
14110
|
+
vector_network: gridaVectorNetwork
|
|
14111
|
+
};
|
|
14112
|
+
}
|
|
14113
|
+
} catch {
|
|
14114
|
+
}
|
|
14115
|
+
}
|
|
14016
14116
|
return {
|
|
14017
14117
|
id: gridaId,
|
|
14018
14118
|
...base_node_trait(node),
|
|
@@ -14363,7 +14463,11 @@ var iofigma;
|
|
|
14363
14463
|
const f = fmt(s.imageType);
|
|
14364
14464
|
if (!f) return null;
|
|
14365
14465
|
const c = s.constraint;
|
|
14366
|
-
return {
|
|
14466
|
+
return {
|
|
14467
|
+
format: f,
|
|
14468
|
+
suffix: s.suffix ?? "",
|
|
14469
|
+
constraint: { type: cstr(c?.type), value: c?.value ?? 1 }
|
|
14470
|
+
};
|
|
14367
14471
|
}).filter((x) => x !== null);
|
|
14368
14472
|
return exportSettings.length ? { exportSettings } : {};
|
|
14369
14473
|
}
|
|
@@ -15607,6 +15711,60 @@ var grida;
|
|
|
15607
15711
|
})(grida || (grida = {}));
|
|
15608
15712
|
var cloneWithUndefinedValues = (obj) => Object.fromEntries(Object.keys(obj).map((key) => [key, void 0]));
|
|
15609
15713
|
|
|
15714
|
+
// figma-default-fonts.ts
|
|
15715
|
+
var FIGMA_DEFAULT_FONT_ENTRIES = [
|
|
15716
|
+
{
|
|
15717
|
+
family: "Inter",
|
|
15718
|
+
url: "https://fonts.gstatic.com/s/inter/v19/UcCo3FwrK3iLTfvlaQc78lA2.ttf"
|
|
15719
|
+
},
|
|
15720
|
+
{
|
|
15721
|
+
family: "Noto Sans KR",
|
|
15722
|
+
url: "https://fonts.gstatic.com/s/notosanskr/v37/PbykFmXiEBPT4ITbgNA5Cgm21nTs4JMMuA.ttf"
|
|
15723
|
+
},
|
|
15724
|
+
{
|
|
15725
|
+
family: "Noto Sans JP",
|
|
15726
|
+
url: "https://fonts.gstatic.com/s/notosansjp/v54/-F62fjtqLzI2JPCgQBnw7HFoxgIO2lZ9hg.ttf"
|
|
15727
|
+
},
|
|
15728
|
+
{
|
|
15729
|
+
family: "Noto Sans SC",
|
|
15730
|
+
url: "https://fonts.gstatic.com/s/notosanssc/v38/k3kXo84MPvpLmixcA63oeALhKYiJ-Q7m8w.ttf"
|
|
15731
|
+
},
|
|
15732
|
+
{
|
|
15733
|
+
family: "Noto Sans TC",
|
|
15734
|
+
url: "https://fonts.gstatic.com/s/notosanstc/v37/-nF7OG829Oofr2wohFbTp9iFPysLA_ZJ1g.ttf"
|
|
15735
|
+
},
|
|
15736
|
+
{
|
|
15737
|
+
family: "Noto Sans HK",
|
|
15738
|
+
url: "https://fonts.gstatic.com/s/notosanshk/v33/nKKQ-GM_FYFRJvXzVXaAPe9hNHB3Eu7mOQ.ttf"
|
|
15739
|
+
},
|
|
15740
|
+
{
|
|
15741
|
+
family: "Noto Color Emoji",
|
|
15742
|
+
url: "https://fonts.gstatic.com/s/notocoloremoji/v35/Yq6P-KqIXTD0t4D9z1ESnKM3-HpFab5s79iz64w.ttf"
|
|
15743
|
+
}
|
|
15744
|
+
];
|
|
15745
|
+
var FIGMA_DEFAULT_FALLBACK_ORDER = [
|
|
15746
|
+
"Inter",
|
|
15747
|
+
"Noto Sans KR",
|
|
15748
|
+
"Noto Sans JP",
|
|
15749
|
+
"Noto Sans SC",
|
|
15750
|
+
"Noto Sans TC",
|
|
15751
|
+
"Noto Sans HK",
|
|
15752
|
+
"Noto Color Emoji"
|
|
15753
|
+
];
|
|
15754
|
+
async function ensureFigmaDefaultFonts(canvas) {
|
|
15755
|
+
for (const entry of FIGMA_DEFAULT_FONT_ENTRIES) {
|
|
15756
|
+
const res = await fetch(entry.url);
|
|
15757
|
+
if (!res.ok) {
|
|
15758
|
+
throw new Error(
|
|
15759
|
+
`Figma default font fetch failed: ${entry.family} ${res.status} ${res.statusText}`
|
|
15760
|
+
);
|
|
15761
|
+
}
|
|
15762
|
+
const buffer = await res.arrayBuffer();
|
|
15763
|
+
canvas.addFont(entry.family, new Uint8Array(buffer));
|
|
15764
|
+
}
|
|
15765
|
+
canvas.setFallbackFonts(FIGMA_DEFAULT_FALLBACK_ORDER);
|
|
15766
|
+
}
|
|
15767
|
+
|
|
15610
15768
|
// lib.ts
|
|
15611
15769
|
var FigmaDocument = class {
|
|
15612
15770
|
/**
|
|
@@ -15960,6 +16118,11 @@ var FigmaRenderer = class {
|
|
|
15960
16118
|
height,
|
|
15961
16119
|
useEmbeddedFonts: this.options.useEmbeddedFonts ?? true
|
|
15962
16120
|
});
|
|
16121
|
+
if (this.options.loadFigmaDefaultFonts !== false) {
|
|
16122
|
+
await ensureFigmaDefaultFonts(
|
|
16123
|
+
this._canvas
|
|
16124
|
+
);
|
|
16125
|
+
}
|
|
15963
16126
|
return this._canvas;
|
|
15964
16127
|
}
|
|
15965
16128
|
loadScene(canvas, nodeId) {
|
package/dist/cli.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
exportSettingToRenderOptions,
|
|
7
7
|
figFileToRestLikeDocument,
|
|
8
8
|
iofigma
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-PK5L35ID.mjs";
|
|
10
10
|
|
|
11
11
|
// cli.ts
|
|
12
12
|
import {
|
|
@@ -80,7 +80,7 @@ function exportAllOutputBasename(nodeId, suffix, format) {
|
|
|
80
80
|
const name = safeSuffix ? `${safeId}_${safeSuffix}` : safeId;
|
|
81
81
|
return `${name}.${ext}`;
|
|
82
82
|
}
|
|
83
|
-
async function runExportAll(documentPath, outDir, imagesDir) {
|
|
83
|
+
async function runExportAll(documentPath, outDir, imagesDir, skipDefaultFonts) {
|
|
84
84
|
const isFig = documentPath.toLowerCase().endsWith(".fig");
|
|
85
85
|
let document;
|
|
86
86
|
let items;
|
|
@@ -109,6 +109,9 @@ async function runExportAll(documentPath, outDir, imagesDir) {
|
|
|
109
109
|
rendererOptions = { images: readImagesFromDir(imagesDir) };
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
+
if (skipDefaultFonts || process.env.REFIG_SKIP_DEFAULT_FONTS === "1") {
|
|
113
|
+
rendererOptions = { ...rendererOptions, loadFigmaDefaultFonts: false };
|
|
114
|
+
}
|
|
112
115
|
if (items.length === 0) {
|
|
113
116
|
process.stdout.write("No nodes with export settings found.\n");
|
|
114
117
|
return;
|
|
@@ -144,6 +147,9 @@ async function runSingleNode(documentPath, nodeId, outPath, opts) {
|
|
|
144
147
|
const isJson = documentPath.toLowerCase().endsWith(".json");
|
|
145
148
|
const document = isJson ? new FigmaDocument(JSON.parse(readFileSync(documentPath, "utf8"))) : new FigmaDocument(new Uint8Array(readFileSync(documentPath)));
|
|
146
149
|
const rendererOptions = isJson && opts.imagesDir ? { images: readImagesFromDir(opts.imagesDir) } : {};
|
|
150
|
+
if (opts.skipDefaultFonts || process.env.REFIG_SKIP_DEFAULT_FONTS === "1") {
|
|
151
|
+
rendererOptions.loadFigmaDefaultFonts = false;
|
|
152
|
+
}
|
|
147
153
|
const renderer = new FigmaRenderer(document, rendererOptions);
|
|
148
154
|
try {
|
|
149
155
|
const result = await renderer.render(nodeId, {
|
|
@@ -183,7 +189,10 @@ async function main() {
|
|
|
183
189
|
).option(
|
|
184
190
|
"--format <fmt>",
|
|
185
191
|
"png | jpeg | webp | pdf | svg (single-node only; default: from --out extension)"
|
|
186
|
-
).option("--width <px>", "Viewport width (single-node only)", "1024").option("--height <px>", "Viewport height (single-node only)", "1024").option("--scale <n>", "Raster scale factor (single-node only)", "1").
|
|
192
|
+
).option("--width <px>", "Viewport width (single-node only)", "1024").option("--height <px>", "Viewport height (single-node only)", "1024").option("--scale <n>", "Raster scale factor (single-node only)", "1").option(
|
|
193
|
+
"--skip-default-fonts",
|
|
194
|
+
"Do not load Figma default fonts (same as REFIG_SKIP_DEFAULT_FONTS=1)"
|
|
195
|
+
).action(
|
|
187
196
|
async (input, options) => {
|
|
188
197
|
const outPath = String(options.out ?? "").trim();
|
|
189
198
|
const exportAll = options.exportAll === true;
|
|
@@ -211,7 +220,12 @@ async function main() {
|
|
|
211
220
|
} else {
|
|
212
221
|
mkdirSync(outDir, { recursive: true });
|
|
213
222
|
}
|
|
214
|
-
await runExportAll(
|
|
223
|
+
await runExportAll(
|
|
224
|
+
documentPath,
|
|
225
|
+
outDir,
|
|
226
|
+
imagesDir,
|
|
227
|
+
options.skipDefaultFonts === true
|
|
228
|
+
);
|
|
215
229
|
return;
|
|
216
230
|
}
|
|
217
231
|
if (!nodeId) {
|
|
@@ -225,7 +239,8 @@ async function main() {
|
|
|
225
239
|
width,
|
|
226
240
|
height,
|
|
227
241
|
scale,
|
|
228
|
-
imagesDir
|
|
242
|
+
imagesDir,
|
|
243
|
+
skipDefaultFonts: options.skipDefaultFonts === true
|
|
229
244
|
});
|
|
230
245
|
}
|
|
231
246
|
);
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grida/refig",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Headless Figma renderer — render .fig and REST API JSON to PNG/JPEG/WebP/PDF/SVG",
|
|
6
6
|
"keywords": [
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"wasm",
|
|
22
22
|
"webp"
|
|
23
23
|
],
|
|
24
|
-
"homepage": "https://
|
|
24
|
+
"homepage": "https://grida.co/docs/packages/@grida/refig",
|
|
25
25
|
"bugs": "https://github.com/gridaco/grida/issues",
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"author": "softmarshmallow",
|
|
@@ -55,11 +55,11 @@
|
|
|
55
55
|
"access": "public"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@grida/canvas-wasm": "0.90.0-canary.
|
|
58
|
+
"@grida/canvas-wasm": "0.90.0-canary.7",
|
|
59
59
|
"commander": "^12.1.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@figma/rest-api-spec": "0.
|
|
62
|
+
"@figma/rest-api-spec": "0.36.0",
|
|
63
63
|
"fflate": "^0.8.2",
|
|
64
64
|
"tsup": "^8.5.0"
|
|
65
65
|
},
|