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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { writeFileSync, existsSync } from 'fs';
|
|
2
|
+
import sharp from 'sharp';
|
|
3
|
+
import { ssim } from 'ssim.js';
|
|
4
|
+
import { FigDeck } from '../fig-deck.mjs';
|
|
5
|
+
import { slideToSvg } from './svg-builder.mjs';
|
|
6
|
+
import { svgToPng } from './deck-rasterizer.mjs';
|
|
7
|
+
|
|
8
|
+
export const RENDER_W = 1920;
|
|
9
|
+
export const RENDER_H = 1080;
|
|
10
|
+
const THUMB_W = 800;
|
|
11
|
+
|
|
12
|
+
export async function toRgbaBuffer(source, width = RENDER_W, height = RENDER_H) {
|
|
13
|
+
const buf = await sharp(source)
|
|
14
|
+
.resize(width, height, { fit: 'fill' })
|
|
15
|
+
.ensureAlpha()
|
|
16
|
+
.raw()
|
|
17
|
+
.toBuffer();
|
|
18
|
+
return { data: new Uint8ClampedArray(buf.buffer, buf.byteOffset, buf.byteLength), width, height };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function computeSsim(rendered, refPath, width = RENDER_W, height = RENDER_H) {
|
|
22
|
+
const [a, b] = await Promise.all([
|
|
23
|
+
toRgbaBuffer(rendered, width, height),
|
|
24
|
+
toRgbaBuffer(refPath, width, height),
|
|
25
|
+
]);
|
|
26
|
+
const { mssim } = ssim(a, b);
|
|
27
|
+
return mssim;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function pngToDataUri(buf) {
|
|
31
|
+
const thumb = await sharp(buf).resize(THUMB_W, null, { fit: 'inside' }).png().toBuffer();
|
|
32
|
+
return `data:image/png;base64,${thumb.toString('base64')}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function refToDataUri(refPath) {
|
|
36
|
+
const thumb = await sharp(refPath).resize(THUMB_W, null, { fit: 'inside' }).png().toBuffer();
|
|
37
|
+
return `data:image/png;base64,${thumb.toString('base64')}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function buildReportRow({ slideNumber, renderedPng, refPath, score, scoreStr }) {
|
|
41
|
+
const renderUri = await pngToDataUri(Buffer.from(renderedPng));
|
|
42
|
+
let refUri = null;
|
|
43
|
+
let resolvedScoreStr = scoreStr ?? '—';
|
|
44
|
+
|
|
45
|
+
if (refPath && existsSync(refPath)) {
|
|
46
|
+
refUri = await refToDataUri(refPath);
|
|
47
|
+
if (typeof score === 'number') {
|
|
48
|
+
resolvedScoreStr = score.toFixed(4);
|
|
49
|
+
} else if (scoreStr == null) {
|
|
50
|
+
const computedScore = await computeSsim(Buffer.from(renderedPng), refPath);
|
|
51
|
+
resolvedScoreStr = computedScore.toFixed(4);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { n: slideNumber, scoreStr: resolvedScoreStr, renderUri, refUri };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function writeRenderReport({ outHtml, rows, title = 'FigmaTK Render Report' }) {
|
|
59
|
+
const html = `<!DOCTYPE html>
|
|
60
|
+
<html lang="en">
|
|
61
|
+
<head>
|
|
62
|
+
<meta charset="utf-8"/>
|
|
63
|
+
<title>${title}</title>
|
|
64
|
+
<style>
|
|
65
|
+
body { font-family: system-ui, sans-serif; background: #111; color: #eee; margin: 0; padding: 16px; }
|
|
66
|
+
h1 { font-size: 1.2rem; margin: 0 0 16px; color: #aaa; }
|
|
67
|
+
.slide-row { display: flex; gap: 12px; margin-bottom: 24px; align-items: flex-start; }
|
|
68
|
+
.panel { flex: 1; }
|
|
69
|
+
.panel label { display: block; font-size: 0.75rem; color: #888; margin-bottom: 4px; }
|
|
70
|
+
.panel img { width: 100%; border-radius: 4px; border: 1px solid #333; display: block; }
|
|
71
|
+
.score { margin-top: 6px; font-size: 1.1rem; font-weight: bold; font-variant-numeric: tabular-nums; text-align: center; }
|
|
72
|
+
.score.good { color: #6f6; }
|
|
73
|
+
.score.bad { color: #f66; }
|
|
74
|
+
h2 { font-size: 0.95rem; margin: 0 0 8px; }
|
|
75
|
+
.slide-block { margin-bottom: 32px; }
|
|
76
|
+
</style>
|
|
77
|
+
</head>
|
|
78
|
+
<body>
|
|
79
|
+
<h1>${title} — ${new Date().toISOString().slice(0,16).replace('T',' ')}</h1>
|
|
80
|
+
${rows.map(({ n, scoreStr, renderUri, refUri }) => {
|
|
81
|
+
const ok = parseFloat(scoreStr) >= 0.70;
|
|
82
|
+
const ssimHtml = scoreStr === '—' ? '' : `<div class="score ${ok ? 'good' : 'bad'}">SSIM ${scoreStr}</div>`;
|
|
83
|
+
return `
|
|
84
|
+
<div class="slide-block">
|
|
85
|
+
<h2>Slide ${n}</h2>
|
|
86
|
+
<div class="slide-row">
|
|
87
|
+
<div class="panel">
|
|
88
|
+
<label>Reference (Figma export)</label>
|
|
89
|
+
${refUri ? `<img src="${refUri}" alt="reference ${n}"/>` : '<em style="color:#555">no reference</em>'}
|
|
90
|
+
</div>
|
|
91
|
+
<div class="panel">
|
|
92
|
+
<label>Rendered (figmatk)</label>
|
|
93
|
+
<img src="${renderUri}" alt="rendered ${n}"/>
|
|
94
|
+
${ssimHtml}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>`;
|
|
98
|
+
}).join('')}
|
|
99
|
+
</body>
|
|
100
|
+
</html>`;
|
|
101
|
+
|
|
102
|
+
writeFileSync(outHtml, html);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function generateRenderReportFromDeck({ deckPath, refDir, outHtml, title = 'FigmaTK Render Report', log = console.log }) {
|
|
106
|
+
log('Loading deck…');
|
|
107
|
+
const deck = await FigDeck.fromDeckFile(deckPath);
|
|
108
|
+
const slides = deck.getActiveSlides();
|
|
109
|
+
log(`${slides.length} slides`);
|
|
110
|
+
|
|
111
|
+
const rows = [];
|
|
112
|
+
for (let i = 0; i < slides.length; i++) {
|
|
113
|
+
const n = i + 1;
|
|
114
|
+
const refPath = `${refDir}/page-${n}.png`;
|
|
115
|
+
const slide = slides[i];
|
|
116
|
+
|
|
117
|
+
process.stdout.write(` Rendering slide ${n}… `);
|
|
118
|
+
const svg = slideToSvg(deck, slide);
|
|
119
|
+
const png = await svgToPng(svg, {});
|
|
120
|
+
const row = await buildReportRow({ slideNumber: n, renderedPng: Buffer.from(png), refPath });
|
|
121
|
+
rows.push(row);
|
|
122
|
+
process.stdout.write(row.scoreStr === '—' ? 'SSIM=—' : `SSIM=${row.scoreStr}`);
|
|
123
|
+
process.stdout.write('\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
writeRenderReport({ outHtml, rows, title });
|
|
127
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate an HTML visual comparison report: reference vs rendered, side-by-side.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node lib/rasterizer/render-report.mjs [file.deck] [ref-dir] [output.html]
|
|
7
|
+
*
|
|
8
|
+
* Defaults:
|
|
9
|
+
* deck = decks/reference/oil-machinations.deck
|
|
10
|
+
* ref-dir = decks/reference/oil-machinations/
|
|
11
|
+
* output = /tmp/figmatk-render-report.html
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { join, dirname, resolve } from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import { generateRenderReportFromDeck } from './render-report-lib.mjs';
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
const [,, deckArg, refDirArg, outArg] = process.argv;
|
|
21
|
+
const DECK_PATH = resolve(deckArg ?? join(__dirname, '../../decks/reference/oil-machinations.deck'));
|
|
22
|
+
const REF_DIR = resolve(refDirArg ?? join(__dirname, '../../decks/reference/oil-machinations'));
|
|
23
|
+
const OUT_HTML = outArg ?? '/tmp/figmatk-render-report.html';
|
|
24
|
+
await generateRenderReportFromDeck({ deckPath: DECK_PATH, refDir: REF_DIR, outHtml: OUT_HTML });
|
|
25
|
+
console.log(`\nReport → ${OUT_HTML}`);
|