openfig-core 0.2.0
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/dist/index.cjs +130 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +103 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +101 -0
- package/dist/index.js.map +1 -0
- package/package.json +35 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
nodeId: () => nodeId,
|
|
24
|
+
parseFig: () => parseFig,
|
|
25
|
+
parseFigBinary: () => parseFigBinary
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/parser.ts
|
|
30
|
+
var import_fflate = require("fflate");
|
|
31
|
+
var import_kiwi_schema = require("kiwi-schema");
|
|
32
|
+
var import_fzstd = require("fzstd");
|
|
33
|
+
|
|
34
|
+
// src/utils.ts
|
|
35
|
+
function nodeId(node) {
|
|
36
|
+
if (!node?.guid) return null;
|
|
37
|
+
return `${node.guid.sessionID}:${node.guid.localID}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/parser.ts
|
|
41
|
+
function parseFigBinary(data) {
|
|
42
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
43
|
+
const prelude = String.fromCharCode(...data.subarray(0, 8));
|
|
44
|
+
if (!prelude.startsWith("fig-")) {
|
|
45
|
+
throw new Error(`Unknown prelude: ${prelude}`);
|
|
46
|
+
}
|
|
47
|
+
const version = view.getUint32(8, true);
|
|
48
|
+
const chunks = [];
|
|
49
|
+
let off = 12;
|
|
50
|
+
while (off < data.byteLength) {
|
|
51
|
+
const len = view.getUint32(off, true);
|
|
52
|
+
off += 4;
|
|
53
|
+
chunks.push(data.subarray(off, off + len));
|
|
54
|
+
off += len;
|
|
55
|
+
}
|
|
56
|
+
if (chunks.length < 2) {
|
|
57
|
+
throw new Error("Expected at least 2 chunks in .fig binary");
|
|
58
|
+
}
|
|
59
|
+
const schemaData = (0, import_fflate.inflateSync)(chunks[0]);
|
|
60
|
+
const schema = (0, import_kiwi_schema.decodeBinarySchema)(schemaData);
|
|
61
|
+
const compiled = (0, import_kiwi_schema.compileSchema)(schema);
|
|
62
|
+
let msgData;
|
|
63
|
+
const c1 = chunks[1];
|
|
64
|
+
if (c1[0] === 40 && c1[1] === 181 && c1[2] === 47 && c1[3] === 253) {
|
|
65
|
+
msgData = (0, import_fzstd.decompress)(c1);
|
|
66
|
+
} else {
|
|
67
|
+
msgData = (0, import_fflate.inflateSync)(c1);
|
|
68
|
+
}
|
|
69
|
+
const message = compiled.decodeMessage(msgData);
|
|
70
|
+
const nodes = message.nodeChanges;
|
|
71
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
72
|
+
const childrenMap = /* @__PURE__ */ new Map();
|
|
73
|
+
for (const node of nodes) {
|
|
74
|
+
const id = nodeId(node);
|
|
75
|
+
if (id) nodeMap.set(id, node);
|
|
76
|
+
}
|
|
77
|
+
for (const node of nodes) {
|
|
78
|
+
if (!node.parentIndex?.guid) continue;
|
|
79
|
+
const pid = `${node.parentIndex.guid.sessionID}:${node.parentIndex.guid.localID}`;
|
|
80
|
+
if (!childrenMap.has(pid)) childrenMap.set(pid, []);
|
|
81
|
+
childrenMap.get(pid).push(node);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
header: { prelude: prelude.trim(), version },
|
|
85
|
+
nodes,
|
|
86
|
+
nodeMap,
|
|
87
|
+
childrenMap,
|
|
88
|
+
schema,
|
|
89
|
+
compiledSchema: compiled,
|
|
90
|
+
rawChunks: chunks,
|
|
91
|
+
message,
|
|
92
|
+
images: /* @__PURE__ */ new Map()
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function parseFig(data) {
|
|
96
|
+
if (data[0] !== 80 || data[1] !== 75) {
|
|
97
|
+
throw new Error("Not a valid .fig file (missing ZIP header)");
|
|
98
|
+
}
|
|
99
|
+
const unzipped = (0, import_fflate.unzipSync)(data);
|
|
100
|
+
const canvasKey = Object.keys(unzipped).find((k) => k.endsWith("canvas.fig"));
|
|
101
|
+
if (!canvasKey) {
|
|
102
|
+
throw new Error("No canvas.fig found in .fig archive");
|
|
103
|
+
}
|
|
104
|
+
const doc = parseFigBinary(unzipped[canvasKey]);
|
|
105
|
+
const metaKey = Object.keys(unzipped).find((k) => k.endsWith("meta.json"));
|
|
106
|
+
if (metaKey) {
|
|
107
|
+
try {
|
|
108
|
+
doc.meta = JSON.parse(new TextDecoder().decode(unzipped[metaKey]));
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const thumbKey = Object.keys(unzipped).find((k) => k.endsWith("thumbnail.png"));
|
|
113
|
+
if (thumbKey) {
|
|
114
|
+
doc.thumbnail = unzipped[thumbKey];
|
|
115
|
+
}
|
|
116
|
+
for (const key of Object.keys(unzipped)) {
|
|
117
|
+
if (key.includes("images/") && key !== "images/") {
|
|
118
|
+
const filename = key.split("/").pop();
|
|
119
|
+
doc.images.set(filename, unzipped[key]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return doc;
|
|
123
|
+
}
|
|
124
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
125
|
+
0 && (module.exports = {
|
|
126
|
+
nodeId,
|
|
127
|
+
parseFig,
|
|
128
|
+
parseFigBinary
|
|
129
|
+
});
|
|
130
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/parser.ts","../src/utils.ts"],"sourcesContent":["export { parseFig, parseFigBinary } from \"./parser.js\";\nexport { nodeId } from \"./utils.js\";\nexport type { FigDocument, FigNode, FigPaint, FigGuid } from \"./types.js\";\n","/**\n * Isomorphic .fig binary parser.\n *\n * .fig files are ZIP archives containing:\n * - canvas.fig (binary: prelude + version + kiwi-encoded chunks)\n * - meta.json (optional)\n * - thumbnail.png (optional)\n * - images/ (optional)\n *\n * Parsing flow:\n * 1. Unzip → extract canvas.fig\n * 2. Read 8-byte prelude + 4-byte version\n * 3. Chunk 0: deflateRaw → kiwi binary schema\n * 4. Chunk 1: zstd or deflateRaw → kiwi message (nodeChanges[])\n * 5. Build node maps\n */\n\nimport { unzipSync, inflateSync } from \"fflate\";\nimport { decodeBinarySchema, compileSchema } from \"kiwi-schema\";\nimport { decompress as zstdDecompress } from \"fzstd\";\nimport type { FigDocument, FigNode } from \"./types.js\";\nimport { nodeId } from \"./utils.js\";\n\n/**\n * Parse raw canvas.fig binary data (the blob inside the ZIP).\n * Use this if you extract the ZIP yourself.\n */\nexport function parseFigBinary(data: Uint8Array): FigDocument {\n const view = new DataView(data.buffer, data.byteOffset, data.byteLength);\n\n // Read 8-byte prelude + 4-byte version\n const prelude = String.fromCharCode(...data.subarray(0, 8));\n if (!prelude.startsWith(\"fig-\")) {\n throw new Error(`Unknown prelude: ${prelude}`);\n }\n const version = view.getUint32(8, true);\n\n // Read length-prefixed chunks\n const chunks: Uint8Array[] = [];\n let off = 12;\n while (off < data.byteLength) {\n const len = view.getUint32(off, true);\n off += 4;\n chunks.push(data.subarray(off, off + len));\n off += len;\n }\n\n if (chunks.length < 2) {\n throw new Error(\"Expected at least 2 chunks in .fig binary\");\n }\n\n // Chunk 0: kiwi schema (deflateRaw compressed)\n const schemaData = inflateSync(chunks[0]);\n const schema = decodeBinarySchema(schemaData);\n const compiled = compileSchema(schema);\n\n // Chunk 1: message (zstd or deflateRaw — auto-detect by magic bytes)\n let msgData: Uint8Array;\n const c1 = chunks[1];\n if (c1[0] === 0x28 && c1[1] === 0xb5 && c1[2] === 0x2f && c1[3] === 0xfd) {\n msgData = zstdDecompress(c1);\n } else {\n msgData = inflateSync(c1);\n }\n const message = compiled.decodeMessage(msgData);\n\n // Build maps\n const nodes: FigNode[] = message.nodeChanges;\n const nodeMap = new Map<string, FigNode>();\n const childrenMap = new Map<string, FigNode[]>();\n\n for (const node of nodes) {\n const id = nodeId(node);\n if (id) nodeMap.set(id, node);\n }\n\n for (const node of nodes) {\n if (!node.parentIndex?.guid) continue;\n const pid = `${node.parentIndex.guid.sessionID}:${node.parentIndex.guid.localID}`;\n if (!childrenMap.has(pid)) childrenMap.set(pid, []);\n childrenMap.get(pid)!.push(node);\n }\n\n return {\n header: { prelude: prelude.trim(), version },\n nodes,\n nodeMap,\n childrenMap,\n schema,\n compiledSchema: compiled,\n rawChunks: chunks,\n message,\n images: new Map(),\n };\n}\n\n/**\n * Parse a complete .fig file (ZIP archive).\n * Extracts canvas.fig, meta.json, thumbnail.png, and images/*.\n */\nexport function parseFig(data: Uint8Array): FigDocument {\n // Check ZIP header\n if (data[0] !== 0x50 || data[1] !== 0x4b) {\n throw new Error(\"Not a valid .fig file (missing ZIP header)\");\n }\n\n const unzipped = unzipSync(data);\n\n // Find and parse canvas.fig\n const canvasKey = Object.keys(unzipped).find((k) => k.endsWith(\"canvas.fig\"));\n if (!canvasKey) {\n throw new Error(\"No canvas.fig found in .fig archive\");\n }\n const doc = parseFigBinary(unzipped[canvasKey]);\n\n // Extract meta.json\n const metaKey = Object.keys(unzipped).find((k) => k.endsWith(\"meta.json\"));\n if (metaKey) {\n try {\n doc.meta = JSON.parse(new TextDecoder().decode(unzipped[metaKey]));\n } catch { /* ignore malformed meta */ }\n }\n\n // Extract thumbnail\n const thumbKey = Object.keys(unzipped).find((k) => k.endsWith(\"thumbnail.png\"));\n if (thumbKey) {\n doc.thumbnail = unzipped[thumbKey];\n }\n\n // Extract images\n for (const key of Object.keys(unzipped)) {\n if (key.includes(\"images/\") && key !== \"images/\") {\n const filename = key.split(\"/\").pop()!;\n doc.images.set(filename, unzipped[key]);\n }\n }\n\n return doc;\n}\n","import type { FigNode } from \"./types.js\";\n\n/**\n * Returns the string ID for a node (\"sessionID:localID\"), or null if no guid.\n */\nexport function nodeId(node: FigNode): string | null {\n if (!node?.guid) return null;\n return `${node.guid.sessionID}:${node.guid.localID}`;\n}\n"],"mappings":"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","names":["zstdDecompress"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
interface FigGuid {
|
|
2
|
+
sessionID: number;
|
|
3
|
+
localID: number;
|
|
4
|
+
}
|
|
5
|
+
interface FigPaint {
|
|
6
|
+
type: string;
|
|
7
|
+
color?: {
|
|
8
|
+
r: number;
|
|
9
|
+
g: number;
|
|
10
|
+
b: number;
|
|
11
|
+
a: number;
|
|
12
|
+
};
|
|
13
|
+
opacity?: number;
|
|
14
|
+
visible?: boolean;
|
|
15
|
+
}
|
|
16
|
+
interface FigNode {
|
|
17
|
+
guid: FigGuid;
|
|
18
|
+
type: string;
|
|
19
|
+
name: string;
|
|
20
|
+
phase?: string;
|
|
21
|
+
parentIndex?: {
|
|
22
|
+
guid: FigGuid;
|
|
23
|
+
position: string;
|
|
24
|
+
};
|
|
25
|
+
size?: {
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
};
|
|
29
|
+
transform?: {
|
|
30
|
+
m00: number;
|
|
31
|
+
m01: number;
|
|
32
|
+
m02: number;
|
|
33
|
+
m10: number;
|
|
34
|
+
m11: number;
|
|
35
|
+
m12: number;
|
|
36
|
+
};
|
|
37
|
+
fillPaints?: FigPaint[];
|
|
38
|
+
strokePaints?: FigPaint[];
|
|
39
|
+
strokeWeight?: number;
|
|
40
|
+
cornerRadius?: number;
|
|
41
|
+
textData?: {
|
|
42
|
+
characters: string;
|
|
43
|
+
};
|
|
44
|
+
fontSize?: number;
|
|
45
|
+
opacity?: number;
|
|
46
|
+
shapeWithTextType?: string;
|
|
47
|
+
[key: string]: any;
|
|
48
|
+
}
|
|
49
|
+
interface FigDocument {
|
|
50
|
+
header: {
|
|
51
|
+
prelude: string;
|
|
52
|
+
version: number;
|
|
53
|
+
};
|
|
54
|
+
nodes: FigNode[];
|
|
55
|
+
nodeMap: Map<string, FigNode>;
|
|
56
|
+
childrenMap: Map<string, FigNode[]>;
|
|
57
|
+
/** Decoded kiwi binary schema (needed for re-encoding). */
|
|
58
|
+
schema: any;
|
|
59
|
+
/** Compiled kiwi schema with encodeMessage/decodeMessage (needed for re-encoding). */
|
|
60
|
+
compiledSchema: any;
|
|
61
|
+
/** Raw length-prefixed chunks from the binary (chunks[2+] are passed through on re-encode). */
|
|
62
|
+
rawChunks: Uint8Array[];
|
|
63
|
+
/** Full decoded kiwi message (contains nodeChanges, blobs, etc. — needed for re-encoding). */
|
|
64
|
+
message: any;
|
|
65
|
+
meta?: Record<string, any>;
|
|
66
|
+
thumbnail?: Uint8Array;
|
|
67
|
+
images: Map<string, Uint8Array>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Isomorphic .fig binary parser.
|
|
72
|
+
*
|
|
73
|
+
* .fig files are ZIP archives containing:
|
|
74
|
+
* - canvas.fig (binary: prelude + version + kiwi-encoded chunks)
|
|
75
|
+
* - meta.json (optional)
|
|
76
|
+
* - thumbnail.png (optional)
|
|
77
|
+
* - images/ (optional)
|
|
78
|
+
*
|
|
79
|
+
* Parsing flow:
|
|
80
|
+
* 1. Unzip → extract canvas.fig
|
|
81
|
+
* 2. Read 8-byte prelude + 4-byte version
|
|
82
|
+
* 3. Chunk 0: deflateRaw → kiwi binary schema
|
|
83
|
+
* 4. Chunk 1: zstd or deflateRaw → kiwi message (nodeChanges[])
|
|
84
|
+
* 5. Build node maps
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Parse raw canvas.fig binary data (the blob inside the ZIP).
|
|
89
|
+
* Use this if you extract the ZIP yourself.
|
|
90
|
+
*/
|
|
91
|
+
declare function parseFigBinary(data: Uint8Array): FigDocument;
|
|
92
|
+
/**
|
|
93
|
+
* Parse a complete .fig file (ZIP archive).
|
|
94
|
+
* Extracts canvas.fig, meta.json, thumbnail.png, and images/*.
|
|
95
|
+
*/
|
|
96
|
+
declare function parseFig(data: Uint8Array): FigDocument;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns the string ID for a node ("sessionID:localID"), or null if no guid.
|
|
100
|
+
*/
|
|
101
|
+
declare function nodeId(node: FigNode): string | null;
|
|
102
|
+
|
|
103
|
+
export { type FigDocument, type FigGuid, type FigNode, type FigPaint, nodeId, parseFig, parseFigBinary };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
interface FigGuid {
|
|
2
|
+
sessionID: number;
|
|
3
|
+
localID: number;
|
|
4
|
+
}
|
|
5
|
+
interface FigPaint {
|
|
6
|
+
type: string;
|
|
7
|
+
color?: {
|
|
8
|
+
r: number;
|
|
9
|
+
g: number;
|
|
10
|
+
b: number;
|
|
11
|
+
a: number;
|
|
12
|
+
};
|
|
13
|
+
opacity?: number;
|
|
14
|
+
visible?: boolean;
|
|
15
|
+
}
|
|
16
|
+
interface FigNode {
|
|
17
|
+
guid: FigGuid;
|
|
18
|
+
type: string;
|
|
19
|
+
name: string;
|
|
20
|
+
phase?: string;
|
|
21
|
+
parentIndex?: {
|
|
22
|
+
guid: FigGuid;
|
|
23
|
+
position: string;
|
|
24
|
+
};
|
|
25
|
+
size?: {
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
};
|
|
29
|
+
transform?: {
|
|
30
|
+
m00: number;
|
|
31
|
+
m01: number;
|
|
32
|
+
m02: number;
|
|
33
|
+
m10: number;
|
|
34
|
+
m11: number;
|
|
35
|
+
m12: number;
|
|
36
|
+
};
|
|
37
|
+
fillPaints?: FigPaint[];
|
|
38
|
+
strokePaints?: FigPaint[];
|
|
39
|
+
strokeWeight?: number;
|
|
40
|
+
cornerRadius?: number;
|
|
41
|
+
textData?: {
|
|
42
|
+
characters: string;
|
|
43
|
+
};
|
|
44
|
+
fontSize?: number;
|
|
45
|
+
opacity?: number;
|
|
46
|
+
shapeWithTextType?: string;
|
|
47
|
+
[key: string]: any;
|
|
48
|
+
}
|
|
49
|
+
interface FigDocument {
|
|
50
|
+
header: {
|
|
51
|
+
prelude: string;
|
|
52
|
+
version: number;
|
|
53
|
+
};
|
|
54
|
+
nodes: FigNode[];
|
|
55
|
+
nodeMap: Map<string, FigNode>;
|
|
56
|
+
childrenMap: Map<string, FigNode[]>;
|
|
57
|
+
/** Decoded kiwi binary schema (needed for re-encoding). */
|
|
58
|
+
schema: any;
|
|
59
|
+
/** Compiled kiwi schema with encodeMessage/decodeMessage (needed for re-encoding). */
|
|
60
|
+
compiledSchema: any;
|
|
61
|
+
/** Raw length-prefixed chunks from the binary (chunks[2+] are passed through on re-encode). */
|
|
62
|
+
rawChunks: Uint8Array[];
|
|
63
|
+
/** Full decoded kiwi message (contains nodeChanges, blobs, etc. — needed for re-encoding). */
|
|
64
|
+
message: any;
|
|
65
|
+
meta?: Record<string, any>;
|
|
66
|
+
thumbnail?: Uint8Array;
|
|
67
|
+
images: Map<string, Uint8Array>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Isomorphic .fig binary parser.
|
|
72
|
+
*
|
|
73
|
+
* .fig files are ZIP archives containing:
|
|
74
|
+
* - canvas.fig (binary: prelude + version + kiwi-encoded chunks)
|
|
75
|
+
* - meta.json (optional)
|
|
76
|
+
* - thumbnail.png (optional)
|
|
77
|
+
* - images/ (optional)
|
|
78
|
+
*
|
|
79
|
+
* Parsing flow:
|
|
80
|
+
* 1. Unzip → extract canvas.fig
|
|
81
|
+
* 2. Read 8-byte prelude + 4-byte version
|
|
82
|
+
* 3. Chunk 0: deflateRaw → kiwi binary schema
|
|
83
|
+
* 4. Chunk 1: zstd or deflateRaw → kiwi message (nodeChanges[])
|
|
84
|
+
* 5. Build node maps
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Parse raw canvas.fig binary data (the blob inside the ZIP).
|
|
89
|
+
* Use this if you extract the ZIP yourself.
|
|
90
|
+
*/
|
|
91
|
+
declare function parseFigBinary(data: Uint8Array): FigDocument;
|
|
92
|
+
/**
|
|
93
|
+
* Parse a complete .fig file (ZIP archive).
|
|
94
|
+
* Extracts canvas.fig, meta.json, thumbnail.png, and images/*.
|
|
95
|
+
*/
|
|
96
|
+
declare function parseFig(data: Uint8Array): FigDocument;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns the string ID for a node ("sessionID:localID"), or null if no guid.
|
|
100
|
+
*/
|
|
101
|
+
declare function nodeId(node: FigNode): string | null;
|
|
102
|
+
|
|
103
|
+
export { type FigDocument, type FigGuid, type FigNode, type FigPaint, nodeId, parseFig, parseFigBinary };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// src/parser.ts
|
|
2
|
+
import { unzipSync, inflateSync } from "fflate";
|
|
3
|
+
import { decodeBinarySchema, compileSchema } from "kiwi-schema";
|
|
4
|
+
import { decompress as zstdDecompress } from "fzstd";
|
|
5
|
+
|
|
6
|
+
// src/utils.ts
|
|
7
|
+
function nodeId(node) {
|
|
8
|
+
if (!node?.guid) return null;
|
|
9
|
+
return `${node.guid.sessionID}:${node.guid.localID}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/parser.ts
|
|
13
|
+
function parseFigBinary(data) {
|
|
14
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
15
|
+
const prelude = String.fromCharCode(...data.subarray(0, 8));
|
|
16
|
+
if (!prelude.startsWith("fig-")) {
|
|
17
|
+
throw new Error(`Unknown prelude: ${prelude}`);
|
|
18
|
+
}
|
|
19
|
+
const version = view.getUint32(8, true);
|
|
20
|
+
const chunks = [];
|
|
21
|
+
let off = 12;
|
|
22
|
+
while (off < data.byteLength) {
|
|
23
|
+
const len = view.getUint32(off, true);
|
|
24
|
+
off += 4;
|
|
25
|
+
chunks.push(data.subarray(off, off + len));
|
|
26
|
+
off += len;
|
|
27
|
+
}
|
|
28
|
+
if (chunks.length < 2) {
|
|
29
|
+
throw new Error("Expected at least 2 chunks in .fig binary");
|
|
30
|
+
}
|
|
31
|
+
const schemaData = inflateSync(chunks[0]);
|
|
32
|
+
const schema = decodeBinarySchema(schemaData);
|
|
33
|
+
const compiled = compileSchema(schema);
|
|
34
|
+
let msgData;
|
|
35
|
+
const c1 = chunks[1];
|
|
36
|
+
if (c1[0] === 40 && c1[1] === 181 && c1[2] === 47 && c1[3] === 253) {
|
|
37
|
+
msgData = zstdDecompress(c1);
|
|
38
|
+
} else {
|
|
39
|
+
msgData = inflateSync(c1);
|
|
40
|
+
}
|
|
41
|
+
const message = compiled.decodeMessage(msgData);
|
|
42
|
+
const nodes = message.nodeChanges;
|
|
43
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
44
|
+
const childrenMap = /* @__PURE__ */ new Map();
|
|
45
|
+
for (const node of nodes) {
|
|
46
|
+
const id = nodeId(node);
|
|
47
|
+
if (id) nodeMap.set(id, node);
|
|
48
|
+
}
|
|
49
|
+
for (const node of nodes) {
|
|
50
|
+
if (!node.parentIndex?.guid) continue;
|
|
51
|
+
const pid = `${node.parentIndex.guid.sessionID}:${node.parentIndex.guid.localID}`;
|
|
52
|
+
if (!childrenMap.has(pid)) childrenMap.set(pid, []);
|
|
53
|
+
childrenMap.get(pid).push(node);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
header: { prelude: prelude.trim(), version },
|
|
57
|
+
nodes,
|
|
58
|
+
nodeMap,
|
|
59
|
+
childrenMap,
|
|
60
|
+
schema,
|
|
61
|
+
compiledSchema: compiled,
|
|
62
|
+
rawChunks: chunks,
|
|
63
|
+
message,
|
|
64
|
+
images: /* @__PURE__ */ new Map()
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function parseFig(data) {
|
|
68
|
+
if (data[0] !== 80 || data[1] !== 75) {
|
|
69
|
+
throw new Error("Not a valid .fig file (missing ZIP header)");
|
|
70
|
+
}
|
|
71
|
+
const unzipped = unzipSync(data);
|
|
72
|
+
const canvasKey = Object.keys(unzipped).find((k) => k.endsWith("canvas.fig"));
|
|
73
|
+
if (!canvasKey) {
|
|
74
|
+
throw new Error("No canvas.fig found in .fig archive");
|
|
75
|
+
}
|
|
76
|
+
const doc = parseFigBinary(unzipped[canvasKey]);
|
|
77
|
+
const metaKey = Object.keys(unzipped).find((k) => k.endsWith("meta.json"));
|
|
78
|
+
if (metaKey) {
|
|
79
|
+
try {
|
|
80
|
+
doc.meta = JSON.parse(new TextDecoder().decode(unzipped[metaKey]));
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const thumbKey = Object.keys(unzipped).find((k) => k.endsWith("thumbnail.png"));
|
|
85
|
+
if (thumbKey) {
|
|
86
|
+
doc.thumbnail = unzipped[thumbKey];
|
|
87
|
+
}
|
|
88
|
+
for (const key of Object.keys(unzipped)) {
|
|
89
|
+
if (key.includes("images/") && key !== "images/") {
|
|
90
|
+
const filename = key.split("/").pop();
|
|
91
|
+
doc.images.set(filename, unzipped[key]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return doc;
|
|
95
|
+
}
|
|
96
|
+
export {
|
|
97
|
+
nodeId,
|
|
98
|
+
parseFig,
|
|
99
|
+
parseFigBinary
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/parser.ts","../src/utils.ts"],"sourcesContent":["/**\n * Isomorphic .fig binary parser.\n *\n * .fig files are ZIP archives containing:\n * - canvas.fig (binary: prelude + version + kiwi-encoded chunks)\n * - meta.json (optional)\n * - thumbnail.png (optional)\n * - images/ (optional)\n *\n * Parsing flow:\n * 1. Unzip → extract canvas.fig\n * 2. Read 8-byte prelude + 4-byte version\n * 3. Chunk 0: deflateRaw → kiwi binary schema\n * 4. Chunk 1: zstd or deflateRaw → kiwi message (nodeChanges[])\n * 5. Build node maps\n */\n\nimport { unzipSync, inflateSync } from \"fflate\";\nimport { decodeBinarySchema, compileSchema } from \"kiwi-schema\";\nimport { decompress as zstdDecompress } from \"fzstd\";\nimport type { FigDocument, FigNode } from \"./types.js\";\nimport { nodeId } from \"./utils.js\";\n\n/**\n * Parse raw canvas.fig binary data (the blob inside the ZIP).\n * Use this if you extract the ZIP yourself.\n */\nexport function parseFigBinary(data: Uint8Array): FigDocument {\n const view = new DataView(data.buffer, data.byteOffset, data.byteLength);\n\n // Read 8-byte prelude + 4-byte version\n const prelude = String.fromCharCode(...data.subarray(0, 8));\n if (!prelude.startsWith(\"fig-\")) {\n throw new Error(`Unknown prelude: ${prelude}`);\n }\n const version = view.getUint32(8, true);\n\n // Read length-prefixed chunks\n const chunks: Uint8Array[] = [];\n let off = 12;\n while (off < data.byteLength) {\n const len = view.getUint32(off, true);\n off += 4;\n chunks.push(data.subarray(off, off + len));\n off += len;\n }\n\n if (chunks.length < 2) {\n throw new Error(\"Expected at least 2 chunks in .fig binary\");\n }\n\n // Chunk 0: kiwi schema (deflateRaw compressed)\n const schemaData = inflateSync(chunks[0]);\n const schema = decodeBinarySchema(schemaData);\n const compiled = compileSchema(schema);\n\n // Chunk 1: message (zstd or deflateRaw — auto-detect by magic bytes)\n let msgData: Uint8Array;\n const c1 = chunks[1];\n if (c1[0] === 0x28 && c1[1] === 0xb5 && c1[2] === 0x2f && c1[3] === 0xfd) {\n msgData = zstdDecompress(c1);\n } else {\n msgData = inflateSync(c1);\n }\n const message = compiled.decodeMessage(msgData);\n\n // Build maps\n const nodes: FigNode[] = message.nodeChanges;\n const nodeMap = new Map<string, FigNode>();\n const childrenMap = new Map<string, FigNode[]>();\n\n for (const node of nodes) {\n const id = nodeId(node);\n if (id) nodeMap.set(id, node);\n }\n\n for (const node of nodes) {\n if (!node.parentIndex?.guid) continue;\n const pid = `${node.parentIndex.guid.sessionID}:${node.parentIndex.guid.localID}`;\n if (!childrenMap.has(pid)) childrenMap.set(pid, []);\n childrenMap.get(pid)!.push(node);\n }\n\n return {\n header: { prelude: prelude.trim(), version },\n nodes,\n nodeMap,\n childrenMap,\n schema,\n compiledSchema: compiled,\n rawChunks: chunks,\n message,\n images: new Map(),\n };\n}\n\n/**\n * Parse a complete .fig file (ZIP archive).\n * Extracts canvas.fig, meta.json, thumbnail.png, and images/*.\n */\nexport function parseFig(data: Uint8Array): FigDocument {\n // Check ZIP header\n if (data[0] !== 0x50 || data[1] !== 0x4b) {\n throw new Error(\"Not a valid .fig file (missing ZIP header)\");\n }\n\n const unzipped = unzipSync(data);\n\n // Find and parse canvas.fig\n const canvasKey = Object.keys(unzipped).find((k) => k.endsWith(\"canvas.fig\"));\n if (!canvasKey) {\n throw new Error(\"No canvas.fig found in .fig archive\");\n }\n const doc = parseFigBinary(unzipped[canvasKey]);\n\n // Extract meta.json\n const metaKey = Object.keys(unzipped).find((k) => k.endsWith(\"meta.json\"));\n if (metaKey) {\n try {\n doc.meta = JSON.parse(new TextDecoder().decode(unzipped[metaKey]));\n } catch { /* ignore malformed meta */ }\n }\n\n // Extract thumbnail\n const thumbKey = Object.keys(unzipped).find((k) => k.endsWith(\"thumbnail.png\"));\n if (thumbKey) {\n doc.thumbnail = unzipped[thumbKey];\n }\n\n // Extract images\n for (const key of Object.keys(unzipped)) {\n if (key.includes(\"images/\") && key !== \"images/\") {\n const filename = key.split(\"/\").pop()!;\n doc.images.set(filename, unzipped[key]);\n }\n }\n\n return doc;\n}\n","import type { FigNode } from \"./types.js\";\n\n/**\n * Returns the string ID for a node (\"sessionID:localID\"), or null if no guid.\n */\nexport function nodeId(node: FigNode): string | null {\n if (!node?.guid) return null;\n return `${node.guid.sessionID}:${node.guid.localID}`;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openfig-core",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Isomorphic .fig file parser — reads Figma binary format in Node.js and browsers",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"fflate": "^0.8.2",
|
|
26
|
+
"fzstd": "^0.1.1",
|
|
27
|
+
"kiwi-schema": "^0.5.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"tsup": "^8.0.0",
|
|
31
|
+
"typescript": "^5.9.0",
|
|
32
|
+
"vitest": "^3.0.0"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT"
|
|
35
|
+
}
|