File parser: - .fig files are ZIP archives with Kiwi-encoded canvas data - Detects canvas blob by name (canvas.fig) or largest binary entry - Decodes Kiwi Message → NodeChange[] → SceneGraph Node conversion: - Maps Figma types to OpenPencil types (COMPONENT → FRAME, etc.) - Extracts position from transform matrix (m02, m12) - Extracts rotation from transform matrix (atan2) - Converts fillPaints/strokePaints to Fill[]/Stroke[] - Converts effects (shadows, blurs) - Handles independent corner radii - Preserves text properties (font, size, alignment, spacing) - Builds parent-child tree from parentIndex GUIDs - Sorts children by parentIndex.position Browser compatibility: - fzstd for Zstd decompression (replaces Bun.zstd*) - fflate for ZIP extraction - Cmd+O opens file dialog for .fig files - UndoManager.clear() for resetting on file open
70 lines
2 KiB
TypeScript
70 lines
2 KiB
TypeScript
import { unzipSync } from 'fflate'
|
|
|
|
import { initCodec, decodeMessage } from '../kiwi/codec'
|
|
import { importNodeChanges } from './fig-import'
|
|
|
|
import type { SceneGraph } from './scene-graph'
|
|
|
|
/**
|
|
* Parse a .fig file into a SceneGraph.
|
|
*
|
|
* A .fig file is a ZIP archive containing:
|
|
* - `canvas.fig` or similar blob: Kiwi-encoded document state
|
|
* - `thumbnail.png`: preview image
|
|
* - `meta.json`: file metadata
|
|
*
|
|
* The Kiwi payload is a serialized Message with nodeChanges.
|
|
*/
|
|
export async function parseFigFile(buffer: ArrayBuffer): Promise<SceneGraph> {
|
|
await initCodec()
|
|
|
|
const zip = unzipSync(new Uint8Array(buffer))
|
|
const entries = Object.keys(zip)
|
|
|
|
// Find the Kiwi-encoded canvas data
|
|
// .fig files may have various structures; the binary blob is typically the largest non-image file
|
|
let canvasData: Uint8Array | null = null
|
|
|
|
// Try known names first
|
|
for (const name of entries) {
|
|
if (name === 'canvas.fig' || name === 'canvas') {
|
|
canvasData = zip[name]
|
|
break
|
|
}
|
|
}
|
|
|
|
// Fallback: find the largest binary file that's not an image/json
|
|
if (!canvasData) {
|
|
let maxSize = 0
|
|
for (const name of entries) {
|
|
const lower = name.toLowerCase()
|
|
if (lower.endsWith('.png') || lower.endsWith('.jpg') || lower.endsWith('.json')) continue
|
|
if (zip[name].byteLength > maxSize) {
|
|
maxSize = zip[name].byteLength
|
|
canvasData = zip[name]
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!canvasData) {
|
|
throw new Error(`No canvas data found in .fig file. Entries: ${entries.join(', ')}`)
|
|
}
|
|
|
|
// Decode the Kiwi message
|
|
const message = decodeMessage(canvasData)
|
|
const nodeChanges = message.nodeChanges
|
|
if (!nodeChanges || nodeChanges.length === 0) {
|
|
throw new Error('No nodes found in .fig file')
|
|
}
|
|
|
|
return importNodeChanges(nodeChanges)
|
|
}
|
|
|
|
/**
|
|
* Read a .fig File object and parse it
|
|
*/
|
|
export async function readFigFile(file: File): Promise<SceneGraph> {
|
|
const buffer = await file.arrayBuffer()
|
|
return parseFigFile(buffer)
|
|
}
|