@json-to-office/jto-ops 1.2.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/LICENSE +18 -0
- package/README.md +37 -0
- package/dist/index.d.ts +412 -0
- package/dist/index.js +1460 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wiseair srl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
12
|
+
portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
15
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
16
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
18
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @json-to-office/jto-ops
|
|
2
|
+
|
|
3
|
+
Host-side operations layer for [json-to-office](https://github.com/Wiseair-srl/json-to-office): the format adapters that validate and generate documents, the LibreOffice rasterizer behind the docx `visual` component, and the per-platform LibreOffice font stagers.
|
|
4
|
+
|
|
5
|
+
Extracted from [`@json-to-office/jto-cli`](https://www.npmjs.com/package/@json-to-office/jto-cli) so a host without a terminal — the MCP server, a serverless function, a job runner — can do the work without installing ink, react, commander or chalk. The CLI re-exports everything here, so nothing changes for its consumers.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
createAdapter,
|
|
10
|
+
createLibreOfficePptxRasterizer,
|
|
11
|
+
getFontStager,
|
|
12
|
+
} from '@json-to-office/jto-ops';
|
|
13
|
+
|
|
14
|
+
const adapter = createAdapter('docx');
|
|
15
|
+
const result = await adapter.generate(document, { output: 'out.docx' });
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Diagnostics
|
|
19
|
+
|
|
20
|
+
This package writes to no stream of its own — a host may own stdout as a protocol channel. Warnings and traces go to a sink you scope around the call:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import {
|
|
24
|
+
runWithDiagnosticSink,
|
|
25
|
+
stderrDiagnosticSink,
|
|
26
|
+
} from '@json-to-office/jto-ops';
|
|
27
|
+
|
|
28
|
+
await runWithDiagnosticSink(stderrDiagnosticSink, () =>
|
|
29
|
+
adapter.generate(document, options)
|
|
30
|
+
);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
With no sink installed the messages are dropped. The CLI installs an Ink-backed sink per task; `stderrDiagnosticSink` is the plain-text fallback for hosts with no UI.
|
|
34
|
+
|
|
35
|
+
## License
|
|
36
|
+
|
|
37
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { FontRuntimeOpts, GenerationWarning, PptxBatchRasterizer, PptxRasterizer, ResolvedFont } from '@json-to-office/shared';
|
|
2
|
+
|
|
3
|
+
type FormatName = 'docx' | 'pptx';
|
|
4
|
+
interface GeneratorOptions {
|
|
5
|
+
theme?: string | any;
|
|
6
|
+
themePath?: string;
|
|
7
|
+
customThemes?: Record<string, any>;
|
|
8
|
+
validation?: {
|
|
9
|
+
strict?: boolean;
|
|
10
|
+
allowUnknownFields?: boolean;
|
|
11
|
+
};
|
|
12
|
+
fonts?: FontRuntimeOpts;
|
|
13
|
+
deterministic?: boolean;
|
|
14
|
+
generatedAt?: string | Date;
|
|
15
|
+
/**
|
|
16
|
+
* Directory that relative asset paths in the document resolve against —
|
|
17
|
+
* normally the input document's own directory (#142).
|
|
18
|
+
*/
|
|
19
|
+
baseDir?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Backend that turns the compiled document into bytes.
|
|
22
|
+
*
|
|
23
|
+
* Format-specific and validated by the core's renderer registry, which is
|
|
24
|
+
* why this is a bare string here: naming the id union would make this
|
|
25
|
+
* package import both cores statically, and they are loaded on demand.
|
|
26
|
+
* Undefined means the format's default (`docxjs` / `pptxgenjs`).
|
|
27
|
+
*/
|
|
28
|
+
renderer?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Optional sink for structured generation warnings (FONT_UNRESOLVED and
|
|
31
|
+
* friends). Mirrors core-docx's `JsonGenerationOptions.warnings`, and is the
|
|
32
|
+
* only delivery mechanism that works off the CLI: `emitGenerationWarnings`
|
|
33
|
+
* routes through an AsyncLocalStorage sink that is a no-op on the server.
|
|
34
|
+
*
|
|
35
|
+
* Adapters PUSH into it; they never replace it. Warnings therefore
|
|
36
|
+
* ACCUMULATE across repeated `generateBuffer` calls on one
|
|
37
|
+
* `GeneratorResult` — allocate one array per logical request.
|
|
38
|
+
*/
|
|
39
|
+
warnings?: GenerationWarning[];
|
|
40
|
+
}
|
|
41
|
+
interface GeneratorResult {
|
|
42
|
+
generateBuffer: (document: any) => Promise<Buffer>;
|
|
43
|
+
/**
|
|
44
|
+
* Post-expansion standard JSON tree without any rendering work — no fonts,
|
|
45
|
+
* no layout, no visual rasterization. Present when the underlying generator
|
|
46
|
+
* supports it (plugin-aware DOCX generation does).
|
|
47
|
+
*/
|
|
48
|
+
getStandardDefinition?: (config: any) => Promise<any>;
|
|
49
|
+
hasPlugins: boolean;
|
|
50
|
+
pluginNames: string[];
|
|
51
|
+
/**
|
|
52
|
+
* Identity of the theme this generator forces on every document, or
|
|
53
|
+
* undefined when nothing was requested and each document's own `props.theme`
|
|
54
|
+
* decides. Reported by the CLI so the summary names what actually rendered.
|
|
55
|
+
*/
|
|
56
|
+
themeLabel?: string;
|
|
57
|
+
}
|
|
58
|
+
interface FormatAdapter {
|
|
59
|
+
name: FormatName;
|
|
60
|
+
extension: string;
|
|
61
|
+
label: string;
|
|
62
|
+
defaultPort: number;
|
|
63
|
+
generateBuffer(json: unknown, options: GeneratorOptions): Promise<Buffer>;
|
|
64
|
+
createGenerator(plugins: any[], options: GeneratorOptions): Promise<GeneratorResult>;
|
|
65
|
+
parseJson(input: string | object): unknown;
|
|
66
|
+
validateDocument(doc: unknown): {
|
|
67
|
+
valid: boolean;
|
|
68
|
+
errors?: any[];
|
|
69
|
+
};
|
|
70
|
+
generateSchema(options?: any): any;
|
|
71
|
+
getBuiltinThemes(): Record<string, any>;
|
|
72
|
+
resolveTheme(options: GeneratorOptions): Promise<any>;
|
|
73
|
+
loadCustomThemes(options: GeneratorOptions): Promise<Record<string, any> | undefined>;
|
|
74
|
+
/**
|
|
75
|
+
* Renderer ids this format registers, defaults first.
|
|
76
|
+
*
|
|
77
|
+
* Async because the core that owns the registry is imported on demand — the
|
|
78
|
+
* list is read from it rather than repeated here, so the two cannot drift.
|
|
79
|
+
*/
|
|
80
|
+
rendererIds(): Promise<readonly string[]>;
|
|
81
|
+
/** Cumulative visual pre-pass dedupe counters (DOCX only) (#156). */
|
|
82
|
+
getVisualPrepassStats?(): Promise<any>;
|
|
83
|
+
/** Reset per-format cache observability counters (DOCX only). */
|
|
84
|
+
resetCacheStats?(): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
declare class DocxFormatAdapter implements FormatAdapter {
|
|
87
|
+
name: FormatName;
|
|
88
|
+
extension: string;
|
|
89
|
+
label: string;
|
|
90
|
+
defaultPort: number;
|
|
91
|
+
rendererIds(): Promise<readonly string[]>;
|
|
92
|
+
generateBuffer(json: unknown, options: GeneratorOptions): Promise<Buffer>;
|
|
93
|
+
createGenerator(plugins: any[], options: GeneratorOptions): Promise<GeneratorResult>;
|
|
94
|
+
parseJson(input: string | object): unknown;
|
|
95
|
+
validateDocument(doc: unknown): {
|
|
96
|
+
valid: boolean;
|
|
97
|
+
errors?: any[];
|
|
98
|
+
};
|
|
99
|
+
generateSchema(_options?: any): any;
|
|
100
|
+
getBuiltinThemes(): Record<string, any>;
|
|
101
|
+
resolveTheme(options: GeneratorOptions): Promise<any>;
|
|
102
|
+
/**
|
|
103
|
+
* Resolve `theme`/`themePath` once for a whole run: `themePath` is read a
|
|
104
|
+
* single time and feeds both the requested theme and the custom-theme
|
|
105
|
+
* registry, so a bad path warns once instead of once per consumer.
|
|
106
|
+
*/
|
|
107
|
+
private resolveThemes;
|
|
108
|
+
loadCustomThemes(options: GeneratorOptions): Promise<Record<string, any> | undefined>;
|
|
109
|
+
getVisualPrepassStats(): Promise<any>;
|
|
110
|
+
resetCacheStats(): Promise<void>;
|
|
111
|
+
}
|
|
112
|
+
declare class PptxFormatAdapter implements FormatAdapter {
|
|
113
|
+
name: FormatName;
|
|
114
|
+
extension: string;
|
|
115
|
+
label: string;
|
|
116
|
+
defaultPort: number;
|
|
117
|
+
rendererIds(): Promise<readonly string[]>;
|
|
118
|
+
generateBuffer(json: unknown, options: GeneratorOptions): Promise<Buffer>;
|
|
119
|
+
createGenerator(plugins: any[], options: GeneratorOptions): Promise<GeneratorResult>;
|
|
120
|
+
parseJson(input: string | object): unknown;
|
|
121
|
+
validateDocument(doc: unknown): {
|
|
122
|
+
valid: boolean;
|
|
123
|
+
errors?: any[];
|
|
124
|
+
};
|
|
125
|
+
generateSchema(_options?: any): any;
|
|
126
|
+
getBuiltinThemes(): Record<string, any>;
|
|
127
|
+
resolveTheme(options: GeneratorOptions): Promise<any>;
|
|
128
|
+
/**
|
|
129
|
+
* Resolve `theme`/`themePath` once for a whole run: `themePath` is read a
|
|
130
|
+
* single time and feeds both the requested theme and the custom-theme
|
|
131
|
+
* registry, so a bad path warns once instead of once per consumer.
|
|
132
|
+
*/
|
|
133
|
+
private resolveThemes;
|
|
134
|
+
loadCustomThemes(options: GeneratorOptions): Promise<Record<string, any> | undefined>;
|
|
135
|
+
}
|
|
136
|
+
declare function createAdapter(format: FormatName): FormatAdapter;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* PPTX rasterizer — the concrete service backing docx `visual` components.
|
|
140
|
+
*
|
|
141
|
+
* Pipeline: presentation JSON → (core-pptx) .pptx → (LibreOffice) PDF →
|
|
142
|
+
* (poppler/pdftoppm) PNG. Returns a base64 data URI plus the natural pixel
|
|
143
|
+
* dimensions. Results are content-addressed and cached on disk so repeated
|
|
144
|
+
* builds of an unchanged visual skip the (multi-second) LibreOffice run.
|
|
145
|
+
*
|
|
146
|
+
* Single and batch rasterization share one engine (#153). A batch keeps one
|
|
147
|
+
* .pptx per slide and converts them all in a single `soffice` launch — the
|
|
148
|
+
* launch is the dominant cost, and per-file conversion keeps slides fully
|
|
149
|
+
* independent: each has its own PDF and PNG (no page↔slide index mapping),
|
|
150
|
+
* its own dpi, and a cache key identical to the single-slide path, so both
|
|
151
|
+
* paths share the same disk cache.
|
|
152
|
+
*
|
|
153
|
+
* A request may carry `fonts`: base64 font faces that are staged for the
|
|
154
|
+
* soffice launches (via the shared FontStager pipeline) so the slide renders
|
|
155
|
+
* with the document's real families instead of whatever the host happens to
|
|
156
|
+
* have installed. Those fonts are part of the disk-cache key — the same
|
|
157
|
+
* slide is genuinely different pixels with and without them, and the cache
|
|
158
|
+
* is shared process-wide across callers.
|
|
159
|
+
*
|
|
160
|
+
* Every engine run works against a wall-clock deadline (one batch-scaled
|
|
161
|
+
* soffice window plus one pdftoppm window) so a wedged conversion fails the
|
|
162
|
+
* remaining slides quickly instead of holding the caller — and its
|
|
163
|
+
* concurrency slot — for minutes.
|
|
164
|
+
*
|
|
165
|
+
* This is injected via `services.pptx.render` / `services.pptx.renderBatch`;
|
|
166
|
+
* the published engine packages never depend on these binaries.
|
|
167
|
+
*/
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Process-wide rasterizer disk-cache counters (#156).
|
|
171
|
+
*/
|
|
172
|
+
interface RasterizerCacheStats {
|
|
173
|
+
/** Slides served from the content-addressed PNG disk cache. */
|
|
174
|
+
diskHits: number;
|
|
175
|
+
/** Unique slides that missed the disk cache and needed the engine. */
|
|
176
|
+
diskMisses: number;
|
|
177
|
+
/** diskHits / (diskHits + diskMisses), 0 when no lookups. */
|
|
178
|
+
hitRate: number;
|
|
179
|
+
/** Requests resolved by batch-internal dedupe (duplicate slides). */
|
|
180
|
+
dedupedRequests: number;
|
|
181
|
+
/** Slides successfully rendered by the engine (LibreOffice + pdftoppm). */
|
|
182
|
+
rendered: number;
|
|
183
|
+
/** Slides that failed at any engine stage. */
|
|
184
|
+
failed: number;
|
|
185
|
+
/** PNG files currently in the disk cache directories. */
|
|
186
|
+
entries: number;
|
|
187
|
+
/** Total bytes of those PNG files. */
|
|
188
|
+
bytes: number;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get rasterizer cache statistics: process-lifetime counters plus a live
|
|
192
|
+
* scan of the disk cache directories (default dir included, so entries from
|
|
193
|
+
* previous processes are visible too).
|
|
194
|
+
*/
|
|
195
|
+
declare function getRasterizerCacheStats(): Promise<RasterizerCacheStats>;
|
|
196
|
+
/**
|
|
197
|
+
* Delete every cached PNG in the known cache directories (default dir
|
|
198
|
+
* included) and reset the counters. Backs "Clear all caches" (#156) — the
|
|
199
|
+
* disk cache used to survive it.
|
|
200
|
+
*/
|
|
201
|
+
declare function clearRasterizerCache(): Promise<void>;
|
|
202
|
+
/**
|
|
203
|
+
* Build a LibreOffice-backed pptx rasterizer.
|
|
204
|
+
*
|
|
205
|
+
* @param options.cacheDir - directory for the content-addressed PNG cache
|
|
206
|
+
* (default: <tmp>/jto-visual-cache). Pass `null` to disable caching.
|
|
207
|
+
*/
|
|
208
|
+
declare function createLibreOfficePptxRasterizer(options?: {
|
|
209
|
+
cacheDir?: string | null;
|
|
210
|
+
}): PptxRasterizer;
|
|
211
|
+
/**
|
|
212
|
+
* Build a LibreOffice-backed BATCH pptx rasterizer (#153): many independent
|
|
213
|
+
* single-slide presentations, one soffice launch, per-slide results. Shares
|
|
214
|
+
* the content-addressed disk cache with the single-slide rasterizer — the
|
|
215
|
+
* per-slide cache key is identical on both paths.
|
|
216
|
+
*/
|
|
217
|
+
declare function createLibreOfficePptxBatchRasterizer(options?: {
|
|
218
|
+
cacheDir?: string | null;
|
|
219
|
+
}): PptxBatchRasterizer;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Make resolved fonts visible to the LibreOffice child process for the
|
|
223
|
+
* duration of one PDF conversion, then clean up.
|
|
224
|
+
*
|
|
225
|
+
* Linux/macOS: fontconfig + FONTCONFIG_FILE env var.
|
|
226
|
+
* Windows: GDI session registration via koffi (AddFontResourceW).
|
|
227
|
+
*
|
|
228
|
+
* The caller calls `stage(fonts, tempDir)` before spawning soffice, merges
|
|
229
|
+
* `envOverrides` into the child process env, waits for conversion, then
|
|
230
|
+
* awaits `cleanup()` regardless of success or failure.
|
|
231
|
+
*
|
|
232
|
+
* Two consumers today: the playground's LibreOffice PDF-preview converter
|
|
233
|
+
* (`@json-to-office/jto`) and the pptx rasterizer that backs docx `visual`
|
|
234
|
+
* components (`../pptx-rasterizer.ts`).
|
|
235
|
+
*/
|
|
236
|
+
|
|
237
|
+
interface FontStageHandle {
|
|
238
|
+
/** Merged into the child process env. Empty object if nothing to stage. */
|
|
239
|
+
envOverrides: Record<string, string>;
|
|
240
|
+
/** Always call in a finally block. Safe to call multiple times (idempotent). */
|
|
241
|
+
cleanup(): Promise<void>;
|
|
242
|
+
}
|
|
243
|
+
interface FontStageOptions {
|
|
244
|
+
/**
|
|
245
|
+
* UserInstallation profile directories the soffice launch(es) will use.
|
|
246
|
+
* The macOS stager seeds its OnStartApp Python macro into EACH of them —
|
|
247
|
+
* a launch with an unseeded profile registers no fonts and silently falls
|
|
248
|
+
* back to system faces. Absent → `<tempDir>/user-profile`, the
|
|
249
|
+
* LibreOfficeConverterService convention.
|
|
250
|
+
*/
|
|
251
|
+
profileDirs?: string[];
|
|
252
|
+
}
|
|
253
|
+
interface FontStager {
|
|
254
|
+
stage(fonts: ResolvedFont[], tempDir: string, options?: FontStageOptions): Promise<FontStageHandle>;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
declare class NoopFontStager implements FontStager {
|
|
258
|
+
stage(_fonts?: unknown, _tempDir?: string, _options?: FontStageOptions): Promise<FontStageHandle>;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Linux + macOS: use fontconfig to expose staged TTFs to LibreOffice.
|
|
263
|
+
*
|
|
264
|
+
* Writes each resolved font to `<tempDir>/fonts/` and a minimal
|
|
265
|
+
* fontconfig.xml that includes that dir plus the system font config.
|
|
266
|
+
* LibreOffice honors the per-invocation FONTCONFIG_FILE env var.
|
|
267
|
+
*
|
|
268
|
+
* The caller removes the whole tempDir in its own finally block; `cleanup()`
|
|
269
|
+
* only has to undo the read-only freeze stage() puts on the fonts dir so
|
|
270
|
+
* that recursive rm can actually unlink.
|
|
271
|
+
*/
|
|
272
|
+
|
|
273
|
+
declare class FontconfigStager implements FontStager {
|
|
274
|
+
stage(fonts: ResolvedFont[], tempDir: string, _options?: FontStageOptions): Promise<FontStageHandle>;
|
|
275
|
+
private pickSystemIncludes;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Windows: register staged TTFs with GDI via AddFontResourceW so the soffice
|
|
280
|
+
* child process finds them at startup. Forces LibreOffice onto the GDI
|
|
281
|
+
* backend via SAL_DISABLE_SKIA=1 — Skia/DirectWrite doesn't reliably pick
|
|
282
|
+
* up GDI-registered fonts on recent LO builds.
|
|
283
|
+
*
|
|
284
|
+
* Scope, precisely: `AddFontResourceW` adds to the **session** font table, not
|
|
285
|
+
* a private per-process one. That is deliberate and unavoidable here — the
|
|
286
|
+
* private variant (`AddFontResourceExW` with `FR_PRIVATE`) is visible only to
|
|
287
|
+
* the registering process, and the process that has to see these fonts is the
|
|
288
|
+
* `soffice` CHILD. Node stays alive for the full conversion so the fonts
|
|
289
|
+
* persist until cleanup, and GDI releases them on process exit if Node
|
|
290
|
+
* crashes, so nothing leaks past the process.
|
|
291
|
+
*
|
|
292
|
+
* KNOWN LIMITATION — concurrent conversions on one Windows host share that
|
|
293
|
+
* session table. Two conversions staging different bytes under the same
|
|
294
|
+
* synthesized family (say two documents that each embed their own "Inter")
|
|
295
|
+
* register two faces claiming one name, and which one GDI hands to soffice is
|
|
296
|
+
* then order-dependent. Staged FILES never collide — each carries a
|
|
297
|
+
* pid-plus-counter suffix — so this is a resolution ambiguity, not corruption.
|
|
298
|
+
*
|
|
299
|
+
* Not fixed here because the only correct fix is a host-wide lease held from
|
|
300
|
+
* stage() through cleanup(), which serializes every Windows conversion; that
|
|
301
|
+
* is a real throughput cost for a risk the deployed images do not carry (both
|
|
302
|
+
* production containers are Linux/fontconfig, where staging is per-process via
|
|
303
|
+
* FONTCONFIG_FILE and cannot collide). It bites a Windows host running
|
|
304
|
+
* concurrent conversions — worth a lease if that becomes a supported topology.
|
|
305
|
+
*/
|
|
306
|
+
|
|
307
|
+
declare class WindowsFontStager implements FontStager {
|
|
308
|
+
stage(fonts: ResolvedFont[], tempDir: string, _options?: FontStageOptions): Promise<FontStageHandle>;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* macOS: make staged fonts visible to the soffice child process by
|
|
313
|
+
* registering them *inside* soffice via a Python macro bound to the
|
|
314
|
+
* `OnStartApp` event.
|
|
315
|
+
*
|
|
316
|
+
* Why this works on macOS 26. Apple tightened `CTFontManagerScopeSession`
|
|
317
|
+
* and `kCTFontManagerScopePersistent` to require signed+notarized callers
|
|
318
|
+
* (unsigned Node processes get `paramErr -50`), and `Process` scope only
|
|
319
|
+
* registers fonts for the calling process — so the Node server can't
|
|
320
|
+
* register fonts the soffice child sees. But Process scope DOES work from
|
|
321
|
+
* inside soffice. LibreOffice for macOS bundles Python 3.12 with `ctypes`,
|
|
322
|
+
* and UNO lets us bind a Python macro to application-start events. We seed
|
|
323
|
+
* a per-invocation UserInstallation profile with:
|
|
324
|
+
*
|
|
325
|
+
* - `user/Scripts/python/JtoFontRegister.py` — a ~20-line macro that
|
|
326
|
+
* reads `JTO_FONT_PATHS` from the process env and calls
|
|
327
|
+
* `CTFontManagerRegisterFontsForURL(url, kScopeProcess, NULL)` via
|
|
328
|
+
* ctypes for each path (Process = 1 in Core Text's scope enum).
|
|
329
|
+
* - `user/registrymodifications.xcu` — binds the macro to `OnStartApp`
|
|
330
|
+
* and sets `MacroSecurityLevel=0` for this ephemeral profile only.
|
|
331
|
+
*
|
|
332
|
+
* SECURITY INVARIANT — macro execution scope. The seeded profile disables
|
|
333
|
+
* soffice's macro-security prompt (`MacroSecurityLevel=0`) so our
|
|
334
|
+
* OnStartApp macro runs without a dialog. This profile MUST ONLY be used
|
|
335
|
+
* to open files this server just generated (i.e., well-formed outputs
|
|
336
|
+
* from `@json-to-office/core-*`). Piping user-supplied .docx/.pptx/.odt
|
|
337
|
+
* through a soffice invocation that uses this stager would execute any
|
|
338
|
+
* embedded VBA/Basic macros silently — an RCE primitive. If a future
|
|
339
|
+
* code path ever converts user-supplied documents (e.g. "PDF-ify my
|
|
340
|
+
* upload"), it must build a separate converter that does NOT share this
|
|
341
|
+
* profile, or call soffice with `--safe-mode` / a default profile.
|
|
342
|
+
*
|
|
343
|
+
* The pptx rasterizer (`./pptx-rasterizer.ts`) is the second consumer and
|
|
344
|
+
* upholds the same invariant: it only ever opens .pptx files this process
|
|
345
|
+
* just built from `@json-to-office/core-pptx` out of the request's own
|
|
346
|
+
* presentation JSON — never a user-supplied binary document.
|
|
347
|
+
*
|
|
348
|
+
* PROFILE DIRS. The macro is only reachable from the UserInstallation
|
|
349
|
+
* profile it was seeded into, so `options.profileDirs` must list EVERY
|
|
350
|
+
* profile the caller will launch soffice with. The converter uses exactly
|
|
351
|
+
* one (`<tempDir>/user-profile`, the default here); the rasterizer uses a
|
|
352
|
+
* batch profile plus one per isolated retry, and a launch against an
|
|
353
|
+
* unseeded profile registers nothing and silently falls back.
|
|
354
|
+
*
|
|
355
|
+
* Flow: the converter spawns
|
|
356
|
+
* `soffice --headless -env:UserInstallation=file://<tempDir>/user-profile ...`
|
|
357
|
+
* with env `{ JTO_FONT_PATHS: "<ttf1>:<ttf2>:..." }`. LO boots, reads our
|
|
358
|
+
* seeded profile, fires `OnStartApp`, runs our macro, registers each
|
|
359
|
+
* staged TTF at Process scope in its own process. Font enumeration then
|
|
360
|
+
* resolves the synthetic family names (`Inter Bold`, `Source Code Pro
|
|
361
|
+
* Medium`, …) against those Process-scope registrations. The PDF export
|
|
362
|
+
* ships the correct glyphs.
|
|
363
|
+
*
|
|
364
|
+
* Elegant side-effects:
|
|
365
|
+
* - Nothing outside the converter's `tempDir` is touched. The user's
|
|
366
|
+
* real `~/Library/Fonts` and `~/Library/Application Support/LibreOffice`
|
|
367
|
+
* stay untouched.
|
|
368
|
+
* - Cleanup is the converter's `fs.rm(tempDir, …)` — no orphan sweep
|
|
369
|
+
* needed. If Node crashes mid-conversion, the per-invocation tempDir
|
|
370
|
+
* is reaped by macOS tmpreaper (or the OS's next boot cleanup).
|
|
371
|
+
* - No DYLD injection, no notarized helper, no filesystem-scan races.
|
|
372
|
+
*
|
|
373
|
+
* Failure mode: if LibreOffice can't run the macro for any reason
|
|
374
|
+
* (macro-security policy applied at bootstrap before our XCU loads,
|
|
375
|
+
* Python not present in a minimal LO build, …) the soffice process still
|
|
376
|
+
* runs and produces a PDF — just with system-fallback fonts, exactly the
|
|
377
|
+
* pre-fix behavior. Non-catastrophic. Diagnose via stderr: the macro
|
|
378
|
+
* writes failures to stderr via `print(..., file=sys.stderr)`.
|
|
379
|
+
*/
|
|
380
|
+
|
|
381
|
+
declare class MacOSCoreTextStager implements FontStager {
|
|
382
|
+
stage(fonts: ResolvedFont[], tempDir: string, options?: FontStageOptions): Promise<FontStageHandle>;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Factory entry point for the font staging pipeline shared by every
|
|
387
|
+
* LibreOffice launch in the toolchain: the playground's PDF preview
|
|
388
|
+
* converter (`@json-to-office/jto`) and the pptx rasterizer that backs docx
|
|
389
|
+
* `visual` components (`../pptx-rasterizer.ts`).
|
|
390
|
+
*/
|
|
391
|
+
|
|
392
|
+
declare function getFontStager(platform?: NodeJS.Platform): FontStager;
|
|
393
|
+
|
|
394
|
+
type DiagnosticTone = 'default' | 'info' | 'success' | 'warning' | 'error' | 'muted';
|
|
395
|
+
type DiagnosticSink = (text: string, tone?: DiagnosticTone) => void;
|
|
396
|
+
/**
|
|
397
|
+
* Scope a sink to one operation. The CLI installs an Ink-backed one per
|
|
398
|
+
* task; the MCP server installs one that collects into its response.
|
|
399
|
+
*/
|
|
400
|
+
declare function runWithDiagnosticSink<T>(sink: DiagnosticSink, callback: () => T): T;
|
|
401
|
+
/**
|
|
402
|
+
* This package writes to no stream of its own — a host may own stdout as a
|
|
403
|
+
* protocol channel — so with no sink installed the message is dropped.
|
|
404
|
+
*/
|
|
405
|
+
declare function emitDiagnostic(text: string, tone?: DiagnosticTone): void;
|
|
406
|
+
/**
|
|
407
|
+
* Plain-text sink for hosts with no UI of their own. stderr, not stdout,
|
|
408
|
+
* so it stays safe to install alongside a stdout protocol stream.
|
|
409
|
+
*/
|
|
410
|
+
declare const stderrDiagnosticSink: DiagnosticSink;
|
|
411
|
+
|
|
412
|
+
export { type DiagnosticSink, type DiagnosticTone, DocxFormatAdapter, type FontStageHandle, type FontStageOptions, type FontStager, FontconfigStager, type FormatAdapter, type FormatName, type GeneratorOptions, type GeneratorResult, MacOSCoreTextStager, NoopFontStager, PptxFormatAdapter, type RasterizerCacheStats, WindowsFontStager, clearRasterizerCache, createAdapter, createLibreOfficePptxBatchRasterizer, createLibreOfficePptxRasterizer, emitDiagnostic, getFontStager, getRasterizerCacheStats, runWithDiagnosticSink, stderrDiagnosticSink };
|