openpencil/packages/cli/src/headless.ts
Danila Poyarkov 1975fae36e Add @open-pencil/cli with headless .fig file operations
Commands:
- open-pencil info <file> — document stats, node types, fonts
- open-pencil tree <file> — visual node tree with agentfmt
- open-pencil find <file> — search by name/type with rich output
- open-pencil export <file> — render to PNG/JPG/WEBP at any scale

Uses canvaskit-wasm/full for headless CPU rasterization.
All commands support --json for machine-readable output.
agentfmt provides consistent colored terminal formatting.
2026-03-01 01:19:24 +03:00

77 lines
2.2 KiB
TypeScript

import CanvasKitInit from 'canvaskit-wasm/full'
import type { CanvasKit } from 'canvaskit-wasm'
import {
parseFigFile,
SceneGraph,
SkiaRenderer,
computeAllLayouts,
loadFont,
renderNodesToImage,
renderThumbnail
} from '@open-pencil/core'
import type { ExportFormat } from '@open-pencil/core'
let ck: CanvasKit | null = null
export async function initCanvasKit(): Promise<CanvasKit> {
if (ck) return ck
const ckPath = import.meta.resolve('canvaskit-wasm/full')
const binDir = new URL('.', ckPath).pathname
ck = await CanvasKitInit({
locateFile: (file) => binDir + file
})
return ck
}
export async function loadDocument(filePath: string): Promise<SceneGraph> {
const data = await Bun.file(filePath).arrayBuffer()
const graph = await parseFigFile(data)
computeAllLayouts(graph)
return graph
}
export async function loadFonts(graph: SceneGraph): Promise<void> {
const families = new Set<string>()
for (const node of graph.getAllNodes()) {
if (node.fontFamily) families.add(node.fontFamily)
}
for (const family of families) {
await loadFont(family)
}
}
function createRenderer(ckInstance: CanvasKit, width: number, height: number): SkiaRenderer {
const surface = ckInstance.MakeSurface(width, height)!
const renderer = new SkiaRenderer(ckInstance, surface)
renderer.viewportWidth = width
renderer.viewportHeight = height
renderer.dpr = 1
return renderer
}
export async function exportNodes(
graph: SceneGraph,
pageId: string,
nodeIds: string[],
options: { scale?: number; format?: ExportFormat; quality?: number }
): Promise<Uint8Array | null> {
const ckInstance = await initCanvasKit()
const renderer = createRenderer(ckInstance, 1, 1)
return renderNodesToImage(ckInstance, renderer, graph, pageId, nodeIds, {
scale: options.scale ?? 1,
format: options.format ?? 'PNG',
quality: options.quality
})
}
export async function exportThumbnail(
graph: SceneGraph,
pageId: string,
width: number,
height: number
): Promise<Uint8Array | null> {
const ckInstance = await initCanvasKit()
const renderer = createRenderer(ckInstance, width, height)
return renderThumbnail(ckInstance, renderer, graph, pageId, width, height)
}