openfig-cli 0.4.5 → 0.4.6

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/bin/cli.mjs CHANGED
@@ -34,6 +34,7 @@ const COMMANDS = {
34
34
  'roundtrip': './commands/roundtrip.mjs',
35
35
  'export': './commands/export.mjs',
36
36
  'pdf': './commands/pdf.mjs',
37
+ 'deck-to-fig': './commands/deck-to-fig.mjs',
37
38
  };
38
39
 
39
40
  const arg2 = process.argv[2];
@@ -45,6 +46,7 @@ if (!arg2 || arg2 === '--help' || arg2 === '-h') {
45
46
  console.log('Commands:');
46
47
  console.log(' create-deck Create a new .deck file from scratch');
47
48
  console.log(' convert-html Convert a Claude Design standalone HTML export into a .deck');
49
+ console.log(' deck-to-fig Convert a Figma Slides .deck file into a Figma Design .fig file');
48
50
  console.log(' export Export slides as images (PNG/JPG/WEBP)');
49
51
  console.log(' pdf Export slides as a multi-page PDF');
50
52
  console.log(' inspect Show document structure (node hierarchy tree)');
@@ -0,0 +1,128 @@
1
+ /**
2
+ * deck-to-fig — Convert a Figma Slides (.deck) file to a standard Figma Design (.fig) file.
3
+ *
4
+ * Usage: openfig deck-to-fig <input.deck> -o <output.fig> [options]
5
+ */
6
+ import { statSync, existsSync, readdirSync, readFileSync } from 'fs';
7
+ import { resolve, join } from 'path';
8
+ import { FigDeck } from '../../lib/core/fig-deck.mjs';
9
+ import { convertDeckToFig } from 'openfig-core';
10
+
11
+ export async function run(args, flags) {
12
+ const inPath = args[0];
13
+ const outPath = flags.o || flags.out || flags.output;
14
+ const dryRun = !!(flags['dry-run'] || flags.dryRun);
15
+
16
+ if (!inPath || (!dryRun && (!outPath || outPath === true))) {
17
+ console.error('Usage: openfig deck-to-fig <input.deck> -o <output.fig> [options]');
18
+ console.error(' <input.deck> (required) Figma Slides .deck file');
19
+ console.error(' -o / --out (required unless --dry-run) output .fig path');
20
+ console.error(' --title (optional) canvas/page name (default: deck title)');
21
+ console.error(' --layout (optional) frame arrangement: "row" or "grid" (default: "row")');
22
+ console.error(' --gap (optional) gap between frames in pixels (default: 200)');
23
+ console.error(' --wrap (optional) number of frames per row in grid layout (default: 5)');
24
+ console.error(' --dry-run (optional) run transform and print summary without emitting file');
25
+ process.exit(1);
26
+ }
27
+
28
+ const title = typeof flags.title === 'string' ? flags.title : undefined;
29
+ const layout = typeof flags.layout === 'string' ? flags.layout.toLowerCase() : 'row';
30
+ const gap = flags.gap ? parseInt(flags.gap, 10) : 200;
31
+ const wrap = flags.wrap ? parseInt(flags.wrap, 10) : 5;
32
+
33
+ if (layout !== 'row' && layout !== 'grid') {
34
+ console.error(`Error: Invalid layout "${layout}". Must be either "row" or "grid".`);
35
+ process.exit(1);
36
+ }
37
+
38
+ if (isNaN(gap) || gap < 0) {
39
+ console.error(`Error: Invalid gap value "${flags.gap}". Must be a non-negative number.`);
40
+ process.exit(1);
41
+ }
42
+
43
+ if (isNaN(wrap) || wrap <= 0) {
44
+ console.error(`Error: Invalid wrap value "${flags.wrap}". Must be a positive integer.`);
45
+ process.exit(1);
46
+ }
47
+
48
+ console.log(`Reading slides deck: ${inPath}`);
49
+ const deck = await FigDeck.fromDeckFile(inPath);
50
+
51
+ const imagesMap = new Map();
52
+ if (deck.imagesDir && existsSync(deck.imagesDir)) {
53
+ const files = readdirSync(deck.imagesDir);
54
+ for (const f of files) {
55
+ const fullPath = join(deck.imagesDir, f);
56
+ const stats = statSync(fullPath);
57
+ if (stats.isFile()) {
58
+ imagesMap.set(f, new Uint8Array(readFileSync(fullPath)));
59
+ }
60
+ }
61
+ }
62
+
63
+ // Build FigDocument for openfig-core convert transform
64
+ const inputDoc = {
65
+ header: deck.header,
66
+ nodes: deck.message.nodeChanges,
67
+ nodeMap: deck.nodeMap,
68
+ childrenMap: deck.childrenMap,
69
+ schema: deck.schema,
70
+ compiledSchema: deck.compiledSchema,
71
+ rawChunks: deck.rawFiles,
72
+ message: deck.message,
73
+ meta: deck.deckMeta,
74
+ thumbnail: deck.deckThumbnail,
75
+ images: imagesMap,
76
+ };
77
+
78
+ console.log(`Baking slides content with overrides...`);
79
+ const designDoc = convertDeckToFig(inputDoc, {
80
+ title,
81
+ layout,
82
+ gap,
83
+ wrap,
84
+ });
85
+
86
+ const canvas = designDoc.nodes.find((n) => n.type === 'CANVAS');
87
+ const canvasGuidStr = canvas ? `${canvas.guid.sessionID}:${canvas.guid.localID}` : '';
88
+ const frames = designDoc.nodes.filter(
89
+ (n) => n.type === 'FRAME' && n.parentIndex?.guid &&
90
+ `${n.parentIndex.guid.sessionID}:${n.parentIndex.guid.localID}` === canvasGuidStr
91
+ );
92
+
93
+ if (dryRun) {
94
+ console.log('\n--- DRY RUN SUMMARY ---');
95
+ console.log(`Canvas Page Title: "${canvas?.name || '(Untitled)'}"`);
96
+ console.log(`Layout strategy : ${layout.toUpperCase()} (gap: ${gap}px${layout === 'grid' ? `, wrap: ${wrap}` : ''})`);
97
+ console.log(`Frame count : ${frames.length}`);
98
+ console.log('\nFrames positioned on canvas:');
99
+ frames.forEach((f, idx) => {
100
+ const x = f.transform?.m02 ?? 0;
101
+ const y = f.transform?.m12 ?? 0;
102
+ const w = f.size?.x ?? 1920;
103
+ const h = f.size?.y ?? 1080;
104
+ console.log(` [${String(idx + 1).padStart(2, '0')}] "${f.name}" size: ${w}x${h} pos: (${x}, ${y})`);
105
+ });
106
+ console.log('\nDry run complete. No file was written.');
107
+ return;
108
+ }
109
+
110
+ console.log(`Encoding output Design file...`);
111
+ const outputDeck = new FigDeck();
112
+ outputDeck.header = { prelude: 'fig-kiwi', version: designDoc.header.version };
113
+ outputDeck.schema = designDoc.schema;
114
+ outputDeck.compiledSchema = designDoc.compiledSchema;
115
+ outputDeck.message = designDoc.message;
116
+ outputDeck.rawFiles = designDoc.rawChunks;
117
+ outputDeck.rebuildMaps();
118
+ outputDeck.deckMeta = designDoc.meta;
119
+ outputDeck.deckThumbnail = designDoc.thumbnail;
120
+ outputDeck.imagesDir = deck.imagesDir;
121
+
122
+ const absoluteOut = resolve(outPath);
123
+ await outputDeck.saveDeck(absoluteOut);
124
+
125
+ const bytes = statSync(absoluteOut).size;
126
+ console.log(`Saved: ${outPath} (${bytes} bytes)`);
127
+ console.log(`Success! Conversion complete. Output file is valid Figma Design format.`);
128
+ }
@@ -14,9 +14,7 @@ export async function run(args, flags) {
14
14
  const filterType = flags.type || null;
15
15
  const jsonOut = flags.json != null;
16
16
 
17
- const deck = file.endsWith('.fig')
18
- ? FigDeck.fromFigFile(file)
19
- : await FigDeck.fromDeckFile(file);
17
+ const deck = await FigDeck.fromFile(file);
20
18
 
21
19
  // Find root nodes (no parentIndex or parent not in nodeMap)
22
20
  const roots = deck.message.nodeChanges.filter(n => {
@@ -12,9 +12,7 @@ export async function run(args, flags) {
12
12
 
13
13
  const filterSym = flags.symbol || null;
14
14
 
15
- const deck = file.endsWith('.fig')
16
- ? FigDeck.fromFigFile(file)
17
- : await FigDeck.fromDeckFile(file);
15
+ const deck = await FigDeck.fromFile(file);
18
16
 
19
17
  const symbols = deck.getSymbols();
20
18
 
@@ -11,9 +11,7 @@ export async function run(args) {
11
11
  const file = args[0];
12
12
  if (!file) { console.error('Usage: list-text <file.deck>'); process.exit(1); }
13
13
 
14
- const deck = file.endsWith('.fig')
15
- ? FigDeck.fromFigFile(file)
16
- : await FigDeck.fromDeckFile(file);
14
+ const deck = await FigDeck.fromFile(file);
17
15
 
18
16
  // Direct text nodes
19
17
  console.log('=== Direct text nodes ===\n');
@@ -125,6 +125,21 @@ export class FigDeck {
125
125
  return deck;
126
126
  }
127
127
 
128
+ /**
129
+ * Load from a file, automatically detecting if it is a ZIP archive (.deck or zipped .fig)
130
+ * or a raw .fig binary by inspecting the magic bytes.
131
+ */
132
+ static async fromFile(filePath) {
133
+ const buf = readFileSync(resolve(filePath));
134
+ if (buf[0] === 0x50 && buf[1] === 0x4b) {
135
+ return this.fromDeckFile(filePath);
136
+ } else {
137
+ const deck = new FigDeck();
138
+ deck._parseFig(buf);
139
+ return deck;
140
+ }
141
+ }
142
+
128
143
  /**
129
144
  * Parse a canvas.fig buffer.
130
145
  * Format: prelude (8 bytes ASCII) + version (uint32 LE) + N×(length uint32 LE + chunk bytes)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openfig-cli",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "OpenFig — Open-source tools for Figma file parsing and rendering",
5
5
  "type": "module",
6
6
  "bin": {
@@ -47,7 +47,7 @@
47
47
  "css": "^3.0.0",
48
48
  "kiwi-schema": "^0.5.0",
49
49
  "node-html-parser": "^7.1.0",
50
- "openfig-core": "^0.3.5",
50
+ "openfig-core": "^0.3.7",
51
51
  "pako": "^2.1.0",
52
52
  "pdf-lib": "^1.17.1",
53
53
  "playwright-core": "^1.59.1",