@json-to-office/jto-ops 2.3.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1619,7 +1619,17 @@ var PptxFormatAdapter = class {
|
|
|
1619
1619
|
path5.resolve(process.cwd(), options.themePath),
|
|
1620
1620
|
"utf-8"
|
|
1621
1621
|
);
|
|
1622
|
-
|
|
1622
|
+
const shared = await import("@json-to-office/shared-pptx");
|
|
1623
|
+
const checked = shared.validatePptxTheme(JSON.parse(content));
|
|
1624
|
+
if (!checked.valid) {
|
|
1625
|
+
const detail = checked.errors.slice(0, 3).map(
|
|
1626
|
+
(error) => error.path ? `${error.path}: ${error.message}` : error.message
|
|
1627
|
+
).join("; ");
|
|
1628
|
+
throw new Error(
|
|
1629
|
+
`not a valid pptx theme \u2014 ${detail}${checked.errors.length > 3 ? ` (and ${checked.errors.length - 3} more)` : ""}`
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
fileTheme = checked.data;
|
|
1623
1633
|
} else {
|
|
1624
1634
|
const themePath = path5.resolve(process.cwd(), options.themePath);
|
|
1625
1635
|
const themeModule = await import(themePath);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/format-adapter.ts","../src/pptx-rasterizer.ts","../src/font-staging/noop-stager.ts","../src/font-staging/fontconfig-stager.ts","../src/font-staging/types.ts","../src/font-staging/windows-stager.ts","../src/font-staging/macos-stager.ts","../src/diagnostics.ts","../src/font-staging/index.ts","../src/pdf-text-geometry.ts"],"sourcesContent":["import * as path from 'path';\nimport * as fs from 'fs';\n\nimport type {\n ServicesConfig,\n FontRuntimeOpts,\n PptxRasterizer,\n PptxBatchRasterizer,\n GenerationWarning,\n RendererStatus,\n} from '@json-to-office/shared';\nimport type {\n PreparedDocument,\n QualityAnalysis,\n QualityPolicy,\n QualityProfile,\n} from '@json-to-office/quality';\nimport { validatePresentationDocument } from '@json-to-office/shared-pptx';\nimport { validate as validateDocx } from '@json-to-office/shared-docx';\nimport {\n createLibreOfficePptxRasterizer,\n createLibreOfficePptxBatchRasterizer,\n} from './pptx-rasterizer.js';\nimport { emitDiagnostic } from './diagnostics.js';\n\n/** Forward structured warnings collected during generation to the host's sink. */\nfunction emitGenerationWarnings(warnings: GenerationWarning[]): void {\n for (const warning of warnings) {\n emitDiagnostic(\n `${warning.component}: ${warning.message}`,\n warning.severity === 'info' ? 'info' : 'warning'\n );\n }\n}\n\n/**\n * Normalize a core's warning array into the single client/CLI-facing shape.\n *\n * DOCX cores already emit `GenerationWarning`; PPTX cores emit\n * `PipelineWarning = {code, message, component?, slide?}` — no `severity` and\n * an optional `component`. Left raw, a PipelineWarning renders as an empty\n * component chip in the playground's WarningsPanel, so both shapes funnel\n * through here and come out self-describing.\n */\nfunction toGenerationWarnings(\n raw: readonly any[] | null | undefined\n): GenerationWarning[] {\n return (raw ?? []).map((w) => ({\n component: w?.component ?? 'pptx',\n message: String(w?.message ?? ''),\n severity: (w?.severity === 'info' ? 'info' : 'warning') as\n | 'warning'\n | 'info',\n context: {\n ...(w?.context && typeof w.context === 'object' ? w.context : {}),\n ...(w?.code !== undefined && { code: w.code }),\n ...(w?.slide !== undefined && { slide: w.slide }),\n },\n }));\n}\n\nfunction preparedThemeLabel(prepared: PreparedDocument): string | undefined {\n const label = prepared.metadata?.themeLabel;\n return typeof label === 'string' ? label : undefined;\n}\n\n/**\n * The document a prepared model was built from, and the warnings its\n * preparation already reported.\n *\n * Symbols, so neither crosses a serialization boundary: `PreparedDocument` is\n * a serializable contract, and a model that arrives without its source is\n * re-prepared rather than rendered in place of the document it was handed.\n */\nconst PREPARED_SOURCE = Symbol('jto.prepared.source');\nconst PREPARED_WARNINGS = Symbol('jto.prepared.warnings');\n\ninterface PreparedInternals {\n [PREPARED_SOURCE]?: unknown;\n [PREPARED_WARNINGS]?: readonly GenerationWarning[];\n}\n\n/** Bind a prepared model to the document and the warnings it came from. */\nfunction stampPrepared<T extends PreparedDocument>(\n prepared: T,\n source: unknown,\n warnings: readonly GenerationWarning[]\n): T {\n return Object.assign(prepared, {\n [PREPARED_SOURCE]: source,\n [PREPARED_WARNINGS]: warnings,\n });\n}\n\n/**\n * A prepared model, but only for the document it was prepared from.\n *\n * `generateBuffer(document)` is a per-document API while `prepared` is fixed\n * when the generator is built, so any other document would be validated as\n * itself and then rendered as the prepared one.\n */\nfunction preparedFor<T extends PreparedDocument>(\n prepared: T | undefined,\n document: unknown\n): T | undefined {\n if (!prepared) return undefined;\n const source = (prepared as PreparedInternals)[PREPARED_SOURCE];\n return source !== undefined && source === document ? prepared : undefined;\n}\n\n/** Warning identity, for the prepare/render overlap only. */\nfunction warningKey(warning: GenerationWarning): string {\n return `${warning.component}|${warning.context?.code ?? ''}|${warning.message}`;\n}\n\n/**\n * Drop render warnings this model's preparation already reported.\n *\n * Preparation repeats work the render does again — pptx placeholder and grid\n * resolution above all — so reusing a prepared model would report those\n * warnings twice. Deliberately narrow: nothing is deduplicated except against\n * what preparing this very model emitted.\n */\nfunction withoutPreparedWarnings(\n warnings: GenerationWarning[],\n prepared: PreparedDocument | undefined\n): GenerationWarning[] {\n const emitted = (prepared as PreparedInternals | undefined)?.[\n PREPARED_WARNINGS\n ];\n if (!emitted || emitted.length === 0) return warnings;\n const seen = new Set(emitted.map(warningKey));\n return warnings.filter((warning) => !seen.has(warningKey(warning)));\n}\n\n/**\n * The renderer the document itself names. Core generation resolves\n * `options.renderer ?? document.renderer`, so preparation that reads only the\n * option stamps the default onto the model and misjudges a profile targeted\n * at the backend the document actually renders with.\n */\nfunction documentRenderer(document: unknown): string | undefined {\n const renderer = (document as { renderer?: unknown } | null | undefined)\n ?.renderer;\n return typeof renderer === 'string' ? renderer : undefined;\n}\n\nconst UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\nfunction safeThemeKey(name: string | undefined): string {\n return name && !UNSAFE_KEYS.has(name) ? name : 'custom';\n}\n\n/** Key the theme named by `--theme`/`--theme-path` is registered under. */\nconst CLI_THEME_KEY = 'jto-cli-theme';\n\n/**\n * Point a document at the explicitly requested theme. The JSON path selects a\n * theme by name off `props.theme`, so `--theme`/`--theme-path` is applied by\n * registering the resolved theme under a reserved key and rewriting the\n * reference — an explicit theme wins over the document's own `props.theme`.\n * With no theme requested the document is passed through untouched.\n */\nfunction withRequestedTheme(\n document: any,\n theme: any | undefined,\n customThemes: Record<string, any> | undefined\n): { document: any; customThemes: Record<string, any> | undefined } {\n if (!theme || typeof document !== 'object' || document === null) {\n return { document, customThemes };\n }\n return {\n document: {\n ...document,\n props: { ...document.props, theme: CLI_THEME_KEY },\n },\n customThemes: { ...customThemes, [CLI_THEME_KEY]: theme },\n };\n}\n\nexport type FormatName = 'docx' | 'pptx';\n\nfunction buildServicesFromEnv(): ServicesConfig | undefined {\n const serverUrl = process.env.HIGHCHARTS_SERVER_URL;\n const apiKey = process.env.HIGHCHARTS_API_KEY;\n const apiKeyHeader = process.env.HIGHCHARTS_API_KEY_HEADER ?? 'x-api-key';\n\n if (!serverUrl && !apiKey) return undefined;\n\n return {\n highcharts: {\n serverUrl,\n ...(apiKey && { headers: { [apiKeyHeader]: apiKey } }),\n },\n };\n}\n\n// Lazily-constructed LibreOffice rasterizers, shared across docx generations.\n// Constructing them is cheap (no binaries touched); they only spawn\n// LibreOffice when a document actually contains a `visual` component. Single\n// and batch share the same content-addressed disk cache.\nlet cachedRasterizer: PptxRasterizer | undefined;\nfunction getPptxRasterizer(): PptxRasterizer {\n if (!cachedRasterizer) {\n cachedRasterizer = createLibreOfficePptxRasterizer();\n }\n return cachedRasterizer;\n}\nlet cachedBatchRasterizer: PptxBatchRasterizer | undefined;\nfunction getPptxBatchRasterizer(): PptxBatchRasterizer {\n if (!cachedBatchRasterizer) {\n cachedBatchRasterizer = createLibreOfficePptxBatchRasterizer();\n }\n return cachedBatchRasterizer;\n}\n\n/**\n * Services for docx generation: highcharts (from env) plus the pptx rasterizer\n * that backs `visual` components. An explicit HIGHCHARTS-style override is not\n * needed for pptx — a running rasterization server can be pointed at via\n * `services.pptx.serverUrl`, but the default is the in-process LibreOffice\n * renderer.\n */\nfunction buildDocxServices(): ServicesConfig {\n const base = buildServicesFromEnv() ?? {};\n const serverUrl = process.env.JTO_PPTX_RASTERIZER_URL?.trim();\n const apiKey =\n process.env.JTO_PPTX_RASTERIZER_API_KEY || process.env.HIGHCHARTS_API_KEY;\n const apiKeyHeader =\n process.env.JTO_PPTX_RASTERIZER_API_KEY_HEADER ||\n process.env.HIGHCHARTS_API_KEY_HEADER ||\n 'x-api-key';\n return {\n ...base,\n pptx: serverUrl\n ? {\n serverUrl,\n ...(apiKey && { headers: { [apiKeyHeader]: apiKey } }),\n }\n : {\n render: getPptxRasterizer(),\n renderBatch: getPptxBatchRasterizer(),\n },\n };\n}\n\n/** Minimal builder shape shared by DOCX and PPTX generators */\ninterface GeneratorBuilder {\n addComponent(component: any): GeneratorBuilder;\n validate(document: any): {\n valid: boolean;\n errors?: { path: string; message: string }[];\n };\n generateBuffer(\n document: any,\n options?: {\n deterministic?: boolean;\n generatedAt?: string | Date;\n validation?: { allowUnknownFields?: boolean };\n baseDir?: string;\n renderer?: string;\n /** DOCX only; PPTX generators ignore it. */\n svgRasterFallback?: boolean;\n }\n ): Promise<{ buffer: Buffer; warnings: any }>;\n /**\n * Cheap standard-definition path: expansion + normalization only, no\n * rendering (DOCX generators expose it; PPTX ones may not).\n */\n expandStandardDefinition?: (\n document: any,\n options?: { validation?: { allowUnknownFields?: boolean } }\n ) => Promise<{ standardDefinition: any; warnings: any }>;\n}\n\n/**\n * Each format's renderer-id union, read from its core without importing it.\n *\n * `typeof import(...)` is a type query — erased at compile time — so this keeps\n * the ids honest while the cores stay dynamically loaded. `GeneratorOptions`\n * carries a bare string because one options bag serves both formats; these are\n * what it narrows to at each call site, and an id outside the union is rejected\n * by the core's registry with the list of valid ones.\n */\ntype DocxRendererId =\n (typeof import('@json-to-office/core-docx'))['DEFAULT_DOCX_RENDERER_ID'];\ntype PptxRendererId =\n (typeof import('@json-to-office/core-pptx'))['DEFAULT_PPTX_RENDERER_ID'];\n\nexport interface GeneratorOptions {\n theme?: string | any;\n themePath?: string;\n customThemes?: Record<string, any>;\n validation?: {\n strict?: boolean;\n allowUnknownFields?: boolean;\n };\n fonts?: FontRuntimeOpts;\n deterministic?: boolean;\n generatedAt?: string | Date;\n /**\n * Directory that relative asset paths in the document resolve against —\n * normally the input document's own directory (#142).\n */\n baseDir?: string;\n /**\n * Backend that turns the compiled document into bytes.\n *\n * Format-specific and validated by the core's renderer registry, which is\n * why this is a bare string here: naming the id union would make this\n * package import both cores statically, and they are loaded on demand.\n * Undefined means the format's default (`docxjs` / `pptxgenjs`).\n */\n renderer?: string;\n /**\n * Rasterize a PNG fallback for each inline SVG. Defaults to true.\n *\n * DOCX only — pptx embeds SVG without a raster twin. Only readers older than\n * Word 2016 draw it, and producing it dominates the render of a document\n * whose artwork is many small SVGs.\n */\n svgRasterFallback?: boolean;\n /**\n * Optional sink for structured generation warnings (FONT_UNRESOLVED and\n * friends). Mirrors core-docx's `JsonGenerationOptions.warnings`, and is the\n * only delivery mechanism that works off the CLI: `emitGenerationWarnings`\n * routes through an AsyncLocalStorage sink that is a no-op on the server.\n *\n * Adapters PUSH into it; they never replace it. Warnings therefore\n * ACCUMULATE across repeated `generateBuffer` calls on one\n * `GeneratorResult` — allocate one array per logical request.\n */\n warnings?: GenerationWarning[];\n /** Design profile and invocation-specific enforcement. */\n quality?: {\n profile?: QualityProfile;\n policy?: QualityPolicy;\n };\n /** Opaque canonical prologue output shared by analysis and rendering. */\n prepared?: PreparedDocument;\n}\n\nexport interface GeneratorResult {\n generateBuffer: (document: any) => Promise<Buffer>;\n /**\n * Post-expansion standard JSON tree without any rendering work — no fonts,\n * no layout, no visual rasterization. Present when the underlying generator\n * supports it (plugin-aware DOCX generation does).\n */\n getStandardDefinition?: (config: any) => Promise<any>;\n hasPlugins: boolean;\n pluginNames: string[];\n /**\n * Identity of the theme this generator forces on every document, or\n * undefined when nothing was requested and each document's own `props.theme`\n * decides. Reported by the CLI so the summary names what actually rendered.\n */\n themeLabel?: string;\n}\n\n/** One resolution of `theme`/`themePath`, shared by every consumer of a run. */\ninterface ResolvedThemes {\n /**\n * The theme named by `theme`/`themePath`, or undefined when neither is set\n * or resolves — callers that must not override the document's own\n * `props.theme` depend on that distinction.\n */\n requested: any | undefined;\n /** Themes registered by name for `props.theme` lookups. */\n customThemes: Record<string, any> | undefined;\n /** What to call `requested`; undefined when the document decides. */\n label: string | undefined;\n}\n\nexport interface FormatAdapter {\n name: FormatName;\n extension: string;\n label: string;\n defaultPort: number;\n\n generateBuffer(json: unknown, options: GeneratorOptions): Promise<Buffer>;\n\n createGenerator(\n plugins: any[],\n options: GeneratorOptions\n ): Promise<GeneratorResult>;\n\n parseJson(input: string | object): unknown;\n validateDocument(doc: unknown): { valid: boolean; errors?: any[] };\n\n /**\n * Validate a document that names plugin components.\n *\n * `validateDocument` above knows the standard components and nothing else,\n * so a registered plugin reads to it as `Unknown component \"weather\"` —\n * the same name the schema route offers for completion and the generator\n * expands. The core validators take the registered components, defer those\n * nodes from the standard walk, and check each one's props against the\n * version it resolves to; this is the seam that reaches them.\n *\n * Async because the core that owns them is imported on demand, as\n * `analyzeQuality` does. Callers with no plugins registered should keep\n * using the sync entry point.\n */\n validateDocumentWithPlugins?(\n doc: unknown,\n plugins: any[]\n ): Promise<{ valid: boolean; errors?: any[] }>;\n\n /** Analyze format-specific design quality with profiles, policy, and gate. */\n analyzeQuality?(\n doc: unknown,\n options?: GeneratorOptions\n ): Promise<QualityAnalysis>;\n\n /** Prepare effective values and provenance once for official pipelines. */\n prepareDocument?(\n doc: unknown,\n options?: GeneratorOptions\n ): Promise<PreparedDocument>;\n\n generateSchema(options?: any): any;\n\n getBuiltinThemes(): Record<string, any>;\n /** Full values for ESM hosts; falls back to `getBuiltinThemes` for plugins. */\n getBuiltinThemeValues?(): Promise<Record<string, any>>;\n resolveTheme(options: GeneratorOptions): Promise<any>;\n loadCustomThemes(\n options: GeneratorOptions\n ): Promise<Record<string, any> | undefined>;\n\n /**\n * Renderer ids this format registers, defaults first.\n *\n * Async because the core that owns the registry is imported on demand — the\n * list is read from it rather than repeated here, so the two cannot drift.\n */\n rendererIds(): Promise<readonly string[]>;\n\n /**\n * The same renderers, each with whether its backend loads on this host.\n *\n * `rendererIds` answers \"what is registered\", which is not the same question:\n * a factory only runs when its renderer is selected, so an id says nothing\n * about whether the render behind it will work. Anything that *advertises*\n * renderers should report this instead — otherwise a caller picks one, gets\n * a green light from validation, and fails a call later.\n */\n rendererStatuses(): Promise<readonly RendererStatus[]>;\n\n /** Cumulative visual pre-pass dedupe counters (DOCX only) (#156). */\n getVisualPrepassStats?(): Promise<any>;\n /** Reset per-format cache observability counters (DOCX only). */\n resetCacheStats?(): Promise<void>;\n}\n\nexport class DocxFormatAdapter implements FormatAdapter {\n name: FormatName = 'docx';\n extension = '.docx';\n label = 'document';\n defaultPort = 3003;\n\n async rendererIds(): Promise<readonly string[]> {\n const core = await import('@json-to-office/core-docx');\n return core.docxRendererIds();\n }\n\n async rendererStatuses(): Promise<readonly RendererStatus[]> {\n const core = await import('@json-to-office/core-docx');\n return core.docxRendererStatuses();\n }\n\n async generateBuffer(\n json: unknown,\n options: GeneratorOptions\n ): Promise<Buffer> {\n const core = await import('@json-to-office/core-docx');\n const parsed = typeof json === 'string' ? JSON.parse(json as string) : json;\n const prepared = preparedFor(\n options.prepared?.format === 'docx'\n ? (options.prepared as ReturnType<\n typeof core.prepareDocxQualityDocument\n >)\n : undefined,\n parsed\n );\n let docDefinition: unknown;\n let customThemes: Record<string, any> | undefined;\n if (prepared) {\n docDefinition = prepared.model.authored;\n } else {\n const resolved = await this.resolveThemes(options);\n const normalized = withRequestedTheme(\n parsed,\n resolved.requested,\n resolved.customThemes\n );\n docDefinition = normalized.document;\n customThemes = normalized.customThemes;\n }\n const services = buildDocxServices();\n // Collect rather than swallow: without a sink, core warnings (an\n // unresolvable `props.theme` among them) never reach the terminal and the\n // render just comes back looking subtly wrong.\n const warnings: GenerationWarning[] = [];\n const buffer = await core.generateBufferFromJson(docDefinition as any, {\n customThemes,\n services,\n fonts: options.fonts,\n validation: {\n allowUnknownFields: options.validation?.allowUnknownFields,\n },\n deterministic: options.deterministic,\n generatedAt: options.generatedAt,\n baseDir: options.baseDir,\n renderer: options.renderer as DocxRendererId | undefined,\n svgRasterFallback: options.svgRasterFallback,\n prepared,\n warnings,\n });\n const emitted = withoutPreparedWarnings(warnings, prepared);\n emitGenerationWarnings(emitted);\n options.warnings?.push(...toGenerationWarnings(emitted));\n return buffer;\n }\n\n async createGenerator(\n plugins: any[],\n options: GeneratorOptions\n ): Promise<GeneratorResult> {\n const core = await import('@json-to-office/core-docx');\n const hasPlugins = plugins.length > 0;\n const pluginNames = plugins.map((p) => p.name);\n const services = buildDocxServices();\n\n // Resolve once unless the canonical model already carries the result.\n const prepared =\n !hasPlugins && options.prepared?.format === 'docx'\n ? (options.prepared as ReturnType<\n typeof core.prepareDocxQualityDocument\n >)\n : undefined;\n const resolved = prepared ? undefined : await this.resolveThemes(options);\n const requestedTheme = resolved?.requested;\n const customThemes = resolved?.customThemes;\n const themeLabel = prepared\n ? preparedThemeLabel(prepared)\n : resolved?.label;\n\n if (!hasPlugins) {\n // A prepared model renders only its own document; any other one falls\n // back to resolving themes here, memoized so a bad `--theme` still\n // warns once per generator.\n let fallbackThemes: Promise<ResolvedThemes> | undefined;\n const themesFor = (): Promise<ResolvedThemes> =>\n (fallbackThemes ??= resolved\n ? Promise.resolve(resolved)\n : this.resolveThemes(options));\n return {\n generateBuffer: async (document: any) => {\n const parsed =\n typeof document === 'string' ? JSON.parse(document) : document;\n const usable = preparedFor(prepared, parsed);\n const themes = usable ? undefined : await themesFor();\n const normalized = usable\n ? { document: usable.model.authored, customThemes: undefined }\n : withRequestedTheme(\n parsed,\n themes?.requested,\n themes?.customThemes\n );\n const warnings: GenerationWarning[] = [];\n const buffer = await core.generateBufferFromJson(\n normalized.document,\n {\n customThemes: normalized.customThemes,\n services,\n fonts: options.fonts,\n validation: {\n allowUnknownFields: options.validation?.allowUnknownFields,\n },\n deterministic: options.deterministic,\n generatedAt: options.generatedAt,\n baseDir: options.baseDir,\n renderer: options.renderer as DocxRendererId | undefined,\n svgRasterFallback: options.svgRasterFallback,\n prepared: usable,\n warnings,\n }\n );\n const emitted = withoutPreparedWarnings(warnings, usable);\n emitGenerationWarnings(emitted);\n options.warnings?.push(...toGenerationWarnings(emitted));\n return buffer;\n },\n hasPlugins: false,\n pluginNames: [],\n themeLabel,\n };\n }\n\n let generator: GeneratorBuilder = core.createDocumentGenerator({\n // Undefined when nothing was requested: a constructor theme beats the\n // generator's own `props.theme` lookup, so forcing one here would render\n // every document in it.\n theme: requestedTheme,\n customThemes: requestedTheme\n ? { ...customThemes, [CLI_THEME_KEY]: requestedTheme }\n : customThemes,\n debug: process.env.DEBUG === 'true',\n services,\n fonts: options.fonts,\n validation: {\n allowUnknownFields: options.validation?.allowUnknownFields,\n },\n deterministic: options.deterministic,\n generatedAt: options.generatedAt,\n baseDir: options.baseDir,\n renderer: options.renderer as DocxRendererId | undefined,\n svgRasterFallback: options.svgRasterFallback,\n });\n\n for (const plugin of plugins) {\n generator = generator.addComponent(plugin);\n }\n\n return {\n generateBuffer: async (document: any) => {\n const parsed =\n typeof document === 'string' ? JSON.parse(document) : document;\n const { document: docDefinition } = withRequestedTheme(\n parsed,\n requestedTheme,\n customThemes\n );\n const result = await generator.generateBuffer(docDefinition, {\n validation: {\n allowUnknownFields: options.validation?.allowUnknownFields,\n },\n deterministic: options.deterministic,\n generatedAt: options.generatedAt,\n baseDir: options.baseDir,\n renderer: options.renderer as DocxRendererId | undefined,\n svgRasterFallback: options.svgRasterFallback,\n });\n emitGenerationWarnings(result.warnings ?? []);\n options.warnings?.push(...toGenerationWarnings(result.warnings));\n return result.buffer;\n },\n getStandardDefinition: generator.expandStandardDefinition\n ? async (config: any) => {\n const parsed =\n typeof config === 'string' ? JSON.parse(config) : config;\n const { document: docDefinition } = withRequestedTheme(\n parsed,\n requestedTheme,\n customThemes\n );\n const result = await generator.expandStandardDefinition!(\n docDefinition,\n {\n validation: {\n allowUnknownFields: options.validation?.allowUnknownFields,\n },\n }\n );\n emitGenerationWarnings(result.warnings ?? []);\n return result.standardDefinition;\n }\n : undefined,\n hasPlugins: true,\n pluginNames,\n themeLabel,\n };\n }\n\n parseJson(input: string | object): unknown {\n return typeof input === 'string' ? JSON.parse(input) : input;\n }\n\n validateDocument(doc: unknown): { valid: boolean; errors?: any[] } {\n const result = validateDocx.jsonDocument(doc as object);\n return {\n valid: result.valid,\n ...(result.errors.length > 0 && { errors: result.errors }),\n };\n }\n\n async validateDocumentWithPlugins(\n doc: unknown,\n plugins: any[]\n ): Promise<{ valid: boolean; errors?: any[] }> {\n const core = await import('@json-to-office/core-docx');\n const parsed = typeof doc === 'string' ? JSON.parse(doc) : doc;\n const result = core.validateDocument(parsed as any, plugins);\n const errors = result.errors ?? [];\n return {\n valid: result.valid,\n ...(errors.length > 0 && { errors }),\n };\n }\n\n async analyzeQuality(\n doc: unknown,\n options: GeneratorOptions = {}\n ): Promise<QualityAnalysis> {\n const core = await import('@json-to-office/core-docx');\n const parsed = typeof doc === 'string' ? JSON.parse(doc) : doc;\n let prepared = preparedFor(\n options.prepared?.format === 'docx'\n ? (options.prepared as ReturnType<\n typeof core.prepareDocxQualityDocument\n >)\n : undefined,\n parsed\n );\n if (!prepared) {\n try {\n prepared = (await this.prepareModel(parsed, options, [])) as ReturnType<\n typeof core.prepareDocxQualityDocument\n >;\n } catch {\n // Structural validation owns malformed trees. The core guards its own\n // preparation, so handing it the document instead of a model turns a\n // throw into an analysis that reports the failure.\n }\n }\n return core.analyzeDocxQuality(prepared?.model.authored ?? parsed, {\n ...(prepared && { prepared }),\n renderer: options.renderer ?? documentRenderer(parsed),\n profile: options.quality?.profile,\n policy: options.quality?.policy,\n });\n }\n\n async prepareDocument(\n doc: unknown,\n options: GeneratorOptions = {}\n ): Promise<PreparedDocument> {\n // Preparation resolves the theme context the render then skips, so these\n // warnings reach the host from here or not at all.\n const warnings: GenerationWarning[] = [];\n const prepared = await this.prepareModel(doc, options, warnings);\n emitGenerationWarnings(warnings);\n options.warnings?.push(...toGenerationWarnings(warnings));\n return prepared;\n }\n\n /** Prepare into a caller-owned sink; `prepareDocument` owns the reporting. */\n private async prepareModel(\n doc: unknown,\n options: GeneratorOptions,\n warnings: GenerationWarning[]\n ): Promise<PreparedDocument> {\n const core = await import('@json-to-office/core-docx');\n const parsed = typeof doc === 'string' ? JSON.parse(doc) : doc;\n const resolved = await this.resolveThemes(options);\n const normalized = withRequestedTheme(\n parsed,\n resolved.requested,\n resolved.customThemes\n );\n const prepared = core.prepareDocxQualityDocument(\n normalized.document as any,\n {\n customThemes: normalized.customThemes,\n fonts: options.fonts,\n renderer: options.renderer ?? documentRenderer(parsed),\n warnings,\n }\n );\n return stampPrepared(\n {\n ...prepared,\n metadata: {\n ...prepared.metadata,\n ...(resolved.label && { themeLabel: resolved.label }),\n },\n },\n parsed,\n toGenerationWarnings(warnings)\n );\n }\n\n generateSchema(_options?: any): any {\n // Delegate to shared-docx\n return null;\n }\n\n getBuiltinThemes(): Record<string, any> {\n try {\n const core = require('@json-to-office/core-docx');\n return core.themes || {};\n } catch {\n return {};\n }\n }\n\n async getBuiltinThemeValues(): Promise<Record<string, any>> {\n const core = await import('@json-to-office/core-docx');\n return core.themes || {};\n }\n\n async resolveTheme(options: GeneratorOptions): Promise<any> {\n const core = await import('@json-to-office/core-docx');\n const { requested } = await this.resolveThemes(options);\n return requested ?? (core.themes as any)?.minimal ?? {};\n }\n\n /**\n * Resolve `theme`/`themePath` once for a whole run: `themePath` is read a\n * single time and feeds both the requested theme and the custom-theme\n * registry, so a bad path warns once instead of once per consumer.\n */\n private async resolveThemes(\n options: GeneratorOptions\n ): Promise<ResolvedThemes> {\n const core = await import('@json-to-office/core-docx');\n // Themes passed directly from the client (playground UI) come first.\n const registry: Record<string, any> = { ...options.customThemes };\n\n if (typeof options.theme === 'object' && options.theme !== null) {\n registry[safeThemeKey(options.theme.name)] = options.theme;\n }\n\n let fileTheme: any | undefined;\n if (options.themePath) {\n try {\n if (options.themePath.endsWith('.json')) {\n fileTheme = await core.loadThemeFromFile(options.themePath);\n } else {\n const themePath = path.resolve(process.cwd(), options.themePath);\n const themeModule = await import(themePath);\n fileTheme = themeModule.default || themeModule.theme;\n }\n } catch (error: any) {\n emitDiagnostic(\n `Failed to load theme from ${options.themePath}: ${error.message}`,\n 'warning'\n );\n }\n if (fileTheme) {\n registry[safeThemeKey(fileTheme.name)] = fileTheme;\n }\n }\n\n const customThemes =\n Object.keys(registry).length > 0 ? registry : undefined;\n\n if (fileTheme) {\n return { requested: fileTheme, customThemes, label: options.themePath };\n }\n\n if (typeof options.theme === 'string') {\n const named =\n options.customThemes?.[options.theme] ??\n (core.themes as Record<string, any>)?.[options.theme];\n if (named)\n return { requested: named, customThemes, label: options.theme };\n\n if (options.theme.endsWith('.json') && fs.existsSync(options.theme)) {\n try {\n return {\n requested: await core.loadThemeFromFile(options.theme),\n customThemes,\n label: options.theme,\n };\n } catch {}\n }\n\n try {\n const inline = await core.loadThemeFromJson(options.theme);\n return {\n requested: inline,\n customThemes,\n label: (inline as any)?.name || options.theme,\n };\n } catch {}\n\n emitDiagnostic(\n `Unknown theme \"${options.theme}\"; keeping the document's own theme`,\n 'warning'\n );\n }\n\n if (typeof options.theme === 'object' && options.theme !== null) {\n return {\n requested: options.theme,\n customThemes,\n label: safeThemeKey(options.theme.name),\n };\n }\n\n return { requested: undefined, customThemes, label: undefined };\n }\n\n async loadCustomThemes(\n options: GeneratorOptions\n ): Promise<Record<string, any> | undefined> {\n return (await this.resolveThemes(options)).customThemes;\n }\n\n async getVisualPrepassStats(): Promise<any> {\n try {\n const core = await import('@json-to-office/core-docx');\n return core.getVisualPrepassStats?.() ?? null;\n } catch {\n return null;\n }\n }\n\n async resetCacheStats(): Promise<void> {\n try {\n const core = await import('@json-to-office/core-docx');\n core.resetVisualPrepassStats?.();\n } catch {\n // Resetting observability is best-effort.\n }\n }\n}\n\nexport class PptxFormatAdapter implements FormatAdapter {\n name: FormatName = 'pptx';\n extension = '.pptx';\n label = 'presentation';\n defaultPort = 3004;\n\n async rendererIds(): Promise<readonly string[]> {\n const core = await import('@json-to-office/core-pptx');\n return core.pptxRendererIds();\n }\n\n async rendererStatuses(): Promise<readonly RendererStatus[]> {\n const core = await import('@json-to-office/core-pptx');\n return core.pptxRendererStatuses();\n }\n\n async generateBuffer(\n json: unknown,\n options: GeneratorOptions\n ): Promise<Buffer> {\n const core = await import('@json-to-office/core-pptx');\n const parsed = typeof json === 'string' ? JSON.parse(json as string) : json;\n const prepared = preparedFor(\n options.prepared?.format === 'pptx'\n ? (options.prepared as ReturnType<\n typeof core.preparePptxQualityDocument\n >)\n : undefined,\n parsed\n );\n let docDefinition: unknown;\n let customThemes: Record<string, any> | undefined;\n if (prepared) {\n docDefinition = prepared.model.authored;\n } else {\n const resolved = await this.resolveThemes(options);\n const normalized = withRequestedTheme(\n parsed,\n resolved.requested,\n resolved.customThemes\n );\n docDefinition = normalized.document;\n customThemes = normalized.customThemes;\n }\n const services = buildServicesFromEnv();\n // The warnings-returning entry point: `generateBufferFromJson` allocates\n // the pipeline's warning array internally and throws it away, so core\n // warnings (FONT_UNRESOLVED among them) never reached the terminal or the\n // server.\n const result = await core.generateBufferWithWarnings(docDefinition as any, {\n customThemes,\n services,\n fonts: options.fonts,\n validation: {\n allowUnknownFields: options.validation?.allowUnknownFields,\n },\n deterministic: options.deterministic,\n generatedAt: options.generatedAt,\n baseDir: options.baseDir,\n renderer: options.renderer as PptxRendererId | undefined,\n prepared,\n });\n const emitted = withoutPreparedWarnings(\n toGenerationWarnings(result.warnings),\n prepared\n );\n emitGenerationWarnings(emitted);\n options.warnings?.push(...emitted);\n return result.buffer;\n }\n\n async createGenerator(\n plugins: any[],\n options: GeneratorOptions\n ): Promise<GeneratorResult> {\n const core = await import('@json-to-office/core-pptx');\n const hasPlugins = plugins.length > 0;\n const pluginNames = plugins.map((p) => p.name);\n const services = buildServicesFromEnv();\n\n // Resolve once unless the canonical model already carries the result.\n const prepared =\n !hasPlugins && options.prepared?.format === 'pptx'\n ? (options.prepared as ReturnType<\n typeof core.preparePptxQualityDocument\n >)\n : undefined;\n const resolved = prepared ? undefined : await this.resolveThemes(options);\n const requestedTheme = resolved?.requested;\n const customThemes = resolved?.customThemes;\n const themeLabel = prepared\n ? preparedThemeLabel(prepared)\n : resolved?.label;\n\n if (!hasPlugins) {\n // A prepared model renders only its own document; any other one falls\n // back to resolving themes here, memoized so a bad `--theme` still\n // warns once per generator.\n let fallbackThemes: Promise<ResolvedThemes> | undefined;\n const themesFor = (): Promise<ResolvedThemes> =>\n (fallbackThemes ??= resolved\n ? Promise.resolve(resolved)\n : this.resolveThemes(options));\n return {\n generateBuffer: async (document: any) => {\n const parsed =\n typeof document === 'string' ? JSON.parse(document) : document;\n const usable = preparedFor(prepared, parsed);\n const themes = usable ? undefined : await themesFor();\n const normalized = usable\n ? { document: usable.model.authored, customThemes: undefined }\n : withRequestedTheme(\n parsed,\n themes?.requested,\n themes?.customThemes\n );\n const result = await core.generateBufferWithWarnings(\n normalized.document,\n {\n customThemes: normalized.customThemes,\n services,\n fonts: options.fonts,\n validation: {\n allowUnknownFields: options.validation?.allowUnknownFields,\n },\n deterministic: options.deterministic,\n generatedAt: options.generatedAt,\n baseDir: options.baseDir,\n renderer: options.renderer as PptxRendererId | undefined,\n prepared: usable,\n }\n );\n const warnings = withoutPreparedWarnings(\n toGenerationWarnings(result.warnings),\n usable\n );\n emitGenerationWarnings(warnings);\n options.warnings?.push(...warnings);\n return result.buffer;\n },\n hasPlugins: false,\n pluginNames: [],\n themeLabel,\n };\n }\n\n let generator: GeneratorBuilder = core.createPresentationGenerator({\n // Undefined when nothing was requested: a constructor theme beats the\n // generator's own `props.theme` lookup, so forcing one here would render\n // every document in it.\n theme: requestedTheme,\n customThemes: requestedTheme\n ? { ...customThemes, [CLI_THEME_KEY]: requestedTheme }\n : customThemes,\n debug: process.env.DEBUG === 'true',\n services,\n fonts: options.fonts,\n validation: {\n allowUnknownFields: options.validation?.allowUnknownFields,\n },\n deterministic: options.deterministic,\n generatedAt: options.generatedAt,\n baseDir: options.baseDir,\n renderer: options.renderer as PptxRendererId | undefined,\n });\n\n for (const plugin of plugins) {\n generator = generator.addComponent(plugin);\n }\n\n return {\n generateBuffer: async (document: any) => {\n const parsed =\n typeof document === 'string' ? JSON.parse(document) : document;\n const { document: docDefinition } = withRequestedTheme(\n parsed,\n requestedTheme,\n customThemes\n );\n const result = await generator.generateBuffer(docDefinition, {\n validation: {\n allowUnknownFields: options.validation?.allowUnknownFields,\n },\n deterministic: options.deterministic,\n generatedAt: options.generatedAt,\n baseDir: options.baseDir,\n renderer: options.renderer as PptxRendererId | undefined,\n });\n const normalized = toGenerationWarnings(result.warnings);\n emitGenerationWarnings(normalized);\n options.warnings?.push(...normalized);\n return result.buffer;\n },\n hasPlugins: true,\n pluginNames,\n themeLabel,\n };\n }\n\n parseJson(input: string | object): unknown {\n return typeof input === 'string' ? JSON.parse(input) : input;\n }\n\n validateDocument(doc: unknown): { valid: boolean; errors?: any[] } {\n const result = validatePresentationDocument(doc);\n return {\n valid: result.valid,\n ...(result.errors.length > 0 && { errors: result.errors }),\n };\n }\n\n async validateDocumentWithPlugins(\n doc: unknown,\n plugins: any[]\n ): Promise<{ valid: boolean; errors?: any[] }> {\n const core = await import('@json-to-office/core-pptx');\n const parsed = typeof doc === 'string' ? JSON.parse(doc) : doc;\n const result = core.validatePresentation(parsed as any, plugins);\n return {\n valid: result.valid,\n ...(result.errors.length > 0 && { errors: result.errors }),\n };\n }\n\n async analyzeQuality(\n doc: unknown,\n options: GeneratorOptions = {}\n ): Promise<QualityAnalysis> {\n const core = await import('@json-to-office/core-pptx');\n const parsed = typeof doc === 'string' ? JSON.parse(doc) : doc;\n let prepared = preparedFor(\n options.prepared?.format === 'pptx'\n ? (options.prepared as ReturnType<\n typeof core.preparePptxQualityDocument\n >)\n : undefined,\n parsed\n );\n if (!prepared) {\n try {\n prepared = (await this.prepareModel(parsed, options, [])) as ReturnType<\n typeof core.preparePptxQualityDocument\n >;\n } catch {\n // Structural validation owns malformed trees. The core guards its own\n // preparation, so handing it the document instead of a model turns a\n // throw into an analysis that reports the failure.\n }\n }\n return core.analyzePptxQuality(prepared?.model.authored ?? parsed, {\n ...(prepared && { prepared }),\n renderer: options.renderer ?? documentRenderer(parsed),\n profile: options.quality?.profile,\n policy: options.quality?.policy,\n });\n }\n\n async prepareDocument(\n doc: unknown,\n options: GeneratorOptions = {}\n ): Promise<PreparedDocument> {\n // Preparation resolves the theme context the render then skips, so these\n // warnings reach the host from here or not at all.\n const warnings: any[] = [];\n const prepared = await this.prepareModel(doc, options, warnings);\n const normalized = toGenerationWarnings(warnings);\n emitGenerationWarnings(normalized);\n options.warnings?.push(...normalized);\n return prepared;\n }\n\n /** Prepare into a caller-owned sink; `prepareDocument` owns the reporting. */\n private async prepareModel(\n doc: unknown,\n options: GeneratorOptions,\n warnings: any[]\n ): Promise<PreparedDocument> {\n const core = await import('@json-to-office/core-pptx');\n const parsed = typeof doc === 'string' ? JSON.parse(doc) : doc;\n const resolved = await this.resolveThemes(options);\n const normalized = withRequestedTheme(\n parsed,\n resolved.requested,\n resolved.customThemes\n );\n const prepared = core.preparePptxQualityDocument(\n normalized.document as any,\n {\n customThemes: normalized.customThemes,\n fonts: options.fonts,\n services: buildServicesFromEnv(),\n renderer: options.renderer ?? documentRenderer(parsed),\n warnings,\n }\n );\n return stampPrepared(\n {\n ...prepared,\n metadata: {\n ...prepared.metadata,\n ...(resolved.label && { themeLabel: resolved.label }),\n },\n },\n parsed,\n toGenerationWarnings(warnings)\n );\n }\n\n generateSchema(_options?: any): any {\n return null;\n }\n\n getBuiltinThemes(): Record<string, any> {\n try {\n const core = require('@json-to-office/core-pptx');\n return core.pptxThemes || {};\n } catch {\n return {};\n }\n }\n\n async getBuiltinThemeValues(): Promise<Record<string, any>> {\n const core = await import('@json-to-office/core-pptx');\n return core.pptxThemes || {};\n }\n\n async resolveTheme(options: GeneratorOptions): Promise<any> {\n const core = await import('@json-to-office/core-pptx');\n const themes = (core as any).pptxThemes || {};\n const { requested } = await this.resolveThemes(options);\n return requested ?? themes.minimal ?? {};\n }\n\n /**\n * Resolve `theme`/`themePath` once for a whole run: `themePath` is read a\n * single time and feeds both the requested theme and the custom-theme\n * registry, so a bad path warns once instead of once per consumer.\n */\n private async resolveThemes(\n options: GeneratorOptions\n ): Promise<ResolvedThemes> {\n const core = await import('@json-to-office/core-pptx');\n const themes = (core as any).pptxThemes || {};\n // Themes passed directly from the client (playground UI) come first.\n const registry: Record<string, any> = { ...options.customThemes };\n\n if (typeof options.theme === 'object' && options.theme !== null) {\n registry[safeThemeKey(options.theme.name)] = options.theme;\n }\n\n let fileTheme: any | undefined;\n if (options.themePath) {\n try {\n if (options.themePath.endsWith('.json')) {\n const content = fs.readFileSync(\n path.resolve(process.cwd(), options.themePath),\n 'utf-8'\n );\n fileTheme = JSON.parse(content);\n } else {\n const themePath = path.resolve(process.cwd(), options.themePath);\n const themeModule = await import(themePath);\n fileTheme = themeModule.default || themeModule.theme;\n }\n } catch (error: any) {\n emitDiagnostic(\n `Failed to load theme from ${options.themePath}: ${error.message}`,\n 'warning'\n );\n }\n if (fileTheme) {\n registry[safeThemeKey(fileTheme.name)] = fileTheme;\n }\n }\n\n const customThemes =\n Object.keys(registry).length > 0 ? registry : undefined;\n\n if (fileTheme) {\n return { requested: fileTheme, customThemes, label: options.themePath };\n }\n\n if (typeof options.theme === 'string') {\n // Deliberately not getPptxTheme(): it answers every unknown name with\n // the default theme, which would silently swap a typo'd `--theme` in\n // over the document's own.\n const named =\n options.customThemes?.[options.theme] ?? themes[options.theme];\n if (named)\n return { requested: named, customThemes, label: options.theme };\n\n if (options.theme.endsWith('.json') && fs.existsSync(options.theme)) {\n try {\n const content = fs.readFileSync(\n path.resolve(process.cwd(), options.theme),\n 'utf-8'\n );\n return {\n requested: JSON.parse(content),\n customThemes,\n label: options.theme,\n };\n } catch {}\n }\n\n emitDiagnostic(\n `Unknown theme \"${options.theme}\"; keeping the document's own theme`,\n 'warning'\n );\n }\n\n if (typeof options.theme === 'object' && options.theme !== null) {\n return {\n requested: options.theme,\n customThemes,\n label: safeThemeKey(options.theme.name),\n };\n }\n\n return { requested: undefined, customThemes, label: undefined };\n }\n\n async loadCustomThemes(\n options: GeneratorOptions\n ): Promise<Record<string, any> | undefined> {\n return (await this.resolveThemes(options)).customThemes;\n }\n}\n\nexport function createAdapter(format: FormatName): FormatAdapter {\n switch (format) {\n case 'docx':\n return new DocxFormatAdapter();\n case 'pptx':\n return new PptxFormatAdapter();\n default:\n throw new Error(`Unknown format: ${format}`);\n }\n}\n","/**\n * PPTX rasterizer — the concrete service backing docx `visual` components.\n *\n * Pipeline: presentation JSON → (core-pptx) .pptx → (LibreOffice) PDF →\n * (poppler/pdftoppm) PNG. Returns a base64 data URI plus the natural pixel\n * dimensions. Results are content-addressed and cached on disk so repeated\n * builds of an unchanged visual skip the (multi-second) LibreOffice run.\n *\n * Single and batch rasterization share one engine (#153). A batch keeps one\n * .pptx per slide and converts them all in a single `soffice` launch — the\n * launch is the dominant cost, and per-file conversion keeps slides fully\n * independent: each has its own PDF and PNG (no page↔slide index mapping),\n * its own dpi, and a cache key identical to the single-slide path, so both\n * paths share the same disk cache.\n *\n * A request may carry `fonts`: base64 font faces that are staged for the\n * soffice launches (via the shared FontStager pipeline) so the slide renders\n * with the document's real families instead of whatever the host happens to\n * have installed. Those fonts are part of the disk-cache key — the same\n * slide is genuinely different pixels with and without them, and the cache\n * is shared process-wide across callers.\n *\n * Every engine run works against a wall-clock deadline (one batch-scaled\n * soffice window plus one pdftoppm window) so a wedged conversion fails the\n * remaining slides quickly instead of holding the caller — and its\n * concurrency slot — for minutes.\n *\n * This is injected via `services.pptx.render` / `services.pptx.renderBatch`;\n * the published engine packages never depend on these binaries.\n */\n\nimport { execFile } from 'node:child_process';\nimport { promises as fs } from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport crypto from 'node:crypto';\nimport {\n DEFAULT_VISUAL_DPI,\n type PptxRasterizer,\n type PptxBatchRasterizer,\n type PptxRasterizeRequest,\n type PptxRasterizeResult,\n type PptxRasterizeBatchSlideResult,\n type PptxRasterizeFailureStage,\n type RasterizeFontFace,\n} from '@json-to-office/shared';\nimport { fromRasterizeFontFaces } from '@json-to-office/shared/fonts/node';\nimport { getFontStager } from './font-staging/index.js';\nimport type { FontStageHandle } from './font-staging/index.js';\n\nconst SOFFICE_TIMEOUT_MS = 60000;\n/** Extra soffice budget per additional slide in a batch launch. */\nconst SOFFICE_BATCH_EXTRA_PER_SLIDE_MS = 15000;\n/** Hard ceiling for one batch soffice launch. */\nconst SOFFICE_BATCH_TIMEOUT_CAP_MS = 300000;\n/**\n * Max isolated single-file launches after a batch launch left PDFs missing.\n * Covers the realistic case (one poisoned slide crashed the batch; its\n * successors are fine) without letting a broken environment turn one request\n * into dozens of sequential 60s timeouts.\n */\nconst MAX_ISOLATED_RETRIES = 3;\nconst PDFTOPPM_TIMEOUT_MS = 30000;\nconst PROBE_TIMEOUT_MS = 5000;\nconst MAX_BUFFER = 64 * 1024 * 1024;\n\nfunction exec(\n binary: string,\n args: string[],\n timeoutMs: number,\n /**\n * Extra env for the child, merged over `process.env`. Carries a font\n * stager's `envOverrides` (FONTCONFIG_FILE / JTO_FONT_PATHS /\n * SAL_DISABLE_SKIA). Only the soffice CONVERSION launches get it: the\n * memoized `--version` probe must not have its resolution polluted, and\n * pdftoppm has no use for it.\n */\n env?: Record<string, string>\n): Promise<void> {\n return new Promise((resolve, reject) => {\n execFile(\n binary,\n args,\n {\n timeout: timeoutMs,\n maxBuffer: MAX_BUFFER,\n windowsHide: true,\n env: env ? { ...process.env, ...env } : process.env,\n },\n (error) => (error ? reject(error) : resolve())\n );\n });\n}\n\nasync function binaryWorks(binary: string): Promise<boolean> {\n if (binary.includes(path.sep)) {\n try {\n await fs.access(binary);\n } catch {\n return false;\n }\n }\n try {\n await exec(binary, ['--version'], PROBE_TIMEOUT_MS);\n return true;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n // A non-ENOENT failure still means the binary exists (e.g. bad flag).\n return code !== 'ENOENT' && code !== 'EACCES';\n }\n}\n\nfunction sofficeCandidates(): string[] {\n const candidates: string[] = [];\n const configured = process.env.LIBREOFFICE_PATH?.trim();\n if (configured) candidates.push(configured);\n if (process.platform === 'darwin') {\n candidates.push('/Applications/LibreOffice.app/Contents/MacOS/soffice');\n } else if (process.platform === 'win32') {\n candidates.push('C:\\\\Program Files\\\\LibreOffice\\\\program\\\\soffice.exe');\n candidates.push(\n 'C:\\\\Program Files (x86)\\\\LibreOffice\\\\program\\\\soffice.exe'\n );\n }\n candidates.push('soffice', 'libreoffice');\n return [...new Set(candidates)];\n}\n\nfunction pdftoppmCandidates(): string[] {\n const candidates: string[] = [];\n const configured = process.env.PDFTOPPM_PATH?.trim();\n if (configured) candidates.push(configured);\n candidates.push('pdftoppm');\n return [...new Set(candidates)];\n}\n\nasync function resolveBinary(\n candidates: string[],\n label: string,\n install: string\n): Promise<string> {\n for (const candidate of candidates) {\n if (await binaryWorks(candidate)) return candidate;\n }\n throw new Error(\n `Visual rasterization needs ${label}, which was not found. ${install} ` +\n `(searched: ${candidates.join(', ')}).`\n );\n}\n\n// Memoize resolved binary paths per process — they don't change at runtime, so\n// re-probing (spawning `--version` for every cache-miss rasterize) is wasted\n// work. A failed resolution is NOT cached, so a later call retries.\nlet sofficePromise: Promise<string> | undefined;\nlet pdftoppmPromise: Promise<string> | undefined;\nfunction resolveSoffice(): Promise<string> {\n if (!sofficePromise) {\n sofficePromise = resolveBinary(\n sofficeCandidates(),\n 'LibreOffice (soffice)',\n 'Install LibreOffice or set LIBREOFFICE_PATH.'\n ).catch((error) => {\n sofficePromise = undefined;\n throw error;\n });\n }\n return sofficePromise;\n}\nfunction resolvePdftoppm(): Promise<string> {\n if (!pdftoppmPromise) {\n pdftoppmPromise = resolveBinary(\n pdftoppmCandidates(),\n 'pdftoppm (poppler)',\n 'Install poppler-utils or set PDFTOPPM_PATH.'\n ).catch((error) => {\n pdftoppmPromise = undefined;\n throw error;\n });\n }\n return pdftoppmPromise;\n}\n\n/**\n * Process-wide rasterizer disk-cache counters (#156).\n */\nexport interface RasterizerCacheStats {\n /** Slides served from the content-addressed PNG disk cache. */\n diskHits: number;\n /** Unique slides that missed the disk cache and needed the engine. */\n diskMisses: number;\n /** diskHits / (diskHits + diskMisses), 0 when no lookups. */\n hitRate: number;\n /** Requests resolved by batch-internal dedupe (duplicate slides). */\n dedupedRequests: number;\n /** Slides successfully rendered by the engine (LibreOffice + pdftoppm). */\n rendered: number;\n /** Slides that failed at any engine stage. */\n failed: number;\n /** PNG files currently in the disk cache directories. */\n entries: number;\n /** Total bytes of those PNG files. */\n bytes: number;\n}\n\nconst rasterizerCounters = {\n diskHits: 0,\n diskMisses: 0,\n dedupedRequests: 0,\n rendered: 0,\n failed: 0,\n};\n\n/** Cache directories any engine run has used (for the disk scan). */\nconst knownCacheDirs = new Set<string>();\n\n/**\n * Get rasterizer cache statistics: process-lifetime counters plus a live\n * scan of the disk cache directories (default dir included, so entries from\n * previous processes are visible too).\n */\nexport async function getRasterizerCacheStats(): Promise<RasterizerCacheStats> {\n const dirs = new Set(knownCacheDirs);\n const defaultDir = resolveCacheDir();\n if (defaultDir) dirs.add(defaultDir);\n\n let entries = 0;\n let bytes = 0;\n for (const dir of dirs) {\n try {\n const files = await fs.readdir(dir);\n for (const file of files) {\n if (!file.endsWith('.png')) continue;\n try {\n const stat = await fs.stat(path.join(dir, file));\n entries++;\n bytes += stat.size;\n } catch {}\n }\n } catch {\n // Missing dir — nothing cached there.\n }\n }\n\n const lookups = rasterizerCounters.diskHits + rasterizerCounters.diskMisses;\n return {\n ...rasterizerCounters,\n hitRate: lookups > 0 ? rasterizerCounters.diskHits / lookups : 0,\n entries,\n bytes,\n };\n}\n\n/**\n * Delete every cached PNG in the known cache directories (default dir\n * included) and reset the counters. Backs \"Clear all caches\" (#156) — the\n * disk cache used to survive it.\n */\nexport async function clearRasterizerCache(): Promise<void> {\n const dirs = new Set(knownCacheDirs);\n const defaultDir = resolveCacheDir();\n if (defaultDir) dirs.add(defaultDir);\n\n for (const dir of dirs) {\n try {\n const files = await fs.readdir(dir);\n await Promise.all(\n files\n .filter((file) => file.endsWith('.png'))\n .map((file) => fs.rm(path.join(dir, file), { force: true }))\n );\n } catch {}\n }\n\n rasterizerCounters.diskHits = 0;\n rasterizerCounters.diskMisses = 0;\n rasterizerCounters.dedupedRequests = 0;\n rasterizerCounters.rendered = 0;\n rasterizerCounters.failed = 0;\n}\n\nconst PNG_SIGNATURE = Buffer.from([\n 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,\n]);\n\n/**\n * Validate a PNG buffer and read its width/height from the IHDR chunk. Returns\n * null for anything that isn't a complete PNG (empty/truncated/corrupt) so the\n * caller can re-render or error rather than emit a 0×0 / broken image. pdftoppm\n * always outputs PNG, so a PNG-only check is sufficient here.\n */\nfunction parsePngSize(png: Buffer): { width: number; height: number } | null {\n // 8-byte signature, then IHDR: length(4) + \"IHDR\"(4) + width(4) + height(4)\n if (png.length < 24) return null;\n if (!png.subarray(0, 8).equals(PNG_SIGNATURE)) return null;\n if (png.toString('ascii', 12, 16) !== 'IHDR') return null;\n const width = png.readUInt32BE(16);\n const height = png.readUInt32BE(20);\n if (width <= 0 || height <= 0) return null;\n return { width, height };\n}\n\nlet tmpCounter = 0;\n/**\n * Write the cache file atomically (temp file + rename) so a concurrent reader\n * never observes a half-written PNG. Best-effort: failures are swallowed (the\n * cache is an optimization), but a partial temp file is cleaned up.\n */\nasync function writeCacheAtomic(\n cacheDir: string,\n cachePath: string,\n png: Buffer\n): Promise<void> {\n const tmp = `${cachePath}.tmp-${process.pid}-${tmpCounter++}`;\n try {\n await fs.mkdir(cacheDir, { recursive: true });\n await fs.writeFile(tmp, png);\n await fs.rename(tmp, cachePath);\n } catch {\n await fs.rm(tmp, { force: true }).catch(() => {});\n }\n}\n\n/**\n * Identity of a font set for cache-keying. Order-insensitive (the per-face\n * digests are sorted) and content-addressed (hashes the decoded bytes, not\n * the base64 blob) so the same faces supplied in a different order, or\n * re-fetched to identical bytes, still hit the same cache entry.\n */\nexport function fontsDigest(\n fonts?: readonly RasterizeFontFace[]\n): string | undefined {\n if (!fonts || fonts.length === 0) return undefined;\n const parts = fonts\n .map(\n (f) =>\n `${f.family}|${f.weight}|${f.italic ? 'i' : 'r'}|` +\n crypto\n .createHash('sha256')\n // DECODED bytes, not the base64 text. `fromRasterizeFontFaces`\n // decodes before staging, and Node's base64 decoder is lenient:\n // missing padding, embedded newlines, and stray characters all\n // decode to the same bytes. Hashing the text would give two\n // spellings of one font two different cache keys — a silent\n // cache-miss multiplier on a shared, restart-surviving render\n // server cache.\n .update(Buffer.from(f.data, 'base64'))\n .digest('hex')\n )\n .sort();\n return crypto.createHash('sha256').update(parts.join('\\n')).digest('hex');\n}\n\nexport function cacheKey(request: {\n presentation: unknown;\n dpi: number;\n baseDir?: string;\n fontsKey?: string;\n}): string {\n return (\n crypto\n .createHash('sha256')\n // baseDir joins the key: the same relative asset path means different\n // pixels under different base directories (#142).\n //\n // fontsKey joins the key for the same reason: the same slide renders\n // DIFFERENT pixels with and without the document's fonts staged. This\n // cache is process-wide, shared by /rasterize and /rasterize/batch,\n // shared by every caller of the hosted render server, and it survives\n // restarts — keying without fonts would serve one document's\n // Inter-rendered PNG to a document that never asked for Inter.\n .update(\n JSON.stringify({\n p: request.presentation,\n dpi: request.dpi,\n base: request.baseDir,\n f: request.fontsKey ?? null,\n })\n )\n .digest('hex')\n );\n}\n\nfunction toDataUri(png: Buffer): string {\n return `data:image/png;base64,${png.toString('base64')}`;\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nasync function fileExists(filePath: string): Promise<boolean> {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Engine-internal result: the wire shape plus the original error object. */\ntype EngineSlideResult = PptxRasterizeBatchSlideResult & { cause?: unknown };\n\n/** One unique unit of work: a slide deck plus every request index it serves. */\ninterface SlideJob {\n presentation: unknown;\n dpi: number;\n /** Request indexes resolved by this job (batch-internal dedup). */\n indexes: number[];\n cachePath: string | null;\n pptxPath: string;\n pdfPath: string;\n pngPrefix: string;\n}\n\nconst sofficeArgs = (\n profileDir: string,\n outDir: string,\n files: string[]\n): string[] => [\n '--headless',\n '--norestore',\n '--nolockcheck',\n '--nodefault',\n `-env:UserInstallation=file://${profileDir.replace(/\\\\/g, '/')}`,\n '--convert-to',\n 'pdf:impress_pdf_Export',\n '--outdir',\n outDir,\n ...files,\n];\n\n/**\n * Rasterize N independent single-slide presentations, amortizing the soffice\n * launch across every cache miss. Returns per-slide results (index-aligned);\n * only environment-level failures (missing binaries) throw.\n */\nasync function rasterizeSlidesWithEngine(\n slides: Array<{ presentation: unknown; dpi: number }>,\n baseDir: string | undefined,\n cacheDir: string | null,\n fonts?: readonly RasterizeFontFace[]\n): Promise<EngineSlideResult[]> {\n const results: EngineSlideResult[] = new Array(slides.length);\n if (cacheDir) knownCacheDirs.add(cacheDir);\n // Request-level, so every slide in this run shares one font set — which is\n // exactly why it can join the per-slide cache key without being per-slide\n // data.\n const fontsKey = fontsDigest(fonts);\n\n // 1. Dedupe by content-addressed key: identical slides build, convert, and\n // hit the cache exactly once, then fan out to every requesting index.\n const jobsByKey = new Map<string, SlideJob>();\n for (let i = 0; i < slides.length; i++) {\n const slide = slides[i];\n const key = cacheKey({\n presentation: slide.presentation,\n dpi: slide.dpi,\n baseDir,\n fontsKey,\n });\n const existing = jobsByKey.get(key);\n if (existing) {\n existing.indexes.push(i);\n } else {\n jobsByKey.set(key, {\n presentation: slide.presentation,\n dpi: slide.dpi,\n indexes: [i],\n cachePath: cacheDir ? path.join(cacheDir, `${key}.png`) : null,\n pptxPath: '',\n pdfPath: '',\n pngPrefix: '',\n });\n }\n }\n\n rasterizerCounters.dedupedRequests += slides.length - jobsByKey.size;\n\n const fail = (\n job: SlideJob,\n stage: PptxRasterizeFailureStage,\n error: string,\n cause?: unknown\n ) => {\n rasterizerCounters.failed++;\n for (const i of job.indexes)\n results[i] = { ok: false, error, stage, cause };\n };\n const succeed = (job: SlideJob, result: PptxRasterizeResult) => {\n for (const i of job.indexes) results[i] = { ok: true, ...result };\n };\n\n // 2. Resolve cache hits for the unique jobs in parallel.\n const uncached: SlideJob[] = [];\n await Promise.all(\n [...jobsByKey.values()].map(async (job) => {\n if (job.cachePath) {\n const cached = await fs.readFile(job.cachePath).catch(() => null);\n if (cached) {\n const size = parsePngSize(cached);\n if (size) {\n rasterizerCounters.diskHits++;\n succeed(job, { base64DataUri: toDataUri(cached), ...size });\n return;\n }\n // Corrupt/partial cache file (e.g. a killed prior render) — discard\n // it and re-render rather than embed a broken image.\n await fs.rm(job.cachePath, { force: true }).catch(() => {});\n }\n rasterizerCounters.diskMisses++;\n }\n uncached.push(job);\n })\n );\n if (uncached.length === 0) return results;\n\n // 3. JSON → .pptx (in-process, pure JS). A build failure is a per-slide\n // content error; the remaining slides still convert.\n const corePptx = await import('@json-to-office/core-pptx');\n const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'jto-visual-'));\n // Declared out here so the `finally` can always close it, however the try\n // exits. Assigned only after we know a soffice launch is actually needed.\n let stageHandle: FontStageHandle | null = null;\n try {\n const built: SlideJob[] = [];\n for (let j = 0; j < uncached.length; j++) {\n const job = uncached[j];\n job.pptxPath = path.join(tempDir, `slide-${j}.pptx`);\n job.pdfPath = path.join(tempDir, `slide-${j}.pdf`);\n job.pngPrefix = path.join(tempDir, `slide-${j}`);\n try {\n const pptxBuffer = await corePptx.generateBufferFromJson(\n job.presentation as any,\n { baseDir }\n );\n await fs.writeFile(job.pptxPath, pptxBuffer);\n built.push(job);\n } catch (error) {\n fail(job, 'build', errorMessage(error), error);\n }\n }\n if (built.length === 0) return results;\n\n // Stage the request's fonts for the soffice launches. Deliberately AFTER\n // the cache probe and the build loop: a request whose slides all hit the\n // disk cache, or whose decks all failed to build, launches no soffice at\n // all and must not pay for writing N font files (and, on macOS, four\n // profile trees).\n //\n // profileDirs MUST enumerate the isolated-retry profiles too. They are\n // distinct UserInstallation directories, and the macOS stager's macro is\n // only reachable from a profile it was seeded into — miss them and a\n // mid-batch soffice crash yields fontless PNGs for the salvaged slides.\n const profileDirs = [\n path.join(tempDir, 'profile'),\n ...Array.from({ length: MAX_ISOLATED_RETRIES }, (_, r) =>\n path.join(tempDir, `profile-retry-${r}`)\n ),\n ];\n const stageFonts = fonts?.length ? fromRasterizeFontFaces(fonts) : [];\n if (stageFonts.length > 0) {\n stageHandle = await getFontStager().stage(stageFonts, tempDir, {\n profileDirs,\n });\n }\n const sofficeEnv = stageHandle?.envOverrides;\n\n const [soffice, pdftoppm] = await Promise.all([\n resolveSoffice(),\n resolvePdftoppm(),\n ]);\n\n // 4. .pptx → PDF: ONE soffice launch converts every deck (the launch is\n // the multi-second cost being amortized). The timeout scales with the\n // slide count so a large batch is not misread as a hang, and the whole\n // engine run gets a deadline of one batch window + one pdftoppm window\n // per slide — bounded work no matter how conversions misbehave. The\n // PNG phase is sequential over every slide, so its share must scale\n // with the slide count like the soffice window does; otherwise a slow\n // soffice launch drains the budget and slides whose PDFs converted\n // fine fail spuriously. An HTTP client that gives up sooner falls\n // back to per-visual calls on its own.\n const batchTimeoutMs = Math.min(\n SOFFICE_TIMEOUT_MS +\n SOFFICE_BATCH_EXTRA_PER_SLIDE_MS * (built.length - 1),\n SOFFICE_BATCH_TIMEOUT_CAP_MS\n );\n const deadlineAt =\n Date.now() + batchTimeoutMs + PDFTOPPM_TIMEOUT_MS * built.length;\n const remainingMs = () => deadlineAt - Date.now();\n\n let batchError: unknown;\n try {\n await exec(\n soffice,\n sofficeArgs(\n profileDirs[0],\n tempDir,\n built.map((job) => job.pptxPath)\n ),\n batchTimeoutMs,\n sofficeEnv\n );\n } catch (error) {\n batchError = error;\n }\n\n // 5. Per-slide PDF check. soffice reports batch conversion coarsely (it\n // can skip a file or die mid-run), so the PDFs on disk are the truth.\n // A missing PDF gets an isolated single-file launch — salvaging slides\n // left unprocessed by a mid-batch crash and pinning the error on the\n // slide that caused it — but only within MAX_ISOLATED_RETRIES and the\n // deadline, and not when nothing at all converted (an environmental\n // failure that a retry would only repeat).\n const converted: SlideJob[] = [];\n const missing: SlideJob[] = [];\n for (const job of built) {\n ((await fileExists(job.pdfPath)) ? converted : missing).push(job);\n }\n\n let retriesLeft =\n missing.length > 0 &&\n built.length > 1 &&\n (batchError === undefined || converted.length > 0)\n ? MAX_ISOLATED_RETRIES\n : 0;\n for (const [r, job] of missing.entries()) {\n const retryBudget = Math.min(SOFFICE_TIMEOUT_MS, remainingMs());\n if (retriesLeft > 0 && retryBudget > 1000) {\n retriesLeft--;\n let retryError: unknown;\n try {\n await exec(\n soffice,\n // Same directories seeded in `profileDirs` above (index 0 is the\n // batch profile, so retry `r` is `profileDirs[r + 1]`); the\n // literal is kept as the fallback for an out-of-range retry.\n sofficeArgs(\n profileDirs[r + 1] ?? path.join(tempDir, `profile-retry-${r}`),\n tempDir,\n [job.pptxPath]\n ),\n retryBudget,\n sofficeEnv\n );\n } catch (error) {\n retryError = error;\n }\n if (await fileExists(job.pdfPath)) {\n converted.push(job);\n continue;\n }\n batchError ??= retryError;\n }\n const cause = batchError;\n fail(\n job,\n 'convert',\n `LibreOffice failed to convert the slide to PDF${\n cause ? `: ${errorMessage(cause)}` : '.'\n }`,\n cause\n );\n }\n\n // 6. PDF → PNG at each slide's own dpi (single page, no page suffix).\n // pdftoppm is cheap per file; each conversion still respects the\n // engine deadline so a pathological PDF cannot stack 30s timeouts.\n for (const job of converted) {\n const budget = Math.min(PDFTOPPM_TIMEOUT_MS, remainingMs());\n if (budget <= 1000) {\n fail(\n job,\n 'rasterize',\n 'Rasterization deadline exceeded before this slide could be converted to PNG.'\n );\n continue;\n }\n try {\n await exec(\n pdftoppm,\n [\n '-r',\n String(job.dpi),\n '-png',\n '-singlefile',\n job.pdfPath,\n job.pngPrefix,\n ],\n budget\n );\n const png = await fs.readFile(`${job.pngPrefix}.png`);\n const size = parsePngSize(png);\n if (!size) {\n throw new Error(\n 'Rasterization produced an invalid PNG (empty or truncated output from pdftoppm).'\n );\n }\n if (job.cachePath) {\n await writeCacheAtomic(cacheDir!, job.cachePath, png);\n }\n rasterizerCounters.rendered++;\n succeed(job, { base64DataUri: toDataUri(png), ...size });\n } catch (error) {\n fail(job, 'rasterize', errorMessage(error), error);\n }\n }\n\n return results;\n } finally {\n // Order matters: FontconfigStager.cleanup() restores write permission on\n // the staged fonts dir (stage() freezes it to 0o555), and rm cannot\n // unlink inside a non-writable directory. Reversed, the whole temp tree\n // leaks — silently, because the rm error is swallowed.\n if (stageHandle) await stageHandle.cleanup().catch(() => {});\n await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});\n }\n}\n\nfunction resolveCacheDir(options?: {\n cacheDir?: string | null;\n}): string | null {\n return options?.cacheDir === undefined\n ? path.join(os.tmpdir(), 'jto-visual-cache')\n : options.cacheDir;\n}\n\n/**\n * Build a LibreOffice-backed pptx rasterizer.\n *\n * @param options.cacheDir - directory for the content-addressed PNG cache\n * (default: <tmp>/jto-visual-cache). Pass `null` to disable caching.\n */\nexport function createLibreOfficePptxRasterizer(options?: {\n cacheDir?: string | null;\n}): PptxRasterizer {\n const cacheDir = resolveCacheDir(options);\n\n return async function rasterize(\n request: PptxRasterizeRequest\n ): Promise<PptxRasterizeResult> {\n const [result] = await rasterizeSlidesWithEngine(\n [{ presentation: request.presentation, dpi: request.dpi }],\n request.baseDir,\n cacheDir,\n request.fonts\n );\n if (!result) throw new Error('Rasterization produced no result.');\n if (!result.ok) {\n const error = new Error(result.error) as Error & { cause?: unknown };\n // Preserve the original failure (exec exit code/signal, fs error) for\n // programmatic consumers — parity with the pre-batch implementation\n // that threw the underlying error directly.\n if (result.cause !== undefined) error.cause = result.cause;\n throw error;\n }\n return {\n base64DataUri: result.base64DataUri,\n width: result.width,\n height: result.height,\n };\n };\n}\n\n/**\n * Build a LibreOffice-backed BATCH pptx rasterizer (#153): many independent\n * single-slide presentations, one soffice launch, per-slide results. Shares\n * the content-addressed disk cache with the single-slide rasterizer — the\n * per-slide cache key is identical on both paths.\n */\nexport function createLibreOfficePptxBatchRasterizer(options?: {\n cacheDir?: string | null;\n}): PptxBatchRasterizer {\n const cacheDir = resolveCacheDir(options);\n\n return async function rasterizeBatch(request) {\n const engineResults = await rasterizeSlidesWithEngine(\n request.slides.map((slide) => ({\n presentation: slide.presentation,\n dpi: slide.dpi ?? DEFAULT_VISUAL_DPI,\n })),\n request.baseDir,\n cacheDir,\n request.fonts\n );\n // Strip the engine-internal `cause` (non-serializable) from the results.\n return {\n results: engineResults.map((result) =>\n result.ok\n ? result\n : { ok: false, error: result.error, stage: result.stage }\n ),\n };\n };\n}\n","import type { FontStager, FontStageHandle, FontStageOptions } from './types';\n\nexport class NoopFontStager implements FontStager {\n // Signature matches FontStager; every parameter (including `options`) is\n // deliberately ignored on platforms with no staging mechanism.\n async stage(\n _fonts?: unknown,\n _tempDir?: string,\n _options?: FontStageOptions\n ): Promise<FontStageHandle> {\n return {\n envOverrides: {},\n cleanup: async () => {},\n };\n }\n}\n","/**\n * Linux + macOS: use fontconfig to expose staged TTFs to LibreOffice.\n *\n * Writes each resolved font to `<tempDir>/fonts/` and a minimal\n * fontconfig.xml that includes that dir plus the system font config.\n * LibreOffice honors the per-invocation FONTCONFIG_FILE env var.\n *\n * The caller removes the whole tempDir in its own finally block; `cleanup()`\n * only has to undo the read-only freeze stage() puts on the fonts dir so\n * that recursive rm can actually unlink.\n */\n\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport type { ResolvedFont } from '@json-to-office/shared';\nimport {\n synthesizeFamilyName,\n rewriteFontFamilyName,\n} from '@json-to-office/shared';\nimport type { FontStager, FontStageHandle, FontStageOptions } from './types';\nimport { nextStagingId, safeFilenamePart } from './types';\n\nconst SYSTEM_FONTS_CONF_CANDIDATES = [\n '/etc/fonts/fonts.conf',\n '/opt/homebrew/etc/fonts/fonts.conf',\n '/usr/local/etc/fonts/fonts.conf',\n];\n\nexport class FontconfigStager implements FontStager {\n async stage(\n fonts: ResolvedFont[],\n tempDir: string,\n // Ignored: fontconfig discovery is driven by FONTCONFIG_FILE, not by the\n // soffice UserInstallation profile.\n _options?: FontStageOptions\n ): Promise<FontStageHandle> {\n const id = nextStagingId();\n const fontsDir = path.join(tempDir, 'fonts');\n await fs.mkdir(fontsDir, { recursive: true });\n\n let serial = 0;\n for (const r of fonts) {\n if (r.sources.length === 0) continue;\n for (const s of r.sources) {\n serial += 1;\n const suffix = s.italic ? 'i' : 'r';\n // Rewrite `name` table so fontconfig indexes the file under the\n // synthetic sub-family (\"Inter Light\"), matching the doc's\n // `rFonts`/`fontFace` references after synthesizeFamilyName.\n //\n // The unrewritten branch (RIBBI: the run rides bold/italic toggles\n // on the base family) leans on the bytes already declaring\n // `r.family`. That is FontRegistry's `stampResolvedFamily` — do not\n // read the skip as \"this file is fine by construction\".\n const synth = synthesizeFamilyName(r.family, s.weight, s.italic);\n const data =\n synth.family === r.family\n ? s.data\n : rewriteFontFamilyName(s.data, synth.family);\n const name = `${safeFilenamePart(synth.family)}-${s.weight}${suffix}-${id}-${serial}.ttf`;\n await fs.writeFile(path.join(fontsDir, name), data);\n }\n }\n\n // Freeze the fonts dir read-only after staging so a misbehaving\n // fontconfig run (or a concurrent soffice spawn) can't corrupt the\n // file contents we just wrote. fc-cache writes its own indexes into\n // `cacheDir` (next step), which stays writable. The whole tree gets\n // rm'd by the converter in finally, so mode matters only during the\n // conversion window.\n await fs.chmod(fontsDir, 0o555).catch(() => {\n // Some filesystems (e.g. certain Windows-mounted shares under WSL)\n // refuse chmod — ignore and proceed. The defensive-in-depth case\n // still wins on native Linux/macOS.\n });\n const includeLines = await this.pickSystemIncludes();\n // Redirect fontconfig's scan cache into tempDir. Without this, fontconfig\n // writes cache entries for `fontsDir` into the user's ~/.cache/fontconfig\n // and leaves them behind after tempDir is rm'd. Per-invocation isolation\n // also prevents two concurrent conversions from racing on the same\n // fontconfig cache directory.\n const cacheDir = path.join(tempDir, 'fc-cache');\n await fs.mkdir(cacheDir, { recursive: true });\n const configPath = path.join(tempDir, 'fontconfig.xml');\n const configXml = [\n '<?xml version=\"1.0\"?>',\n '<!DOCTYPE fontconfig SYSTEM \"fonts.dtd\">',\n '<fontconfig>',\n ` <dir>${escapeXml(fontsDir)}</dir>`,\n ` <cachedir>${escapeXml(cacheDir)}</cachedir>`,\n ...includeLines,\n '</fontconfig>',\n '',\n ].join('\\n');\n await fs.writeFile(configPath, configXml, 'utf8');\n\n return {\n envOverrides: {\n FONTCONFIG_FILE: configPath,\n XDG_CACHE_HOME: cacheDir,\n },\n cleanup: async () => {\n // Restore write permission before the caller's recursive rm. stage()\n // froze fontsDir to 0o555, and you cannot unlink entries inside a\n // non-writable directory: `fs.rm(tempDir, { recursive: true })` fails\n // with EACCES and leaks the whole temp tree (both callers swallow\n // that error, so it was silent). Removing the files themselves is\n // still the caller's job.\n await fs.chmod(fontsDir, 0o755).catch(() => {});\n },\n };\n }\n\n private async pickSystemIncludes(): Promise<string[]> {\n for (const candidate of SYSTEM_FONTS_CONF_CANDIDATES) {\n try {\n await fs.access(candidate);\n return [\n ` <include ignore_missing=\"yes\">${escapeXml(candidate)}</include>`,\n ];\n } catch {\n /* try next */\n }\n }\n // Fall back to the conventional path; fontconfig will fail softly.\n return [` <include ignore_missing=\"yes\">/etc/fonts/fonts.conf</include>`];\n }\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n","/**\n * Make resolved fonts visible to the LibreOffice child process for the\n * duration of one PDF conversion, then clean up.\n *\n * Linux/macOS: fontconfig + FONTCONFIG_FILE env var.\n * Windows: GDI session registration via koffi (AddFontResourceW).\n *\n * The caller calls `stage(fonts, tempDir)` before spawning soffice, merges\n * `envOverrides` into the child process env, waits for conversion, then\n * awaits `cleanup()` regardless of success or failure.\n *\n * Two consumers today: the playground's LibreOffice PDF-preview converter\n * (`@json-to-office/jto`) and the pptx rasterizer that backs docx `visual`\n * components (`../pptx-rasterizer.ts`).\n */\n\nimport type { ResolvedFont } from '@json-to-office/shared';\n\nexport interface FontStageHandle {\n /** Merged into the child process env. Empty object if nothing to stage. */\n envOverrides: Record<string, string>;\n /** Always call in a finally block. Safe to call multiple times (idempotent). */\n cleanup(): Promise<void>;\n}\n\nexport interface FontStageOptions {\n /**\n * UserInstallation profile directories the soffice launch(es) will use.\n * The macOS stager seeds its OnStartApp Python macro into EACH of them —\n * a launch with an unseeded profile registers no fonts and silently falls\n * back to system faces. Absent → `<tempDir>/user-profile`, the\n * LibreOfficeConverterService convention.\n */\n profileDirs?: string[];\n}\n\nexport interface FontStager {\n stage(\n fonts: ResolvedFont[],\n tempDir: string,\n options?: FontStageOptions\n ): Promise<FontStageHandle>;\n}\n\n/** Per-process monotonic counter to disambiguate concurrent conversions. */\nlet counter = 0;\nexport function nextStagingId(): string {\n counter += 1;\n return `${process.pid}-${counter}`;\n}\n\n/** Sanitize a font family for use in a filename. */\nexport function safeFilenamePart(s: string): string {\n return s.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 48);\n}\n","/**\n * Windows: register staged TTFs with GDI via AddFontResourceW so the soffice\n * child process finds them at startup. Forces LibreOffice onto the GDI\n * backend via SAL_DISABLE_SKIA=1 — Skia/DirectWrite doesn't reliably pick\n * up GDI-registered fonts on recent LO builds.\n *\n * Scope, precisely: `AddFontResourceW` adds to the **session** font table, not\n * a private per-process one. That is deliberate and unavoidable here — the\n * private variant (`AddFontResourceExW` with `FR_PRIVATE`) is visible only to\n * the registering process, and the process that has to see these fonts is the\n * `soffice` CHILD. Node stays alive for the full conversion so the fonts\n * persist until cleanup, and GDI releases them on process exit if Node\n * crashes, so nothing leaks past the process.\n *\n * KNOWN LIMITATION — concurrent conversions on one Windows host share that\n * session table. Two conversions staging different bytes under the same\n * synthesized family (say two documents that each embed their own \"Inter\")\n * register two faces claiming one name, and which one GDI hands to soffice is\n * then order-dependent. Staged FILES never collide — each carries a\n * pid-plus-counter suffix — so this is a resolution ambiguity, not corruption.\n *\n * Not fixed here because the only correct fix is a host-wide lease held from\n * stage() through cleanup(), which serializes every Windows conversion; that\n * is a real throughput cost for a risk the deployed images do not carry (both\n * production containers are Linux/fontconfig, where staging is per-process via\n * FONTCONFIG_FILE and cannot collide). It bites a Windows host running\n * concurrent conversions — worth a lease if that becomes a supported topology.\n */\n\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport type { ResolvedFont } from '@json-to-office/shared';\nimport {\n synthesizeFamilyName,\n rewriteFontFamilyName,\n} from '@json-to-office/shared';\nimport type { FontStager, FontStageHandle, FontStageOptions } from './types';\nimport { nextStagingId, safeFilenamePart } from './types';\n\ntype KoffiLib = {\n func: (sig: string) => (...args: unknown[]) => number | boolean;\n};\ntype KoffiModule = {\n load: (libName: string) => KoffiLib;\n};\n\n// Lazy-load koffi so Linux/macOS don't incur the FFI init cost.\nlet cachedBindings: {\n addFont: (pathW: string) => number;\n removeFont: (pathW: string) => boolean;\n} | null = null;\n\nasync function getGdiBindings() {\n if (cachedBindings) return cachedBindings;\n const koffi = (await import('koffi')) as unknown as {\n default?: KoffiModule;\n } & KoffiModule;\n const mod = koffi.default ?? koffi;\n const gdi32 = mod.load('gdi32.dll');\n cachedBindings = {\n addFont: gdi32.func('int __stdcall AddFontResourceW(str16)') as (\n path: string\n ) => number,\n removeFont: gdi32.func('bool __stdcall RemoveFontResourceW(str16)') as (\n path: string\n ) => boolean,\n };\n return cachedBindings;\n}\n\nexport class WindowsFontStager implements FontStager {\n async stage(\n fonts: ResolvedFont[],\n tempDir: string,\n // Ignored: GDI registration is session-wide, not scoped to any one\n // LibreOffice UserInstallation profile, so the retry profiles need no\n // separate seeding the way the macOS Core Text stager's do.\n _options?: FontStageOptions\n ): Promise<FontStageHandle> {\n const id = nextStagingId();\n const fontsDir = path.join(tempDir, 'fonts');\n await fs.mkdir(fontsDir, { recursive: true });\n\n const stagedPaths: string[] = [];\n let serial = 0;\n for (const r of fonts) {\n if (r.sources.length === 0) continue;\n for (const s of r.sources) {\n serial += 1;\n const suffix = s.italic ? 'i' : 'r';\n // Rewrite the TTF's internal family name so GDI registers the\n // file under the synthetic sub-family the doc references.\n //\n // The unrewritten branch (RIBBI: the run rides bold/italic toggles\n // on the base family) leans on the bytes already declaring\n // `r.family`. That is FontRegistry's `stampResolvedFamily` — do not\n // read the skip as \"this file is fine by construction\".\n const synth = synthesizeFamilyName(r.family, s.weight, s.italic);\n const data =\n synth.family === r.family\n ? s.data\n : rewriteFontFamilyName(s.data, synth.family);\n const name = `${safeFilenamePart(synth.family)}-${s.weight}${suffix}-${id}-${serial}.ttf`;\n const fullPath = path.join(fontsDir, name);\n await fs.writeFile(fullPath, data);\n stagedPaths.push(fullPath);\n }\n }\n\n if (stagedPaths.length === 0) {\n return { envOverrides: {}, cleanup: async () => {} };\n }\n\n const { addFont, removeFont } = await getGdiBindings();\n const registered: string[] = [];\n for (const p of stagedPaths) {\n const added = addFont(p);\n if (added > 0) registered.push(p);\n }\n\n let cleaned = false;\n return {\n envOverrides: {\n // Force GDI backend so the freshly-registered fonts are visible.\n // Skia on Windows uses DirectWrite which does not reliably see\n // fonts added via AddFontResourceW.\n SAL_DISABLE_SKIA: '1',\n },\n cleanup: async () => {\n if (cleaned) return;\n cleaned = true;\n for (const p of registered) {\n try {\n removeFont(p);\n } catch {\n // Swallow — GDI will drop it on process exit anyway.\n }\n }\n },\n };\n }\n}\n","/**\n * macOS: make staged fonts visible to the soffice child process by\n * registering them *inside* soffice via a Python macro bound to the\n * `OnStartApp` event.\n *\n * Why this works on macOS 26. Apple tightened `CTFontManagerScopeSession`\n * and `kCTFontManagerScopePersistent` to require signed+notarized callers\n * (unsigned Node processes get `paramErr -50`), and `Process` scope only\n * registers fonts for the calling process — so the Node server can't\n * register fonts the soffice child sees. But Process scope DOES work from\n * inside soffice. LibreOffice for macOS bundles Python 3.12 with `ctypes`,\n * and UNO lets us bind a Python macro to application-start events. We seed\n * a per-invocation UserInstallation profile with:\n *\n * - `user/Scripts/python/JtoFontRegister.py` — a ~20-line macro that\n * reads `JTO_FONT_PATHS` from the process env and calls\n * `CTFontManagerRegisterFontsForURL(url, kScopeProcess, NULL)` via\n * ctypes for each path (Process = 1 in Core Text's scope enum).\n * - `user/registrymodifications.xcu` — binds the macro to `OnStartApp`\n * and sets `MacroSecurityLevel=0` for this ephemeral profile only.\n *\n * SECURITY INVARIANT — macro execution scope. The seeded profile disables\n * soffice's macro-security prompt (`MacroSecurityLevel=0`) so our\n * OnStartApp macro runs without a dialog. This profile MUST ONLY be used\n * to open files this server just generated (i.e., well-formed outputs\n * from `@json-to-office/core-*`). Piping user-supplied .docx/.pptx/.odt\n * through a soffice invocation that uses this stager would execute any\n * embedded VBA/Basic macros silently — an RCE primitive. If a future\n * code path ever converts user-supplied documents (e.g. \"PDF-ify my\n * upload\"), it must build a separate converter that does NOT share this\n * profile, or call soffice with `--safe-mode` / a default profile.\n *\n * The pptx rasterizer (`./pptx-rasterizer.ts`) is the second consumer and\n * upholds the same invariant: it only ever opens .pptx files this process\n * just built from `@json-to-office/core-pptx` out of the request's own\n * presentation JSON — never a user-supplied binary document.\n *\n * PROFILE DIRS. The macro is only reachable from the UserInstallation\n * profile it was seeded into, so `options.profileDirs` must list EVERY\n * profile the caller will launch soffice with. The converter uses exactly\n * one (`<tempDir>/user-profile`, the default here); the rasterizer uses a\n * batch profile plus one per isolated retry, and a launch against an\n * unseeded profile registers nothing and silently falls back.\n *\n * Flow: the converter spawns\n * `soffice --headless -env:UserInstallation=file://<tempDir>/user-profile ...`\n * with env `{ JTO_FONT_PATHS: \"<ttf1>:<ttf2>:...\" }`. LO boots, reads our\n * seeded profile, fires `OnStartApp`, runs our macro, registers each\n * staged TTF at Process scope in its own process. Font enumeration then\n * resolves the synthetic family names (`Inter Bold`, `Source Code Pro\n * Medium`, …) against those Process-scope registrations. The PDF export\n * ships the correct glyphs.\n *\n * Elegant side-effects:\n * - Nothing outside the converter's `tempDir` is touched. The user's\n * real `~/Library/Fonts` and `~/Library/Application Support/LibreOffice`\n * stay untouched.\n * - Cleanup is the converter's `fs.rm(tempDir, …)` — no orphan sweep\n * needed. If Node crashes mid-conversion, the per-invocation tempDir\n * is reaped by macOS tmpreaper (or the OS's next boot cleanup).\n * - No DYLD injection, no notarized helper, no filesystem-scan races.\n *\n * Failure mode: if LibreOffice can't run the macro for any reason\n * (macro-security policy applied at bootstrap before our XCU loads,\n * Python not present in a minimal LO build, …) the soffice process still\n * runs and produces a PDF — just with system-fallback fonts, exactly the\n * pre-fix behavior. Non-catastrophic. Diagnose via stderr: the macro\n * writes failures to stderr via `print(..., file=sys.stderr)`.\n */\n\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport type { ResolvedFont } from '@json-to-office/shared';\nimport {\n synthesizeFamilyName,\n rewriteFontFamilyName,\n} from '@json-to-office/shared';\nimport type { FontStager, FontStageHandle, FontStageOptions } from './types';\nimport { nextStagingId, safeFilenamePart } from './types';\nimport { emitDiagnostic } from '../diagnostics.js';\n\n/**\n * Python macro source. Written verbatim into\n * `<profile>/user/Scripts/python/JtoFontRegister.py`. Kept inline (rather\n * than a separate .py asset) so this TypeScript module is self-contained\n * and survives bundler reshuffles — no runtime file-system lookup for a\n * static asset.\n */\nconst PYTHON_MACRO = `# Auto-generated by @json-to-office/jto. Runs inside soffice on OnStartApp\n# to make staged fonts visible to LibreOffice's font enumeration. macOS 26\n# blocks Session/Persistent CT registration for unsigned callers, but\n# Process scope still works from inside the target process — which is\n# exactly where this macro runs.\nimport os\nimport sys\nimport ctypes\nimport ctypes.util\n\n\ndef _log(msg):\n # soffice swallows Python stdout in headless mode; stderr surfaces to\n # the parent's pipe. The stager doesn't read this today but the\n # converter logs stderr on failure, which is how we'll debug.\n sys.stderr.write(\"[jto-font-register] \" + msg + \"\\\\n\")\n\n\ndef register(*_args):\n paths_env = os.environ.get(\"JTO_FONT_PATHS\", \"\")\n if not paths_env:\n return\n try:\n cf = ctypes.CDLL(ctypes.util.find_library(\"CoreFoundation\"))\n ct = ctypes.CDLL(ctypes.util.find_library(\"CoreText\"))\n except Exception as e:\n _log(\"failed to load CoreFoundation/CoreText: \" + repr(e))\n return\n cf.CFURLCreateFromFileSystemRepresentation.argtypes = [\n ctypes.c_void_p, ctypes.c_char_p, ctypes.c_long, ctypes.c_bool,\n ]\n cf.CFURLCreateFromFileSystemRepresentation.restype = ctypes.c_void_p\n cf.CFRelease.argtypes = [ctypes.c_void_p]\n ct.CTFontManagerRegisterFontsForURL.argtypes = [\n ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p,\n ]\n ct.CTFontManagerRegisterFontsForURL.restype = ctypes.c_bool\n\n kCTFontManagerScopeProcess = 1\n registered = 0\n for p in paths_env.split(os.pathsep):\n if not p:\n continue\n try:\n b = p.encode(\"utf-8\")\n url = cf.CFURLCreateFromFileSystemRepresentation(\n None, b, len(b), False\n )\n if not url:\n _log(\"CFURL failed for \" + p)\n continue\n ok = ct.CTFontManagerRegisterFontsForURL(\n url, kCTFontManagerScopeProcess, None\n )\n cf.CFRelease(url)\n if ok:\n registered += 1\n else:\n _log(\"CT register returned false for \" + p)\n except Exception as e:\n _log(\"exception registering \" + p + \": \" + repr(e))\n _log(\"registered \" + str(registered) + \" font(s) at Process scope\")\n\n\n# Expose under \"register\" (event-binding URL) and module-level run so\n# command-line vnd.sun.star.script invocation works either way.\ng_exportedScripts = (register,)\n`;\n\n/**\n * XCU that LO merges into its Bootstrap registry on startup. Two keys:\n *\n * 1. `/org.openoffice.Office.Events/ApplicationEvents/Bindings/OnStartApp`\n * → fires our Python macro before any document is loaded, so font\n * registration completes before LO enumerates fonts for rendering.\n *\n * 2. `/org.openoffice.Office.Common/Security/Scripting/MacroSecurityLevel`\n * → 0 (allow all). This only applies to the ephemeral UserInstallation\n * profile we create under `<tempDir>/user-profile`; the user's real\n * LibreOffice profile config is untouched. Scope of the security\n * relaxation is bounded by the per-invocation profile's lifetime.\n */\nconst REGISTRY_MOD_XCU = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<oor:items xmlns:oor=\"http://openoffice.org/2001/registry\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">\n <item oor:path=\"/org.openoffice.Office.Events/ApplicationEvents/Bindings\">\n <node oor:name=\"OnStartApp\" oor:op=\"replace\">\n <prop oor:name=\"BindingURL\" oor:type=\"xs:string\">\n <value>vnd.sun.star.script:JtoFontRegister.py$register?language=Python&location=user</value>\n </prop>\n </node>\n </item>\n <item oor:path=\"/org.openoffice.Office.Common/Security/Scripting\">\n <prop oor:name=\"MacroSecurityLevel\" oor:op=\"fuse\">\n <value>0</value>\n </prop>\n </item>\n</oor:items>\n`;\n\nexport class MacOSCoreTextStager implements FontStager {\n async stage(\n fonts: ResolvedFont[],\n tempDir: string,\n options?: FontStageOptions\n ): Promise<FontStageHandle> {\n const embeddable = fonts.filter((r) => r.sources.length > 0);\n if (embeddable.length === 0) {\n return { envOverrides: {}, cleanup: async () => {} };\n }\n\n // Fonts go to a subdir of the converter's tempDir. No ~/Library writes.\n const fontsDir = path.join(tempDir, 'fonts');\n await fs.mkdir(fontsDir, { recursive: true });\n\n const id = nextStagingId();\n const fontPaths: string[] = [];\n const staged: {\n family: string;\n weight: number;\n italic: boolean;\n path: string;\n }[] = [];\n let serial = 0;\n for (const r of embeddable) {\n for (const s of r.sources) {\n serial += 1;\n const suffix = s.italic ? 'i' : 'r';\n // Rewrite the TTF's internal family name to match the synthetic\n // sub-family the doc references (e.g. \"Inter Light\"). Without\n // this, Core Text indexes the staged file as \"Inter\" and\n // LibreOffice can't resolve `rFonts w:ascii=\"Inter Light\"`.\n //\n // The unrewritten branch (RIBBI: the run rides bold/italic toggles\n // on the base family) leans on the bytes already declaring\n // `r.family`. That is FontRegistry's `stampResolvedFamily` — do not\n // read the skip as \"this file is fine by construction\".\n const synth = synthesizeFamilyName(r.family, s.weight, s.italic);\n const data =\n synth.family === r.family\n ? s.data\n : rewriteFontFamilyName(s.data, synth.family);\n const name = `${safeFilenamePart(synth.family)}-${s.weight}${suffix}-${id}-${serial}.ttf`;\n const full = path.join(fontsDir, name);\n await fs.writeFile(full, data);\n fontPaths.push(full);\n staged.push({\n family: synth.family,\n weight: s.weight,\n italic: s.italic,\n path: full,\n });\n }\n }\n\n if (process.env.JTO_DEBUG_FONTS === '1') {\n // Through the sink, never a stream: a host may own stdout as a\n // protocol channel (the MCP server does), so this package writes\n // nowhere on its own. Install a sink to see it.\n emitDiagnostic(\n '[jto macos-stager] staged ' +\n staged.length +\n ' font(s) for CT Process-scope registration; JTO_FONT_PATHS has ' +\n fontPaths.length +\n ' entries\\n' +\n staged\n .map(\n (s) =>\n ` ${s.family} (w=${s.weight}${s.italic ? ' italic' : ''}) → ${path.basename(s.path)}`\n )\n .join('\\n')\n );\n }\n\n // Seed EVERY per-invocation UserInstallation profile the caller will\n // launch with, so LO reads the macro + event binding from the exact\n // path its `-env:UserInstallation=` points at. The converter passes\n // nothing and gets `<tempDir>/user-profile`; the rasterizer passes its\n // batch profile plus every isolated-retry profile.\n const profileDirs = options?.profileDirs?.length\n ? options.profileDirs\n : [path.join(tempDir, 'user-profile')];\n for (const profileDir of profileDirs) {\n const profileUser = path.join(profileDir, 'user');\n const scriptsDir = path.join(profileUser, 'Scripts', 'python');\n await fs.mkdir(scriptsDir, { recursive: true });\n await fs.writeFile(\n path.join(scriptsDir, 'JtoFontRegister.py'),\n PYTHON_MACRO\n );\n await fs.writeFile(\n path.join(profileUser, 'registrymodifications.xcu'),\n REGISTRY_MOD_XCU\n );\n }\n\n return {\n envOverrides: {\n // Colon-separated list of staged TTF paths (`:` matches Python's\n // `os.pathsep` on macOS). The macro reads this at OnStartApp.\n JTO_FONT_PATHS: fontPaths.join(':'),\n // Force LibreOffice's Core Graphics backend. Skia on macOS can\n // skip Core Text's freshly-registered fonts in some builds.\n SAL_DISABLE_SKIA: '1',\n },\n cleanup: async () => {\n // No-op: the converter's `fs.rm(tempDir, { recursive: true })` in\n // its own finally block sweeps everything we wrote here.\n },\n };\n }\n}\n","import { AsyncLocalStorage } from 'node:async_hooks';\n\nexport type DiagnosticTone =\n | 'default'\n | 'info'\n | 'success'\n | 'warning'\n | 'error'\n | 'muted';\n\nexport type DiagnosticSink = (text: string, tone?: DiagnosticTone) => void;\n\nconst sinks = new AsyncLocalStorage<DiagnosticSink>();\n\n/**\n * Scope a sink to one operation. The CLI installs an Ink-backed one per\n * task; the MCP server installs one that collects into its response.\n */\nexport function runWithDiagnosticSink<T>(\n sink: DiagnosticSink,\n callback: () => T\n): T {\n return sinks.run(sink, callback);\n}\n\n/**\n * This package writes to no stream of its own — a host may own stdout as a\n * protocol channel — so with no sink installed the message is dropped.\n */\nexport function emitDiagnostic(\n text: string,\n tone: DiagnosticTone = 'muted'\n): void {\n sinks.getStore()?.(text, tone);\n}\n\n/**\n * Plain-text sink for hosts with no UI of their own. stderr, not stdout,\n * so it stays safe to install alongside a stdout protocol stream.\n */\nexport const stderrDiagnosticSink: DiagnosticSink = (text) => {\n process.stderr.write(`${text}\\n`);\n};\n","/**\n * Factory entry point for the font staging pipeline shared by every\n * LibreOffice launch in the toolchain: the playground's PDF preview\n * converter (`@json-to-office/jto`) and the pptx rasterizer that backs docx\n * `visual` components (`../pptx-rasterizer.ts`).\n */\n\nimport type { FontStager } from './types';\nimport { NoopFontStager } from './noop-stager';\nimport { FontconfigStager } from './fontconfig-stager';\nimport { WindowsFontStager } from './windows-stager';\nimport { MacOSCoreTextStager } from './macos-stager';\n\nexport type { FontStager, FontStageHandle, FontStageOptions } from './types';\n\nconst cached = new Map<NodeJS.Platform, FontStager>();\n\nexport function getFontStager(\n platform: NodeJS.Platform = process.platform\n): FontStager {\n const hit = cached.get(platform);\n if (hit) return hit;\n let stager: FontStager;\n switch (platform) {\n case 'win32':\n stager = new WindowsFontStager();\n break;\n case 'darwin':\n // LibreOffice-for-macOS uses Core Text for font enumeration and does\n // not honor FONTCONFIG_FILE reliably. macOS 26 further blocks\n // Session/Persistent CT registration for unsigned callers, and\n // Process scope only works inside the target process. We register\n // fonts from *inside* soffice via a Python UNO macro bound to\n // OnStartApp, seeded into a per-invocation UserInstallation profile.\n // The user's ~/Library/Fonts is never touched. See macos-stager.ts.\n stager = new MacOSCoreTextStager();\n break;\n case 'linux':\n case 'freebsd':\n case 'openbsd':\n stager = new FontconfigStager();\n break;\n default:\n stager = new NoopFontStager();\n }\n cached.set(platform, stager);\n return stager;\n}\n\nexport {\n NoopFontStager,\n FontconfigStager,\n WindowsFontStager,\n MacOSCoreTextStager,\n};\n","/**\n * Text geometry from a rendered PDF — the ground truth the quality\n * estimators are guessing at (#216 follow-up).\n *\n * The pptx preview pipeline already produces a PDF (soffice → pdftoppm) and\n * uses it purely as a bitmap source. That PDF records the exact position of\n * every glyph as laid out by LibreOffice — the same engine the quality rules\n * try to predict. `pdftotext -bbox` (poppler, already a rasterizer\n * dependency alongside pdftoppm) dumps per-word bounding boxes; this module\n * parses them into slide-space points so callers can compare a rule's\n * estimate against what the renderer actually did.\n *\n * Coordinates: PDF points (1/72 in), origin at the page's top-left corner,\n * y increasing downward — the same frame as authored inches × 72. A PDF page\n * rendered from a slide has the slide's dimensions, so word boxes compare\n * directly against authored shape geometry with no transform.\n *\n * Consumers: the quality ground-truth harness (estimator calibration) today;\n * a `rendered`-certainty analysis pass tomorrow.\n */\n\nimport { execFile } from 'child_process';\nimport * as path from 'path';\n\n/** One word as laid out on the page, in PDF points, top-left origin. */\nexport interface PdfTextWord {\n text: string;\n xMin: number;\n yMin: number;\n xMax: number;\n yMax: number;\n}\n\n/** One PDF page: its size in points plus every word poppler segmented. */\nexport interface PdfTextPage {\n widthPt: number;\n heightPt: number;\n words: PdfTextWord[];\n}\n\nconst ENTITIES: Readonly<Record<string, string>> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '"': '\"',\n ''': \"'\",\n '"': '\"',\n ''': \"'\",\n};\n\nfunction decodeEntities(value: string): string {\n return value.replace(\n /&(?:amp|lt|gt|quot|apos|#34|#39);/g,\n (entity) => ENTITIES[entity] ?? entity\n );\n}\n\nconst PAGE_PATTERN =\n /<page\\s+width=\"([\\d.]+)\"\\s+height=\"([\\d.]+)\">([\\s\\S]*?)<\\/page>/g;\nconst WORD_PATTERN =\n /<word\\s+xMin=\"(-?[\\d.]+)\"\\s+yMin=\"(-?[\\d.]+)\"\\s+xMax=\"(-?[\\d.]+)\"\\s+yMax=\"(-?[\\d.]+)\">([\\s\\S]*?)<\\/word>/g;\n\n/**\n * Parse `pdftotext -bbox` output (XHTML with `<page>`/`<word>` elements).\n * Pure — feed it a captured document for tests, or the runner's stdout.\n */\nexport function parsePdfTextBbox(bboxXml: string): PdfTextPage[] {\n const pages: PdfTextPage[] = [];\n for (const pageMatch of bboxXml.matchAll(PAGE_PATTERN)) {\n const words: PdfTextWord[] = [];\n for (const wordMatch of pageMatch[3].matchAll(WORD_PATTERN)) {\n words.push({\n xMin: Number(wordMatch[1]),\n yMin: Number(wordMatch[2]),\n xMax: Number(wordMatch[3]),\n yMax: Number(wordMatch[4]),\n text: decodeEntities(wordMatch[5]),\n });\n }\n pages.push({\n widthPt: Number(pageMatch[1]),\n heightPt: Number(pageMatch[2]),\n words,\n });\n }\n return pages;\n}\n\nfunction pdftotextCandidates(): string[] {\n const candidates: string[] = [];\n const configured = process.env.PDFTOTEXT_PATH?.trim();\n if (configured) candidates.push(configured);\n candidates.push('pdftotext');\n return [...new Set(candidates)];\n}\n\nasync function run(\n binary: string,\n args: string[],\n timeoutMs: number\n): Promise<string> {\n return new Promise((resolve, reject) => {\n execFile(\n binary,\n args,\n { timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 },\n (error, stdout) => {\n if (error) reject(error);\n else resolve(stdout);\n }\n );\n });\n}\n\n// Same memoization shape as the rasterizer's soffice/pdftoppm resolution:\n// success is cached per process, failure retries on the next call.\nlet pdftotextPromise: Promise<string> | undefined;\nasync function resolvePdftotext(): Promise<string> {\n if (!pdftotextPromise) {\n pdftotextPromise = (async () => {\n for (const candidate of pdftotextCandidates()) {\n if (candidate.includes(path.sep)) {\n try {\n await run(candidate, ['-v'], 10_000);\n return candidate;\n } catch {\n continue;\n }\n }\n try {\n await run(candidate, ['-v'], 10_000);\n return candidate;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n // pdftotext -v exits 0 on modern poppler; a non-ENOENT failure\n // still means the binary exists.\n if (code !== 'ENOENT' && code !== 'EACCES') return candidate;\n }\n }\n throw new Error(\n 'Text geometry extraction needs pdftotext (poppler), which was not ' +\n 'found. Install poppler-utils or set PDFTOTEXT_PATH ' +\n `(searched: ${pdftotextCandidates().join(', ')}).`\n );\n })().catch((error) => {\n pdftotextPromise = undefined;\n throw error;\n });\n }\n return pdftotextPromise;\n}\n\n/** True when a `pdftotext` binary is reachable — lets harnesses skip early. */\nexport async function pdftotextAvailable(): Promise<boolean> {\n try {\n await resolvePdftotext();\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Extract per-word text geometry from a PDF on disk. One pdftotext spawn,\n * output streamed through stdout — nothing else touches the filesystem.\n */\nexport async function extractPdfTextGeometry(\n pdfPath: string,\n options: { timeoutMs?: number } = {}\n): Promise<PdfTextPage[]> {\n const binary = await resolvePdftotext();\n const stdout = await run(\n binary,\n ['-bbox', pdfPath, '-'],\n options.timeoutMs ?? 60_000\n );\n return parsePdfTextBbox(stdout);\n}\n"],"mappings":"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","names":["path","fs","fs","path","fs","path","synthesizeFamilyName","rewriteFontFamilyName","path","fs","synthesizeFamilyName","rewriteFontFamilyName","fs","path","synthesizeFamilyName","rewriteFontFamilyName","path","fs","synthesizeFamilyName","rewriteFontFamilyName","resolve","path","fs","cached","execFile","path","resolve"]}
|
|
1
|
+
{"version":3,"sources":["../src/format-adapter.ts","../src/pptx-rasterizer.ts","../src/font-staging/noop-stager.ts","../src/font-staging/fontconfig-stager.ts","../src/font-staging/types.ts","../src/font-staging/windows-stager.ts","../src/font-staging/macos-stager.ts","../src/diagnostics.ts","../src/font-staging/index.ts","../src/pdf-text-geometry.ts"],"sourcesContent":["import * as path from 'path';\nimport * as fs from 'fs';\n\nimport type {\n ServicesConfig,\n FontRuntimeOpts,\n PptxRasterizer,\n PptxBatchRasterizer,\n GenerationWarning,\n RendererStatus,\n} from '@json-to-office/shared';\nimport type {\n PreparedDocument,\n QualityAnalysis,\n QualityPolicy,\n QualityProfile,\n} from '@json-to-office/quality';\nimport { validatePresentationDocument } from '@json-to-office/shared-pptx';\nimport { validate as validateDocx } from '@json-to-office/shared-docx';\nimport {\n createLibreOfficePptxRasterizer,\n createLibreOfficePptxBatchRasterizer,\n} from './pptx-rasterizer.js';\nimport { emitDiagnostic } from './diagnostics.js';\n\n/** Forward structured warnings collected during generation to the host's sink. */\nfunction emitGenerationWarnings(warnings: GenerationWarning[]): void {\n for (const warning of warnings) {\n emitDiagnostic(\n `${warning.component}: ${warning.message}`,\n warning.severity === 'info' ? 'info' : 'warning'\n );\n }\n}\n\n/**\n * Normalize a core's warning array into the single client/CLI-facing shape.\n *\n * DOCX cores already emit `GenerationWarning`; PPTX cores emit\n * `PipelineWarning = {code, message, component?, slide?}` — no `severity` and\n * an optional `component`. Left raw, a PipelineWarning renders as an empty\n * component chip in the playground's WarningsPanel, so both shapes funnel\n * through here and come out self-describing.\n */\nfunction toGenerationWarnings(\n raw: readonly any[] | null | undefined\n): GenerationWarning[] {\n return (raw ?? []).map((w) => ({\n component: w?.component ?? 'pptx',\n message: String(w?.message ?? ''),\n severity: (w?.severity === 'info' ? 'info' : 'warning') as\n | 'warning'\n | 'info',\n context: {\n ...(w?.context && typeof w.context === 'object' ? w.context : {}),\n ...(w?.code !== undefined && { code: w.code }),\n ...(w?.slide !== undefined && { slide: w.slide }),\n },\n }));\n}\n\nfunction preparedThemeLabel(prepared: PreparedDocument): string | undefined {\n const label = prepared.metadata?.themeLabel;\n return typeof label === 'string' ? label : undefined;\n}\n\n/**\n * The document a prepared model was built from, and the warnings its\n * preparation already reported.\n *\n * Symbols, so neither crosses a serialization boundary: `PreparedDocument` is\n * a serializable contract, and a model that arrives without its source is\n * re-prepared rather than rendered in place of the document it was handed.\n */\nconst PREPARED_SOURCE = Symbol('jto.prepared.source');\nconst PREPARED_WARNINGS = Symbol('jto.prepared.warnings');\n\ninterface PreparedInternals {\n [PREPARED_SOURCE]?: unknown;\n [PREPARED_WARNINGS]?: readonly GenerationWarning[];\n}\n\n/** Bind a prepared model to the document and the warnings it came from. */\nfunction stampPrepared<T extends PreparedDocument>(\n prepared: T,\n source: unknown,\n warnings: readonly GenerationWarning[]\n): T {\n return Object.assign(prepared, {\n [PREPARED_SOURCE]: source,\n [PREPARED_WARNINGS]: warnings,\n });\n}\n\n/**\n * A prepared model, but only for the document it was prepared from.\n *\n * `generateBuffer(document)` is a per-document API while `prepared` is fixed\n * when the generator is built, so any other document would be validated as\n * itself and then rendered as the prepared one.\n */\nfunction preparedFor<T extends PreparedDocument>(\n prepared: T | undefined,\n document: unknown\n): T | undefined {\n if (!prepared) return undefined;\n const source = (prepared as PreparedInternals)[PREPARED_SOURCE];\n return source !== undefined && source === document ? prepared : undefined;\n}\n\n/** Warning identity, for the prepare/render overlap only. */\nfunction warningKey(warning: GenerationWarning): string {\n return `${warning.component}|${warning.context?.code ?? ''}|${warning.message}`;\n}\n\n/**\n * Drop render warnings this model's preparation already reported.\n *\n * Preparation repeats work the render does again — pptx block, layout and grid\n * resolution above all — so reusing a prepared model would report those\n * warnings twice. Deliberately narrow: nothing is deduplicated except against\n * what preparing this very model emitted.\n */\nfunction withoutPreparedWarnings(\n warnings: GenerationWarning[],\n prepared: PreparedDocument | undefined\n): GenerationWarning[] {\n const emitted = (prepared as PreparedInternals | undefined)?.[\n PREPARED_WARNINGS\n ];\n if (!emitted || emitted.length === 0) return warnings;\n const seen = new Set(emitted.map(warningKey));\n return warnings.filter((warning) => !seen.has(warningKey(warning)));\n}\n\n/**\n * The renderer the document itself names. Core generation resolves\n * `options.renderer ?? document.renderer`, so preparation that reads only the\n * option stamps the default onto the model and misjudges a profile targeted\n * at the backend the document actually renders with.\n */\nfunction documentRenderer(document: unknown): string | undefined {\n const renderer = (document as { renderer?: unknown } | null | undefined)\n ?.renderer;\n return typeof renderer === 'string' ? renderer : undefined;\n}\n\nconst UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\nfunction safeThemeKey(name: string | undefined): string {\n return name && !UNSAFE_KEYS.has(name) ? name : 'custom';\n}\n\n/** Key the theme named by `--theme`/`--theme-path` is registered under. */\nconst CLI_THEME_KEY = 'jto-cli-theme';\n\n/**\n * Point a document at the explicitly requested theme. The JSON path selects a\n * theme by name off `props.theme`, so `--theme`/`--theme-path` is applied by\n * registering the resolved theme under a reserved key and rewriting the\n * reference — an explicit theme wins over the document's own `props.theme`.\n * With no theme requested the document is passed through untouched.\n */\nfunction withRequestedTheme(\n document: any,\n theme: any | undefined,\n customThemes: Record<string, any> | undefined\n): { document: any; customThemes: Record<string, any> | undefined } {\n if (!theme || typeof document !== 'object' || document === null) {\n return { document, customThemes };\n }\n return {\n document: {\n ...document,\n props: { ...document.props, theme: CLI_THEME_KEY },\n },\n customThemes: { ...customThemes, [CLI_THEME_KEY]: theme },\n };\n}\n\nexport type FormatName = 'docx' | 'pptx';\n\nfunction buildServicesFromEnv(): ServicesConfig | undefined {\n const serverUrl = process.env.HIGHCHARTS_SERVER_URL;\n const apiKey = process.env.HIGHCHARTS_API_KEY;\n const apiKeyHeader = process.env.HIGHCHARTS_API_KEY_HEADER ?? 'x-api-key';\n\n if (!serverUrl && !apiKey) return undefined;\n\n return {\n highcharts: {\n serverUrl,\n ...(apiKey && { headers: { [apiKeyHeader]: apiKey } }),\n },\n };\n}\n\n// Lazily-constructed LibreOffice rasterizers, shared across docx generations.\n// Constructing them is cheap (no binaries touched); they only spawn\n// LibreOffice when a document actually contains a `visual` component. Single\n// and batch share the same content-addressed disk cache.\nlet cachedRasterizer: PptxRasterizer | undefined;\nfunction getPptxRasterizer(): PptxRasterizer {\n if (!cachedRasterizer) {\n cachedRasterizer = createLibreOfficePptxRasterizer();\n }\n return cachedRasterizer;\n}\nlet cachedBatchRasterizer: PptxBatchRasterizer | undefined;\nfunction getPptxBatchRasterizer(): PptxBatchRasterizer {\n if (!cachedBatchRasterizer) {\n cachedBatchRasterizer = createLibreOfficePptxBatchRasterizer();\n }\n return cachedBatchRasterizer;\n}\n\n/**\n * Services for docx generation: highcharts (from env) plus the pptx rasterizer\n * that backs `visual` components. An explicit HIGHCHARTS-style override is not\n * needed for pptx — a running rasterization server can be pointed at via\n * `services.pptx.serverUrl`, but the default is the in-process LibreOffice\n * renderer.\n */\nfunction buildDocxServices(): ServicesConfig {\n const base = buildServicesFromEnv() ?? {};\n const serverUrl = process.env.JTO_PPTX_RASTERIZER_URL?.trim();\n const apiKey =\n process.env.JTO_PPTX_RASTERIZER_API_KEY || process.env.HIGHCHARTS_API_KEY;\n const apiKeyHeader =\n process.env.JTO_PPTX_RASTERIZER_API_KEY_HEADER ||\n process.env.HIGHCHARTS_API_KEY_HEADER ||\n 'x-api-key';\n return {\n ...base,\n pptx: serverUrl\n ? {\n serverUrl,\n ...(apiKey && { headers: { [apiKeyHeader]: apiKey } }),\n }\n : {\n render: getPptxRasterizer(),\n renderBatch: getPptxBatchRasterizer(),\n },\n };\n}\n\n/** Minimal builder shape shared by DOCX and PPTX generators */\ninterface GeneratorBuilder {\n addComponent(component: any): GeneratorBuilder;\n validate(document: any): {\n valid: boolean;\n errors?: { path: string; message: string }[];\n };\n generateBuffer(\n document: any,\n options?: {\n deterministic?: boolean;\n generatedAt?: string | Date;\n validation?: { allowUnknownFields?: boolean };\n baseDir?: string;\n renderer?: string;\n /** DOCX only; PPTX generators ignore it. */\n svgRasterFallback?: boolean;\n }\n ): Promise<{ buffer: Buffer; warnings: any }>;\n /**\n * Cheap standard-definition path: expansion + normalization only, no\n * rendering (DOCX generators expose it; PPTX ones may not).\n */\n expandStandardDefinition?: (\n document: any,\n options?: { validation?: { allowUnknownFields?: boolean } }\n ) => Promise<{ standardDefinition: any; warnings: any }>;\n}\n\n/**\n * Each format's renderer-id union, read from its core without importing it.\n *\n * `typeof import(...)` is a type query — erased at compile time — so this keeps\n * the ids honest while the cores stay dynamically loaded. `GeneratorOptions`\n * carries a bare string because one options bag serves both formats; these are\n * what it narrows to at each call site, and an id outside the union is rejected\n * by the core's registry with the list of valid ones.\n */\ntype DocxRendererId =\n (typeof import('@json-to-office/core-docx'))['DEFAULT_DOCX_RENDERER_ID'];\ntype PptxRendererId =\n (typeof import('@json-to-office/core-pptx'))['DEFAULT_PPTX_RENDERER_ID'];\n\nexport interface GeneratorOptions {\n theme?: string | any;\n themePath?: string;\n customThemes?: Record<string, any>;\n validation?: {\n strict?: boolean;\n allowUnknownFields?: boolean;\n };\n fonts?: FontRuntimeOpts;\n deterministic?: boolean;\n generatedAt?: string | Date;\n /**\n * Directory that relative asset paths in the document resolve against —\n * normally the input document's own directory (#142).\n */\n baseDir?: string;\n /**\n * Backend that turns the compiled document into bytes.\n *\n * Format-specific and validated by the core's renderer registry, which is\n * why this is a bare string here: naming the id union would make this\n * package import both cores statically, and they are loaded on demand.\n * Undefined means the format's default (`docxjs` / `pptxgenjs`).\n */\n renderer?: string;\n /**\n * Rasterize a PNG fallback for each inline SVG. Defaults to true.\n *\n * DOCX only — pptx embeds SVG without a raster twin. Only readers older than\n * Word 2016 draw it, and producing it dominates the render of a document\n * whose artwork is many small SVGs.\n */\n svgRasterFallback?: boolean;\n /**\n * Optional sink for structured generation warnings (FONT_UNRESOLVED and\n * friends). Mirrors core-docx's `JsonGenerationOptions.warnings`, and is the\n * only delivery mechanism that works off the CLI: `emitGenerationWarnings`\n * routes through an AsyncLocalStorage sink that is a no-op on the server.\n *\n * Adapters PUSH into it; they never replace it. Warnings therefore\n * ACCUMULATE across repeated `generateBuffer` calls on one\n * `GeneratorResult` — allocate one array per logical request.\n */\n warnings?: GenerationWarning[];\n /** Design profile and invocation-specific enforcement. */\n quality?: {\n profile?: QualityProfile;\n policy?: QualityPolicy;\n };\n /** Opaque canonical prologue output shared by analysis and rendering. */\n prepared?: PreparedDocument;\n}\n\nexport interface GeneratorResult {\n generateBuffer: (document: any) => Promise<Buffer>;\n /**\n * Post-expansion standard JSON tree without any rendering work — no fonts,\n * no layout, no visual rasterization. Present when the underlying generator\n * supports it (plugin-aware DOCX generation does).\n */\n getStandardDefinition?: (config: any) => Promise<any>;\n hasPlugins: boolean;\n pluginNames: string[];\n /**\n * Identity of the theme this generator forces on every document, or\n * undefined when nothing was requested and each document's own `props.theme`\n * decides. Reported by the CLI so the summary names what actually rendered.\n */\n themeLabel?: string;\n}\n\n/** One resolution of `theme`/`themePath`, shared by every consumer of a run. */\ninterface ResolvedThemes {\n /**\n * The theme named by `theme`/`themePath`, or undefined when neither is set\n * or resolves — callers that must not override the document's own\n * `props.theme` depend on that distinction.\n */\n requested: any | undefined;\n /** Themes registered by name for `props.theme` lookups. */\n customThemes: Record<string, any> | undefined;\n /** What to call `requested`; undefined when the document decides. */\n label: string | undefined;\n}\n\nexport interface FormatAdapter {\n name: FormatName;\n extension: string;\n label: string;\n defaultPort: number;\n\n generateBuffer(json: unknown, options: GeneratorOptions): Promise<Buffer>;\n\n createGenerator(\n plugins: any[],\n options: GeneratorOptions\n ): Promise<GeneratorResult>;\n\n parseJson(input: string | object): unknown;\n validateDocument(doc: unknown): { valid: boolean; errors?: any[] };\n\n /**\n * Validate a document that names plugin components.\n *\n * `validateDocument` above knows the standard components and nothing else,\n * so a registered plugin reads to it as `Unknown component \"weather\"` —\n * the same name the schema route offers for completion and the generator\n * expands. The core validators take the registered components, defer those\n * nodes from the standard walk, and check each one's props against the\n * version it resolves to; this is the seam that reaches them.\n *\n * Async because the core that owns them is imported on demand, as\n * `analyzeQuality` does. Callers with no plugins registered should keep\n * using the sync entry point.\n */\n validateDocumentWithPlugins?(\n doc: unknown,\n plugins: any[]\n ): Promise<{ valid: boolean; errors?: any[] }>;\n\n /** Analyze format-specific design quality with profiles, policy, and gate. */\n analyzeQuality?(\n doc: unknown,\n options?: GeneratorOptions\n ): Promise<QualityAnalysis>;\n\n /** Prepare effective values and provenance once for official pipelines. */\n prepareDocument?(\n doc: unknown,\n options?: GeneratorOptions\n ): Promise<PreparedDocument>;\n\n generateSchema(options?: any): any;\n\n getBuiltinThemes(): Record<string, any>;\n /** Full values for ESM hosts; falls back to `getBuiltinThemes` for plugins. */\n getBuiltinThemeValues?(): Promise<Record<string, any>>;\n resolveTheme(options: GeneratorOptions): Promise<any>;\n loadCustomThemes(\n options: GeneratorOptions\n ): Promise<Record<string, any> | undefined>;\n\n /**\n * Renderer ids this format registers, defaults first.\n *\n * Async because the core that owns the registry is imported on demand — the\n * list is read from it rather than repeated here, so the two cannot drift.\n */\n rendererIds(): Promise<readonly string[]>;\n\n /**\n * The same renderers, each with whether its backend loads on this host.\n *\n * `rendererIds` answers \"what is registered\", which is not the same question:\n * a factory only runs when its renderer is selected, so an id says nothing\n * about whether the render behind it will work. Anything that *advertises*\n * renderers should report this instead — otherwise a caller picks one, gets\n * a green light from validation, and fails a call later.\n */\n rendererStatuses(): Promise<readonly RendererStatus[]>;\n\n /** Cumulative visual pre-pass dedupe counters (DOCX only) (#156). */\n getVisualPrepassStats?(): Promise<any>;\n /** Reset per-format cache observability counters (DOCX only). */\n resetCacheStats?(): Promise<void>;\n}\n\nexport class DocxFormatAdapter implements FormatAdapter {\n name: FormatName = 'docx';\n extension = '.docx';\n label = 'document';\n defaultPort = 3003;\n\n async rendererIds(): Promise<readonly string[]> {\n const core = await import('@json-to-office/core-docx');\n return core.docxRendererIds();\n }\n\n async rendererStatuses(): Promise<readonly RendererStatus[]> {\n const core = await import('@json-to-office/core-docx');\n return core.docxRendererStatuses();\n }\n\n async generateBuffer(\n json: unknown,\n options: GeneratorOptions\n ): Promise<Buffer> {\n const core = await import('@json-to-office/core-docx');\n const parsed = typeof json === 'string' ? JSON.parse(json as string) : json;\n const prepared = preparedFor(\n options.prepared?.format === 'docx'\n ? (options.prepared as ReturnType<\n typeof core.prepareDocxQualityDocument\n >)\n : undefined,\n parsed\n );\n let docDefinition: unknown;\n let customThemes: Record<string, any> | undefined;\n if (prepared) {\n docDefinition = prepared.model.authored;\n } else {\n const resolved = await this.resolveThemes(options);\n const normalized = withRequestedTheme(\n parsed,\n resolved.requested,\n resolved.customThemes\n );\n docDefinition = normalized.document;\n customThemes = normalized.customThemes;\n }\n const services = buildDocxServices();\n // Collect rather than swallow: without a sink, core warnings (an\n // unresolvable `props.theme` among them) never reach the terminal and the\n // render just comes back looking subtly wrong.\n const warnings: GenerationWarning[] = [];\n const buffer = await core.generateBufferFromJson(docDefinition as any, {\n customThemes,\n services,\n fonts: options.fonts,\n validation: {\n allowUnknownFields: options.validation?.allowUnknownFields,\n },\n deterministic: options.deterministic,\n generatedAt: options.generatedAt,\n baseDir: options.baseDir,\n renderer: options.renderer as DocxRendererId | undefined,\n svgRasterFallback: options.svgRasterFallback,\n prepared,\n warnings,\n });\n const emitted = withoutPreparedWarnings(warnings, prepared);\n emitGenerationWarnings(emitted);\n options.warnings?.push(...toGenerationWarnings(emitted));\n return buffer;\n }\n\n async createGenerator(\n plugins: any[],\n options: GeneratorOptions\n ): Promise<GeneratorResult> {\n const core = await import('@json-to-office/core-docx');\n const hasPlugins = plugins.length > 0;\n const pluginNames = plugins.map((p) => p.name);\n const services = buildDocxServices();\n\n // Resolve once unless the canonical model already carries the result.\n const prepared =\n !hasPlugins && options.prepared?.format === 'docx'\n ? (options.prepared as ReturnType<\n typeof core.prepareDocxQualityDocument\n >)\n : undefined;\n const resolved = prepared ? undefined : await this.resolveThemes(options);\n const requestedTheme = resolved?.requested;\n const customThemes = resolved?.customThemes;\n const themeLabel = prepared\n ? preparedThemeLabel(prepared)\n : resolved?.label;\n\n if (!hasPlugins) {\n // A prepared model renders only its own document; any other one falls\n // back to resolving themes here, memoized so a bad `--theme` still\n // warns once per generator.\n let fallbackThemes: Promise<ResolvedThemes> | undefined;\n const themesFor = (): Promise<ResolvedThemes> =>\n (fallbackThemes ??= resolved\n ? Promise.resolve(resolved)\n : this.resolveThemes(options));\n return {\n generateBuffer: async (document: any) => {\n const parsed =\n typeof document === 'string' ? JSON.parse(document) : document;\n const usable = preparedFor(prepared, parsed);\n const themes = usable ? undefined : await themesFor();\n const normalized = usable\n ? { document: usable.model.authored, customThemes: undefined }\n : withRequestedTheme(\n parsed,\n themes?.requested,\n themes?.customThemes\n );\n const warnings: GenerationWarning[] = [];\n const buffer = await core.generateBufferFromJson(\n normalized.document,\n {\n customThemes: normalized.customThemes,\n services,\n fonts: options.fonts,\n validation: {\n allowUnknownFields: options.validation?.allowUnknownFields,\n },\n deterministic: options.deterministic,\n generatedAt: options.generatedAt,\n baseDir: options.baseDir,\n renderer: options.renderer as DocxRendererId | undefined,\n svgRasterFallback: options.svgRasterFallback,\n prepared: usable,\n warnings,\n }\n );\n const emitted = withoutPreparedWarnings(warnings, usable);\n emitGenerationWarnings(emitted);\n options.warnings?.push(...toGenerationWarnings(emitted));\n return buffer;\n },\n hasPlugins: false,\n pluginNames: [],\n themeLabel,\n };\n }\n\n let generator: GeneratorBuilder = core.createDocumentGenerator({\n // Undefined when nothing was requested: a constructor theme beats the\n // generator's own `props.theme` lookup, so forcing one here would render\n // every document in it.\n theme: requestedTheme,\n customThemes: requestedTheme\n ? { ...customThemes, [CLI_THEME_KEY]: requestedTheme }\n : customThemes,\n debug: process.env.DEBUG === 'true',\n services,\n fonts: options.fonts,\n validation: {\n allowUnknownFields: options.validation?.allowUnknownFields,\n },\n deterministic: options.deterministic,\n generatedAt: options.generatedAt,\n baseDir: options.baseDir,\n renderer: options.renderer as DocxRendererId | undefined,\n svgRasterFallback: options.svgRasterFallback,\n });\n\n for (const plugin of plugins) {\n generator = generator.addComponent(plugin);\n }\n\n return {\n generateBuffer: async (document: any) => {\n const parsed =\n typeof document === 'string' ? JSON.parse(document) : document;\n const { document: docDefinition } = withRequestedTheme(\n parsed,\n requestedTheme,\n customThemes\n );\n const result = await generator.generateBuffer(docDefinition, {\n validation: {\n allowUnknownFields: options.validation?.allowUnknownFields,\n },\n deterministic: options.deterministic,\n generatedAt: options.generatedAt,\n baseDir: options.baseDir,\n renderer: options.renderer as DocxRendererId | undefined,\n svgRasterFallback: options.svgRasterFallback,\n });\n emitGenerationWarnings(result.warnings ?? []);\n options.warnings?.push(...toGenerationWarnings(result.warnings));\n return result.buffer;\n },\n getStandardDefinition: generator.expandStandardDefinition\n ? async (config: any) => {\n const parsed =\n typeof config === 'string' ? JSON.parse(config) : config;\n const { document: docDefinition } = withRequestedTheme(\n parsed,\n requestedTheme,\n customThemes\n );\n const result = await generator.expandStandardDefinition!(\n docDefinition,\n {\n validation: {\n allowUnknownFields: options.validation?.allowUnknownFields,\n },\n }\n );\n emitGenerationWarnings(result.warnings ?? []);\n return result.standardDefinition;\n }\n : undefined,\n hasPlugins: true,\n pluginNames,\n themeLabel,\n };\n }\n\n parseJson(input: string | object): unknown {\n return typeof input === 'string' ? JSON.parse(input) : input;\n }\n\n validateDocument(doc: unknown): { valid: boolean; errors?: any[] } {\n const result = validateDocx.jsonDocument(doc as object);\n return {\n valid: result.valid,\n ...(result.errors.length > 0 && { errors: result.errors }),\n };\n }\n\n async validateDocumentWithPlugins(\n doc: unknown,\n plugins: any[]\n ): Promise<{ valid: boolean; errors?: any[] }> {\n const core = await import('@json-to-office/core-docx');\n const parsed = typeof doc === 'string' ? JSON.parse(doc) : doc;\n const result = core.validateDocument(parsed as any, plugins);\n const errors = result.errors ?? [];\n return {\n valid: result.valid,\n ...(errors.length > 0 && { errors }),\n };\n }\n\n async analyzeQuality(\n doc: unknown,\n options: GeneratorOptions = {}\n ): Promise<QualityAnalysis> {\n const core = await import('@json-to-office/core-docx');\n const parsed = typeof doc === 'string' ? JSON.parse(doc) : doc;\n let prepared = preparedFor(\n options.prepared?.format === 'docx'\n ? (options.prepared as ReturnType<\n typeof core.prepareDocxQualityDocument\n >)\n : undefined,\n parsed\n );\n if (!prepared) {\n try {\n prepared = (await this.prepareModel(parsed, options, [])) as ReturnType<\n typeof core.prepareDocxQualityDocument\n >;\n } catch {\n // Structural validation owns malformed trees. The core guards its own\n // preparation, so handing it the document instead of a model turns a\n // throw into an analysis that reports the failure.\n }\n }\n return core.analyzeDocxQuality(prepared?.model.authored ?? parsed, {\n ...(prepared && { prepared }),\n renderer: options.renderer ?? documentRenderer(parsed),\n profile: options.quality?.profile,\n policy: options.quality?.policy,\n });\n }\n\n async prepareDocument(\n doc: unknown,\n options: GeneratorOptions = {}\n ): Promise<PreparedDocument> {\n // Preparation resolves the theme context the render then skips, so these\n // warnings reach the host from here or not at all.\n const warnings: GenerationWarning[] = [];\n const prepared = await this.prepareModel(doc, options, warnings);\n emitGenerationWarnings(warnings);\n options.warnings?.push(...toGenerationWarnings(warnings));\n return prepared;\n }\n\n /** Prepare into a caller-owned sink; `prepareDocument` owns the reporting. */\n private async prepareModel(\n doc: unknown,\n options: GeneratorOptions,\n warnings: GenerationWarning[]\n ): Promise<PreparedDocument> {\n const core = await import('@json-to-office/core-docx');\n const parsed = typeof doc === 'string' ? JSON.parse(doc) : doc;\n const resolved = await this.resolveThemes(options);\n const normalized = withRequestedTheme(\n parsed,\n resolved.requested,\n resolved.customThemes\n );\n const prepared = core.prepareDocxQualityDocument(\n normalized.document as any,\n {\n customThemes: normalized.customThemes,\n fonts: options.fonts,\n renderer: options.renderer ?? documentRenderer(parsed),\n warnings,\n }\n );\n return stampPrepared(\n {\n ...prepared,\n metadata: {\n ...prepared.metadata,\n ...(resolved.label && { themeLabel: resolved.label }),\n },\n },\n parsed,\n toGenerationWarnings(warnings)\n );\n }\n\n generateSchema(_options?: any): any {\n // Delegate to shared-docx\n return null;\n }\n\n getBuiltinThemes(): Record<string, any> {\n try {\n const core = require('@json-to-office/core-docx');\n return core.themes || {};\n } catch {\n return {};\n }\n }\n\n async getBuiltinThemeValues(): Promise<Record<string, any>> {\n const core = await import('@json-to-office/core-docx');\n return core.themes || {};\n }\n\n async resolveTheme(options: GeneratorOptions): Promise<any> {\n const core = await import('@json-to-office/core-docx');\n const { requested } = await this.resolveThemes(options);\n return requested ?? (core.themes as any)?.minimal ?? {};\n }\n\n /**\n * Resolve `theme`/`themePath` once for a whole run: `themePath` is read a\n * single time and feeds both the requested theme and the custom-theme\n * registry, so a bad path warns once instead of once per consumer.\n */\n private async resolveThemes(\n options: GeneratorOptions\n ): Promise<ResolvedThemes> {\n const core = await import('@json-to-office/core-docx');\n // Themes passed directly from the client (playground UI) come first.\n const registry: Record<string, any> = { ...options.customThemes };\n\n if (typeof options.theme === 'object' && options.theme !== null) {\n registry[safeThemeKey(options.theme.name)] = options.theme;\n }\n\n let fileTheme: any | undefined;\n if (options.themePath) {\n try {\n if (options.themePath.endsWith('.json')) {\n fileTheme = await core.loadThemeFromFile(options.themePath);\n } else {\n const themePath = path.resolve(process.cwd(), options.themePath);\n const themeModule = await import(themePath);\n fileTheme = themeModule.default || themeModule.theme;\n }\n } catch (error: any) {\n emitDiagnostic(\n `Failed to load theme from ${options.themePath}: ${error.message}`,\n 'warning'\n );\n }\n if (fileTheme) {\n registry[safeThemeKey(fileTheme.name)] = fileTheme;\n }\n }\n\n const customThemes =\n Object.keys(registry).length > 0 ? registry : undefined;\n\n if (fileTheme) {\n return { requested: fileTheme, customThemes, label: options.themePath };\n }\n\n if (typeof options.theme === 'string') {\n const named =\n options.customThemes?.[options.theme] ??\n (core.themes as Record<string, any>)?.[options.theme];\n if (named)\n return { requested: named, customThemes, label: options.theme };\n\n if (options.theme.endsWith('.json') && fs.existsSync(options.theme)) {\n try {\n return {\n requested: await core.loadThemeFromFile(options.theme),\n customThemes,\n label: options.theme,\n };\n } catch {}\n }\n\n try {\n const inline = await core.loadThemeFromJson(options.theme);\n return {\n requested: inline,\n customThemes,\n label: (inline as any)?.name || options.theme,\n };\n } catch {}\n\n emitDiagnostic(\n `Unknown theme \"${options.theme}\"; keeping the document's own theme`,\n 'warning'\n );\n }\n\n if (typeof options.theme === 'object' && options.theme !== null) {\n return {\n requested: options.theme,\n customThemes,\n label: safeThemeKey(options.theme.name),\n };\n }\n\n return { requested: undefined, customThemes, label: undefined };\n }\n\n async loadCustomThemes(\n options: GeneratorOptions\n ): Promise<Record<string, any> | undefined> {\n return (await this.resolveThemes(options)).customThemes;\n }\n\n async getVisualPrepassStats(): Promise<any> {\n try {\n const core = await import('@json-to-office/core-docx');\n return core.getVisualPrepassStats?.() ?? null;\n } catch {\n return null;\n }\n }\n\n async resetCacheStats(): Promise<void> {\n try {\n const core = await import('@json-to-office/core-docx');\n core.resetVisualPrepassStats?.();\n } catch {\n // Resetting observability is best-effort.\n }\n }\n}\n\nexport class PptxFormatAdapter implements FormatAdapter {\n name: FormatName = 'pptx';\n extension = '.pptx';\n label = 'presentation';\n defaultPort = 3004;\n\n async rendererIds(): Promise<readonly string[]> {\n const core = await import('@json-to-office/core-pptx');\n return core.pptxRendererIds();\n }\n\n async rendererStatuses(): Promise<readonly RendererStatus[]> {\n const core = await import('@json-to-office/core-pptx');\n return core.pptxRendererStatuses();\n }\n\n async generateBuffer(\n json: unknown,\n options: GeneratorOptions\n ): Promise<Buffer> {\n const core = await import('@json-to-office/core-pptx');\n const parsed = typeof json === 'string' ? JSON.parse(json as string) : json;\n const prepared = preparedFor(\n options.prepared?.format === 'pptx'\n ? (options.prepared as ReturnType<\n typeof core.preparePptxQualityDocument\n >)\n : undefined,\n parsed\n );\n let docDefinition: unknown;\n let customThemes: Record<string, any> | undefined;\n if (prepared) {\n docDefinition = prepared.model.authored;\n } else {\n const resolved = await this.resolveThemes(options);\n const normalized = withRequestedTheme(\n parsed,\n resolved.requested,\n resolved.customThemes\n );\n docDefinition = normalized.document;\n customThemes = normalized.customThemes;\n }\n const services = buildServicesFromEnv();\n // The warnings-returning entry point: `generateBufferFromJson` allocates\n // the pipeline's warning array internally and throws it away, so core\n // warnings (FONT_UNRESOLVED among them) never reached the terminal or the\n // server.\n const result = await core.generateBufferWithWarnings(docDefinition as any, {\n customThemes,\n services,\n fonts: options.fonts,\n validation: {\n allowUnknownFields: options.validation?.allowUnknownFields,\n },\n deterministic: options.deterministic,\n generatedAt: options.generatedAt,\n baseDir: options.baseDir,\n renderer: options.renderer as PptxRendererId | undefined,\n prepared,\n });\n const emitted = withoutPreparedWarnings(\n toGenerationWarnings(result.warnings),\n prepared\n );\n emitGenerationWarnings(emitted);\n options.warnings?.push(...emitted);\n return result.buffer;\n }\n\n async createGenerator(\n plugins: any[],\n options: GeneratorOptions\n ): Promise<GeneratorResult> {\n const core = await import('@json-to-office/core-pptx');\n const hasPlugins = plugins.length > 0;\n const pluginNames = plugins.map((p) => p.name);\n const services = buildServicesFromEnv();\n\n // Resolve once unless the canonical model already carries the result.\n const prepared =\n !hasPlugins && options.prepared?.format === 'pptx'\n ? (options.prepared as ReturnType<\n typeof core.preparePptxQualityDocument\n >)\n : undefined;\n const resolved = prepared ? undefined : await this.resolveThemes(options);\n const requestedTheme = resolved?.requested;\n const customThemes = resolved?.customThemes;\n const themeLabel = prepared\n ? preparedThemeLabel(prepared)\n : resolved?.label;\n\n if (!hasPlugins) {\n // A prepared model renders only its own document; any other one falls\n // back to resolving themes here, memoized so a bad `--theme` still\n // warns once per generator.\n let fallbackThemes: Promise<ResolvedThemes> | undefined;\n const themesFor = (): Promise<ResolvedThemes> =>\n (fallbackThemes ??= resolved\n ? Promise.resolve(resolved)\n : this.resolveThemes(options));\n return {\n generateBuffer: async (document: any) => {\n const parsed =\n typeof document === 'string' ? JSON.parse(document) : document;\n const usable = preparedFor(prepared, parsed);\n const themes = usable ? undefined : await themesFor();\n const normalized = usable\n ? { document: usable.model.authored, customThemes: undefined }\n : withRequestedTheme(\n parsed,\n themes?.requested,\n themes?.customThemes\n );\n const result = await core.generateBufferWithWarnings(\n normalized.document,\n {\n customThemes: normalized.customThemes,\n services,\n fonts: options.fonts,\n validation: {\n allowUnknownFields: options.validation?.allowUnknownFields,\n },\n deterministic: options.deterministic,\n generatedAt: options.generatedAt,\n baseDir: options.baseDir,\n renderer: options.renderer as PptxRendererId | undefined,\n prepared: usable,\n }\n );\n const warnings = withoutPreparedWarnings(\n toGenerationWarnings(result.warnings),\n usable\n );\n emitGenerationWarnings(warnings);\n options.warnings?.push(...warnings);\n return result.buffer;\n },\n hasPlugins: false,\n pluginNames: [],\n themeLabel,\n };\n }\n\n let generator: GeneratorBuilder = core.createPresentationGenerator({\n // Undefined when nothing was requested: a constructor theme beats the\n // generator's own `props.theme` lookup, so forcing one here would render\n // every document in it.\n theme: requestedTheme,\n customThemes: requestedTheme\n ? { ...customThemes, [CLI_THEME_KEY]: requestedTheme }\n : customThemes,\n debug: process.env.DEBUG === 'true',\n services,\n fonts: options.fonts,\n validation: {\n allowUnknownFields: options.validation?.allowUnknownFields,\n },\n deterministic: options.deterministic,\n generatedAt: options.generatedAt,\n baseDir: options.baseDir,\n renderer: options.renderer as PptxRendererId | undefined,\n });\n\n for (const plugin of plugins) {\n generator = generator.addComponent(plugin);\n }\n\n return {\n generateBuffer: async (document: any) => {\n const parsed =\n typeof document === 'string' ? JSON.parse(document) : document;\n const { document: docDefinition } = withRequestedTheme(\n parsed,\n requestedTheme,\n customThemes\n );\n const result = await generator.generateBuffer(docDefinition, {\n validation: {\n allowUnknownFields: options.validation?.allowUnknownFields,\n },\n deterministic: options.deterministic,\n generatedAt: options.generatedAt,\n baseDir: options.baseDir,\n renderer: options.renderer as PptxRendererId | undefined,\n });\n const normalized = toGenerationWarnings(result.warnings);\n emitGenerationWarnings(normalized);\n options.warnings?.push(...normalized);\n return result.buffer;\n },\n hasPlugins: true,\n pluginNames,\n themeLabel,\n };\n }\n\n parseJson(input: string | object): unknown {\n return typeof input === 'string' ? JSON.parse(input) : input;\n }\n\n validateDocument(doc: unknown): { valid: boolean; errors?: any[] } {\n const result = validatePresentationDocument(doc);\n return {\n valid: result.valid,\n ...(result.errors.length > 0 && { errors: result.errors }),\n };\n }\n\n async validateDocumentWithPlugins(\n doc: unknown,\n plugins: any[]\n ): Promise<{ valid: boolean; errors?: any[] }> {\n const core = await import('@json-to-office/core-pptx');\n const parsed = typeof doc === 'string' ? JSON.parse(doc) : doc;\n const result = core.validatePresentation(parsed as any, plugins);\n return {\n valid: result.valid,\n ...(result.errors.length > 0 && { errors: result.errors }),\n };\n }\n\n async analyzeQuality(\n doc: unknown,\n options: GeneratorOptions = {}\n ): Promise<QualityAnalysis> {\n const core = await import('@json-to-office/core-pptx');\n const parsed = typeof doc === 'string' ? JSON.parse(doc) : doc;\n let prepared = preparedFor(\n options.prepared?.format === 'pptx'\n ? (options.prepared as ReturnType<\n typeof core.preparePptxQualityDocument\n >)\n : undefined,\n parsed\n );\n if (!prepared) {\n try {\n prepared = (await this.prepareModel(parsed, options, [])) as ReturnType<\n typeof core.preparePptxQualityDocument\n >;\n } catch {\n // Structural validation owns malformed trees. The core guards its own\n // preparation, so handing it the document instead of a model turns a\n // throw into an analysis that reports the failure.\n }\n }\n return core.analyzePptxQuality(prepared?.model.authored ?? parsed, {\n ...(prepared && { prepared }),\n renderer: options.renderer ?? documentRenderer(parsed),\n profile: options.quality?.profile,\n policy: options.quality?.policy,\n });\n }\n\n async prepareDocument(\n doc: unknown,\n options: GeneratorOptions = {}\n ): Promise<PreparedDocument> {\n // Preparation resolves the theme context the render then skips, so these\n // warnings reach the host from here or not at all.\n const warnings: any[] = [];\n const prepared = await this.prepareModel(doc, options, warnings);\n const normalized = toGenerationWarnings(warnings);\n emitGenerationWarnings(normalized);\n options.warnings?.push(...normalized);\n return prepared;\n }\n\n /** Prepare into a caller-owned sink; `prepareDocument` owns the reporting. */\n private async prepareModel(\n doc: unknown,\n options: GeneratorOptions,\n warnings: any[]\n ): Promise<PreparedDocument> {\n const core = await import('@json-to-office/core-pptx');\n const parsed = typeof doc === 'string' ? JSON.parse(doc) : doc;\n const resolved = await this.resolveThemes(options);\n const normalized = withRequestedTheme(\n parsed,\n resolved.requested,\n resolved.customThemes\n );\n const prepared = core.preparePptxQualityDocument(\n normalized.document as any,\n {\n customThemes: normalized.customThemes,\n fonts: options.fonts,\n services: buildServicesFromEnv(),\n renderer: options.renderer ?? documentRenderer(parsed),\n warnings,\n }\n );\n return stampPrepared(\n {\n ...prepared,\n metadata: {\n ...prepared.metadata,\n ...(resolved.label && { themeLabel: resolved.label }),\n },\n },\n parsed,\n toGenerationWarnings(warnings)\n );\n }\n\n generateSchema(_options?: any): any {\n return null;\n }\n\n getBuiltinThemes(): Record<string, any> {\n try {\n const core = require('@json-to-office/core-pptx');\n return core.pptxThemes || {};\n } catch {\n return {};\n }\n }\n\n async getBuiltinThemeValues(): Promise<Record<string, any>> {\n const core = await import('@json-to-office/core-pptx');\n return core.pptxThemes || {};\n }\n\n async resolveTheme(options: GeneratorOptions): Promise<any> {\n const core = await import('@json-to-office/core-pptx');\n const themes = (core as any).pptxThemes || {};\n const { requested } = await this.resolveThemes(options);\n return requested ?? themes.minimal ?? {};\n }\n\n /**\n * Resolve `theme`/`themePath` once for a whole run: `themePath` is read a\n * single time and feeds both the requested theme and the custom-theme\n * registry, so a bad path warns once instead of once per consumer.\n */\n private async resolveThemes(\n options: GeneratorOptions\n ): Promise<ResolvedThemes> {\n const core = await import('@json-to-office/core-pptx');\n const themes = (core as any).pptxThemes || {};\n // Themes passed directly from the client (playground UI) come first.\n const registry: Record<string, any> = { ...options.customThemes };\n\n if (typeof options.theme === 'object' && options.theme !== null) {\n registry[safeThemeKey(options.theme.name)] = options.theme;\n }\n\n let fileTheme: any | undefined;\n if (options.themePath) {\n try {\n if (options.themePath.endsWith('.json')) {\n const content = fs.readFileSync(\n path.resolve(process.cwd(), options.themePath),\n 'utf-8'\n );\n // Validated, not merely parsed. The DOCX branch has always gone\n // through `loadThemeFromFile`; this one used to hand whatever JSON\n // it found straight to the compiler, which reads\n // `theme.defaults.fontSize` unguarded — so a theme with the wrong\n // shape surfaced as a TypeError in the IR rather than as a\n // diagnostic naming the bad field.\n const shared = await import('@json-to-office/shared-pptx');\n const checked = shared.validatePptxTheme(JSON.parse(content));\n if (!checked.valid) {\n const detail = checked.errors\n .slice(0, 3)\n .map((error: { path?: string; message: string }) =>\n error.path ? `${error.path}: ${error.message}` : error.message\n )\n .join('; ');\n throw new Error(\n `not a valid pptx theme — ${detail}${\n checked.errors.length > 3\n ? ` (and ${checked.errors.length - 3} more)`\n : ''\n }`\n );\n }\n fileTheme = checked.data;\n } else {\n const themePath = path.resolve(process.cwd(), options.themePath);\n const themeModule = await import(themePath);\n fileTheme = themeModule.default || themeModule.theme;\n }\n } catch (error: any) {\n emitDiagnostic(\n `Failed to load theme from ${options.themePath}: ${error.message}`,\n 'warning'\n );\n }\n if (fileTheme) {\n registry[safeThemeKey(fileTheme.name)] = fileTheme;\n }\n }\n\n const customThemes =\n Object.keys(registry).length > 0 ? registry : undefined;\n\n if (fileTheme) {\n return { requested: fileTheme, customThemes, label: options.themePath };\n }\n\n if (typeof options.theme === 'string') {\n // Deliberately not getPptxTheme(): it answers every unknown name with\n // the default theme, which would silently swap a typo'd `--theme` in\n // over the document's own.\n const named =\n options.customThemes?.[options.theme] ?? themes[options.theme];\n if (named)\n return { requested: named, customThemes, label: options.theme };\n\n if (options.theme.endsWith('.json') && fs.existsSync(options.theme)) {\n try {\n const content = fs.readFileSync(\n path.resolve(process.cwd(), options.theme),\n 'utf-8'\n );\n return {\n requested: JSON.parse(content),\n customThemes,\n label: options.theme,\n };\n } catch {}\n }\n\n emitDiagnostic(\n `Unknown theme \"${options.theme}\"; keeping the document's own theme`,\n 'warning'\n );\n }\n\n if (typeof options.theme === 'object' && options.theme !== null) {\n return {\n requested: options.theme,\n customThemes,\n label: safeThemeKey(options.theme.name),\n };\n }\n\n return { requested: undefined, customThemes, label: undefined };\n }\n\n async loadCustomThemes(\n options: GeneratorOptions\n ): Promise<Record<string, any> | undefined> {\n return (await this.resolveThemes(options)).customThemes;\n }\n}\n\nexport function createAdapter(format: FormatName): FormatAdapter {\n switch (format) {\n case 'docx':\n return new DocxFormatAdapter();\n case 'pptx':\n return new PptxFormatAdapter();\n default:\n throw new Error(`Unknown format: ${format}`);\n }\n}\n","/**\n * PPTX rasterizer — the concrete service backing docx `visual` components.\n *\n * Pipeline: presentation JSON → (core-pptx) .pptx → (LibreOffice) PDF →\n * (poppler/pdftoppm) PNG. Returns a base64 data URI plus the natural pixel\n * dimensions. Results are content-addressed and cached on disk so repeated\n * builds of an unchanged visual skip the (multi-second) LibreOffice run.\n *\n * Single and batch rasterization share one engine (#153). A batch keeps one\n * .pptx per slide and converts them all in a single `soffice` launch — the\n * launch is the dominant cost, and per-file conversion keeps slides fully\n * independent: each has its own PDF and PNG (no page↔slide index mapping),\n * its own dpi, and a cache key identical to the single-slide path, so both\n * paths share the same disk cache.\n *\n * A request may carry `fonts`: base64 font faces that are staged for the\n * soffice launches (via the shared FontStager pipeline) so the slide renders\n * with the document's real families instead of whatever the host happens to\n * have installed. Those fonts are part of the disk-cache key — the same\n * slide is genuinely different pixels with and without them, and the cache\n * is shared process-wide across callers.\n *\n * Every engine run works against a wall-clock deadline (one batch-scaled\n * soffice window plus one pdftoppm window) so a wedged conversion fails the\n * remaining slides quickly instead of holding the caller — and its\n * concurrency slot — for minutes.\n *\n * This is injected via `services.pptx.render` / `services.pptx.renderBatch`;\n * the published engine packages never depend on these binaries.\n */\n\nimport { execFile } from 'node:child_process';\nimport { promises as fs } from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport crypto from 'node:crypto';\nimport {\n DEFAULT_VISUAL_DPI,\n type PptxRasterizer,\n type PptxBatchRasterizer,\n type PptxRasterizeRequest,\n type PptxRasterizeResult,\n type PptxRasterizeBatchSlideResult,\n type PptxRasterizeFailureStage,\n type RasterizeFontFace,\n} from '@json-to-office/shared';\nimport { fromRasterizeFontFaces } from '@json-to-office/shared/fonts/node';\nimport { getFontStager } from './font-staging/index.js';\nimport type { FontStageHandle } from './font-staging/index.js';\n\nconst SOFFICE_TIMEOUT_MS = 60000;\n/** Extra soffice budget per additional slide in a batch launch. */\nconst SOFFICE_BATCH_EXTRA_PER_SLIDE_MS = 15000;\n/** Hard ceiling for one batch soffice launch. */\nconst SOFFICE_BATCH_TIMEOUT_CAP_MS = 300000;\n/**\n * Max isolated single-file launches after a batch launch left PDFs missing.\n * Covers the realistic case (one poisoned slide crashed the batch; its\n * successors are fine) without letting a broken environment turn one request\n * into dozens of sequential 60s timeouts.\n */\nconst MAX_ISOLATED_RETRIES = 3;\nconst PDFTOPPM_TIMEOUT_MS = 30000;\nconst PROBE_TIMEOUT_MS = 5000;\nconst MAX_BUFFER = 64 * 1024 * 1024;\n\nfunction exec(\n binary: string,\n args: string[],\n timeoutMs: number,\n /**\n * Extra env for the child, merged over `process.env`. Carries a font\n * stager's `envOverrides` (FONTCONFIG_FILE / JTO_FONT_PATHS /\n * SAL_DISABLE_SKIA). Only the soffice CONVERSION launches get it: the\n * memoized `--version` probe must not have its resolution polluted, and\n * pdftoppm has no use for it.\n */\n env?: Record<string, string>\n): Promise<void> {\n return new Promise((resolve, reject) => {\n execFile(\n binary,\n args,\n {\n timeout: timeoutMs,\n maxBuffer: MAX_BUFFER,\n windowsHide: true,\n env: env ? { ...process.env, ...env } : process.env,\n },\n (error) => (error ? reject(error) : resolve())\n );\n });\n}\n\nasync function binaryWorks(binary: string): Promise<boolean> {\n if (binary.includes(path.sep)) {\n try {\n await fs.access(binary);\n } catch {\n return false;\n }\n }\n try {\n await exec(binary, ['--version'], PROBE_TIMEOUT_MS);\n return true;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n // A non-ENOENT failure still means the binary exists (e.g. bad flag).\n return code !== 'ENOENT' && code !== 'EACCES';\n }\n}\n\nfunction sofficeCandidates(): string[] {\n const candidates: string[] = [];\n const configured = process.env.LIBREOFFICE_PATH?.trim();\n if (configured) candidates.push(configured);\n if (process.platform === 'darwin') {\n candidates.push('/Applications/LibreOffice.app/Contents/MacOS/soffice');\n } else if (process.platform === 'win32') {\n candidates.push('C:\\\\Program Files\\\\LibreOffice\\\\program\\\\soffice.exe');\n candidates.push(\n 'C:\\\\Program Files (x86)\\\\LibreOffice\\\\program\\\\soffice.exe'\n );\n }\n candidates.push('soffice', 'libreoffice');\n return [...new Set(candidates)];\n}\n\nfunction pdftoppmCandidates(): string[] {\n const candidates: string[] = [];\n const configured = process.env.PDFTOPPM_PATH?.trim();\n if (configured) candidates.push(configured);\n candidates.push('pdftoppm');\n return [...new Set(candidates)];\n}\n\nasync function resolveBinary(\n candidates: string[],\n label: string,\n install: string\n): Promise<string> {\n for (const candidate of candidates) {\n if (await binaryWorks(candidate)) return candidate;\n }\n throw new Error(\n `Visual rasterization needs ${label}, which was not found. ${install} ` +\n `(searched: ${candidates.join(', ')}).`\n );\n}\n\n// Memoize resolved binary paths per process — they don't change at runtime, so\n// re-probing (spawning `--version` for every cache-miss rasterize) is wasted\n// work. A failed resolution is NOT cached, so a later call retries.\nlet sofficePromise: Promise<string> | undefined;\nlet pdftoppmPromise: Promise<string> | undefined;\nfunction resolveSoffice(): Promise<string> {\n if (!sofficePromise) {\n sofficePromise = resolveBinary(\n sofficeCandidates(),\n 'LibreOffice (soffice)',\n 'Install LibreOffice or set LIBREOFFICE_PATH.'\n ).catch((error) => {\n sofficePromise = undefined;\n throw error;\n });\n }\n return sofficePromise;\n}\nfunction resolvePdftoppm(): Promise<string> {\n if (!pdftoppmPromise) {\n pdftoppmPromise = resolveBinary(\n pdftoppmCandidates(),\n 'pdftoppm (poppler)',\n 'Install poppler-utils or set PDFTOPPM_PATH.'\n ).catch((error) => {\n pdftoppmPromise = undefined;\n throw error;\n });\n }\n return pdftoppmPromise;\n}\n\n/**\n * Process-wide rasterizer disk-cache counters (#156).\n */\nexport interface RasterizerCacheStats {\n /** Slides served from the content-addressed PNG disk cache. */\n diskHits: number;\n /** Unique slides that missed the disk cache and needed the engine. */\n diskMisses: number;\n /** diskHits / (diskHits + diskMisses), 0 when no lookups. */\n hitRate: number;\n /** Requests resolved by batch-internal dedupe (duplicate slides). */\n dedupedRequests: number;\n /** Slides successfully rendered by the engine (LibreOffice + pdftoppm). */\n rendered: number;\n /** Slides that failed at any engine stage. */\n failed: number;\n /** PNG files currently in the disk cache directories. */\n entries: number;\n /** Total bytes of those PNG files. */\n bytes: number;\n}\n\nconst rasterizerCounters = {\n diskHits: 0,\n diskMisses: 0,\n dedupedRequests: 0,\n rendered: 0,\n failed: 0,\n};\n\n/** Cache directories any engine run has used (for the disk scan). */\nconst knownCacheDirs = new Set<string>();\n\n/**\n * Get rasterizer cache statistics: process-lifetime counters plus a live\n * scan of the disk cache directories (default dir included, so entries from\n * previous processes are visible too).\n */\nexport async function getRasterizerCacheStats(): Promise<RasterizerCacheStats> {\n const dirs = new Set(knownCacheDirs);\n const defaultDir = resolveCacheDir();\n if (defaultDir) dirs.add(defaultDir);\n\n let entries = 0;\n let bytes = 0;\n for (const dir of dirs) {\n try {\n const files = await fs.readdir(dir);\n for (const file of files) {\n if (!file.endsWith('.png')) continue;\n try {\n const stat = await fs.stat(path.join(dir, file));\n entries++;\n bytes += stat.size;\n } catch {}\n }\n } catch {\n // Missing dir — nothing cached there.\n }\n }\n\n const lookups = rasterizerCounters.diskHits + rasterizerCounters.diskMisses;\n return {\n ...rasterizerCounters,\n hitRate: lookups > 0 ? rasterizerCounters.diskHits / lookups : 0,\n entries,\n bytes,\n };\n}\n\n/**\n * Delete every cached PNG in the known cache directories (default dir\n * included) and reset the counters. Backs \"Clear all caches\" (#156) — the\n * disk cache used to survive it.\n */\nexport async function clearRasterizerCache(): Promise<void> {\n const dirs = new Set(knownCacheDirs);\n const defaultDir = resolveCacheDir();\n if (defaultDir) dirs.add(defaultDir);\n\n for (const dir of dirs) {\n try {\n const files = await fs.readdir(dir);\n await Promise.all(\n files\n .filter((file) => file.endsWith('.png'))\n .map((file) => fs.rm(path.join(dir, file), { force: true }))\n );\n } catch {}\n }\n\n rasterizerCounters.diskHits = 0;\n rasterizerCounters.diskMisses = 0;\n rasterizerCounters.dedupedRequests = 0;\n rasterizerCounters.rendered = 0;\n rasterizerCounters.failed = 0;\n}\n\nconst PNG_SIGNATURE = Buffer.from([\n 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,\n]);\n\n/**\n * Validate a PNG buffer and read its width/height from the IHDR chunk. Returns\n * null for anything that isn't a complete PNG (empty/truncated/corrupt) so the\n * caller can re-render or error rather than emit a 0×0 / broken image. pdftoppm\n * always outputs PNG, so a PNG-only check is sufficient here.\n */\nfunction parsePngSize(png: Buffer): { width: number; height: number } | null {\n // 8-byte signature, then IHDR: length(4) + \"IHDR\"(4) + width(4) + height(4)\n if (png.length < 24) return null;\n if (!png.subarray(0, 8).equals(PNG_SIGNATURE)) return null;\n if (png.toString('ascii', 12, 16) !== 'IHDR') return null;\n const width = png.readUInt32BE(16);\n const height = png.readUInt32BE(20);\n if (width <= 0 || height <= 0) return null;\n return { width, height };\n}\n\nlet tmpCounter = 0;\n/**\n * Write the cache file atomically (temp file + rename) so a concurrent reader\n * never observes a half-written PNG. Best-effort: failures are swallowed (the\n * cache is an optimization), but a partial temp file is cleaned up.\n */\nasync function writeCacheAtomic(\n cacheDir: string,\n cachePath: string,\n png: Buffer\n): Promise<void> {\n const tmp = `${cachePath}.tmp-${process.pid}-${tmpCounter++}`;\n try {\n await fs.mkdir(cacheDir, { recursive: true });\n await fs.writeFile(tmp, png);\n await fs.rename(tmp, cachePath);\n } catch {\n await fs.rm(tmp, { force: true }).catch(() => {});\n }\n}\n\n/**\n * Identity of a font set for cache-keying. Order-insensitive (the per-face\n * digests are sorted) and content-addressed (hashes the decoded bytes, not\n * the base64 blob) so the same faces supplied in a different order, or\n * re-fetched to identical bytes, still hit the same cache entry.\n */\nexport function fontsDigest(\n fonts?: readonly RasterizeFontFace[]\n): string | undefined {\n if (!fonts || fonts.length === 0) return undefined;\n const parts = fonts\n .map(\n (f) =>\n `${f.family}|${f.weight}|${f.italic ? 'i' : 'r'}|` +\n crypto\n .createHash('sha256')\n // DECODED bytes, not the base64 text. `fromRasterizeFontFaces`\n // decodes before staging, and Node's base64 decoder is lenient:\n // missing padding, embedded newlines, and stray characters all\n // decode to the same bytes. Hashing the text would give two\n // spellings of one font two different cache keys — a silent\n // cache-miss multiplier on a shared, restart-surviving render\n // server cache.\n .update(Buffer.from(f.data, 'base64'))\n .digest('hex')\n )\n .sort();\n return crypto.createHash('sha256').update(parts.join('\\n')).digest('hex');\n}\n\nexport function cacheKey(request: {\n presentation: unknown;\n dpi: number;\n baseDir?: string;\n fontsKey?: string;\n}): string {\n return (\n crypto\n .createHash('sha256')\n // baseDir joins the key: the same relative asset path means different\n // pixels under different base directories (#142).\n //\n // fontsKey joins the key for the same reason: the same slide renders\n // DIFFERENT pixels with and without the document's fonts staged. This\n // cache is process-wide, shared by /rasterize and /rasterize/batch,\n // shared by every caller of the hosted render server, and it survives\n // restarts — keying without fonts would serve one document's\n // Inter-rendered PNG to a document that never asked for Inter.\n .update(\n JSON.stringify({\n p: request.presentation,\n dpi: request.dpi,\n base: request.baseDir,\n f: request.fontsKey ?? null,\n })\n )\n .digest('hex')\n );\n}\n\nfunction toDataUri(png: Buffer): string {\n return `data:image/png;base64,${png.toString('base64')}`;\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nasync function fileExists(filePath: string): Promise<boolean> {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Engine-internal result: the wire shape plus the original error object. */\ntype EngineSlideResult = PptxRasterizeBatchSlideResult & { cause?: unknown };\n\n/** One unique unit of work: a slide deck plus every request index it serves. */\ninterface SlideJob {\n presentation: unknown;\n dpi: number;\n /** Request indexes resolved by this job (batch-internal dedup). */\n indexes: number[];\n cachePath: string | null;\n pptxPath: string;\n pdfPath: string;\n pngPrefix: string;\n}\n\nconst sofficeArgs = (\n profileDir: string,\n outDir: string,\n files: string[]\n): string[] => [\n '--headless',\n '--norestore',\n '--nolockcheck',\n '--nodefault',\n `-env:UserInstallation=file://${profileDir.replace(/\\\\/g, '/')}`,\n '--convert-to',\n 'pdf:impress_pdf_Export',\n '--outdir',\n outDir,\n ...files,\n];\n\n/**\n * Rasterize N independent single-slide presentations, amortizing the soffice\n * launch across every cache miss. Returns per-slide results (index-aligned);\n * only environment-level failures (missing binaries) throw.\n */\nasync function rasterizeSlidesWithEngine(\n slides: Array<{ presentation: unknown; dpi: number }>,\n baseDir: string | undefined,\n cacheDir: string | null,\n fonts?: readonly RasterizeFontFace[]\n): Promise<EngineSlideResult[]> {\n const results: EngineSlideResult[] = new Array(slides.length);\n if (cacheDir) knownCacheDirs.add(cacheDir);\n // Request-level, so every slide in this run shares one font set — which is\n // exactly why it can join the per-slide cache key without being per-slide\n // data.\n const fontsKey = fontsDigest(fonts);\n\n // 1. Dedupe by content-addressed key: identical slides build, convert, and\n // hit the cache exactly once, then fan out to every requesting index.\n const jobsByKey = new Map<string, SlideJob>();\n for (let i = 0; i < slides.length; i++) {\n const slide = slides[i];\n const key = cacheKey({\n presentation: slide.presentation,\n dpi: slide.dpi,\n baseDir,\n fontsKey,\n });\n const existing = jobsByKey.get(key);\n if (existing) {\n existing.indexes.push(i);\n } else {\n jobsByKey.set(key, {\n presentation: slide.presentation,\n dpi: slide.dpi,\n indexes: [i],\n cachePath: cacheDir ? path.join(cacheDir, `${key}.png`) : null,\n pptxPath: '',\n pdfPath: '',\n pngPrefix: '',\n });\n }\n }\n\n rasterizerCounters.dedupedRequests += slides.length - jobsByKey.size;\n\n const fail = (\n job: SlideJob,\n stage: PptxRasterizeFailureStage,\n error: string,\n cause?: unknown\n ) => {\n rasterizerCounters.failed++;\n for (const i of job.indexes)\n results[i] = { ok: false, error, stage, cause };\n };\n const succeed = (job: SlideJob, result: PptxRasterizeResult) => {\n for (const i of job.indexes) results[i] = { ok: true, ...result };\n };\n\n // 2. Resolve cache hits for the unique jobs in parallel.\n const uncached: SlideJob[] = [];\n await Promise.all(\n [...jobsByKey.values()].map(async (job) => {\n if (job.cachePath) {\n const cached = await fs.readFile(job.cachePath).catch(() => null);\n if (cached) {\n const size = parsePngSize(cached);\n if (size) {\n rasterizerCounters.diskHits++;\n succeed(job, { base64DataUri: toDataUri(cached), ...size });\n return;\n }\n // Corrupt/partial cache file (e.g. a killed prior render) — discard\n // it and re-render rather than embed a broken image.\n await fs.rm(job.cachePath, { force: true }).catch(() => {});\n }\n rasterizerCounters.diskMisses++;\n }\n uncached.push(job);\n })\n );\n if (uncached.length === 0) return results;\n\n // 3. JSON → .pptx (in-process, pure JS). A build failure is a per-slide\n // content error; the remaining slides still convert.\n const corePptx = await import('@json-to-office/core-pptx');\n const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'jto-visual-'));\n // Declared out here so the `finally` can always close it, however the try\n // exits. Assigned only after we know a soffice launch is actually needed.\n let stageHandle: FontStageHandle | null = null;\n try {\n const built: SlideJob[] = [];\n for (let j = 0; j < uncached.length; j++) {\n const job = uncached[j];\n job.pptxPath = path.join(tempDir, `slide-${j}.pptx`);\n job.pdfPath = path.join(tempDir, `slide-${j}.pdf`);\n job.pngPrefix = path.join(tempDir, `slide-${j}`);\n try {\n const pptxBuffer = await corePptx.generateBufferFromJson(\n job.presentation as any,\n { baseDir }\n );\n await fs.writeFile(job.pptxPath, pptxBuffer);\n built.push(job);\n } catch (error) {\n fail(job, 'build', errorMessage(error), error);\n }\n }\n if (built.length === 0) return results;\n\n // Stage the request's fonts for the soffice launches. Deliberately AFTER\n // the cache probe and the build loop: a request whose slides all hit the\n // disk cache, or whose decks all failed to build, launches no soffice at\n // all and must not pay for writing N font files (and, on macOS, four\n // profile trees).\n //\n // profileDirs MUST enumerate the isolated-retry profiles too. They are\n // distinct UserInstallation directories, and the macOS stager's macro is\n // only reachable from a profile it was seeded into — miss them and a\n // mid-batch soffice crash yields fontless PNGs for the salvaged slides.\n const profileDirs = [\n path.join(tempDir, 'profile'),\n ...Array.from({ length: MAX_ISOLATED_RETRIES }, (_, r) =>\n path.join(tempDir, `profile-retry-${r}`)\n ),\n ];\n const stageFonts = fonts?.length ? fromRasterizeFontFaces(fonts) : [];\n if (stageFonts.length > 0) {\n stageHandle = await getFontStager().stage(stageFonts, tempDir, {\n profileDirs,\n });\n }\n const sofficeEnv = stageHandle?.envOverrides;\n\n const [soffice, pdftoppm] = await Promise.all([\n resolveSoffice(),\n resolvePdftoppm(),\n ]);\n\n // 4. .pptx → PDF: ONE soffice launch converts every deck (the launch is\n // the multi-second cost being amortized). The timeout scales with the\n // slide count so a large batch is not misread as a hang, and the whole\n // engine run gets a deadline of one batch window + one pdftoppm window\n // per slide — bounded work no matter how conversions misbehave. The\n // PNG phase is sequential over every slide, so its share must scale\n // with the slide count like the soffice window does; otherwise a slow\n // soffice launch drains the budget and slides whose PDFs converted\n // fine fail spuriously. An HTTP client that gives up sooner falls\n // back to per-visual calls on its own.\n const batchTimeoutMs = Math.min(\n SOFFICE_TIMEOUT_MS +\n SOFFICE_BATCH_EXTRA_PER_SLIDE_MS * (built.length - 1),\n SOFFICE_BATCH_TIMEOUT_CAP_MS\n );\n const deadlineAt =\n Date.now() + batchTimeoutMs + PDFTOPPM_TIMEOUT_MS * built.length;\n const remainingMs = () => deadlineAt - Date.now();\n\n let batchError: unknown;\n try {\n await exec(\n soffice,\n sofficeArgs(\n profileDirs[0],\n tempDir,\n built.map((job) => job.pptxPath)\n ),\n batchTimeoutMs,\n sofficeEnv\n );\n } catch (error) {\n batchError = error;\n }\n\n // 5. Per-slide PDF check. soffice reports batch conversion coarsely (it\n // can skip a file or die mid-run), so the PDFs on disk are the truth.\n // A missing PDF gets an isolated single-file launch — salvaging slides\n // left unprocessed by a mid-batch crash and pinning the error on the\n // slide that caused it — but only within MAX_ISOLATED_RETRIES and the\n // deadline, and not when nothing at all converted (an environmental\n // failure that a retry would only repeat).\n const converted: SlideJob[] = [];\n const missing: SlideJob[] = [];\n for (const job of built) {\n ((await fileExists(job.pdfPath)) ? converted : missing).push(job);\n }\n\n let retriesLeft =\n missing.length > 0 &&\n built.length > 1 &&\n (batchError === undefined || converted.length > 0)\n ? MAX_ISOLATED_RETRIES\n : 0;\n for (const [r, job] of missing.entries()) {\n const retryBudget = Math.min(SOFFICE_TIMEOUT_MS, remainingMs());\n if (retriesLeft > 0 && retryBudget > 1000) {\n retriesLeft--;\n let retryError: unknown;\n try {\n await exec(\n soffice,\n // Same directories seeded in `profileDirs` above (index 0 is the\n // batch profile, so retry `r` is `profileDirs[r + 1]`); the\n // literal is kept as the fallback for an out-of-range retry.\n sofficeArgs(\n profileDirs[r + 1] ?? path.join(tempDir, `profile-retry-${r}`),\n tempDir,\n [job.pptxPath]\n ),\n retryBudget,\n sofficeEnv\n );\n } catch (error) {\n retryError = error;\n }\n if (await fileExists(job.pdfPath)) {\n converted.push(job);\n continue;\n }\n batchError ??= retryError;\n }\n const cause = batchError;\n fail(\n job,\n 'convert',\n `LibreOffice failed to convert the slide to PDF${\n cause ? `: ${errorMessage(cause)}` : '.'\n }`,\n cause\n );\n }\n\n // 6. PDF → PNG at each slide's own dpi (single page, no page suffix).\n // pdftoppm is cheap per file; each conversion still respects the\n // engine deadline so a pathological PDF cannot stack 30s timeouts.\n for (const job of converted) {\n const budget = Math.min(PDFTOPPM_TIMEOUT_MS, remainingMs());\n if (budget <= 1000) {\n fail(\n job,\n 'rasterize',\n 'Rasterization deadline exceeded before this slide could be converted to PNG.'\n );\n continue;\n }\n try {\n await exec(\n pdftoppm,\n [\n '-r',\n String(job.dpi),\n '-png',\n '-singlefile',\n job.pdfPath,\n job.pngPrefix,\n ],\n budget\n );\n const png = await fs.readFile(`${job.pngPrefix}.png`);\n const size = parsePngSize(png);\n if (!size) {\n throw new Error(\n 'Rasterization produced an invalid PNG (empty or truncated output from pdftoppm).'\n );\n }\n if (job.cachePath) {\n await writeCacheAtomic(cacheDir!, job.cachePath, png);\n }\n rasterizerCounters.rendered++;\n succeed(job, { base64DataUri: toDataUri(png), ...size });\n } catch (error) {\n fail(job, 'rasterize', errorMessage(error), error);\n }\n }\n\n return results;\n } finally {\n // Order matters: FontconfigStager.cleanup() restores write permission on\n // the staged fonts dir (stage() freezes it to 0o555), and rm cannot\n // unlink inside a non-writable directory. Reversed, the whole temp tree\n // leaks — silently, because the rm error is swallowed.\n if (stageHandle) await stageHandle.cleanup().catch(() => {});\n await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});\n }\n}\n\nfunction resolveCacheDir(options?: {\n cacheDir?: string | null;\n}): string | null {\n return options?.cacheDir === undefined\n ? path.join(os.tmpdir(), 'jto-visual-cache')\n : options.cacheDir;\n}\n\n/**\n * Build a LibreOffice-backed pptx rasterizer.\n *\n * @param options.cacheDir - directory for the content-addressed PNG cache\n * (default: <tmp>/jto-visual-cache). Pass `null` to disable caching.\n */\nexport function createLibreOfficePptxRasterizer(options?: {\n cacheDir?: string | null;\n}): PptxRasterizer {\n const cacheDir = resolveCacheDir(options);\n\n return async function rasterize(\n request: PptxRasterizeRequest\n ): Promise<PptxRasterizeResult> {\n const [result] = await rasterizeSlidesWithEngine(\n [{ presentation: request.presentation, dpi: request.dpi }],\n request.baseDir,\n cacheDir,\n request.fonts\n );\n if (!result) throw new Error('Rasterization produced no result.');\n if (!result.ok) {\n const error = new Error(result.error) as Error & { cause?: unknown };\n // Preserve the original failure (exec exit code/signal, fs error) for\n // programmatic consumers — parity with the pre-batch implementation\n // that threw the underlying error directly.\n if (result.cause !== undefined) error.cause = result.cause;\n throw error;\n }\n return {\n base64DataUri: result.base64DataUri,\n width: result.width,\n height: result.height,\n };\n };\n}\n\n/**\n * Build a LibreOffice-backed BATCH pptx rasterizer (#153): many independent\n * single-slide presentations, one soffice launch, per-slide results. Shares\n * the content-addressed disk cache with the single-slide rasterizer — the\n * per-slide cache key is identical on both paths.\n */\nexport function createLibreOfficePptxBatchRasterizer(options?: {\n cacheDir?: string | null;\n}): PptxBatchRasterizer {\n const cacheDir = resolveCacheDir(options);\n\n return async function rasterizeBatch(request) {\n const engineResults = await rasterizeSlidesWithEngine(\n request.slides.map((slide) => ({\n presentation: slide.presentation,\n dpi: slide.dpi ?? DEFAULT_VISUAL_DPI,\n })),\n request.baseDir,\n cacheDir,\n request.fonts\n );\n // Strip the engine-internal `cause` (non-serializable) from the results.\n return {\n results: engineResults.map((result) =>\n result.ok\n ? result\n : { ok: false, error: result.error, stage: result.stage }\n ),\n };\n };\n}\n","import type { FontStager, FontStageHandle, FontStageOptions } from './types';\n\nexport class NoopFontStager implements FontStager {\n // Signature matches FontStager; every parameter (including `options`) is\n // deliberately ignored on platforms with no staging mechanism.\n async stage(\n _fonts?: unknown,\n _tempDir?: string,\n _options?: FontStageOptions\n ): Promise<FontStageHandle> {\n return {\n envOverrides: {},\n cleanup: async () => {},\n };\n }\n}\n","/**\n * Linux + macOS: use fontconfig to expose staged TTFs to LibreOffice.\n *\n * Writes each resolved font to `<tempDir>/fonts/` and a minimal\n * fontconfig.xml that includes that dir plus the system font config.\n * LibreOffice honors the per-invocation FONTCONFIG_FILE env var.\n *\n * The caller removes the whole tempDir in its own finally block; `cleanup()`\n * only has to undo the read-only freeze stage() puts on the fonts dir so\n * that recursive rm can actually unlink.\n */\n\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport type { ResolvedFont } from '@json-to-office/shared';\nimport {\n synthesizeFamilyName,\n rewriteFontFamilyName,\n} from '@json-to-office/shared';\nimport type { FontStager, FontStageHandle, FontStageOptions } from './types';\nimport { nextStagingId, safeFilenamePart } from './types';\n\nconst SYSTEM_FONTS_CONF_CANDIDATES = [\n '/etc/fonts/fonts.conf',\n '/opt/homebrew/etc/fonts/fonts.conf',\n '/usr/local/etc/fonts/fonts.conf',\n];\n\nexport class FontconfigStager implements FontStager {\n async stage(\n fonts: ResolvedFont[],\n tempDir: string,\n // Ignored: fontconfig discovery is driven by FONTCONFIG_FILE, not by the\n // soffice UserInstallation profile.\n _options?: FontStageOptions\n ): Promise<FontStageHandle> {\n const id = nextStagingId();\n const fontsDir = path.join(tempDir, 'fonts');\n await fs.mkdir(fontsDir, { recursive: true });\n\n let serial = 0;\n for (const r of fonts) {\n if (r.sources.length === 0) continue;\n for (const s of r.sources) {\n serial += 1;\n const suffix = s.italic ? 'i' : 'r';\n // Rewrite `name` table so fontconfig indexes the file under the\n // synthetic sub-family (\"Inter Light\"), matching the doc's\n // `rFonts`/`fontFace` references after synthesizeFamilyName.\n //\n // The unrewritten branch (RIBBI: the run rides bold/italic toggles\n // on the base family) leans on the bytes already declaring\n // `r.family`. That is FontRegistry's `stampResolvedFamily` — do not\n // read the skip as \"this file is fine by construction\".\n const synth = synthesizeFamilyName(r.family, s.weight, s.italic);\n const data =\n synth.family === r.family\n ? s.data\n : rewriteFontFamilyName(s.data, synth.family);\n const name = `${safeFilenamePart(synth.family)}-${s.weight}${suffix}-${id}-${serial}.ttf`;\n await fs.writeFile(path.join(fontsDir, name), data);\n }\n }\n\n // Freeze the fonts dir read-only after staging so a misbehaving\n // fontconfig run (or a concurrent soffice spawn) can't corrupt the\n // file contents we just wrote. fc-cache writes its own indexes into\n // `cacheDir` (next step), which stays writable. The whole tree gets\n // rm'd by the converter in finally, so mode matters only during the\n // conversion window.\n await fs.chmod(fontsDir, 0o555).catch(() => {\n // Some filesystems (e.g. certain Windows-mounted shares under WSL)\n // refuse chmod — ignore and proceed. The defensive-in-depth case\n // still wins on native Linux/macOS.\n });\n const includeLines = await this.pickSystemIncludes();\n // Redirect fontconfig's scan cache into tempDir. Without this, fontconfig\n // writes cache entries for `fontsDir` into the user's ~/.cache/fontconfig\n // and leaves them behind after tempDir is rm'd. Per-invocation isolation\n // also prevents two concurrent conversions from racing on the same\n // fontconfig cache directory.\n const cacheDir = path.join(tempDir, 'fc-cache');\n await fs.mkdir(cacheDir, { recursive: true });\n const configPath = path.join(tempDir, 'fontconfig.xml');\n const configXml = [\n '<?xml version=\"1.0\"?>',\n '<!DOCTYPE fontconfig SYSTEM \"fonts.dtd\">',\n '<fontconfig>',\n ` <dir>${escapeXml(fontsDir)}</dir>`,\n ` <cachedir>${escapeXml(cacheDir)}</cachedir>`,\n ...includeLines,\n '</fontconfig>',\n '',\n ].join('\\n');\n await fs.writeFile(configPath, configXml, 'utf8');\n\n return {\n envOverrides: {\n FONTCONFIG_FILE: configPath,\n XDG_CACHE_HOME: cacheDir,\n },\n cleanup: async () => {\n // Restore write permission before the caller's recursive rm. stage()\n // froze fontsDir to 0o555, and you cannot unlink entries inside a\n // non-writable directory: `fs.rm(tempDir, { recursive: true })` fails\n // with EACCES and leaks the whole temp tree (both callers swallow\n // that error, so it was silent). Removing the files themselves is\n // still the caller's job.\n await fs.chmod(fontsDir, 0o755).catch(() => {});\n },\n };\n }\n\n private async pickSystemIncludes(): Promise<string[]> {\n for (const candidate of SYSTEM_FONTS_CONF_CANDIDATES) {\n try {\n await fs.access(candidate);\n return [\n ` <include ignore_missing=\"yes\">${escapeXml(candidate)}</include>`,\n ];\n } catch {\n /* try next */\n }\n }\n // Fall back to the conventional path; fontconfig will fail softly.\n return [` <include ignore_missing=\"yes\">/etc/fonts/fonts.conf</include>`];\n }\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n","/**\n * Make resolved fonts visible to the LibreOffice child process for the\n * duration of one PDF conversion, then clean up.\n *\n * Linux/macOS: fontconfig + FONTCONFIG_FILE env var.\n * Windows: GDI session registration via koffi (AddFontResourceW).\n *\n * The caller calls `stage(fonts, tempDir)` before spawning soffice, merges\n * `envOverrides` into the child process env, waits for conversion, then\n * awaits `cleanup()` regardless of success or failure.\n *\n * Two consumers today: the playground's LibreOffice PDF-preview converter\n * (`@json-to-office/jto`) and the pptx rasterizer that backs docx `visual`\n * components (`../pptx-rasterizer.ts`).\n */\n\nimport type { ResolvedFont } from '@json-to-office/shared';\n\nexport interface FontStageHandle {\n /** Merged into the child process env. Empty object if nothing to stage. */\n envOverrides: Record<string, string>;\n /** Always call in a finally block. Safe to call multiple times (idempotent). */\n cleanup(): Promise<void>;\n}\n\nexport interface FontStageOptions {\n /**\n * UserInstallation profile directories the soffice launch(es) will use.\n * The macOS stager seeds its OnStartApp Python macro into EACH of them —\n * a launch with an unseeded profile registers no fonts and silently falls\n * back to system faces. Absent → `<tempDir>/user-profile`, the\n * LibreOfficeConverterService convention.\n */\n profileDirs?: string[];\n}\n\nexport interface FontStager {\n stage(\n fonts: ResolvedFont[],\n tempDir: string,\n options?: FontStageOptions\n ): Promise<FontStageHandle>;\n}\n\n/** Per-process monotonic counter to disambiguate concurrent conversions. */\nlet counter = 0;\nexport function nextStagingId(): string {\n counter += 1;\n return `${process.pid}-${counter}`;\n}\n\n/** Sanitize a font family for use in a filename. */\nexport function safeFilenamePart(s: string): string {\n return s.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 48);\n}\n","/**\n * Windows: register staged TTFs with GDI via AddFontResourceW so the soffice\n * child process finds them at startup. Forces LibreOffice onto the GDI\n * backend via SAL_DISABLE_SKIA=1 — Skia/DirectWrite doesn't reliably pick\n * up GDI-registered fonts on recent LO builds.\n *\n * Scope, precisely: `AddFontResourceW` adds to the **session** font table, not\n * a private per-process one. That is deliberate and unavoidable here — the\n * private variant (`AddFontResourceExW` with `FR_PRIVATE`) is visible only to\n * the registering process, and the process that has to see these fonts is the\n * `soffice` CHILD. Node stays alive for the full conversion so the fonts\n * persist until cleanup, and GDI releases them on process exit if Node\n * crashes, so nothing leaks past the process.\n *\n * KNOWN LIMITATION — concurrent conversions on one Windows host share that\n * session table. Two conversions staging different bytes under the same\n * synthesized family (say two documents that each embed their own \"Inter\")\n * register two faces claiming one name, and which one GDI hands to soffice is\n * then order-dependent. Staged FILES never collide — each carries a\n * pid-plus-counter suffix — so this is a resolution ambiguity, not corruption.\n *\n * Not fixed here because the only correct fix is a host-wide lease held from\n * stage() through cleanup(), which serializes every Windows conversion; that\n * is a real throughput cost for a risk the deployed images do not carry (both\n * production containers are Linux/fontconfig, where staging is per-process via\n * FONTCONFIG_FILE and cannot collide). It bites a Windows host running\n * concurrent conversions — worth a lease if that becomes a supported topology.\n */\n\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport type { ResolvedFont } from '@json-to-office/shared';\nimport {\n synthesizeFamilyName,\n rewriteFontFamilyName,\n} from '@json-to-office/shared';\nimport type { FontStager, FontStageHandle, FontStageOptions } from './types';\nimport { nextStagingId, safeFilenamePart } from './types';\n\ntype KoffiLib = {\n func: (sig: string) => (...args: unknown[]) => number | boolean;\n};\ntype KoffiModule = {\n load: (libName: string) => KoffiLib;\n};\n\n// Lazy-load koffi so Linux/macOS don't incur the FFI init cost.\nlet cachedBindings: {\n addFont: (pathW: string) => number;\n removeFont: (pathW: string) => boolean;\n} | null = null;\n\nasync function getGdiBindings() {\n if (cachedBindings) return cachedBindings;\n const koffi = (await import('koffi')) as unknown as {\n default?: KoffiModule;\n } & KoffiModule;\n const mod = koffi.default ?? koffi;\n const gdi32 = mod.load('gdi32.dll');\n cachedBindings = {\n addFont: gdi32.func('int __stdcall AddFontResourceW(str16)') as (\n path: string\n ) => number,\n removeFont: gdi32.func('bool __stdcall RemoveFontResourceW(str16)') as (\n path: string\n ) => boolean,\n };\n return cachedBindings;\n}\n\nexport class WindowsFontStager implements FontStager {\n async stage(\n fonts: ResolvedFont[],\n tempDir: string,\n // Ignored: GDI registration is session-wide, not scoped to any one\n // LibreOffice UserInstallation profile, so the retry profiles need no\n // separate seeding the way the macOS Core Text stager's do.\n _options?: FontStageOptions\n ): Promise<FontStageHandle> {\n const id = nextStagingId();\n const fontsDir = path.join(tempDir, 'fonts');\n await fs.mkdir(fontsDir, { recursive: true });\n\n const stagedPaths: string[] = [];\n let serial = 0;\n for (const r of fonts) {\n if (r.sources.length === 0) continue;\n for (const s of r.sources) {\n serial += 1;\n const suffix = s.italic ? 'i' : 'r';\n // Rewrite the TTF's internal family name so GDI registers the\n // file under the synthetic sub-family the doc references.\n //\n // The unrewritten branch (RIBBI: the run rides bold/italic toggles\n // on the base family) leans on the bytes already declaring\n // `r.family`. That is FontRegistry's `stampResolvedFamily` — do not\n // read the skip as \"this file is fine by construction\".\n const synth = synthesizeFamilyName(r.family, s.weight, s.italic);\n const data =\n synth.family === r.family\n ? s.data\n : rewriteFontFamilyName(s.data, synth.family);\n const name = `${safeFilenamePart(synth.family)}-${s.weight}${suffix}-${id}-${serial}.ttf`;\n const fullPath = path.join(fontsDir, name);\n await fs.writeFile(fullPath, data);\n stagedPaths.push(fullPath);\n }\n }\n\n if (stagedPaths.length === 0) {\n return { envOverrides: {}, cleanup: async () => {} };\n }\n\n const { addFont, removeFont } = await getGdiBindings();\n const registered: string[] = [];\n for (const p of stagedPaths) {\n const added = addFont(p);\n if (added > 0) registered.push(p);\n }\n\n let cleaned = false;\n return {\n envOverrides: {\n // Force GDI backend so the freshly-registered fonts are visible.\n // Skia on Windows uses DirectWrite which does not reliably see\n // fonts added via AddFontResourceW.\n SAL_DISABLE_SKIA: '1',\n },\n cleanup: async () => {\n if (cleaned) return;\n cleaned = true;\n for (const p of registered) {\n try {\n removeFont(p);\n } catch {\n // Swallow — GDI will drop it on process exit anyway.\n }\n }\n },\n };\n }\n}\n","/**\n * macOS: make staged fonts visible to the soffice child process by\n * registering them *inside* soffice via a Python macro bound to the\n * `OnStartApp` event.\n *\n * Why this works on macOS 26. Apple tightened `CTFontManagerScopeSession`\n * and `kCTFontManagerScopePersistent` to require signed+notarized callers\n * (unsigned Node processes get `paramErr -50`), and `Process` scope only\n * registers fonts for the calling process — so the Node server can't\n * register fonts the soffice child sees. But Process scope DOES work from\n * inside soffice. LibreOffice for macOS bundles Python 3.12 with `ctypes`,\n * and UNO lets us bind a Python macro to application-start events. We seed\n * a per-invocation UserInstallation profile with:\n *\n * - `user/Scripts/python/JtoFontRegister.py` — a ~20-line macro that\n * reads `JTO_FONT_PATHS` from the process env and calls\n * `CTFontManagerRegisterFontsForURL(url, kScopeProcess, NULL)` via\n * ctypes for each path (Process = 1 in Core Text's scope enum).\n * - `user/registrymodifications.xcu` — binds the macro to `OnStartApp`\n * and sets `MacroSecurityLevel=0` for this ephemeral profile only.\n *\n * SECURITY INVARIANT — macro execution scope. The seeded profile disables\n * soffice's macro-security prompt (`MacroSecurityLevel=0`) so our\n * OnStartApp macro runs without a dialog. This profile MUST ONLY be used\n * to open files this server just generated (i.e., well-formed outputs\n * from `@json-to-office/core-*`). Piping user-supplied .docx/.pptx/.odt\n * through a soffice invocation that uses this stager would execute any\n * embedded VBA/Basic macros silently — an RCE primitive. If a future\n * code path ever converts user-supplied documents (e.g. \"PDF-ify my\n * upload\"), it must build a separate converter that does NOT share this\n * profile, or call soffice with `--safe-mode` / a default profile.\n *\n * The pptx rasterizer (`./pptx-rasterizer.ts`) is the second consumer and\n * upholds the same invariant: it only ever opens .pptx files this process\n * just built from `@json-to-office/core-pptx` out of the request's own\n * presentation JSON — never a user-supplied binary document.\n *\n * PROFILE DIRS. The macro is only reachable from the UserInstallation\n * profile it was seeded into, so `options.profileDirs` must list EVERY\n * profile the caller will launch soffice with. The converter uses exactly\n * one (`<tempDir>/user-profile`, the default here); the rasterizer uses a\n * batch profile plus one per isolated retry, and a launch against an\n * unseeded profile registers nothing and silently falls back.\n *\n * Flow: the converter spawns\n * `soffice --headless -env:UserInstallation=file://<tempDir>/user-profile ...`\n * with env `{ JTO_FONT_PATHS: \"<ttf1>:<ttf2>:...\" }`. LO boots, reads our\n * seeded profile, fires `OnStartApp`, runs our macro, registers each\n * staged TTF at Process scope in its own process. Font enumeration then\n * resolves the synthetic family names (`Inter Bold`, `Source Code Pro\n * Medium`, …) against those Process-scope registrations. The PDF export\n * ships the correct glyphs.\n *\n * Elegant side-effects:\n * - Nothing outside the converter's `tempDir` is touched. The user's\n * real `~/Library/Fonts` and `~/Library/Application Support/LibreOffice`\n * stay untouched.\n * - Cleanup is the converter's `fs.rm(tempDir, …)` — no orphan sweep\n * needed. If Node crashes mid-conversion, the per-invocation tempDir\n * is reaped by macOS tmpreaper (or the OS's next boot cleanup).\n * - No DYLD injection, no notarized helper, no filesystem-scan races.\n *\n * Failure mode: if LibreOffice can't run the macro for any reason\n * (macro-security policy applied at bootstrap before our XCU loads,\n * Python not present in a minimal LO build, …) the soffice process still\n * runs and produces a PDF — just with system-fallback fonts, exactly the\n * pre-fix behavior. Non-catastrophic. Diagnose via stderr: the macro\n * writes failures to stderr via `print(..., file=sys.stderr)`.\n */\n\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport type { ResolvedFont } from '@json-to-office/shared';\nimport {\n synthesizeFamilyName,\n rewriteFontFamilyName,\n} from '@json-to-office/shared';\nimport type { FontStager, FontStageHandle, FontStageOptions } from './types';\nimport { nextStagingId, safeFilenamePart } from './types';\nimport { emitDiagnostic } from '../diagnostics.js';\n\n/**\n * Python macro source. Written verbatim into\n * `<profile>/user/Scripts/python/JtoFontRegister.py`. Kept inline (rather\n * than a separate .py asset) so this TypeScript module is self-contained\n * and survives bundler reshuffles — no runtime file-system lookup for a\n * static asset.\n */\nconst PYTHON_MACRO = `# Auto-generated by @json-to-office/jto. Runs inside soffice on OnStartApp\n# to make staged fonts visible to LibreOffice's font enumeration. macOS 26\n# blocks Session/Persistent CT registration for unsigned callers, but\n# Process scope still works from inside the target process — which is\n# exactly where this macro runs.\nimport os\nimport sys\nimport ctypes\nimport ctypes.util\n\n\ndef _log(msg):\n # soffice swallows Python stdout in headless mode; stderr surfaces to\n # the parent's pipe. The stager doesn't read this today but the\n # converter logs stderr on failure, which is how we'll debug.\n sys.stderr.write(\"[jto-font-register] \" + msg + \"\\\\n\")\n\n\ndef register(*_args):\n paths_env = os.environ.get(\"JTO_FONT_PATHS\", \"\")\n if not paths_env:\n return\n try:\n cf = ctypes.CDLL(ctypes.util.find_library(\"CoreFoundation\"))\n ct = ctypes.CDLL(ctypes.util.find_library(\"CoreText\"))\n except Exception as e:\n _log(\"failed to load CoreFoundation/CoreText: \" + repr(e))\n return\n cf.CFURLCreateFromFileSystemRepresentation.argtypes = [\n ctypes.c_void_p, ctypes.c_char_p, ctypes.c_long, ctypes.c_bool,\n ]\n cf.CFURLCreateFromFileSystemRepresentation.restype = ctypes.c_void_p\n cf.CFRelease.argtypes = [ctypes.c_void_p]\n ct.CTFontManagerRegisterFontsForURL.argtypes = [\n ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p,\n ]\n ct.CTFontManagerRegisterFontsForURL.restype = ctypes.c_bool\n\n kCTFontManagerScopeProcess = 1\n registered = 0\n for p in paths_env.split(os.pathsep):\n if not p:\n continue\n try:\n b = p.encode(\"utf-8\")\n url = cf.CFURLCreateFromFileSystemRepresentation(\n None, b, len(b), False\n )\n if not url:\n _log(\"CFURL failed for \" + p)\n continue\n ok = ct.CTFontManagerRegisterFontsForURL(\n url, kCTFontManagerScopeProcess, None\n )\n cf.CFRelease(url)\n if ok:\n registered += 1\n else:\n _log(\"CT register returned false for \" + p)\n except Exception as e:\n _log(\"exception registering \" + p + \": \" + repr(e))\n _log(\"registered \" + str(registered) + \" font(s) at Process scope\")\n\n\n# Expose under \"register\" (event-binding URL) and module-level run so\n# command-line vnd.sun.star.script invocation works either way.\ng_exportedScripts = (register,)\n`;\n\n/**\n * XCU that LO merges into its Bootstrap registry on startup. Two keys:\n *\n * 1. `/org.openoffice.Office.Events/ApplicationEvents/Bindings/OnStartApp`\n * → fires our Python macro before any document is loaded, so font\n * registration completes before LO enumerates fonts for rendering.\n *\n * 2. `/org.openoffice.Office.Common/Security/Scripting/MacroSecurityLevel`\n * → 0 (allow all). This only applies to the ephemeral UserInstallation\n * profile we create under `<tempDir>/user-profile`; the user's real\n * LibreOffice profile config is untouched. Scope of the security\n * relaxation is bounded by the per-invocation profile's lifetime.\n */\nconst REGISTRY_MOD_XCU = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<oor:items xmlns:oor=\"http://openoffice.org/2001/registry\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">\n <item oor:path=\"/org.openoffice.Office.Events/ApplicationEvents/Bindings\">\n <node oor:name=\"OnStartApp\" oor:op=\"replace\">\n <prop oor:name=\"BindingURL\" oor:type=\"xs:string\">\n <value>vnd.sun.star.script:JtoFontRegister.py$register?language=Python&location=user</value>\n </prop>\n </node>\n </item>\n <item oor:path=\"/org.openoffice.Office.Common/Security/Scripting\">\n <prop oor:name=\"MacroSecurityLevel\" oor:op=\"fuse\">\n <value>0</value>\n </prop>\n </item>\n</oor:items>\n`;\n\nexport class MacOSCoreTextStager implements FontStager {\n async stage(\n fonts: ResolvedFont[],\n tempDir: string,\n options?: FontStageOptions\n ): Promise<FontStageHandle> {\n const embeddable = fonts.filter((r) => r.sources.length > 0);\n if (embeddable.length === 0) {\n return { envOverrides: {}, cleanup: async () => {} };\n }\n\n // Fonts go to a subdir of the converter's tempDir. No ~/Library writes.\n const fontsDir = path.join(tempDir, 'fonts');\n await fs.mkdir(fontsDir, { recursive: true });\n\n const id = nextStagingId();\n const fontPaths: string[] = [];\n const staged: {\n family: string;\n weight: number;\n italic: boolean;\n path: string;\n }[] = [];\n let serial = 0;\n for (const r of embeddable) {\n for (const s of r.sources) {\n serial += 1;\n const suffix = s.italic ? 'i' : 'r';\n // Rewrite the TTF's internal family name to match the synthetic\n // sub-family the doc references (e.g. \"Inter Light\"). Without\n // this, Core Text indexes the staged file as \"Inter\" and\n // LibreOffice can't resolve `rFonts w:ascii=\"Inter Light\"`.\n //\n // The unrewritten branch (RIBBI: the run rides bold/italic toggles\n // on the base family) leans on the bytes already declaring\n // `r.family`. That is FontRegistry's `stampResolvedFamily` — do not\n // read the skip as \"this file is fine by construction\".\n const synth = synthesizeFamilyName(r.family, s.weight, s.italic);\n const data =\n synth.family === r.family\n ? s.data\n : rewriteFontFamilyName(s.data, synth.family);\n const name = `${safeFilenamePart(synth.family)}-${s.weight}${suffix}-${id}-${serial}.ttf`;\n const full = path.join(fontsDir, name);\n await fs.writeFile(full, data);\n fontPaths.push(full);\n staged.push({\n family: synth.family,\n weight: s.weight,\n italic: s.italic,\n path: full,\n });\n }\n }\n\n if (process.env.JTO_DEBUG_FONTS === '1') {\n // Through the sink, never a stream: a host may own stdout as a\n // protocol channel (the MCP server does), so this package writes\n // nowhere on its own. Install a sink to see it.\n emitDiagnostic(\n '[jto macos-stager] staged ' +\n staged.length +\n ' font(s) for CT Process-scope registration; JTO_FONT_PATHS has ' +\n fontPaths.length +\n ' entries\\n' +\n staged\n .map(\n (s) =>\n ` ${s.family} (w=${s.weight}${s.italic ? ' italic' : ''}) → ${path.basename(s.path)}`\n )\n .join('\\n')\n );\n }\n\n // Seed EVERY per-invocation UserInstallation profile the caller will\n // launch with, so LO reads the macro + event binding from the exact\n // path its `-env:UserInstallation=` points at. The converter passes\n // nothing and gets `<tempDir>/user-profile`; the rasterizer passes its\n // batch profile plus every isolated-retry profile.\n const profileDirs = options?.profileDirs?.length\n ? options.profileDirs\n : [path.join(tempDir, 'user-profile')];\n for (const profileDir of profileDirs) {\n const profileUser = path.join(profileDir, 'user');\n const scriptsDir = path.join(profileUser, 'Scripts', 'python');\n await fs.mkdir(scriptsDir, { recursive: true });\n await fs.writeFile(\n path.join(scriptsDir, 'JtoFontRegister.py'),\n PYTHON_MACRO\n );\n await fs.writeFile(\n path.join(profileUser, 'registrymodifications.xcu'),\n REGISTRY_MOD_XCU\n );\n }\n\n return {\n envOverrides: {\n // Colon-separated list of staged TTF paths (`:` matches Python's\n // `os.pathsep` on macOS). The macro reads this at OnStartApp.\n JTO_FONT_PATHS: fontPaths.join(':'),\n // Force LibreOffice's Core Graphics backend. Skia on macOS can\n // skip Core Text's freshly-registered fonts in some builds.\n SAL_DISABLE_SKIA: '1',\n },\n cleanup: async () => {\n // No-op: the converter's `fs.rm(tempDir, { recursive: true })` in\n // its own finally block sweeps everything we wrote here.\n },\n };\n }\n}\n","import { AsyncLocalStorage } from 'node:async_hooks';\n\nexport type DiagnosticTone =\n | 'default'\n | 'info'\n | 'success'\n | 'warning'\n | 'error'\n | 'muted';\n\nexport type DiagnosticSink = (text: string, tone?: DiagnosticTone) => void;\n\nconst sinks = new AsyncLocalStorage<DiagnosticSink>();\n\n/**\n * Scope a sink to one operation. The CLI installs an Ink-backed one per\n * task; the MCP server installs one that collects into its response.\n */\nexport function runWithDiagnosticSink<T>(\n sink: DiagnosticSink,\n callback: () => T\n): T {\n return sinks.run(sink, callback);\n}\n\n/**\n * This package writes to no stream of its own — a host may own stdout as a\n * protocol channel — so with no sink installed the message is dropped.\n */\nexport function emitDiagnostic(\n text: string,\n tone: DiagnosticTone = 'muted'\n): void {\n sinks.getStore()?.(text, tone);\n}\n\n/**\n * Plain-text sink for hosts with no UI of their own. stderr, not stdout,\n * so it stays safe to install alongside a stdout protocol stream.\n */\nexport const stderrDiagnosticSink: DiagnosticSink = (text) => {\n process.stderr.write(`${text}\\n`);\n};\n","/**\n * Factory entry point for the font staging pipeline shared by every\n * LibreOffice launch in the toolchain: the playground's PDF preview\n * converter (`@json-to-office/jto`) and the pptx rasterizer that backs docx\n * `visual` components (`../pptx-rasterizer.ts`).\n */\n\nimport type { FontStager } from './types';\nimport { NoopFontStager } from './noop-stager';\nimport { FontconfigStager } from './fontconfig-stager';\nimport { WindowsFontStager } from './windows-stager';\nimport { MacOSCoreTextStager } from './macos-stager';\n\nexport type { FontStager, FontStageHandle, FontStageOptions } from './types';\n\nconst cached = new Map<NodeJS.Platform, FontStager>();\n\nexport function getFontStager(\n platform: NodeJS.Platform = process.platform\n): FontStager {\n const hit = cached.get(platform);\n if (hit) return hit;\n let stager: FontStager;\n switch (platform) {\n case 'win32':\n stager = new WindowsFontStager();\n break;\n case 'darwin':\n // LibreOffice-for-macOS uses Core Text for font enumeration and does\n // not honor FONTCONFIG_FILE reliably. macOS 26 further blocks\n // Session/Persistent CT registration for unsigned callers, and\n // Process scope only works inside the target process. We register\n // fonts from *inside* soffice via a Python UNO macro bound to\n // OnStartApp, seeded into a per-invocation UserInstallation profile.\n // The user's ~/Library/Fonts is never touched. See macos-stager.ts.\n stager = new MacOSCoreTextStager();\n break;\n case 'linux':\n case 'freebsd':\n case 'openbsd':\n stager = new FontconfigStager();\n break;\n default:\n stager = new NoopFontStager();\n }\n cached.set(platform, stager);\n return stager;\n}\n\nexport {\n NoopFontStager,\n FontconfigStager,\n WindowsFontStager,\n MacOSCoreTextStager,\n};\n","/**\n * Text geometry from a rendered PDF — the ground truth the quality\n * estimators are guessing at (#216 follow-up).\n *\n * The pptx preview pipeline already produces a PDF (soffice → pdftoppm) and\n * uses it purely as a bitmap source. That PDF records the exact position of\n * every glyph as laid out by LibreOffice — the same engine the quality rules\n * try to predict. `pdftotext -bbox` (poppler, already a rasterizer\n * dependency alongside pdftoppm) dumps per-word bounding boxes; this module\n * parses them into slide-space points so callers can compare a rule's\n * estimate against what the renderer actually did.\n *\n * Coordinates: PDF points (1/72 in), origin at the page's top-left corner,\n * y increasing downward — the same frame as authored inches × 72. A PDF page\n * rendered from a slide has the slide's dimensions, so word boxes compare\n * directly against authored shape geometry with no transform.\n *\n * Consumers: the quality ground-truth harness (estimator calibration) today;\n * a `rendered`-certainty analysis pass tomorrow.\n */\n\nimport { execFile } from 'child_process';\nimport * as path from 'path';\n\n/** One word as laid out on the page, in PDF points, top-left origin. */\nexport interface PdfTextWord {\n text: string;\n xMin: number;\n yMin: number;\n xMax: number;\n yMax: number;\n}\n\n/** One PDF page: its size in points plus every word poppler segmented. */\nexport interface PdfTextPage {\n widthPt: number;\n heightPt: number;\n words: PdfTextWord[];\n}\n\nconst ENTITIES: Readonly<Record<string, string>> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '"': '\"',\n ''': \"'\",\n '"': '\"',\n ''': \"'\",\n};\n\nfunction decodeEntities(value: string): string {\n return value.replace(\n /&(?:amp|lt|gt|quot|apos|#34|#39);/g,\n (entity) => ENTITIES[entity] ?? entity\n );\n}\n\nconst PAGE_PATTERN =\n /<page\\s+width=\"([\\d.]+)\"\\s+height=\"([\\d.]+)\">([\\s\\S]*?)<\\/page>/g;\nconst WORD_PATTERN =\n /<word\\s+xMin=\"(-?[\\d.]+)\"\\s+yMin=\"(-?[\\d.]+)\"\\s+xMax=\"(-?[\\d.]+)\"\\s+yMax=\"(-?[\\d.]+)\">([\\s\\S]*?)<\\/word>/g;\n\n/**\n * Parse `pdftotext -bbox` output (XHTML with `<page>`/`<word>` elements).\n * Pure — feed it a captured document for tests, or the runner's stdout.\n */\nexport function parsePdfTextBbox(bboxXml: string): PdfTextPage[] {\n const pages: PdfTextPage[] = [];\n for (const pageMatch of bboxXml.matchAll(PAGE_PATTERN)) {\n const words: PdfTextWord[] = [];\n for (const wordMatch of pageMatch[3].matchAll(WORD_PATTERN)) {\n words.push({\n xMin: Number(wordMatch[1]),\n yMin: Number(wordMatch[2]),\n xMax: Number(wordMatch[3]),\n yMax: Number(wordMatch[4]),\n text: decodeEntities(wordMatch[5]),\n });\n }\n pages.push({\n widthPt: Number(pageMatch[1]),\n heightPt: Number(pageMatch[2]),\n words,\n });\n }\n return pages;\n}\n\nfunction pdftotextCandidates(): string[] {\n const candidates: string[] = [];\n const configured = process.env.PDFTOTEXT_PATH?.trim();\n if (configured) candidates.push(configured);\n candidates.push('pdftotext');\n return [...new Set(candidates)];\n}\n\nasync function run(\n binary: string,\n args: string[],\n timeoutMs: number\n): Promise<string> {\n return new Promise((resolve, reject) => {\n execFile(\n binary,\n args,\n { timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 },\n (error, stdout) => {\n if (error) reject(error);\n else resolve(stdout);\n }\n );\n });\n}\n\n// Same memoization shape as the rasterizer's soffice/pdftoppm resolution:\n// success is cached per process, failure retries on the next call.\nlet pdftotextPromise: Promise<string> | undefined;\nasync function resolvePdftotext(): Promise<string> {\n if (!pdftotextPromise) {\n pdftotextPromise = (async () => {\n for (const candidate of pdftotextCandidates()) {\n if (candidate.includes(path.sep)) {\n try {\n await run(candidate, ['-v'], 10_000);\n return candidate;\n } catch {\n continue;\n }\n }\n try {\n await run(candidate, ['-v'], 10_000);\n return candidate;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n // pdftotext -v exits 0 on modern poppler; a non-ENOENT failure\n // still means the binary exists.\n if (code !== 'ENOENT' && code !== 'EACCES') return candidate;\n }\n }\n throw new Error(\n 'Text geometry extraction needs pdftotext (poppler), which was not ' +\n 'found. Install poppler-utils or set PDFTOTEXT_PATH ' +\n `(searched: ${pdftotextCandidates().join(', ')}).`\n );\n })().catch((error) => {\n pdftotextPromise = undefined;\n throw error;\n });\n }\n return pdftotextPromise;\n}\n\n/** True when a `pdftotext` binary is reachable — lets harnesses skip early. */\nexport async function pdftotextAvailable(): Promise<boolean> {\n try {\n await resolvePdftotext();\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Extract per-word text geometry from a PDF on disk. One pdftotext spawn,\n * output streamed through stdout — nothing else touches the filesystem.\n */\nexport async function extractPdfTextGeometry(\n pdfPath: string,\n options: { timeoutMs?: number } = {}\n): Promise<PdfTextPage[]> {\n const binary = await resolvePdftotext();\n const stdout = await run(\n binary,\n ['-bbox', pdfPath, '-'],\n options.timeoutMs ?? 60_000\n );\n return parsePdfTextBbox(stdout);\n}\n"],"mappings":"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","names":["path","fs","fs","path","fs","path","synthesizeFamilyName","rewriteFontFamilyName","path","fs","synthesizeFamilyName","rewriteFontFamilyName","fs","path","synthesizeFamilyName","rewriteFontFamilyName","path","fs","synthesizeFamilyName","rewriteFontFamilyName","resolve","path","fs","cached","execFile","path","resolve"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@json-to-office/jto-ops",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "JSON to Office host operations - format adapters, LibreOffice rasterizer and font staging, free of terminal dependencies",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"koffi": "^2.16.1",
|
|
21
|
-
"@json-to-office/core-docx": "^
|
|
22
|
-
"@json-to-office/core-pptx": "^
|
|
23
|
-
"@json-to-office/quality": "^
|
|
24
|
-
"@json-to-office/shared": "^
|
|
25
|
-
"@json-to-office/shared-docx": "^
|
|
26
|
-
"@json-to-office/shared-pptx": "^
|
|
21
|
+
"@json-to-office/core-docx": "^3.0.0",
|
|
22
|
+
"@json-to-office/core-pptx": "^3.0.0",
|
|
23
|
+
"@json-to-office/quality": "^3.0.0",
|
|
24
|
+
"@json-to-office/shared": "^3.0.0",
|
|
25
|
+
"@json-to-office/shared-docx": "^3.0.0",
|
|
26
|
+
"@json-to-office/shared-pptx": "^3.0.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "20.11.0",
|