@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/dist/index.js ADDED
@@ -0,0 +1,1460 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/format-adapter.ts
9
+ import * as path5 from "path";
10
+ import * as fs5 from "fs";
11
+ import { validatePresentationDocument } from "@json-to-office/shared-pptx";
12
+ import { validate as validateDocx } from "@json-to-office/shared-docx";
13
+
14
+ // src/pptx-rasterizer.ts
15
+ import { execFile } from "child_process";
16
+ import { promises as fs4 } from "fs";
17
+ import os from "os";
18
+ import path4 from "path";
19
+ import crypto from "crypto";
20
+ import {
21
+ DEFAULT_VISUAL_DPI
22
+ } from "@json-to-office/shared";
23
+ import { fromRasterizeFontFaces } from "@json-to-office/shared/fonts/node";
24
+
25
+ // src/font-staging/noop-stager.ts
26
+ var NoopFontStager = class {
27
+ // Signature matches FontStager; every parameter (including `options`) is
28
+ // deliberately ignored on platforms with no staging mechanism.
29
+ async stage(_fonts, _tempDir, _options) {
30
+ return {
31
+ envOverrides: {},
32
+ cleanup: async () => {
33
+ }
34
+ };
35
+ }
36
+ };
37
+
38
+ // src/font-staging/fontconfig-stager.ts
39
+ import { promises as fs } from "fs";
40
+ import path from "path";
41
+ import {
42
+ synthesizeFamilyName,
43
+ rewriteFontFamilyName
44
+ } from "@json-to-office/shared";
45
+
46
+ // src/font-staging/types.ts
47
+ var counter = 0;
48
+ function nextStagingId() {
49
+ counter += 1;
50
+ return `${process.pid}-${counter}`;
51
+ }
52
+ function safeFilenamePart(s) {
53
+ return s.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 48);
54
+ }
55
+
56
+ // src/font-staging/fontconfig-stager.ts
57
+ var SYSTEM_FONTS_CONF_CANDIDATES = [
58
+ "/etc/fonts/fonts.conf",
59
+ "/opt/homebrew/etc/fonts/fonts.conf",
60
+ "/usr/local/etc/fonts/fonts.conf"
61
+ ];
62
+ var FontconfigStager = class {
63
+ async stage(fonts, tempDir, _options) {
64
+ const id = nextStagingId();
65
+ const fontsDir = path.join(tempDir, "fonts");
66
+ await fs.mkdir(fontsDir, { recursive: true });
67
+ let serial = 0;
68
+ for (const r of fonts) {
69
+ if (r.sources.length === 0) continue;
70
+ for (const s of r.sources) {
71
+ serial += 1;
72
+ const suffix = s.italic ? "i" : "r";
73
+ const synth = synthesizeFamilyName(r.family, s.weight, s.italic);
74
+ const data = synth.family === r.family ? s.data : rewriteFontFamilyName(s.data, synth.family);
75
+ const name = `${safeFilenamePart(synth.family)}-${s.weight}${suffix}-${id}-${serial}.ttf`;
76
+ await fs.writeFile(path.join(fontsDir, name), data);
77
+ }
78
+ }
79
+ await fs.chmod(fontsDir, 365).catch(() => {
80
+ });
81
+ const includeLines = await this.pickSystemIncludes();
82
+ const cacheDir = path.join(tempDir, "fc-cache");
83
+ await fs.mkdir(cacheDir, { recursive: true });
84
+ const configPath = path.join(tempDir, "fontconfig.xml");
85
+ const configXml = [
86
+ '<?xml version="1.0"?>',
87
+ '<!DOCTYPE fontconfig SYSTEM "fonts.dtd">',
88
+ "<fontconfig>",
89
+ ` <dir>${escapeXml(fontsDir)}</dir>`,
90
+ ` <cachedir>${escapeXml(cacheDir)}</cachedir>`,
91
+ ...includeLines,
92
+ "</fontconfig>",
93
+ ""
94
+ ].join("\n");
95
+ await fs.writeFile(configPath, configXml, "utf8");
96
+ return {
97
+ envOverrides: {
98
+ FONTCONFIG_FILE: configPath,
99
+ XDG_CACHE_HOME: cacheDir
100
+ },
101
+ cleanup: async () => {
102
+ await fs.chmod(fontsDir, 493).catch(() => {
103
+ });
104
+ }
105
+ };
106
+ }
107
+ async pickSystemIncludes() {
108
+ for (const candidate of SYSTEM_FONTS_CONF_CANDIDATES) {
109
+ try {
110
+ await fs.access(candidate);
111
+ return [
112
+ ` <include ignore_missing="yes">${escapeXml(candidate)}</include>`
113
+ ];
114
+ } catch {
115
+ }
116
+ }
117
+ return [` <include ignore_missing="yes">/etc/fonts/fonts.conf</include>`];
118
+ }
119
+ };
120
+ function escapeXml(s) {
121
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
122
+ }
123
+
124
+ // src/font-staging/windows-stager.ts
125
+ import { promises as fs2 } from "fs";
126
+ import path2 from "path";
127
+ import {
128
+ synthesizeFamilyName as synthesizeFamilyName2,
129
+ rewriteFontFamilyName as rewriteFontFamilyName2
130
+ } from "@json-to-office/shared";
131
+ var cachedBindings = null;
132
+ async function getGdiBindings() {
133
+ if (cachedBindings) return cachedBindings;
134
+ const koffi = await import("koffi");
135
+ const mod = koffi.default ?? koffi;
136
+ const gdi32 = mod.load("gdi32.dll");
137
+ cachedBindings = {
138
+ addFont: gdi32.func("int __stdcall AddFontResourceW(str16)"),
139
+ removeFont: gdi32.func("bool __stdcall RemoveFontResourceW(str16)")
140
+ };
141
+ return cachedBindings;
142
+ }
143
+ var WindowsFontStager = class {
144
+ async stage(fonts, tempDir, _options) {
145
+ const id = nextStagingId();
146
+ const fontsDir = path2.join(tempDir, "fonts");
147
+ await fs2.mkdir(fontsDir, { recursive: true });
148
+ const stagedPaths = [];
149
+ let serial = 0;
150
+ for (const r of fonts) {
151
+ if (r.sources.length === 0) continue;
152
+ for (const s of r.sources) {
153
+ serial += 1;
154
+ const suffix = s.italic ? "i" : "r";
155
+ const synth = synthesizeFamilyName2(r.family, s.weight, s.italic);
156
+ const data = synth.family === r.family ? s.data : rewriteFontFamilyName2(s.data, synth.family);
157
+ const name = `${safeFilenamePart(synth.family)}-${s.weight}${suffix}-${id}-${serial}.ttf`;
158
+ const fullPath = path2.join(fontsDir, name);
159
+ await fs2.writeFile(fullPath, data);
160
+ stagedPaths.push(fullPath);
161
+ }
162
+ }
163
+ if (stagedPaths.length === 0) {
164
+ return { envOverrides: {}, cleanup: async () => {
165
+ } };
166
+ }
167
+ const { addFont, removeFont } = await getGdiBindings();
168
+ const registered = [];
169
+ for (const p of stagedPaths) {
170
+ const added = addFont(p);
171
+ if (added > 0) registered.push(p);
172
+ }
173
+ let cleaned = false;
174
+ return {
175
+ envOverrides: {
176
+ // Force GDI backend so the freshly-registered fonts are visible.
177
+ // Skia on Windows uses DirectWrite which does not reliably see
178
+ // fonts added via AddFontResourceW.
179
+ SAL_DISABLE_SKIA: "1"
180
+ },
181
+ cleanup: async () => {
182
+ if (cleaned) return;
183
+ cleaned = true;
184
+ for (const p of registered) {
185
+ try {
186
+ removeFont(p);
187
+ } catch {
188
+ }
189
+ }
190
+ }
191
+ };
192
+ }
193
+ };
194
+
195
+ // src/font-staging/macos-stager.ts
196
+ import { promises as fs3 } from "fs";
197
+ import path3 from "path";
198
+ import {
199
+ synthesizeFamilyName as synthesizeFamilyName3,
200
+ rewriteFontFamilyName as rewriteFontFamilyName3
201
+ } from "@json-to-office/shared";
202
+
203
+ // src/diagnostics.ts
204
+ import { AsyncLocalStorage } from "async_hooks";
205
+ var sinks = new AsyncLocalStorage();
206
+ function runWithDiagnosticSink(sink, callback) {
207
+ return sinks.run(sink, callback);
208
+ }
209
+ function emitDiagnostic(text, tone = "muted") {
210
+ sinks.getStore()?.(text, tone);
211
+ }
212
+ var stderrDiagnosticSink = (text) => {
213
+ process.stderr.write(`${text}
214
+ `);
215
+ };
216
+
217
+ // src/font-staging/macos-stager.ts
218
+ var PYTHON_MACRO = `# Auto-generated by @json-to-office/jto. Runs inside soffice on OnStartApp
219
+ # to make staged fonts visible to LibreOffice's font enumeration. macOS 26
220
+ # blocks Session/Persistent CT registration for unsigned callers, but
221
+ # Process scope still works from inside the target process \u2014 which is
222
+ # exactly where this macro runs.
223
+ import os
224
+ import sys
225
+ import ctypes
226
+ import ctypes.util
227
+
228
+
229
+ def _log(msg):
230
+ # soffice swallows Python stdout in headless mode; stderr surfaces to
231
+ # the parent's pipe. The stager doesn't read this today but the
232
+ # converter logs stderr on failure, which is how we'll debug.
233
+ sys.stderr.write("[jto-font-register] " + msg + "\\n")
234
+
235
+
236
+ def register(*_args):
237
+ paths_env = os.environ.get("JTO_FONT_PATHS", "")
238
+ if not paths_env:
239
+ return
240
+ try:
241
+ cf = ctypes.CDLL(ctypes.util.find_library("CoreFoundation"))
242
+ ct = ctypes.CDLL(ctypes.util.find_library("CoreText"))
243
+ except Exception as e:
244
+ _log("failed to load CoreFoundation/CoreText: " + repr(e))
245
+ return
246
+ cf.CFURLCreateFromFileSystemRepresentation.argtypes = [
247
+ ctypes.c_void_p, ctypes.c_char_p, ctypes.c_long, ctypes.c_bool,
248
+ ]
249
+ cf.CFURLCreateFromFileSystemRepresentation.restype = ctypes.c_void_p
250
+ cf.CFRelease.argtypes = [ctypes.c_void_p]
251
+ ct.CTFontManagerRegisterFontsForURL.argtypes = [
252
+ ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p,
253
+ ]
254
+ ct.CTFontManagerRegisterFontsForURL.restype = ctypes.c_bool
255
+
256
+ kCTFontManagerScopeProcess = 1
257
+ registered = 0
258
+ for p in paths_env.split(os.pathsep):
259
+ if not p:
260
+ continue
261
+ try:
262
+ b = p.encode("utf-8")
263
+ url = cf.CFURLCreateFromFileSystemRepresentation(
264
+ None, b, len(b), False
265
+ )
266
+ if not url:
267
+ _log("CFURL failed for " + p)
268
+ continue
269
+ ok = ct.CTFontManagerRegisterFontsForURL(
270
+ url, kCTFontManagerScopeProcess, None
271
+ )
272
+ cf.CFRelease(url)
273
+ if ok:
274
+ registered += 1
275
+ else:
276
+ _log("CT register returned false for " + p)
277
+ except Exception as e:
278
+ _log("exception registering " + p + ": " + repr(e))
279
+ _log("registered " + str(registered) + " font(s) at Process scope")
280
+
281
+
282
+ # Expose under "register" (event-binding URL) and module-level run so
283
+ # command-line vnd.sun.star.script invocation works either way.
284
+ g_exportedScripts = (register,)
285
+ `;
286
+ var REGISTRY_MOD_XCU = `<?xml version="1.0" encoding="UTF-8"?>
287
+ <oor:items xmlns:oor="http://openoffice.org/2001/registry" xmlns:xs="http://www.w3.org/2001/XMLSchema">
288
+ <item oor:path="/org.openoffice.Office.Events/ApplicationEvents/Bindings">
289
+ <node oor:name="OnStartApp" oor:op="replace">
290
+ <prop oor:name="BindingURL" oor:type="xs:string">
291
+ <value>vnd.sun.star.script:JtoFontRegister.py$register?language=Python&amp;location=user</value>
292
+ </prop>
293
+ </node>
294
+ </item>
295
+ <item oor:path="/org.openoffice.Office.Common/Security/Scripting">
296
+ <prop oor:name="MacroSecurityLevel" oor:op="fuse">
297
+ <value>0</value>
298
+ </prop>
299
+ </item>
300
+ </oor:items>
301
+ `;
302
+ var MacOSCoreTextStager = class {
303
+ async stage(fonts, tempDir, options) {
304
+ const embeddable = fonts.filter((r) => r.sources.length > 0);
305
+ if (embeddable.length === 0) {
306
+ return { envOverrides: {}, cleanup: async () => {
307
+ } };
308
+ }
309
+ const fontsDir = path3.join(tempDir, "fonts");
310
+ await fs3.mkdir(fontsDir, { recursive: true });
311
+ const id = nextStagingId();
312
+ const fontPaths = [];
313
+ const staged = [];
314
+ let serial = 0;
315
+ for (const r of embeddable) {
316
+ for (const s of r.sources) {
317
+ serial += 1;
318
+ const suffix = s.italic ? "i" : "r";
319
+ const synth = synthesizeFamilyName3(r.family, s.weight, s.italic);
320
+ const data = synth.family === r.family ? s.data : rewriteFontFamilyName3(s.data, synth.family);
321
+ const name = `${safeFilenamePart(synth.family)}-${s.weight}${suffix}-${id}-${serial}.ttf`;
322
+ const full = path3.join(fontsDir, name);
323
+ await fs3.writeFile(full, data);
324
+ fontPaths.push(full);
325
+ staged.push({
326
+ family: synth.family,
327
+ weight: s.weight,
328
+ italic: s.italic,
329
+ path: full
330
+ });
331
+ }
332
+ }
333
+ if (process.env.JTO_DEBUG_FONTS === "1") {
334
+ emitDiagnostic(
335
+ "[jto macos-stager] staged " + staged.length + " font(s) for CT Process-scope registration; JTO_FONT_PATHS has " + fontPaths.length + " entries\n" + staged.map(
336
+ (s) => ` ${s.family} (w=${s.weight}${s.italic ? " italic" : ""}) \u2192 ${path3.basename(s.path)}`
337
+ ).join("\n")
338
+ );
339
+ }
340
+ const profileDirs = options?.profileDirs?.length ? options.profileDirs : [path3.join(tempDir, "user-profile")];
341
+ for (const profileDir of profileDirs) {
342
+ const profileUser = path3.join(profileDir, "user");
343
+ const scriptsDir = path3.join(profileUser, "Scripts", "python");
344
+ await fs3.mkdir(scriptsDir, { recursive: true });
345
+ await fs3.writeFile(
346
+ path3.join(scriptsDir, "JtoFontRegister.py"),
347
+ PYTHON_MACRO
348
+ );
349
+ await fs3.writeFile(
350
+ path3.join(profileUser, "registrymodifications.xcu"),
351
+ REGISTRY_MOD_XCU
352
+ );
353
+ }
354
+ return {
355
+ envOverrides: {
356
+ // Colon-separated list of staged TTF paths (`:` matches Python's
357
+ // `os.pathsep` on macOS). The macro reads this at OnStartApp.
358
+ JTO_FONT_PATHS: fontPaths.join(":"),
359
+ // Force LibreOffice's Core Graphics backend. Skia on macOS can
360
+ // skip Core Text's freshly-registered fonts in some builds.
361
+ SAL_DISABLE_SKIA: "1"
362
+ },
363
+ cleanup: async () => {
364
+ }
365
+ };
366
+ }
367
+ };
368
+
369
+ // src/font-staging/index.ts
370
+ var cached = /* @__PURE__ */ new Map();
371
+ function getFontStager(platform = process.platform) {
372
+ const hit = cached.get(platform);
373
+ if (hit) return hit;
374
+ let stager;
375
+ switch (platform) {
376
+ case "win32":
377
+ stager = new WindowsFontStager();
378
+ break;
379
+ case "darwin":
380
+ stager = new MacOSCoreTextStager();
381
+ break;
382
+ case "linux":
383
+ case "freebsd":
384
+ case "openbsd":
385
+ stager = new FontconfigStager();
386
+ break;
387
+ default:
388
+ stager = new NoopFontStager();
389
+ }
390
+ cached.set(platform, stager);
391
+ return stager;
392
+ }
393
+
394
+ // src/pptx-rasterizer.ts
395
+ var SOFFICE_TIMEOUT_MS = 6e4;
396
+ var SOFFICE_BATCH_EXTRA_PER_SLIDE_MS = 15e3;
397
+ var SOFFICE_BATCH_TIMEOUT_CAP_MS = 3e5;
398
+ var MAX_ISOLATED_RETRIES = 3;
399
+ var PDFTOPPM_TIMEOUT_MS = 3e4;
400
+ var PROBE_TIMEOUT_MS = 5e3;
401
+ var MAX_BUFFER = 64 * 1024 * 1024;
402
+ function exec(binary, args, timeoutMs, env) {
403
+ return new Promise((resolve2, reject) => {
404
+ execFile(
405
+ binary,
406
+ args,
407
+ {
408
+ timeout: timeoutMs,
409
+ maxBuffer: MAX_BUFFER,
410
+ windowsHide: true,
411
+ env: env ? { ...process.env, ...env } : process.env
412
+ },
413
+ (error) => error ? reject(error) : resolve2()
414
+ );
415
+ });
416
+ }
417
+ async function binaryWorks(binary) {
418
+ if (binary.includes(path4.sep)) {
419
+ try {
420
+ await fs4.access(binary);
421
+ } catch {
422
+ return false;
423
+ }
424
+ }
425
+ try {
426
+ await exec(binary, ["--version"], PROBE_TIMEOUT_MS);
427
+ return true;
428
+ } catch (error) {
429
+ const code = error.code;
430
+ return code !== "ENOENT" && code !== "EACCES";
431
+ }
432
+ }
433
+ function sofficeCandidates() {
434
+ const candidates = [];
435
+ const configured = process.env.LIBREOFFICE_PATH?.trim();
436
+ if (configured) candidates.push(configured);
437
+ if (process.platform === "darwin") {
438
+ candidates.push("/Applications/LibreOffice.app/Contents/MacOS/soffice");
439
+ } else if (process.platform === "win32") {
440
+ candidates.push("C:\\Program Files\\LibreOffice\\program\\soffice.exe");
441
+ candidates.push(
442
+ "C:\\Program Files (x86)\\LibreOffice\\program\\soffice.exe"
443
+ );
444
+ }
445
+ candidates.push("soffice", "libreoffice");
446
+ return [...new Set(candidates)];
447
+ }
448
+ function pdftoppmCandidates() {
449
+ const candidates = [];
450
+ const configured = process.env.PDFTOPPM_PATH?.trim();
451
+ if (configured) candidates.push(configured);
452
+ candidates.push("pdftoppm");
453
+ return [...new Set(candidates)];
454
+ }
455
+ async function resolveBinary(candidates, label, install) {
456
+ for (const candidate of candidates) {
457
+ if (await binaryWorks(candidate)) return candidate;
458
+ }
459
+ throw new Error(
460
+ `Visual rasterization needs ${label}, which was not found. ${install} (searched: ${candidates.join(", ")}).`
461
+ );
462
+ }
463
+ var sofficePromise;
464
+ var pdftoppmPromise;
465
+ function resolveSoffice() {
466
+ if (!sofficePromise) {
467
+ sofficePromise = resolveBinary(
468
+ sofficeCandidates(),
469
+ "LibreOffice (soffice)",
470
+ "Install LibreOffice or set LIBREOFFICE_PATH."
471
+ ).catch((error) => {
472
+ sofficePromise = void 0;
473
+ throw error;
474
+ });
475
+ }
476
+ return sofficePromise;
477
+ }
478
+ function resolvePdftoppm() {
479
+ if (!pdftoppmPromise) {
480
+ pdftoppmPromise = resolveBinary(
481
+ pdftoppmCandidates(),
482
+ "pdftoppm (poppler)",
483
+ "Install poppler-utils or set PDFTOPPM_PATH."
484
+ ).catch((error) => {
485
+ pdftoppmPromise = void 0;
486
+ throw error;
487
+ });
488
+ }
489
+ return pdftoppmPromise;
490
+ }
491
+ var rasterizerCounters = {
492
+ diskHits: 0,
493
+ diskMisses: 0,
494
+ dedupedRequests: 0,
495
+ rendered: 0,
496
+ failed: 0
497
+ };
498
+ var knownCacheDirs = /* @__PURE__ */ new Set();
499
+ async function getRasterizerCacheStats() {
500
+ const dirs = new Set(knownCacheDirs);
501
+ const defaultDir = resolveCacheDir();
502
+ if (defaultDir) dirs.add(defaultDir);
503
+ let entries = 0;
504
+ let bytes = 0;
505
+ for (const dir of dirs) {
506
+ try {
507
+ const files = await fs4.readdir(dir);
508
+ for (const file of files) {
509
+ if (!file.endsWith(".png")) continue;
510
+ try {
511
+ const stat = await fs4.stat(path4.join(dir, file));
512
+ entries++;
513
+ bytes += stat.size;
514
+ } catch {
515
+ }
516
+ }
517
+ } catch {
518
+ }
519
+ }
520
+ const lookups = rasterizerCounters.diskHits + rasterizerCounters.diskMisses;
521
+ return {
522
+ ...rasterizerCounters,
523
+ hitRate: lookups > 0 ? rasterizerCounters.diskHits / lookups : 0,
524
+ entries,
525
+ bytes
526
+ };
527
+ }
528
+ async function clearRasterizerCache() {
529
+ const dirs = new Set(knownCacheDirs);
530
+ const defaultDir = resolveCacheDir();
531
+ if (defaultDir) dirs.add(defaultDir);
532
+ for (const dir of dirs) {
533
+ try {
534
+ const files = await fs4.readdir(dir);
535
+ await Promise.all(
536
+ files.filter((file) => file.endsWith(".png")).map((file) => fs4.rm(path4.join(dir, file), { force: true }))
537
+ );
538
+ } catch {
539
+ }
540
+ }
541
+ rasterizerCounters.diskHits = 0;
542
+ rasterizerCounters.diskMisses = 0;
543
+ rasterizerCounters.dedupedRequests = 0;
544
+ rasterizerCounters.rendered = 0;
545
+ rasterizerCounters.failed = 0;
546
+ }
547
+ var PNG_SIGNATURE = Buffer.from([
548
+ 137,
549
+ 80,
550
+ 78,
551
+ 71,
552
+ 13,
553
+ 10,
554
+ 26,
555
+ 10
556
+ ]);
557
+ function parsePngSize(png) {
558
+ if (png.length < 24) return null;
559
+ if (!png.subarray(0, 8).equals(PNG_SIGNATURE)) return null;
560
+ if (png.toString("ascii", 12, 16) !== "IHDR") return null;
561
+ const width = png.readUInt32BE(16);
562
+ const height = png.readUInt32BE(20);
563
+ if (width <= 0 || height <= 0) return null;
564
+ return { width, height };
565
+ }
566
+ var tmpCounter = 0;
567
+ async function writeCacheAtomic(cacheDir, cachePath, png) {
568
+ const tmp = `${cachePath}.tmp-${process.pid}-${tmpCounter++}`;
569
+ try {
570
+ await fs4.mkdir(cacheDir, { recursive: true });
571
+ await fs4.writeFile(tmp, png);
572
+ await fs4.rename(tmp, cachePath);
573
+ } catch {
574
+ await fs4.rm(tmp, { force: true }).catch(() => {
575
+ });
576
+ }
577
+ }
578
+ function fontsDigest(fonts) {
579
+ if (!fonts || fonts.length === 0) return void 0;
580
+ const parts = fonts.map(
581
+ (f) => `${f.family}|${f.weight}|${f.italic ? "i" : "r"}|` + crypto.createHash("sha256").update(Buffer.from(f.data, "base64")).digest("hex")
582
+ ).sort();
583
+ return crypto.createHash("sha256").update(parts.join("\n")).digest("hex");
584
+ }
585
+ function cacheKey(request) {
586
+ return crypto.createHash("sha256").update(
587
+ JSON.stringify({
588
+ p: request.presentation,
589
+ dpi: request.dpi,
590
+ base: request.baseDir,
591
+ f: request.fontsKey ?? null
592
+ })
593
+ ).digest("hex");
594
+ }
595
+ function toDataUri(png) {
596
+ return `data:image/png;base64,${png.toString("base64")}`;
597
+ }
598
+ function errorMessage(error) {
599
+ return error instanceof Error ? error.message : String(error);
600
+ }
601
+ async function fileExists(filePath) {
602
+ try {
603
+ await fs4.access(filePath);
604
+ return true;
605
+ } catch {
606
+ return false;
607
+ }
608
+ }
609
+ var sofficeArgs = (profileDir, outDir, files) => [
610
+ "--headless",
611
+ "--norestore",
612
+ "--nolockcheck",
613
+ "--nodefault",
614
+ `-env:UserInstallation=file://${profileDir.replace(/\\/g, "/")}`,
615
+ "--convert-to",
616
+ "pdf:impress_pdf_Export",
617
+ "--outdir",
618
+ outDir,
619
+ ...files
620
+ ];
621
+ async function rasterizeSlidesWithEngine(slides, baseDir, cacheDir, fonts) {
622
+ const results = new Array(slides.length);
623
+ if (cacheDir) knownCacheDirs.add(cacheDir);
624
+ const fontsKey = fontsDigest(fonts);
625
+ const jobsByKey = /* @__PURE__ */ new Map();
626
+ for (let i = 0; i < slides.length; i++) {
627
+ const slide = slides[i];
628
+ const key = cacheKey({
629
+ presentation: slide.presentation,
630
+ dpi: slide.dpi,
631
+ baseDir,
632
+ fontsKey
633
+ });
634
+ const existing = jobsByKey.get(key);
635
+ if (existing) {
636
+ existing.indexes.push(i);
637
+ } else {
638
+ jobsByKey.set(key, {
639
+ presentation: slide.presentation,
640
+ dpi: slide.dpi,
641
+ indexes: [i],
642
+ cachePath: cacheDir ? path4.join(cacheDir, `${key}.png`) : null,
643
+ pptxPath: "",
644
+ pdfPath: "",
645
+ pngPrefix: ""
646
+ });
647
+ }
648
+ }
649
+ rasterizerCounters.dedupedRequests += slides.length - jobsByKey.size;
650
+ const fail = (job, stage, error, cause) => {
651
+ rasterizerCounters.failed++;
652
+ for (const i of job.indexes)
653
+ results[i] = { ok: false, error, stage, cause };
654
+ };
655
+ const succeed = (job, result) => {
656
+ for (const i of job.indexes) results[i] = { ok: true, ...result };
657
+ };
658
+ const uncached = [];
659
+ await Promise.all(
660
+ [...jobsByKey.values()].map(async (job) => {
661
+ if (job.cachePath) {
662
+ const cached2 = await fs4.readFile(job.cachePath).catch(() => null);
663
+ if (cached2) {
664
+ const size = parsePngSize(cached2);
665
+ if (size) {
666
+ rasterizerCounters.diskHits++;
667
+ succeed(job, { base64DataUri: toDataUri(cached2), ...size });
668
+ return;
669
+ }
670
+ await fs4.rm(job.cachePath, { force: true }).catch(() => {
671
+ });
672
+ }
673
+ rasterizerCounters.diskMisses++;
674
+ }
675
+ uncached.push(job);
676
+ })
677
+ );
678
+ if (uncached.length === 0) return results;
679
+ const corePptx = await import("@json-to-office/core-pptx");
680
+ const tempDir = await fs4.mkdtemp(path4.join(os.tmpdir(), "jto-visual-"));
681
+ let stageHandle = null;
682
+ try {
683
+ const built = [];
684
+ for (let j = 0; j < uncached.length; j++) {
685
+ const job = uncached[j];
686
+ job.pptxPath = path4.join(tempDir, `slide-${j}.pptx`);
687
+ job.pdfPath = path4.join(tempDir, `slide-${j}.pdf`);
688
+ job.pngPrefix = path4.join(tempDir, `slide-${j}`);
689
+ try {
690
+ const pptxBuffer = await corePptx.generateBufferFromJson(
691
+ job.presentation,
692
+ { baseDir }
693
+ );
694
+ await fs4.writeFile(job.pptxPath, pptxBuffer);
695
+ built.push(job);
696
+ } catch (error) {
697
+ fail(job, "build", errorMessage(error), error);
698
+ }
699
+ }
700
+ if (built.length === 0) return results;
701
+ const profileDirs = [
702
+ path4.join(tempDir, "profile"),
703
+ ...Array.from(
704
+ { length: MAX_ISOLATED_RETRIES },
705
+ (_, r) => path4.join(tempDir, `profile-retry-${r}`)
706
+ )
707
+ ];
708
+ const stageFonts = fonts?.length ? fromRasterizeFontFaces(fonts) : [];
709
+ if (stageFonts.length > 0) {
710
+ stageHandle = await getFontStager().stage(stageFonts, tempDir, {
711
+ profileDirs
712
+ });
713
+ }
714
+ const sofficeEnv = stageHandle?.envOverrides;
715
+ const [soffice, pdftoppm] = await Promise.all([
716
+ resolveSoffice(),
717
+ resolvePdftoppm()
718
+ ]);
719
+ const batchTimeoutMs = Math.min(
720
+ SOFFICE_TIMEOUT_MS + SOFFICE_BATCH_EXTRA_PER_SLIDE_MS * (built.length - 1),
721
+ SOFFICE_BATCH_TIMEOUT_CAP_MS
722
+ );
723
+ const deadlineAt = Date.now() + batchTimeoutMs + PDFTOPPM_TIMEOUT_MS * built.length;
724
+ const remainingMs = () => deadlineAt - Date.now();
725
+ let batchError;
726
+ try {
727
+ await exec(
728
+ soffice,
729
+ sofficeArgs(
730
+ profileDirs[0],
731
+ tempDir,
732
+ built.map((job) => job.pptxPath)
733
+ ),
734
+ batchTimeoutMs,
735
+ sofficeEnv
736
+ );
737
+ } catch (error) {
738
+ batchError = error;
739
+ }
740
+ const converted = [];
741
+ const missing = [];
742
+ for (const job of built) {
743
+ (await fileExists(job.pdfPath) ? converted : missing).push(job);
744
+ }
745
+ let retriesLeft = missing.length > 0 && built.length > 1 && (batchError === void 0 || converted.length > 0) ? MAX_ISOLATED_RETRIES : 0;
746
+ for (const [r, job] of missing.entries()) {
747
+ const retryBudget = Math.min(SOFFICE_TIMEOUT_MS, remainingMs());
748
+ if (retriesLeft > 0 && retryBudget > 1e3) {
749
+ retriesLeft--;
750
+ let retryError;
751
+ try {
752
+ await exec(
753
+ soffice,
754
+ // Same directories seeded in `profileDirs` above (index 0 is the
755
+ // batch profile, so retry `r` is `profileDirs[r + 1]`); the
756
+ // literal is kept as the fallback for an out-of-range retry.
757
+ sofficeArgs(
758
+ profileDirs[r + 1] ?? path4.join(tempDir, `profile-retry-${r}`),
759
+ tempDir,
760
+ [job.pptxPath]
761
+ ),
762
+ retryBudget,
763
+ sofficeEnv
764
+ );
765
+ } catch (error) {
766
+ retryError = error;
767
+ }
768
+ if (await fileExists(job.pdfPath)) {
769
+ converted.push(job);
770
+ continue;
771
+ }
772
+ batchError ??= retryError;
773
+ }
774
+ const cause = batchError;
775
+ fail(
776
+ job,
777
+ "convert",
778
+ `LibreOffice failed to convert the slide to PDF${cause ? `: ${errorMessage(cause)}` : "."}`,
779
+ cause
780
+ );
781
+ }
782
+ for (const job of converted) {
783
+ const budget = Math.min(PDFTOPPM_TIMEOUT_MS, remainingMs());
784
+ if (budget <= 1e3) {
785
+ fail(
786
+ job,
787
+ "rasterize",
788
+ "Rasterization deadline exceeded before this slide could be converted to PNG."
789
+ );
790
+ continue;
791
+ }
792
+ try {
793
+ await exec(
794
+ pdftoppm,
795
+ [
796
+ "-r",
797
+ String(job.dpi),
798
+ "-png",
799
+ "-singlefile",
800
+ job.pdfPath,
801
+ job.pngPrefix
802
+ ],
803
+ budget
804
+ );
805
+ const png = await fs4.readFile(`${job.pngPrefix}.png`);
806
+ const size = parsePngSize(png);
807
+ if (!size) {
808
+ throw new Error(
809
+ "Rasterization produced an invalid PNG (empty or truncated output from pdftoppm)."
810
+ );
811
+ }
812
+ if (job.cachePath) {
813
+ await writeCacheAtomic(cacheDir, job.cachePath, png);
814
+ }
815
+ rasterizerCounters.rendered++;
816
+ succeed(job, { base64DataUri: toDataUri(png), ...size });
817
+ } catch (error) {
818
+ fail(job, "rasterize", errorMessage(error), error);
819
+ }
820
+ }
821
+ return results;
822
+ } finally {
823
+ if (stageHandle) await stageHandle.cleanup().catch(() => {
824
+ });
825
+ await fs4.rm(tempDir, { recursive: true, force: true }).catch(() => {
826
+ });
827
+ }
828
+ }
829
+ function resolveCacheDir(options) {
830
+ return options?.cacheDir === void 0 ? path4.join(os.tmpdir(), "jto-visual-cache") : options.cacheDir;
831
+ }
832
+ function createLibreOfficePptxRasterizer(options) {
833
+ const cacheDir = resolveCacheDir(options);
834
+ return async function rasterize(request) {
835
+ const [result] = await rasterizeSlidesWithEngine(
836
+ [{ presentation: request.presentation, dpi: request.dpi }],
837
+ request.baseDir,
838
+ cacheDir,
839
+ request.fonts
840
+ );
841
+ if (!result) throw new Error("Rasterization produced no result.");
842
+ if (!result.ok) {
843
+ const error = new Error(result.error);
844
+ if (result.cause !== void 0) error.cause = result.cause;
845
+ throw error;
846
+ }
847
+ return {
848
+ base64DataUri: result.base64DataUri,
849
+ width: result.width,
850
+ height: result.height
851
+ };
852
+ };
853
+ }
854
+ function createLibreOfficePptxBatchRasterizer(options) {
855
+ const cacheDir = resolveCacheDir(options);
856
+ return async function rasterizeBatch(request) {
857
+ const engineResults = await rasterizeSlidesWithEngine(
858
+ request.slides.map((slide) => ({
859
+ presentation: slide.presentation,
860
+ dpi: slide.dpi ?? DEFAULT_VISUAL_DPI
861
+ })),
862
+ request.baseDir,
863
+ cacheDir,
864
+ request.fonts
865
+ );
866
+ return {
867
+ results: engineResults.map(
868
+ (result) => result.ok ? result : { ok: false, error: result.error, stage: result.stage }
869
+ )
870
+ };
871
+ };
872
+ }
873
+
874
+ // src/format-adapter.ts
875
+ function emitGenerationWarnings(warnings) {
876
+ for (const warning of warnings) {
877
+ emitDiagnostic(
878
+ `${warning.component}: ${warning.message}`,
879
+ warning.severity === "info" ? "info" : "warning"
880
+ );
881
+ }
882
+ }
883
+ function toGenerationWarnings(raw) {
884
+ return (raw ?? []).map((w) => ({
885
+ component: w?.component ?? "pptx",
886
+ message: String(w?.message ?? ""),
887
+ severity: w?.severity === "info" ? "info" : "warning",
888
+ context: {
889
+ ...w?.context && typeof w.context === "object" ? w.context : {},
890
+ ...w?.code !== void 0 && { code: w.code },
891
+ ...w?.slide !== void 0 && { slide: w.slide }
892
+ }
893
+ }));
894
+ }
895
+ var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
896
+ function safeThemeKey(name) {
897
+ return name && !UNSAFE_KEYS.has(name) ? name : "custom";
898
+ }
899
+ var CLI_THEME_KEY = "jto-cli-theme";
900
+ function withRequestedTheme(document, theme, customThemes) {
901
+ if (!theme || typeof document !== "object" || document === null) {
902
+ return { document, customThemes };
903
+ }
904
+ return {
905
+ document: {
906
+ ...document,
907
+ props: { ...document.props, theme: CLI_THEME_KEY }
908
+ },
909
+ customThemes: { ...customThemes, [CLI_THEME_KEY]: theme }
910
+ };
911
+ }
912
+ function buildServicesFromEnv() {
913
+ const serverUrl = process.env.HIGHCHARTS_SERVER_URL;
914
+ const apiKey = process.env.HIGHCHARTS_API_KEY;
915
+ const apiKeyHeader = process.env.HIGHCHARTS_API_KEY_HEADER ?? "x-api-key";
916
+ if (!serverUrl && !apiKey) return void 0;
917
+ return {
918
+ highcharts: {
919
+ serverUrl,
920
+ ...apiKey && { headers: { [apiKeyHeader]: apiKey } }
921
+ }
922
+ };
923
+ }
924
+ var cachedRasterizer;
925
+ function getPptxRasterizer() {
926
+ if (!cachedRasterizer) {
927
+ cachedRasterizer = createLibreOfficePptxRasterizer();
928
+ }
929
+ return cachedRasterizer;
930
+ }
931
+ var cachedBatchRasterizer;
932
+ function getPptxBatchRasterizer() {
933
+ if (!cachedBatchRasterizer) {
934
+ cachedBatchRasterizer = createLibreOfficePptxBatchRasterizer();
935
+ }
936
+ return cachedBatchRasterizer;
937
+ }
938
+ function buildDocxServices() {
939
+ const base = buildServicesFromEnv() ?? {};
940
+ const serverUrl = process.env.JTO_PPTX_RASTERIZER_URL?.trim();
941
+ const apiKey = process.env.JTO_PPTX_RASTERIZER_API_KEY || process.env.HIGHCHARTS_API_KEY;
942
+ const apiKeyHeader = process.env.JTO_PPTX_RASTERIZER_API_KEY_HEADER || process.env.HIGHCHARTS_API_KEY_HEADER || "x-api-key";
943
+ return {
944
+ ...base,
945
+ pptx: serverUrl ? {
946
+ serverUrl,
947
+ ...apiKey && { headers: { [apiKeyHeader]: apiKey } }
948
+ } : {
949
+ render: getPptxRasterizer(),
950
+ renderBatch: getPptxBatchRasterizer()
951
+ }
952
+ };
953
+ }
954
+ var DocxFormatAdapter = class {
955
+ name = "docx";
956
+ extension = ".docx";
957
+ label = "document";
958
+ defaultPort = 3003;
959
+ async rendererIds() {
960
+ const core = await import("@json-to-office/core-docx");
961
+ return core.docxRendererIds();
962
+ }
963
+ async generateBuffer(json, options) {
964
+ const core = await import("@json-to-office/core-docx");
965
+ const parsed = typeof json === "string" ? JSON.parse(json) : json;
966
+ const resolved = await this.resolveThemes(options);
967
+ const { document: docDefinition, customThemes } = withRequestedTheme(
968
+ parsed,
969
+ resolved.requested,
970
+ resolved.customThemes
971
+ );
972
+ const services = buildDocxServices();
973
+ const warnings = [];
974
+ const buffer = await core.generateBufferFromJson(docDefinition, {
975
+ customThemes,
976
+ services,
977
+ fonts: options.fonts,
978
+ validation: {
979
+ allowUnknownFields: options.validation?.allowUnknownFields
980
+ },
981
+ deterministic: options.deterministic,
982
+ generatedAt: options.generatedAt,
983
+ baseDir: options.baseDir,
984
+ renderer: options.renderer,
985
+ warnings
986
+ });
987
+ emitGenerationWarnings(warnings);
988
+ options.warnings?.push(...toGenerationWarnings(warnings));
989
+ return buffer;
990
+ }
991
+ async createGenerator(plugins, options) {
992
+ const core = await import("@json-to-office/core-docx");
993
+ const hasPlugins = plugins.length > 0;
994
+ const pluginNames = plugins.map((p) => p.name);
995
+ const services = buildDocxServices();
996
+ const {
997
+ requested: requestedTheme,
998
+ customThemes,
999
+ label: themeLabel
1000
+ } = await this.resolveThemes(options);
1001
+ if (!hasPlugins) {
1002
+ return {
1003
+ generateBuffer: async (document) => {
1004
+ const parsed = typeof document === "string" ? JSON.parse(document) : document;
1005
+ const { document: docDefinition, customThemes: themes } = withRequestedTheme(parsed, requestedTheme, customThemes);
1006
+ const warnings = [];
1007
+ const buffer = await core.generateBufferFromJson(docDefinition, {
1008
+ customThemes: themes,
1009
+ services,
1010
+ fonts: options.fonts,
1011
+ validation: {
1012
+ allowUnknownFields: options.validation?.allowUnknownFields
1013
+ },
1014
+ deterministic: options.deterministic,
1015
+ generatedAt: options.generatedAt,
1016
+ baseDir: options.baseDir,
1017
+ renderer: options.renderer,
1018
+ warnings
1019
+ });
1020
+ emitGenerationWarnings(warnings);
1021
+ options.warnings?.push(...toGenerationWarnings(warnings));
1022
+ return buffer;
1023
+ },
1024
+ hasPlugins: false,
1025
+ pluginNames: [],
1026
+ themeLabel
1027
+ };
1028
+ }
1029
+ let generator = core.createDocumentGenerator({
1030
+ // Undefined when nothing was requested: a constructor theme beats the
1031
+ // generator's own `props.theme` lookup, so forcing one here would render
1032
+ // every document in it.
1033
+ theme: requestedTheme,
1034
+ customThemes: requestedTheme ? { ...customThemes, [CLI_THEME_KEY]: requestedTheme } : customThemes,
1035
+ debug: process.env.DEBUG === "true",
1036
+ services,
1037
+ fonts: options.fonts,
1038
+ validation: {
1039
+ allowUnknownFields: options.validation?.allowUnknownFields
1040
+ },
1041
+ deterministic: options.deterministic,
1042
+ generatedAt: options.generatedAt,
1043
+ baseDir: options.baseDir,
1044
+ renderer: options.renderer
1045
+ });
1046
+ for (const plugin of plugins) {
1047
+ generator = generator.addComponent(plugin);
1048
+ }
1049
+ return {
1050
+ generateBuffer: async (document) => {
1051
+ const parsed = typeof document === "string" ? JSON.parse(document) : document;
1052
+ const { document: docDefinition } = withRequestedTheme(
1053
+ parsed,
1054
+ requestedTheme,
1055
+ customThemes
1056
+ );
1057
+ const result = await generator.generateBuffer(docDefinition, {
1058
+ validation: {
1059
+ allowUnknownFields: options.validation?.allowUnknownFields
1060
+ },
1061
+ deterministic: options.deterministic,
1062
+ generatedAt: options.generatedAt,
1063
+ baseDir: options.baseDir,
1064
+ renderer: options.renderer
1065
+ });
1066
+ emitGenerationWarnings(result.warnings ?? []);
1067
+ options.warnings?.push(...toGenerationWarnings(result.warnings));
1068
+ return result.buffer;
1069
+ },
1070
+ getStandardDefinition: generator.expandStandardDefinition ? async (config) => {
1071
+ const parsed = typeof config === "string" ? JSON.parse(config) : config;
1072
+ const { document: docDefinition } = withRequestedTheme(
1073
+ parsed,
1074
+ requestedTheme,
1075
+ customThemes
1076
+ );
1077
+ const result = await generator.expandStandardDefinition(
1078
+ docDefinition,
1079
+ {
1080
+ validation: {
1081
+ allowUnknownFields: options.validation?.allowUnknownFields
1082
+ }
1083
+ }
1084
+ );
1085
+ emitGenerationWarnings(result.warnings ?? []);
1086
+ return result.standardDefinition;
1087
+ } : void 0,
1088
+ hasPlugins: true,
1089
+ pluginNames,
1090
+ themeLabel
1091
+ };
1092
+ }
1093
+ parseJson(input) {
1094
+ return typeof input === "string" ? JSON.parse(input) : input;
1095
+ }
1096
+ validateDocument(doc) {
1097
+ const result = validateDocx.jsonDocument(doc);
1098
+ return {
1099
+ valid: result.valid,
1100
+ ...result.errors.length > 0 && { errors: result.errors }
1101
+ };
1102
+ }
1103
+ generateSchema(_options) {
1104
+ return null;
1105
+ }
1106
+ getBuiltinThemes() {
1107
+ try {
1108
+ const core = __require("@json-to-office/core-docx");
1109
+ return core.themes || {};
1110
+ } catch {
1111
+ return {};
1112
+ }
1113
+ }
1114
+ async resolveTheme(options) {
1115
+ const core = await import("@json-to-office/core-docx");
1116
+ const { requested } = await this.resolveThemes(options);
1117
+ return requested ?? core.themes?.minimal ?? {};
1118
+ }
1119
+ /**
1120
+ * Resolve `theme`/`themePath` once for a whole run: `themePath` is read a
1121
+ * single time and feeds both the requested theme and the custom-theme
1122
+ * registry, so a bad path warns once instead of once per consumer.
1123
+ */
1124
+ async resolveThemes(options) {
1125
+ const core = await import("@json-to-office/core-docx");
1126
+ const registry = { ...options.customThemes };
1127
+ if (typeof options.theme === "object" && options.theme !== null) {
1128
+ registry[safeThemeKey(options.theme.name)] = options.theme;
1129
+ }
1130
+ let fileTheme;
1131
+ if (options.themePath) {
1132
+ try {
1133
+ if (options.themePath.endsWith(".json")) {
1134
+ fileTheme = await core.loadThemeFromFile(options.themePath);
1135
+ } else {
1136
+ const themePath = path5.resolve(process.cwd(), options.themePath);
1137
+ const themeModule = await import(themePath);
1138
+ fileTheme = themeModule.default || themeModule.theme;
1139
+ }
1140
+ } catch (error) {
1141
+ emitDiagnostic(
1142
+ `Failed to load theme from ${options.themePath}: ${error.message}`,
1143
+ "warning"
1144
+ );
1145
+ }
1146
+ if (fileTheme) {
1147
+ registry[safeThemeKey(fileTheme.name)] = fileTheme;
1148
+ }
1149
+ }
1150
+ const customThemes = Object.keys(registry).length > 0 ? registry : void 0;
1151
+ if (fileTheme) {
1152
+ return { requested: fileTheme, customThemes, label: options.themePath };
1153
+ }
1154
+ if (typeof options.theme === "string") {
1155
+ const named = options.customThemes?.[options.theme] ?? core.themes?.[options.theme];
1156
+ if (named)
1157
+ return { requested: named, customThemes, label: options.theme };
1158
+ if (options.theme.endsWith(".json") && fs5.existsSync(options.theme)) {
1159
+ try {
1160
+ return {
1161
+ requested: await core.loadThemeFromFile(options.theme),
1162
+ customThemes,
1163
+ label: options.theme
1164
+ };
1165
+ } catch {
1166
+ }
1167
+ }
1168
+ try {
1169
+ const inline = await core.loadThemeFromJson(options.theme);
1170
+ return {
1171
+ requested: inline,
1172
+ customThemes,
1173
+ label: inline?.name || options.theme
1174
+ };
1175
+ } catch {
1176
+ }
1177
+ emitDiagnostic(
1178
+ `Unknown theme "${options.theme}"; keeping the document's own theme`,
1179
+ "warning"
1180
+ );
1181
+ }
1182
+ if (typeof options.theme === "object" && options.theme !== null) {
1183
+ return {
1184
+ requested: options.theme,
1185
+ customThemes,
1186
+ label: safeThemeKey(options.theme.name)
1187
+ };
1188
+ }
1189
+ return { requested: void 0, customThemes, label: void 0 };
1190
+ }
1191
+ async loadCustomThemes(options) {
1192
+ return (await this.resolveThemes(options)).customThemes;
1193
+ }
1194
+ async getVisualPrepassStats() {
1195
+ try {
1196
+ const core = await import("@json-to-office/core-docx");
1197
+ return core.getVisualPrepassStats?.() ?? null;
1198
+ } catch {
1199
+ return null;
1200
+ }
1201
+ }
1202
+ async resetCacheStats() {
1203
+ try {
1204
+ const core = await import("@json-to-office/core-docx");
1205
+ core.resetVisualPrepassStats?.();
1206
+ } catch {
1207
+ }
1208
+ }
1209
+ };
1210
+ var PptxFormatAdapter = class {
1211
+ name = "pptx";
1212
+ extension = ".pptx";
1213
+ label = "presentation";
1214
+ defaultPort = 3004;
1215
+ async rendererIds() {
1216
+ const core = await import("@json-to-office/core-pptx");
1217
+ return core.pptxRendererIds();
1218
+ }
1219
+ async generateBuffer(json, options) {
1220
+ const core = await import("@json-to-office/core-pptx");
1221
+ const parsed = typeof json === "string" ? JSON.parse(json) : json;
1222
+ const resolved = await this.resolveThemes(options);
1223
+ const { document: docDefinition, customThemes } = withRequestedTheme(
1224
+ parsed,
1225
+ resolved.requested,
1226
+ resolved.customThemes
1227
+ );
1228
+ const services = buildServicesFromEnv();
1229
+ const result = await core.generateBufferWithWarnings(docDefinition, {
1230
+ customThemes,
1231
+ services,
1232
+ fonts: options.fonts,
1233
+ validation: {
1234
+ allowUnknownFields: options.validation?.allowUnknownFields
1235
+ },
1236
+ deterministic: options.deterministic,
1237
+ generatedAt: options.generatedAt,
1238
+ baseDir: options.baseDir,
1239
+ renderer: options.renderer
1240
+ });
1241
+ const normalized = toGenerationWarnings(result.warnings);
1242
+ emitGenerationWarnings(normalized);
1243
+ options.warnings?.push(...normalized);
1244
+ return result.buffer;
1245
+ }
1246
+ async createGenerator(plugins, options) {
1247
+ const core = await import("@json-to-office/core-pptx");
1248
+ const hasPlugins = plugins.length > 0;
1249
+ const pluginNames = plugins.map((p) => p.name);
1250
+ const services = buildServicesFromEnv();
1251
+ const {
1252
+ requested: requestedTheme,
1253
+ customThemes,
1254
+ label: themeLabel
1255
+ } = await this.resolveThemes(options);
1256
+ if (!hasPlugins) {
1257
+ return {
1258
+ generateBuffer: async (document) => {
1259
+ const parsed = typeof document === "string" ? JSON.parse(document) : document;
1260
+ const { document: docDefinition, customThemes: themes } = withRequestedTheme(parsed, requestedTheme, customThemes);
1261
+ const result = await core.generateBufferWithWarnings(docDefinition, {
1262
+ customThemes: themes,
1263
+ services,
1264
+ fonts: options.fonts,
1265
+ validation: {
1266
+ allowUnknownFields: options.validation?.allowUnknownFields
1267
+ },
1268
+ deterministic: options.deterministic,
1269
+ generatedAt: options.generatedAt,
1270
+ baseDir: options.baseDir,
1271
+ renderer: options.renderer
1272
+ });
1273
+ const normalized = toGenerationWarnings(result.warnings);
1274
+ emitGenerationWarnings(normalized);
1275
+ options.warnings?.push(...normalized);
1276
+ return result.buffer;
1277
+ },
1278
+ hasPlugins: false,
1279
+ pluginNames: [],
1280
+ themeLabel
1281
+ };
1282
+ }
1283
+ let generator = core.createPresentationGenerator({
1284
+ // Undefined when nothing was requested: a constructor theme beats the
1285
+ // generator's own `props.theme` lookup, so forcing one here would render
1286
+ // every document in it.
1287
+ theme: requestedTheme,
1288
+ customThemes: requestedTheme ? { ...customThemes, [CLI_THEME_KEY]: requestedTheme } : customThemes,
1289
+ debug: process.env.DEBUG === "true",
1290
+ services,
1291
+ fonts: options.fonts,
1292
+ validation: {
1293
+ allowUnknownFields: options.validation?.allowUnknownFields
1294
+ },
1295
+ deterministic: options.deterministic,
1296
+ generatedAt: options.generatedAt,
1297
+ baseDir: options.baseDir,
1298
+ renderer: options.renderer
1299
+ });
1300
+ for (const plugin of plugins) {
1301
+ generator = generator.addComponent(plugin);
1302
+ }
1303
+ return {
1304
+ generateBuffer: async (document) => {
1305
+ const parsed = typeof document === "string" ? JSON.parse(document) : document;
1306
+ const { document: docDefinition } = withRequestedTheme(
1307
+ parsed,
1308
+ requestedTheme,
1309
+ customThemes
1310
+ );
1311
+ const result = await generator.generateBuffer(docDefinition, {
1312
+ validation: {
1313
+ allowUnknownFields: options.validation?.allowUnknownFields
1314
+ },
1315
+ deterministic: options.deterministic,
1316
+ generatedAt: options.generatedAt,
1317
+ baseDir: options.baseDir,
1318
+ renderer: options.renderer
1319
+ });
1320
+ const normalized = toGenerationWarnings(result.warnings);
1321
+ emitGenerationWarnings(normalized);
1322
+ options.warnings?.push(...normalized);
1323
+ return result.buffer;
1324
+ },
1325
+ hasPlugins: true,
1326
+ pluginNames,
1327
+ themeLabel
1328
+ };
1329
+ }
1330
+ parseJson(input) {
1331
+ return typeof input === "string" ? JSON.parse(input) : input;
1332
+ }
1333
+ validateDocument(doc) {
1334
+ const result = validatePresentationDocument(doc);
1335
+ return {
1336
+ valid: result.valid,
1337
+ ...result.errors.length > 0 && { errors: result.errors }
1338
+ };
1339
+ }
1340
+ generateSchema(_options) {
1341
+ return null;
1342
+ }
1343
+ getBuiltinThemes() {
1344
+ try {
1345
+ const core = __require("@json-to-office/core-pptx");
1346
+ return core.pptxThemes || {};
1347
+ } catch {
1348
+ return {};
1349
+ }
1350
+ }
1351
+ async resolveTheme(options) {
1352
+ const core = await import("@json-to-office/core-pptx");
1353
+ const themes = core.pptxThemes || {};
1354
+ const { requested } = await this.resolveThemes(options);
1355
+ return requested ?? themes.minimal ?? {};
1356
+ }
1357
+ /**
1358
+ * Resolve `theme`/`themePath` once for a whole run: `themePath` is read a
1359
+ * single time and feeds both the requested theme and the custom-theme
1360
+ * registry, so a bad path warns once instead of once per consumer.
1361
+ */
1362
+ async resolveThemes(options) {
1363
+ const core = await import("@json-to-office/core-pptx");
1364
+ const themes = core.pptxThemes || {};
1365
+ const registry = { ...options.customThemes };
1366
+ if (typeof options.theme === "object" && options.theme !== null) {
1367
+ registry[safeThemeKey(options.theme.name)] = options.theme;
1368
+ }
1369
+ let fileTheme;
1370
+ if (options.themePath) {
1371
+ try {
1372
+ if (options.themePath.endsWith(".json")) {
1373
+ const content = fs5.readFileSync(
1374
+ path5.resolve(process.cwd(), options.themePath),
1375
+ "utf-8"
1376
+ );
1377
+ fileTheme = JSON.parse(content);
1378
+ } else {
1379
+ const themePath = path5.resolve(process.cwd(), options.themePath);
1380
+ const themeModule = await import(themePath);
1381
+ fileTheme = themeModule.default || themeModule.theme;
1382
+ }
1383
+ } catch (error) {
1384
+ emitDiagnostic(
1385
+ `Failed to load theme from ${options.themePath}: ${error.message}`,
1386
+ "warning"
1387
+ );
1388
+ }
1389
+ if (fileTheme) {
1390
+ registry[safeThemeKey(fileTheme.name)] = fileTheme;
1391
+ }
1392
+ }
1393
+ const customThemes = Object.keys(registry).length > 0 ? registry : void 0;
1394
+ if (fileTheme) {
1395
+ return { requested: fileTheme, customThemes, label: options.themePath };
1396
+ }
1397
+ if (typeof options.theme === "string") {
1398
+ const named = options.customThemes?.[options.theme] ?? themes[options.theme];
1399
+ if (named)
1400
+ return { requested: named, customThemes, label: options.theme };
1401
+ if (options.theme.endsWith(".json") && fs5.existsSync(options.theme)) {
1402
+ try {
1403
+ const content = fs5.readFileSync(
1404
+ path5.resolve(process.cwd(), options.theme),
1405
+ "utf-8"
1406
+ );
1407
+ return {
1408
+ requested: JSON.parse(content),
1409
+ customThemes,
1410
+ label: options.theme
1411
+ };
1412
+ } catch {
1413
+ }
1414
+ }
1415
+ emitDiagnostic(
1416
+ `Unknown theme "${options.theme}"; keeping the document's own theme`,
1417
+ "warning"
1418
+ );
1419
+ }
1420
+ if (typeof options.theme === "object" && options.theme !== null) {
1421
+ return {
1422
+ requested: options.theme,
1423
+ customThemes,
1424
+ label: safeThemeKey(options.theme.name)
1425
+ };
1426
+ }
1427
+ return { requested: void 0, customThemes, label: void 0 };
1428
+ }
1429
+ async loadCustomThemes(options) {
1430
+ return (await this.resolveThemes(options)).customThemes;
1431
+ }
1432
+ };
1433
+ function createAdapter(format) {
1434
+ switch (format) {
1435
+ case "docx":
1436
+ return new DocxFormatAdapter();
1437
+ case "pptx":
1438
+ return new PptxFormatAdapter();
1439
+ default:
1440
+ throw new Error(`Unknown format: ${format}`);
1441
+ }
1442
+ }
1443
+ export {
1444
+ DocxFormatAdapter,
1445
+ FontconfigStager,
1446
+ MacOSCoreTextStager,
1447
+ NoopFontStager,
1448
+ PptxFormatAdapter,
1449
+ WindowsFontStager,
1450
+ clearRasterizerCache,
1451
+ createAdapter,
1452
+ createLibreOfficePptxBatchRasterizer,
1453
+ createLibreOfficePptxRasterizer,
1454
+ emitDiagnostic,
1455
+ getFontStager,
1456
+ getRasterizerCacheStats,
1457
+ runWithDiagnosticSink,
1458
+ stderrDiagnosticSink
1459
+ };
1460
+ //# sourceMappingURL=index.js.map