openfig-core
v0.3.6
Published
Isomorphic .fig file parser — reads Figma binary format in Node.js and browsers
Maintainers
Readme
openfig-core
Isomorphic .fig file parser — reads Figma binary format in Node.js and browsers.
Parses .fig (Figma Design), .deck (Figma Slides), and .jam (FigJam) files into a traversable node tree. Zero Node.js dependencies — works in browsers via bundlers.
Install
npm install openfig-coreQuick start
import { parseFig, nodeId } from 'openfig-core';
import { readFileSync } from 'fs';
const data = new Uint8Array(readFileSync('design.fig'));
const doc = parseFig(data);
console.log(doc.header); // { prelude: 'fig-kiwi', version: 52 }
console.log(doc.nodes.length); // number of nodes in the file
// Traverse the node tree
for (const node of doc.nodes) {
const id = nodeId(node);
const children = doc.childrenMap.get(id) ?? [];
console.log(`${id} ${node.type} "${node.name}" (${children.length} children)`);
}Browser
const resp = await fetch('/design.fig');
const data = new Uint8Array(await resp.arrayBuffer());
const doc = parseFig(data);API
parseFig(data: Uint8Array): FigDocument
Parse a complete .fig ZIP archive. Extracts canvas.fig, meta.json, thumbnail.png, and images/*.
parseFigBinary(data: Uint8Array): FigDocument
Parse raw canvas.fig binary data (the blob inside the ZIP). Use this if you extract the ZIP yourself.
nodeId(node: FigNode): string | null
Format a node's GUID as "sessionID:localID" (e.g. "1:127"). Returns null if the node has no GUID.
FigDocument
interface FigDocument {
header: { prelude: string; version: number };
nodes: FigNode[]; // all nodes (flat array)
nodeMap: Map<string, FigNode>; // id → node
childrenMap: Map<string, FigNode[]>; // parent id → children
schema: any; // decoded kiwi binary schema
compiledSchema: any; // compiled schema (encodeMessage/decodeMessage)
rawChunks: Uint8Array[]; // raw length-prefixed binary chunks
message: any; // full decoded kiwi message
meta?: Record<string, any>; // meta.json contents
thumbnail?: Uint8Array; // thumbnail.png bytes
images: Map<string, Uint8Array>; // filename → image bytes
}FigNode
interface FigNode {
guid: FigGuid;
type: string; // FRAME, TEXT, ELLIPSE, SYMBOL, INSTANCE, ...
name: string;
phase?: string; // CREATED, REMOVED, etc.
parentIndex?: { guid: FigGuid; position: string };
size?: { x: number; y: number };
transform?: { m00, m01, m02, m10, m11, m12: number };
fillPaints?: FigPaint[];
textData?: { characters: string };
[key: string]: any; // open for all kiwi-decoded fields
}Encoding (write .fig files)
Round-trip: open, edit, save
import { parseFig, encodeFigParts, assembleCanvasFig, createFigZip } from 'openfig-core';
const doc = parseFig(data);
// Edit nodes...
doc.message.nodeChanges[2].name = "Renamed";
const parts = encodeFigParts(doc);
// Caller must zstd-compress the message (openfig-core stays isomorphic)
const messageCompressed = yourZstdCompress(parts.messageRaw, 3);
const canvasFig = assembleCanvasFig({
prelude: parts.prelude,
version: parts.version,
schemaCompressed: parts.schemaCompressed,
messageCompressed,
passThrough: parts.passThrough,
});
const figZip = createFigZip({
canvasFig,
meta: doc.meta,
thumbnail: doc.thumbnail,
images: doc.images,
});
// figZip is a Uint8Array — write to disk or trigger downloadFrom scratch: create a new .fig file
import { createEmptyFigDoc, encodeFigParts, assembleCanvasFig, createFigZip } from 'openfig-core';
const doc = createEmptyFigDoc();
// Add nodes to doc.message.nodeChanges...
const parts = encodeFigParts(doc);
const messageCompressed = yourZstdCompress(parts.messageRaw, 3);
const canvasFig = assembleCanvasFig({ ...parts, messageCompressed });
const figZip = createFigZip({ canvasFig, meta: doc.meta, thumbnail: doc.thumbnail });encodeFigParts(doc: FigDocument): EncodedFigParts
Encodes a FigDocument into kiwi binary parts. Returns the raw message (caller must zstd-compress for chunk 1) and the deflate-compressed schema (chunk 0).
assembleCanvasFig(input: AssembleCanvasFigInput): Uint8Array
Assembles the canvas.fig binary from pre-compressed chunks. Format: [prelude 8B][version uint32 LE][len+chunk0][len+chunk1][len+chunk2+]...
createFigZip(input: CreateFigZipInput): Uint8Array
Packages canvas.fig + optional meta.json, thumbnail.png, and images/* into a .fig ZIP archive (store mode).
createEmptyFigDoc(): FigDocument
Creates an empty FigDocument with a bundled kiwi schema, a Document node, and a Page node. Ready for adding content and encoding.
File format documentation
Detailed documentation of the Figma binary format is in docs/:
| Doc | Covers | |-----|--------| | Archive structure | ZIP layout, canvas.fig binary, kiwi schema, encoding pipeline | | Nodes | Node types, GUIDs, parentIndex, hierarchy | | Shapes | ROUNDED_RECTANGLE, FRAME, transforms, geometry | | Vector | VECTOR nodes, commandsBlob format, blob resolution, helper API | | Text | TEXT nodes, styles, fonts | | Images | Image storage, SHA-1 hashing, thumbnails | | Colors | Color variables, palette | | Overrides | Symbol overrides (text, image, nested) | | Slides | Slide dimensions, cloning, ordering | | Modes | Slides mode vs Design mode | | Invariants | Hard rules and sentinel values | | Research | Binary format reverse-engineering notes |
Dependencies
fflate— ZIP extraction + deflate decompressionkiwi-schema— Kiwi binary format decodingfzstd— Zstandard decompression
License
MIT
Disclaimer
Figma is a trademark of Figma, Inc. This project is not affiliated with, endorsed by, or sponsored by Figma, Inc.
