Move font loading and layout into SkiaRenderer.prepareForExport

The headless render path duplicated font collection, text measurer
setup, and layout recomputation separately from the renderer. Move
this into SkiaRenderer.prepareForExport() so browser and headless
share the same code path for export preparation.

Remove the now-redundant loadFonts from CLI headless and the manual
setTextMeasurer/collectFontKeys/computeAllLayouts from headless-render.
This commit is contained in:
Danila Poyarkov 2026-03-14 12:06:46 +03:00
parent b9d6afcdbc
commit 7d9eb2931c
4 changed files with 21 additions and 22 deletions

View file

@ -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]

View file

@ -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<SceneGraph> {
return graph
}
export async function loadFonts(graph: SceneGraph, nodeIds?: string[]): Promise<void> {
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,

View file

@ -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<void> {
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<Uint8Array | null> {
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<Uint8Array | null> {
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)
}

View file

@ -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<void> {
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