From 7d9eb2931c5965a99366b3fcca8450a4f0561bbc Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sat, 14 Mar 2026 12:06:46 +0300 Subject: [PATCH] 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. --- packages/cli/src/commands/export.ts | 3 +-- packages/cli/src/headless.ts | 8 -------- packages/core/src/headless-render.ts | 14 ++------------ packages/core/src/renderer/renderer.ts | 18 ++++++++++++++++++ 4 files changed, 21 insertions(+), 22 deletions(-) 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