diff --git a/packages/cli/src/commands/export.ts b/packages/cli/src/commands/export.ts index bef3ac9c8..e6b2eff05 100644 --- a/packages/cli/src/commands/export.ts +++ b/packages/cli/src/commands/export.ts @@ -3,7 +3,7 @@ import { basename, extname, resolve } from 'node:path' import { renderNodesToSVG, sceneNodeToJSX, selectionToJSX } from '@open-pencil/core' -import { loadDocument, loadFonts, exportNodes, exportThumbnail } from '../headless' +import { loadDocument, exportNodes, exportThumbnail } from '../headless' import { isAppMode, requireFile, rpc } from '../app-client' import { ok, printError } from '../format' import type { ExportFormat, JSXFormat } from '@open-pencil/core' @@ -60,7 +60,6 @@ async function exportViaApp(format: string, args: ExportArgs) { async function exportFromFile(format: string, args: ExportArgs) { const file = requireFile(args.file) const graph = await loadDocument(file) - await loadFonts(graph) const pages = graph.getPages() const page = args.page ? pages.find((p) => p.name === args.page) : pages[0] diff --git a/packages/cli/src/headless.ts b/packages/cli/src/headless.ts index 0bfff2b97..f5f86ea7e 100644 --- a/packages/cli/src/headless.ts +++ b/packages/cli/src/headless.ts @@ -4,8 +4,6 @@ import { type SceneGraph, type ExportFormat, computeAllLayouts, - collectFontKeys, - loadFont, headlessRenderNodes, headlessRenderThumbnail } from '@open-pencil/core' @@ -19,12 +17,6 @@ export async function loadDocument(filePath: string): Promise { return graph } -export async function loadFonts(graph: SceneGraph, nodeIds?: string[]): Promise { - const ids = nodeIds ?? [...graph.getAllNodes()].map((n) => n.id) - const fontKeys = collectFontKeys(graph, ids) - await Promise.all(fontKeys.map(([family, style]) => loadFont(family, style))) -} - export async function exportNodes( graph: SceneGraph, pageId: string, diff --git a/packages/core/src/headless-render.ts b/packages/core/src/headless-render.ts index 7309cc09a..07ef1deee 100644 --- a/packages/core/src/headless-render.ts +++ b/packages/core/src/headless-render.ts @@ -1,7 +1,5 @@ import type { CanvasKit } from 'canvaskit-wasm' -import { collectFontKeys, loadFont } from './fonts' -import { computeAllLayouts, setTextMeasurer } from './layout' import { renderNodesToImage, renderThumbnail, type ExportFormat } from './render-image' import { SkiaRenderer } from './renderer' import type { SceneGraph } from './scene-graph' @@ -28,16 +26,10 @@ async function getRenderer(): Promise<{ ck: CanvasKit; renderer: SkiaRenderer }> renderer.viewportHeight = 1 renderer.dpr = 1 await renderer.loadFonts() - setTextMeasurer((node, maxWidth) => renderer.measureTextNode(node, maxWidth)) cachedRenderer = renderer return { ck, renderer } } -async function loadNodeFonts(graph: SceneGraph, nodeIds: string[]): Promise { - const fontKeys = collectFontKeys(graph, nodeIds) - await Promise.all(fontKeys.map(([family, style]) => loadFont(family, style))) -} - export async function headlessRenderNodes( graph: SceneGraph, pageId: string, @@ -45,8 +37,7 @@ export async function headlessRenderNodes( options: { scale?: number; format?: ExportFormat; quality?: number } = {} ): Promise { const { ck, renderer } = await getRenderer() - await loadNodeFonts(graph, nodeIds) - computeAllLayouts(graph, pageId) + await renderer.prepareForExport(graph, pageId, nodeIds) return renderNodesToImage(ck, renderer, graph, pageId, nodeIds, { scale: options.scale ?? 1, format: options.format ?? 'PNG', @@ -62,7 +53,6 @@ export async function headlessRenderThumbnail( ): Promise { const { ck, renderer } = await getRenderer() const page = graph.getNode(pageId) - if (page) await loadNodeFonts(graph, page.childIds) - computeAllLayouts(graph, pageId) + if (page) await renderer.prepareForExport(graph, pageId, page.childIds) return renderThumbnail(ck, renderer, graph, pageId, width, height) } diff --git a/packages/core/src/renderer/renderer.ts b/packages/core/src/renderer/renderer.ts index 558d3bd5b..7168176d4 100644 --- a/packages/core/src/renderer/renderer.ts +++ b/packages/core/src/renderer/renderer.ts @@ -416,6 +416,24 @@ export class SkiaRenderer { }) } + /** + * Load document fonts and set up text measurement for layout. + * Call after `loadFonts()` and before rendering a document headlessly. + * Collects all font family+weight pairs used by `nodeIds`, loads them, + * wires up the text measurer for Yoga layout, and recomputes layout. + */ + async prepareForExport(graph: SceneGraph, pageId: string, nodeIds: string[]): Promise { + const { collectFontKeys, loadFont } = await import('../fonts') + const { setTextMeasurer, computeAllLayouts } = await import('../layout') + + setTextMeasurer((node, maxWidth) => this.measureTextNode(node, maxWidth)) + + const fontKeys = collectFontKeys(graph, nodeIds) + await Promise.all(fontKeys.map(([family, style]) => loadFont(family, style))) + + computeAllLayouts(graph, pageId) + } + replaceSurface(surface: Surface): void { this.surface.delete() this.surface = surface