figmatk 0.3.1 → 0.3.8
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +39 -21
- package/cli.mjs +2 -0
- package/commands/render.mjs +56 -0
- package/lib/rasterizer/deck-rasterizer.mjs +228 -0
- package/lib/rasterizer/download-font.mjs +57 -0
- package/lib/rasterizer/font-resolver.mjs +602 -0
- package/lib/rasterizer/fonts/DarkerGrotesque-400.ttf +0 -0
- package/lib/rasterizer/fonts/DarkerGrotesque-500.ttf +0 -0
- package/lib/rasterizer/fonts/DarkerGrotesque-600.ttf +0 -0
- package/lib/rasterizer/fonts/Inter-Regular.ttf +0 -0
- package/lib/rasterizer/fonts/Inter-Variable.ttf +0 -0
- package/lib/rasterizer/fonts/Inter.var.woff2 +0 -0
- package/lib/rasterizer/fonts/darker-grotesque-patched-400-normal.woff2 +0 -0
- package/lib/rasterizer/fonts/darker-grotesque-patched-500-normal.woff2 +0 -0
- package/lib/rasterizer/fonts/darker-grotesque-patched-600-normal.woff2 +0 -0
- package/lib/rasterizer/fonts/darker-grotesque-patched-700-normal.woff2 +0 -0
- package/lib/rasterizer/fonts/inter-v3-400-italic.woff2 +0 -0
- package/lib/rasterizer/fonts/inter-v3-400-normal.woff2 +0 -0
- package/lib/rasterizer/fonts/inter-v3-500-normal.woff2 +0 -0
- package/lib/rasterizer/fonts/inter-v3-600-normal.woff2 +0 -0
- package/lib/rasterizer/fonts/inter-v3-700-italic.woff2 +0 -0
- package/lib/rasterizer/fonts/inter-v3-700-normal.woff2 +0 -0
- package/lib/rasterizer/fonts/inter-v3-meta.json +6 -0
- package/lib/rasterizer/render-report-lib.mjs +127 -0
- package/lib/rasterizer/render-report.mjs +25 -0
- package/lib/rasterizer/svg-builder.mjs +626 -0
- package/lib/rasterizer/test-render.mjs +63 -0
- package/lib/template-deck.mjs +29 -1
- package/manifest.json +21 -0
- package/mcp-server.mjs +65 -4
- package/package.json +17 -2
- package/skills/figma-slides-creator/SKILL.md +82 -209
- package/skills/figma-template-builder/SKILL.md +11 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Figma Slides lets you download presentations as `.deck` files and re-upload them
|
|
|
21
21
|
|
|
22
22
|
FigmaTK makes this round-trip programmable. Download a `.deck`, modify it, re-upload. Everything stays native.
|
|
23
23
|
|
|
24
|
-
Plug in
|
|
24
|
+
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.
|
|
25
25
|
|
|
26
26
|
## Use Cases
|
|
27
27
|
|
|
@@ -48,26 +48,48 @@ figmatk list-overrides my-presentation.deck # editable fields per symbol
|
|
|
48
48
|
|
|
49
49
|
→ Full CLI reference: [docs/cli.md](docs/cli.md)
|
|
50
50
|
|
|
51
|
-
## Claude
|
|
51
|
+
## Claude Cowork / MCP Integration
|
|
52
52
|
|
|
53
|
-
FigmaTK
|
|
53
|
+
FigmaTK supports two Claude Cowork install paths:
|
|
54
|
+
|
|
55
|
+
- GitHub-backed personal plugin install, which preserves Claude Cowork's repo update-check behavior
|
|
56
|
+
- Local `.mcpb` bundle install, which matches Anthropic's current desktop-extension packaging model
|
|
57
|
+
|
|
58
|
+
### Option 1 — Install from GitHub in Claude Cowork
|
|
59
|
+
|
|
60
|
+
If you want Claude Cowork to keep checking the repo for updates, install `figmatk` from GitHub/personal plugins inside Claude Cowork.
|
|
61
|
+
|
|
62
|
+
That path uses the checked-in [plugin.json](/Users/rob/Dev/figmatk/.claude-plugin/plugin.json) and [marketplace.json](/Users/rob/Dev/figmatk/.claude-plugin/marketplace.json) metadata.
|
|
63
|
+
|
|
64
|
+
### Option 2 — Install the local MCPB bundle
|
|
65
|
+
|
|
66
|
+
Build the extension bundle:
|
|
54
67
|
|
|
55
68
|
```bash
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
npm install
|
|
70
|
+
npm run pack
|
|
58
71
|
```
|
|
59
72
|
|
|
60
|
-
|
|
73
|
+
This creates `dist/figmatk.mcpb`. Install that bundle from Claude Desktop/Cowork's Extensions UI.
|
|
61
74
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
Install in Claude Cowork:
|
|
76
|
+
|
|
77
|
+
1. Open Claude Cowork or Claude Desktop.
|
|
78
|
+
2. Go to `Settings`.
|
|
79
|
+
3. Open `Extensions`.
|
|
80
|
+
4. Choose the local install/add option.
|
|
81
|
+
5. Select `dist/figmatk.mcpb`.
|
|
82
|
+
|
|
83
|
+
Use this path when you want a local extension artifact. Unlike the GitHub-backed personal plugin path, local `.mcpb` installs do not poll the repo for updates automatically.
|
|
69
84
|
|
|
70
|
-
|
|
85
|
+
The MCP server covers four high-level workflows:
|
|
86
|
+
|
|
87
|
+
- create a new deck from scratch
|
|
88
|
+
- author a reusable Slides template
|
|
89
|
+
- instantiate a new deck from a template
|
|
90
|
+
- inspect or edit an existing deck
|
|
91
|
+
|
|
92
|
+
→ MCP tool reference: [docs/mcp.md](docs/mcp.md)
|
|
71
93
|
|
|
72
94
|
## Template Workflows
|
|
73
95
|
|
|
@@ -76,14 +98,9 @@ FigmaTK supports two related template states:
|
|
|
76
98
|
- Draft templates: `SLIDE_ROW -> SLIDE -> ...`
|
|
77
99
|
- Published templates: `SLIDE_ROW -> MODULE -> SLIDE -> ...`
|
|
78
100
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
- Layouts: `layout:<name>`
|
|
82
|
-
- Text slots: `slot:text:<name>`
|
|
83
|
-
- Image slots: `slot:image:<name>`
|
|
84
|
-
- Decorative fixed imagery: `fixed:image:<name>`
|
|
101
|
+
Reusable template authoring is built around explicit layout and slot naming, then a publish-like wrapping step before later instantiation.
|
|
85
102
|
|
|
86
|
-
|
|
103
|
+
→ Template workflow guide: [docs/template-workflows.md](docs/template-workflows.md)
|
|
87
104
|
|
|
88
105
|
## Programmatic API
|
|
89
106
|
|
|
@@ -98,6 +115,7 @@ await deck.save('output.deck');
|
|
|
98
115
|
|
|
99
116
|
| Docs | |
|
|
100
117
|
|------|---|
|
|
118
|
+
| MCP / Claude workflows | [docs/mcp.md](docs/mcp.md) |
|
|
101
119
|
| High-level API | [docs/figmatk-api-spec.md](docs/figmatk-api-spec.md) |
|
|
102
120
|
| Low-level FigDeck API | [docs/library.md](docs/library.md) |
|
|
103
121
|
| Template workflows | [docs/template-workflows.md](docs/template-workflows.md) |
|
package/cli.mjs
CHANGED
|
@@ -24,6 +24,7 @@ const COMMANDS = {
|
|
|
24
24
|
'clone-slide': './commands/clone-slide.mjs',
|
|
25
25
|
'remove-slide': './commands/remove-slide.mjs',
|
|
26
26
|
'roundtrip': './commands/roundtrip.mjs',
|
|
27
|
+
'render': './commands/render.mjs',
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
const command = process.argv[2];
|
|
@@ -39,6 +40,7 @@ if (!command || command === '--help' || command === '-h') {
|
|
|
39
40
|
console.log(' clone-slide Duplicate a template slide with new content');
|
|
40
41
|
console.log(' remove-slide Mark slides as REMOVED');
|
|
41
42
|
console.log(' roundtrip Decode and re-encode (pipeline validation)');
|
|
43
|
+
console.log(' render Rasterize slides to PNG');
|
|
42
44
|
console.log('\nUsage: figmatk <command> [args...]');
|
|
43
45
|
process.exit(0);
|
|
44
46
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render — Rasterize slides in a .deck file to PNG.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* figmatk render <file.deck> -o <output-dir> [options]
|
|
6
|
+
*
|
|
7
|
+
* Options:
|
|
8
|
+
* -o <dir> Output directory (default: ./render-out)
|
|
9
|
+
* --slide <n> Render only slide N (1-based). Omit to render all.
|
|
10
|
+
* --scale <n> Zoom factor: 1 = 1920×1080, 0.5 = 960×540 (default: 1)
|
|
11
|
+
* --width <px> Output width in pixels (height scales proportionally)
|
|
12
|
+
* --fonts <dir> Extra font directory to load (can repeat)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
16
|
+
import { join, resolve } from 'path';
|
|
17
|
+
import { FigDeck } from '../lib/fig-deck.mjs';
|
|
18
|
+
import { renderDeck, registerFontDir } from '../lib/rasterizer/deck-rasterizer.mjs';
|
|
19
|
+
import { resolveFonts } from '../lib/rasterizer/font-resolver.mjs';
|
|
20
|
+
|
|
21
|
+
export async function run(args, flags) {
|
|
22
|
+
const file = args[0];
|
|
23
|
+
if (!file) {
|
|
24
|
+
console.error('Usage: render <file.deck> -o <output-dir> [--slide N] [--scale 0.5] [--width 400] [--fonts <dir>]');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const outDir = resolve(flags.o ?? flags.output ?? './render-out');
|
|
29
|
+
|
|
30
|
+
// Build render options
|
|
31
|
+
const renderOpts = {};
|
|
32
|
+
if (flags.width) renderOpts.width = parseInt(flags.width);
|
|
33
|
+
else if (flags.scale) renderOpts.scale = parseFloat(flags.scale);
|
|
34
|
+
|
|
35
|
+
// Load extra font directories
|
|
36
|
+
const fontDirs = [].concat(flags.fonts ?? []);
|
|
37
|
+
for (const d of fontDirs) registerFontDir(resolve(d));
|
|
38
|
+
|
|
39
|
+
const deck = await FigDeck.fromDeckFile(file);
|
|
40
|
+
await resolveFonts(deck, { quiet: false });
|
|
41
|
+
mkdirSync(outDir, { recursive: true });
|
|
42
|
+
|
|
43
|
+
// Filter to single slide if requested
|
|
44
|
+
const slideFilter = flags.slide ? parseInt(flags.slide) : null;
|
|
45
|
+
const slides = await renderDeck(deck, renderOpts);
|
|
46
|
+
|
|
47
|
+
for (const { index, slideId, png } of slides) {
|
|
48
|
+
if (slideFilter && index + 1 !== slideFilter) continue;
|
|
49
|
+
const outFile = join(outDir, `slide-${String(index + 1).padStart(3, '0')}.png`);
|
|
50
|
+
writeFileSync(outFile, png);
|
|
51
|
+
console.log(` slide ${index + 1} → ${outFile}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const count = slideFilter ? 1 : slides.length;
|
|
55
|
+
console.log(`\nRendered ${count} slide(s) to ${outDir}`);
|
|
56
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deck-rasterizer.mjs — Render FigDeck slides to PNG via WASM (resvg).
|
|
3
|
+
*
|
|
4
|
+
* WASM is initialized once per process. Fonts are loaded at init time and
|
|
5
|
+
* can be hot-plugged via registerFont() before or after initialization.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { renderDeck, registerFont } from './deck-rasterizer.mjs';
|
|
9
|
+
* await registerFont('/path/to/CustomFont.ttf');
|
|
10
|
+
* const pngs = await renderDeck(deck); // Map<slideIndex, Uint8Array>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
14
|
+
import { join, dirname } from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import { initWasm, Resvg } from '@resvg/resvg-wasm';
|
|
17
|
+
import { slideToSvg } from './svg-builder.mjs';
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const WASM_PATH = join(__dirname, '../../node_modules/@resvg/resvg-wasm/index_bg.wasm');
|
|
21
|
+
|
|
22
|
+
// ── Font registry ─────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Try to load a font file from a path — returns Buffer or null (never throws).
|
|
26
|
+
* Tries the given path variants in order, returning the first that exists.
|
|
27
|
+
*/
|
|
28
|
+
function tryFont(...paths) {
|
|
29
|
+
for (const p of paths) {
|
|
30
|
+
try { return readFileSync(p); } catch { /* not found */ }
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Load all available weights of a font from @fontsource, trying WOFF2 then TTF.
|
|
37
|
+
* Silently skips missing files/packages.
|
|
38
|
+
* @param {string} pkg e.g. '@fontsource/darker-grotesque'
|
|
39
|
+
* @param {string} slug e.g. 'darker-grotesque'
|
|
40
|
+
* @param {number[]} weights e.g. [400, 500, 600, 700]
|
|
41
|
+
*/
|
|
42
|
+
function tryFontsourceFamily(pkg, slug, weights = [400, 500, 600, 700]) {
|
|
43
|
+
const base = join(__dirname, `../../node_modules/${pkg}/files`);
|
|
44
|
+
const bufs = [];
|
|
45
|
+
for (const w of weights) {
|
|
46
|
+
const buf = tryFont(
|
|
47
|
+
`${base}/${slug}-latin-${w}-normal.woff2`,
|
|
48
|
+
`${base}/${slug}-latin-${w}-normal.woff`,
|
|
49
|
+
`${base}/${slug}-latin-${w}-normal.ttf`,
|
|
50
|
+
`${base}/${slug}-all-${w}-normal.woff2`,
|
|
51
|
+
);
|
|
52
|
+
if (buf) bufs.push(buf);
|
|
53
|
+
}
|
|
54
|
+
return bufs;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Inter font loading with version warning ────────────────────────────────────
|
|
58
|
+
//
|
|
59
|
+
// Figma bundles Inter v3.015 (variable font, wght+slnt axes) inside its app.
|
|
60
|
+
// @fontsource/inter ships v4.x — italic glyph shapes (e.g. "a") differ visibly.
|
|
61
|
+
//
|
|
62
|
+
// Run node lib/rasterizer/extract-figma-fonts.mjs once to generate the v3
|
|
63
|
+
// static WOFF2 files in lib/rasterizer/fonts/inter-v3-*.woff2.
|
|
64
|
+
//
|
|
65
|
+
// KNOWN_FIGMA_INTER_VERSION is the nameID 5 string from Figma's bundled font.
|
|
66
|
+
const KNOWN_FIGMA_INTER_VERSION = 'Version 3.015;git-7f5c04026';
|
|
67
|
+
|
|
68
|
+
function loadInterFonts() {
|
|
69
|
+
const v3Meta = join(__dirname, 'fonts/inter-v3-meta.json');
|
|
70
|
+
const v3Instances = ['400-normal', '500-normal', '600-normal', '700-normal', '400-italic', '700-italic'];
|
|
71
|
+
const bufs = v3Instances.map(s => tryFont(join(__dirname, `fonts/inter-v3-${s}.woff2`))).filter(Boolean);
|
|
72
|
+
|
|
73
|
+
if (bufs.length === v3Instances.length) {
|
|
74
|
+
// All v3 static fonts present — check version matches expectation
|
|
75
|
+
try {
|
|
76
|
+
const meta = JSON.parse(readFileSync(v3Meta, 'utf8'));
|
|
77
|
+
if (meta.version !== KNOWN_FIGMA_INTER_VERSION) {
|
|
78
|
+
process.stderr.write(
|
|
79
|
+
`[figmatk] Inter version warning: fonts/inter-v3-*.woff2 reports "${meta.version}", ` +
|
|
80
|
+
`expected "${KNOWN_FIGMA_INTER_VERSION}". Renders may differ.\n`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
} catch { /* meta missing — skip version check */ }
|
|
84
|
+
return bufs;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Fallback: @fontsource/inter (v4.x)
|
|
88
|
+
process.stderr.write(
|
|
89
|
+
`[figmatk] Inter version warning: Figma uses Inter "${KNOWN_FIGMA_INTER_VERSION}" but ` +
|
|
90
|
+
`local v3 fonts not found. Falling back to @fontsource/inter (v4.x) — ` +
|
|
91
|
+
`italic glyphs (e.g. "a") may differ. ` +
|
|
92
|
+
`Run: node lib/rasterizer/extract-figma-fonts.mjs\n`
|
|
93
|
+
);
|
|
94
|
+
return [
|
|
95
|
+
...tryFontsourceFamily('@fontsource/inter', 'inter', [400, 500, 600, 700]),
|
|
96
|
+
...['400', '700'].flatMap(w => {
|
|
97
|
+
const buf = tryFont(join(__dirname, `../../node_modules/@fontsource/inter/files/inter-latin-${w}-italic.woff2`));
|
|
98
|
+
return buf ? [buf] : [];
|
|
99
|
+
}),
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const fontBuffers = [
|
|
104
|
+
...loadInterFonts(),
|
|
105
|
+
// Darker Grotesque — patched WOFF2 with family name "Darker Grotesque" so resvg can match it
|
|
106
|
+
...['400', '500', '600', '700'].flatMap(w => {
|
|
107
|
+
const buf = tryFont(join(__dirname, `fonts/darker-grotesque-patched-${w}-normal.woff2`));
|
|
108
|
+
return buf ? [buf] : [];
|
|
109
|
+
}),
|
|
110
|
+
// Irish Grover — internal name already matches, load directly from @fontsource
|
|
111
|
+
...tryFontsourceFamily('@fontsource/irish-grover', 'irish-grover', [400]),
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Register an additional font for rendering.
|
|
116
|
+
* Can be called at any time — takes effect on the next render call.
|
|
117
|
+
* @param {string|Buffer|Uint8Array} source File path or raw buffer.
|
|
118
|
+
*/
|
|
119
|
+
export function registerFont(source) {
|
|
120
|
+
const buf = typeof source === 'string'
|
|
121
|
+
? readFileSync(source)
|
|
122
|
+
: Buffer.isBuffer(source) ? source : Buffer.from(source);
|
|
123
|
+
fontBuffers.push(buf);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Register all fonts in a directory (recursively scans .ttf/.otf/.woff/.woff2).
|
|
128
|
+
* Call this before rendering if slides use custom fonts.
|
|
129
|
+
* @param {string} dir Directory path to scan.
|
|
130
|
+
*/
|
|
131
|
+
export function registerFontDir(dir) {
|
|
132
|
+
const scan = (d) => {
|
|
133
|
+
for (const entry of readdirSync(d)) {
|
|
134
|
+
const full = join(d, entry);
|
|
135
|
+
if (statSync(full).isDirectory()) { scan(full); continue; }
|
|
136
|
+
if (/\.(ttf|otf|woff2?)$/i.test(entry)) registerFont(full);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
scan(dir);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── WASM init (lazy, once) ────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
let wasmReady = false;
|
|
145
|
+
let wasmInitPromise = null;
|
|
146
|
+
|
|
147
|
+
async function ensureWasm() {
|
|
148
|
+
if (wasmReady) return;
|
|
149
|
+
if (!wasmInitPromise) {
|
|
150
|
+
wasmInitPromise = initWasm(readFileSync(WASM_PATH)).then(() => {
|
|
151
|
+
wasmReady = true;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
await wasmInitPromise;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Core render ───────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
const SLIDE_W = 1920;
|
|
160
|
+
const SLIDE_H = 1080;
|
|
161
|
+
|
|
162
|
+
const DEFAULT_OPTS = {
|
|
163
|
+
scale: 1, // 1 = 1920×1080, 0.5 = 960×540 — capped at 1
|
|
164
|
+
background: '#ffffff',
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Resolve scale from opts. Accepts:
|
|
169
|
+
* scale (float) — direct multiplier, capped at 1
|
|
170
|
+
* width (px) — fit to width, preserving aspect ratio
|
|
171
|
+
* height (px) — fit to height, preserving aspect ratio
|
|
172
|
+
* Never upscales beyond native 1920×1080.
|
|
173
|
+
*/
|
|
174
|
+
function resolveScale(opts) {
|
|
175
|
+
if (opts.width) return opts.width / SLIDE_W;
|
|
176
|
+
if (opts.height) return opts.height / SLIDE_H;
|
|
177
|
+
return opts.scale ?? 1;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Render a single SVG string to PNG.
|
|
182
|
+
* @param {string} svg
|
|
183
|
+
* @param {object} opts
|
|
184
|
+
* @returns {Promise<Uint8Array>} PNG bytes
|
|
185
|
+
*/
|
|
186
|
+
export async function svgToPng(svg, opts = {}) {
|
|
187
|
+
await ensureWasm();
|
|
188
|
+
const { background } = { ...DEFAULT_OPTS, ...opts };
|
|
189
|
+
const scale = resolveScale(opts);
|
|
190
|
+
|
|
191
|
+
const resvg = new Resvg(svg, {
|
|
192
|
+
background,
|
|
193
|
+
fitTo: scale !== 1 ? { mode: 'zoom', value: scale } : { mode: 'original' },
|
|
194
|
+
font: {
|
|
195
|
+
fontBuffers: fontBuffers.map(b => new Uint8Array(b)),
|
|
196
|
+
loadSystemFonts: false,
|
|
197
|
+
sansSerifFamily: 'Inter',
|
|
198
|
+
defaultFontFamily: 'Inter',
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const rendered = resvg.render();
|
|
203
|
+
const png = rendered.asPng();
|
|
204
|
+
rendered.free();
|
|
205
|
+
resvg.free();
|
|
206
|
+
return png;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Render all active slides in a deck to PNG.
|
|
211
|
+
* @param {import('../fig-deck.mjs').FigDeck} deck
|
|
212
|
+
* @param {object} opts
|
|
213
|
+
* @param {number} [opts.scale=1] Zoom factor (e.g. 0.5 for thumbnails)
|
|
214
|
+
* @returns {Promise<Array<{index: number, slideId: string, png: Uint8Array}>>}
|
|
215
|
+
*/
|
|
216
|
+
export async function renderDeck(deck, opts = {}) {
|
|
217
|
+
const slides = deck.getActiveSlides();
|
|
218
|
+
const results = [];
|
|
219
|
+
for (let i = 0; i < slides.length; i++) {
|
|
220
|
+
const slide = slides[i];
|
|
221
|
+
const svg = slideToSvg(deck, slide);
|
|
222
|
+
const png = await svgToPng(svg, opts);
|
|
223
|
+
results.push({ index: i, slideId: slide.guid
|
|
224
|
+
? `${slide.guid.sessionID}:${slide.guid.localID}`
|
|
225
|
+
: String(i), png });
|
|
226
|
+
}
|
|
227
|
+
return results;
|
|
228
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* font-helper — Install fonts from @fontsource and register with the rasterizer.
|
|
4
|
+
*
|
|
5
|
+
* resvg-wasm accepts WOFF2 directly — no conversion needed.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node lib/rasterizer/download-font.mjs "Darker Grotesque" 500 600
|
|
9
|
+
* node lib/rasterizer/download-font.mjs "Inter" 400 700
|
|
10
|
+
*
|
|
11
|
+
* What it does:
|
|
12
|
+
* 1. npm install @fontsource/<family> (if not already installed)
|
|
13
|
+
* 2. Prints the registerFont() calls to add to deck-rasterizer.mjs
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync } from 'fs';
|
|
17
|
+
import { join, dirname } from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
import { execSync } from 'child_process';
|
|
20
|
+
|
|
21
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const ROOT = join(__dirname, '../..');
|
|
23
|
+
|
|
24
|
+
const [,, familyArg, ...weightArgs] = process.argv;
|
|
25
|
+
if (!familyArg) {
|
|
26
|
+
console.error('Usage: node download-font.mjs "Family Name" [weight...]\n');
|
|
27
|
+
console.error(' node download-font.mjs "Darker Grotesque" 500 600');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const weights = weightArgs.length ? weightArgs.map(Number) : [400];
|
|
32
|
+
const family = familyArg.trim();
|
|
33
|
+
const pkgSlug = family.toLowerCase().replace(/\s+/g, '-');
|
|
34
|
+
const pkgName = `@fontsource/${pkgSlug}`;
|
|
35
|
+
const pkgDir = join(ROOT, 'node_modules', pkgName, 'files');
|
|
36
|
+
|
|
37
|
+
if (!existsSync(pkgDir)) {
|
|
38
|
+
console.log(`Installing ${pkgName}…`);
|
|
39
|
+
execSync(`npm install ${pkgName} --save-dev`, { cwd: ROOT, stdio: 'inherit' });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!existsSync(pkgDir)) {
|
|
43
|
+
console.error(`${pkgName} not found after install.`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(`\n✓ ${pkgName} ready. Add to deck-rasterizer.mjs fontBuffers:\n`);
|
|
48
|
+
for (const w of weights) {
|
|
49
|
+
const file = `${pkgSlug}-latin-${w}-normal.woff2`;
|
|
50
|
+
const full = join(pkgDir, file);
|
|
51
|
+
if (existsSync(full)) {
|
|
52
|
+
const rel = full.replace(ROOT + '/', '');
|
|
53
|
+
console.log(` readFileSync(join(ROOT, '${rel}')), // ${family} ${w}`);
|
|
54
|
+
} else {
|
|
55
|
+
console.warn(` ⚠ not found: ${file}`);
|
|
56
|
+
}
|
|
57
|
+
}
|