@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/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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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&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
|