@json-to-office/jto 0.34.0 → 0.35.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.
Files changed (42) hide show
  1. package/dist/cli.js +214 -516
  2. package/dist/cli.js.map +1 -1
  3. package/dist/client/assets/HomePage-DgVVosCp.js +106 -0
  4. package/dist/client/assets/HomePage-DgVVosCp.js.map +1 -0
  5. package/dist/client/assets/{JsonEditorPage-BOyS9roy.js → JsonEditorPage-13oxEHfY.js} +3 -3
  6. package/dist/client/assets/{JsonEditorPage-BOyS9roy.js.map → JsonEditorPage-13oxEHfY.js.map} +1 -1
  7. package/dist/client/assets/{MonacoPluginProvider-DS8Ov7V6.js → MonacoPluginProvider-DiUyU9px.js} +3 -3
  8. package/dist/client/assets/{MonacoPluginProvider-DS8Ov7V6.js.map → MonacoPluginProvider-DiUyU9px.js.map} +1 -1
  9. package/dist/client/assets/editor-monaco-json-CTrPZKuA.js +24 -0
  10. package/dist/client/assets/{editor-monaco-json-5MnjNhYy.js.map → editor-monaco-json-CTrPZKuA.js.map} +1 -1
  11. package/dist/client/assets/{editor-sQA3rPOP.js → editor-ouhvdGgK.js} +2 -2
  12. package/dist/client/assets/{editor-sQA3rPOP.js.map → editor-ouhvdGgK.js.map} +1 -1
  13. package/dist/client/assets/{editor-refs-store-DLWwmGGm.js → editor-refs-store-DNQzN0_z.js} +2 -2
  14. package/dist/client/assets/{editor-refs-store-DLWwmGGm.js.map → editor-refs-store-DNQzN0_z.js.map} +1 -1
  15. package/dist/client/assets/index-CRuLxurM.js +5 -0
  16. package/dist/client/assets/index-CRuLxurM.js.map +1 -0
  17. package/dist/client/assets/index-CnUTzeor.css +1 -0
  18. package/dist/client/assets/preview-D3QYaLPp.js +4 -0
  19. package/dist/client/assets/preview-D3QYaLPp.js.map +1 -0
  20. package/dist/client/assets/{ui-vendor-Cs3OXpi7.js → ui-vendor-DyQLnWB8.js} +16 -11
  21. package/dist/client/assets/ui-vendor-DyQLnWB8.js.map +1 -0
  22. package/dist/client/index.html +3 -3
  23. package/dist/client/templates/Alternative deck 16_9.pptx.json +91 -49
  24. package/dist/client/templates/Company deck 16_9.pptx.json +134 -70
  25. package/dist/client/templates/Company deck 4_3.pptx.json +84 -44
  26. package/dist/client/templates/data-report-presentation.pptx.json +100 -52
  27. package/dist/client/templates/modern-annual-report-1.docx.json +207 -116
  28. package/dist/client/templates/modern-annual-report-2.docx.json +52 -37
  29. package/dist/client/templates/modern-annual-report-3.docx.json +40 -20
  30. package/dist/client/templates/tech-report.docx.json +96 -48
  31. package/dist/render-server.js +168 -106
  32. package/dist/render-server.js.map +1 -1
  33. package/package.json +7 -8
  34. package/dist/client/assets/HomePage-B6EwpLeU.js +0 -106
  35. package/dist/client/assets/HomePage-B6EwpLeU.js.map +0 -1
  36. package/dist/client/assets/editor-monaco-json-5MnjNhYy.js +0 -24
  37. package/dist/client/assets/index-C9ply7vX.css +0 -1
  38. package/dist/client/assets/index-CkyZoRBe.js +0 -5
  39. package/dist/client/assets/index-CkyZoRBe.js.map +0 -1
  40. package/dist/client/assets/preview-CisNYRaE.js +0 -3
  41. package/dist/client/assets/preview-CisNYRaE.js.map +0 -1
  42. package/dist/client/assets/ui-vendor-Cs3OXpi7.js.map +0 -1
package/dist/cli.js CHANGED
@@ -221,7 +221,9 @@ import {
221
221
  collectFontNamesFromPptx,
222
222
  POPULAR_GOOGLE_FONTS,
223
223
  getUpstreamOverride,
224
- isSafeFont
224
+ isSafeFont,
225
+ documentFontRegistry,
226
+ themeFontRegistry
225
227
  } from "@json-to-office/shared";
226
228
  function collectReferencedNames(config2, customThemes, adapterName) {
227
229
  const collect = adapterName === "docx" ? collectFontNamesFromDocx : collectFontNamesFromPptx;
@@ -281,6 +283,14 @@ function collectReferencedItalic(config2, customThemes) {
281
283
  }
282
284
  return found;
283
285
  }
286
+ function toSupplementalWarnings(warnings) {
287
+ return warnings.map(({ code, message }) => ({
288
+ component: "fontRegistry",
289
+ message,
290
+ severity: "info",
291
+ context: { code }
292
+ }));
293
+ }
284
294
  function autoGoogleFontEntries(names, skipFamilies, referencedWeights, referencedItalic, warnings) {
285
295
  const googleByLower = new Map(
286
296
  POPULAR_GOOGLE_FONTS.map((f) => [f.family.toLowerCase(), f])
@@ -316,9 +326,10 @@ function autoGoogleFontEntries(names, skipFamilies, referencedWeights, reference
316
326
  let selected = variants;
317
327
  if (selected.length === 0) {
318
328
  const missing = [...overrideWantedSet].filter((w) => !override.variants.some((v) => v.weight === w)).sort((a, b) => a - b);
319
- warnings?.push(
320
- `FONT_WEIGHT_NOT_IN_OVERRIDE: family "${match.family}" \u2014 referenced weight(s) ${missing.join(", ")} not in upstream override (has ${override.variants.map((v) => v.weight).filter((w, i, a) => a.indexOf(w) === i).sort((a, b) => a - b).join(", ")}). Fetching all override variants as a fallback.`
321
- );
329
+ warnings?.push({
330
+ code: "FONT_WEIGHT_NOT_IN_OVERRIDE",
331
+ message: `FONT_WEIGHT_NOT_IN_OVERRIDE: family "${match.family}" \u2014 referenced weight(s) ${missing.join(", ")} not in upstream override (has ${override.variants.map((v) => v.weight).filter((w, i, a) => a.indexOf(w) === i).sort((a, b) => a - b).join(", ")}). Fetching all override variants as a fallback.`
332
+ });
322
333
  selected = override.variants;
323
334
  }
324
335
  entries.push({
@@ -396,8 +407,13 @@ var init_generator = __esm({
396
407
  const fontMode = rawMode === "substitute" || rawMode === "custom" ? rawMode : void 0;
397
408
  const rawSub = callerFonts?.substitution;
398
409
  const fontSubstitution = rawSub && typeof rawSub === "object" && !Array.isArray(rawSub) ? rawSub : void 0;
410
+ const declaredFamilies = [
411
+ ...callerExtraEntries,
412
+ ...documentFontRegistry(config2),
413
+ ...Object.values(customThemes ?? {}).flatMap((t) => themeFontRegistry(t))
414
+ ];
399
415
  const callerFamilies = new Set(
400
- callerExtraEntries.map((e) => e.family.toLowerCase())
416
+ declaredFamilies.map((e) => e.family.toLowerCase())
401
417
  );
402
418
  const overrideWarnings = [];
403
419
  const autoEntries = fontMode === "substitute" ? [] : autoGoogleFontEntries(
@@ -415,13 +431,17 @@ var init_generator = __esm({
415
431
  for (const e of callerExtraEntries) {
416
432
  const lower = e.family.toLowerCase();
417
433
  if (googleFamiliesLower.has(lower) && referencedNames.has(e.family)) {
418
- overrideWarnings.push(
419
- `[FONT_OVERRIDE_LOCAL] ${e.family}: caller-supplied source used; Google Fonts auto-fetch skipped for this family.`
420
- );
434
+ overrideWarnings.push({
435
+ code: "FONT_OVERRIDE_LOCAL",
436
+ message: `[FONT_OVERRIDE_LOCAL] ${e.family}: caller-supplied source used; Google Fonts auto-fetch skipped for this family.`
437
+ });
421
438
  }
422
439
  }
423
440
  }
424
- const bypassCache = options?.bypassCache === true || extraEntries.length > 0;
441
+ const declaresRegistry = documentFontRegistry(config2).length > 0 || Object.values(customThemes ?? {}).some(
442
+ (t) => themeFontRegistry(t).length > 0
443
+ );
444
+ const bypassCache = options?.bypassCache === true || extraEntries.length > 0 || declaresRegistry;
425
445
  const baseDir = typeof options?.baseDir === "string" ? options.baseDir : void 0;
426
446
  const cacheKeyData = {
427
447
  config: config2,
@@ -435,15 +455,17 @@ var init_generator = __esm({
435
455
  const cacheKey = this.cacheService.generateCacheKey(cacheKeyData);
436
456
  const hasDynamicContent = this.cacheService.hasDynamicContent(config2);
437
457
  if (!bypassCache && !hasDynamicContent) {
438
- const cachedBuffer = this.cacheService.get(cacheKey);
439
- if (cachedBuffer) {
458
+ const cached = this.cacheService.get(cacheKey);
459
+ if (cached) {
440
460
  logger.info("Served from cache", { title: config2.metadata?.title });
441
461
  return {
442
462
  filename: `${config2.metadata?.title || this.adapter.label}${this.adapter.extension}`,
443
463
  fileId: Date.now().toString(),
444
- buffer: cachedBuffer,
464
+ buffer: cached.buffer,
465
+ // Warnings ride along with the bytes: a cache hit describes the same
466
+ // document, so it must report the same font problems.
445
467
  cached: true,
446
- warnings: null
468
+ warnings: cached.warnings
447
469
  };
448
470
  }
449
471
  }
@@ -453,7 +475,7 @@ var init_generator = __esm({
453
475
  const registry = PluginRegistry.getInstance();
454
476
  let buffer;
455
477
  const resolvedFonts = [];
456
- const needsFontOpts = extraEntries.length > 0 || fontMode !== void 0 || fontSubstitution !== void 0 || callerStrict !== void 0;
478
+ const needsFontOpts = extraEntries.length > 0 || declaresRegistry || fontMode !== void 0 || fontSubstitution !== void 0 || callerStrict !== void 0;
457
479
  const fontOpts = needsFontOpts ? {
458
480
  ...extraEntries.length > 0 && { extraEntries },
459
481
  ...fontMode && { mode: fontMode },
@@ -463,24 +485,24 @@ var init_generator = __esm({
463
485
  resolvedFonts.push(...r);
464
486
  }
465
487
  } : void 0;
488
+ const coreWarnings = [];
466
489
  if (registry.hasPlugins()) {
467
490
  const plugins = registry.getPlugins();
468
491
  const generator = await this.adapter.createGenerator(plugins, {
469
492
  customThemes,
470
493
  fonts: fontOpts,
471
- baseDir
494
+ baseDir,
495
+ warnings: coreWarnings
472
496
  });
473
497
  buffer = await generator.generateBuffer(config2);
474
498
  } else {
475
499
  buffer = await this.adapter.generateBuffer(config2, {
476
500
  customThemes,
477
501
  fonts: fontOpts,
478
- baseDir
502
+ baseDir,
503
+ warnings: coreWarnings
479
504
  });
480
505
  }
481
- this.cacheService.set(cacheKey, buffer, config2, {
482
- bypassCache: bypassCache || hasDynamicContent
483
- });
484
506
  const CANONICAL_WEIGHTS = /* @__PURE__ */ new Set([
485
507
  100,
486
508
  200,
@@ -495,12 +517,7 @@ var init_generator = __esm({
495
517
  const nonCanonical = [...referencedWeights].filter(
496
518
  (w) => !CANONICAL_WEIGHTS.has(w)
497
519
  );
498
- const extraWarnings = overrideWarnings.map((message) => ({
499
- component: "fontRegistry",
500
- message,
501
- severity: "info",
502
- context: { code: "FONT_OVERRIDE_LOCAL" }
503
- }));
520
+ const extraWarnings = toSupplementalWarnings(overrideWarnings);
504
521
  for (const w of nonCanonical) {
505
522
  extraWarnings.push({
506
523
  component: "fontRegistry",
@@ -509,12 +526,19 @@ var init_generator = __esm({
509
526
  context: { code: "FONT_NONCANONICAL_WEIGHT" }
510
527
  });
511
528
  }
529
+ const allWarnings = [...coreWarnings, ...extraWarnings];
530
+ this.cacheService.set(
531
+ cacheKey,
532
+ { buffer, warnings: allWarnings.length > 0 ? allWarnings : null },
533
+ config2,
534
+ { bypassCache: bypassCache || hasDynamicContent }
535
+ );
512
536
  return {
513
537
  filename: `${config2.metadata?.title || this.adapter.label}${this.adapter.extension}`,
514
538
  fileId: Date.now().toString(),
515
539
  buffer,
516
540
  cached: false,
517
- warnings: extraWarnings.length > 0 ? extraWarnings : null,
541
+ warnings: allWarnings.length > 0 ? allWarnings : null,
518
542
  resolvedFonts
519
543
  };
520
544
  }
@@ -559,7 +583,9 @@ var init_cache = __esm({
559
583
  this.cache = new LRUCache({
560
584
  max: config.cache.maxItems,
561
585
  maxSize: maxSizeBytes,
562
- sizeCalculation: (value) => value.length,
586
+ // Must stay the buffer's byte length: lru-cache rejects a zero or
587
+ // undefined size when `maxSize` is set.
588
+ sizeCalculation: (value) => value.buffer.length,
563
589
  ttl: config.cache.ttlSeconds * 1e3,
564
590
  dispose: (_value, _key, reason) => {
565
591
  if (reason === "evict" || reason === "delete") {
@@ -641,404 +667,12 @@ var init_cache = __esm({
641
667
  }
642
668
  });
643
669
 
644
- // src/server/services/font-staging/noop-stager.ts
645
- var NoopFontStager;
646
- var init_noop_stager = __esm({
647
- "src/server/services/font-staging/noop-stager.ts"() {
648
- "use strict";
649
- init_esm_shims();
650
- NoopFontStager = class {
651
- async stage() {
652
- return {
653
- envOverrides: {},
654
- cleanup: async () => {
655
- }
656
- };
657
- }
658
- };
659
- }
660
- });
661
-
662
- // src/server/services/font-staging/types.ts
663
- function nextStagingId() {
664
- counter += 1;
665
- return `${process.pid}-${counter}`;
666
- }
667
- function safeFilenamePart(s) {
668
- return s.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 48);
669
- }
670
- var counter;
671
- var init_types = __esm({
672
- "src/server/services/font-staging/types.ts"() {
673
- "use strict";
674
- init_esm_shims();
675
- counter = 0;
676
- }
677
- });
678
-
679
- // src/server/services/font-staging/fontconfig-stager.ts
680
- import { promises as fs } from "fs";
681
- import path2 from "path";
682
- import {
683
- synthesizeFamilyName,
684
- rewriteFontFamilyName
685
- } from "@json-to-office/shared";
686
- function escapeXml(s) {
687
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
688
- }
689
- var SYSTEM_FONTS_CONF_CANDIDATES, FontconfigStager;
690
- var init_fontconfig_stager = __esm({
691
- "src/server/services/font-staging/fontconfig-stager.ts"() {
692
- "use strict";
693
- init_esm_shims();
694
- init_types();
695
- SYSTEM_FONTS_CONF_CANDIDATES = [
696
- "/etc/fonts/fonts.conf",
697
- "/opt/homebrew/etc/fonts/fonts.conf",
698
- "/usr/local/etc/fonts/fonts.conf"
699
- ];
700
- FontconfigStager = class {
701
- async stage(fonts, tempDir) {
702
- const id = nextStagingId();
703
- const fontsDir = path2.join(tempDir, "fonts");
704
- await fs.mkdir(fontsDir, { recursive: true });
705
- let serial = 0;
706
- for (const r of fonts) {
707
- if (r.sources.length === 0) continue;
708
- for (const s of r.sources) {
709
- serial += 1;
710
- const suffix = s.italic ? "i" : "r";
711
- const synth = synthesizeFamilyName(r.family, s.weight, s.italic);
712
- const data = synth.family === r.family ? s.data : rewriteFontFamilyName(s.data, synth.family);
713
- const name = `${safeFilenamePart(synth.family)}-${s.weight}${suffix}-${id}-${serial}.ttf`;
714
- await fs.writeFile(path2.join(fontsDir, name), data);
715
- }
716
- }
717
- await fs.chmod(fontsDir, 365).catch(() => {
718
- });
719
- const includeLines = await this.pickSystemIncludes();
720
- const cacheDir = path2.join(tempDir, "fc-cache");
721
- await fs.mkdir(cacheDir, { recursive: true });
722
- const configPath = path2.join(tempDir, "fontconfig.xml");
723
- const configXml = [
724
- '<?xml version="1.0"?>',
725
- '<!DOCTYPE fontconfig SYSTEM "fonts.dtd">',
726
- "<fontconfig>",
727
- ` <dir>${escapeXml(fontsDir)}</dir>`,
728
- ` <cachedir>${escapeXml(cacheDir)}</cachedir>`,
729
- ...includeLines,
730
- "</fontconfig>",
731
- ""
732
- ].join("\n");
733
- await fs.writeFile(configPath, configXml, "utf8");
734
- return {
735
- envOverrides: {
736
- FONTCONFIG_FILE: configPath,
737
- XDG_CACHE_HOME: cacheDir
738
- },
739
- cleanup: async () => {
740
- }
741
- };
742
- }
743
- async pickSystemIncludes() {
744
- for (const candidate of SYSTEM_FONTS_CONF_CANDIDATES) {
745
- try {
746
- await fs.access(candidate);
747
- return [
748
- ` <include ignore_missing="yes">${escapeXml(candidate)}</include>`
749
- ];
750
- } catch {
751
- }
752
- }
753
- return [` <include ignore_missing="yes">/etc/fonts/fonts.conf</include>`];
754
- }
755
- };
756
- }
757
- });
758
-
759
- // src/server/services/font-staging/windows-stager.ts
760
- import { promises as fs2 } from "fs";
761
- import path3 from "path";
762
- import {
763
- synthesizeFamilyName as synthesizeFamilyName2,
764
- rewriteFontFamilyName as rewriteFontFamilyName2
765
- } from "@json-to-office/shared";
766
- async function getGdiBindings() {
767
- if (cachedBindings) return cachedBindings;
768
- const koffi = await import("koffi");
769
- const mod = koffi.default ?? koffi;
770
- const gdi32 = mod.load("gdi32.dll");
771
- cachedBindings = {
772
- addFont: gdi32.func("int __stdcall AddFontResourceW(str16)"),
773
- removeFont: gdi32.func("bool __stdcall RemoveFontResourceW(str16)")
774
- };
775
- return cachedBindings;
776
- }
777
- var cachedBindings, WindowsFontStager;
778
- var init_windows_stager = __esm({
779
- "src/server/services/font-staging/windows-stager.ts"() {
780
- "use strict";
781
- init_esm_shims();
782
- init_types();
783
- cachedBindings = null;
784
- WindowsFontStager = class {
785
- async stage(fonts, tempDir) {
786
- const id = nextStagingId();
787
- const fontsDir = path3.join(tempDir, "fonts");
788
- await fs2.mkdir(fontsDir, { recursive: true });
789
- const stagedPaths = [];
790
- let serial = 0;
791
- for (const r of fonts) {
792
- if (r.sources.length === 0) continue;
793
- for (const s of r.sources) {
794
- serial += 1;
795
- const suffix = s.italic ? "i" : "r";
796
- const synth = synthesizeFamilyName2(r.family, s.weight, s.italic);
797
- const data = synth.family === r.family ? s.data : rewriteFontFamilyName2(s.data, synth.family);
798
- const name = `${safeFilenamePart(synth.family)}-${s.weight}${suffix}-${id}-${serial}.ttf`;
799
- const fullPath = path3.join(fontsDir, name);
800
- await fs2.writeFile(fullPath, data);
801
- stagedPaths.push(fullPath);
802
- }
803
- }
804
- if (stagedPaths.length === 0) {
805
- return { envOverrides: {}, cleanup: async () => {
806
- } };
807
- }
808
- const { addFont, removeFont } = await getGdiBindings();
809
- const registered = [];
810
- for (const p of stagedPaths) {
811
- const added = addFont(p);
812
- if (added > 0) registered.push(p);
813
- }
814
- let cleaned = false;
815
- return {
816
- envOverrides: {
817
- // Force GDI backend so the freshly-registered fonts are visible.
818
- // Skia on Windows uses DirectWrite which does not reliably see
819
- // fonts added via AddFontResourceW.
820
- SAL_DISABLE_SKIA: "1"
821
- },
822
- cleanup: async () => {
823
- if (cleaned) return;
824
- cleaned = true;
825
- for (const p of registered) {
826
- try {
827
- removeFont(p);
828
- } catch {
829
- }
830
- }
831
- }
832
- };
833
- }
834
- };
835
- }
836
- });
837
-
838
- // src/server/services/font-staging/macos-stager.ts
839
- import { promises as fs3 } from "fs";
840
- import path4 from "path";
841
- import {
842
- synthesizeFamilyName as synthesizeFamilyName3,
843
- rewriteFontFamilyName as rewriteFontFamilyName3
844
- } from "@json-to-office/shared";
845
- var PYTHON_MACRO, REGISTRY_MOD_XCU, MacOSCoreTextStager;
846
- var init_macos_stager = __esm({
847
- "src/server/services/font-staging/macos-stager.ts"() {
848
- "use strict";
849
- init_esm_shims();
850
- init_types();
851
- PYTHON_MACRO = `# Auto-generated by @json-to-office/jto. Runs inside soffice on OnStartApp
852
- # to make staged fonts visible to LibreOffice's font enumeration. macOS 26
853
- # blocks Session/Persistent CT registration for unsigned callers, but
854
- # Process scope still works from inside the target process \u2014 which is
855
- # exactly where this macro runs.
856
- import os
857
- import sys
858
- import ctypes
859
- import ctypes.util
860
-
861
-
862
- def _log(msg):
863
- # soffice swallows Python stdout in headless mode; stderr surfaces to
864
- # the parent's pipe. The stager doesn't read this today but the
865
- # converter logs stderr on failure, which is how we'll debug.
866
- sys.stderr.write("[jto-font-register] " + msg + "\\n")
867
-
868
-
869
- def register(*_args):
870
- paths_env = os.environ.get("JTO_FONT_PATHS", "")
871
- if not paths_env:
872
- return
873
- try:
874
- cf = ctypes.CDLL(ctypes.util.find_library("CoreFoundation"))
875
- ct = ctypes.CDLL(ctypes.util.find_library("CoreText"))
876
- except Exception as e:
877
- _log("failed to load CoreFoundation/CoreText: " + repr(e))
878
- return
879
- cf.CFURLCreateFromFileSystemRepresentation.argtypes = [
880
- ctypes.c_void_p, ctypes.c_char_p, ctypes.c_long, ctypes.c_bool,
881
- ]
882
- cf.CFURLCreateFromFileSystemRepresentation.restype = ctypes.c_void_p
883
- cf.CFRelease.argtypes = [ctypes.c_void_p]
884
- ct.CTFontManagerRegisterFontsForURL.argtypes = [
885
- ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p,
886
- ]
887
- ct.CTFontManagerRegisterFontsForURL.restype = ctypes.c_bool
888
-
889
- kCTFontManagerScopeProcess = 1
890
- registered = 0
891
- for p in paths_env.split(os.pathsep):
892
- if not p:
893
- continue
894
- try:
895
- b = p.encode("utf-8")
896
- url = cf.CFURLCreateFromFileSystemRepresentation(
897
- None, b, len(b), False
898
- )
899
- if not url:
900
- _log("CFURL failed for " + p)
901
- continue
902
- ok = ct.CTFontManagerRegisterFontsForURL(
903
- url, kCTFontManagerScopeProcess, None
904
- )
905
- cf.CFRelease(url)
906
- if ok:
907
- registered += 1
908
- else:
909
- _log("CT register returned false for " + p)
910
- except Exception as e:
911
- _log("exception registering " + p + ": " + repr(e))
912
- _log("registered " + str(registered) + " font(s) at Process scope")
913
-
914
-
915
- # Expose under "register" (event-binding URL) and module-level run so
916
- # command-line vnd.sun.star.script invocation works either way.
917
- g_exportedScripts = (register,)
918
- `;
919
- REGISTRY_MOD_XCU = `<?xml version="1.0" encoding="UTF-8"?>
920
- <oor:items xmlns:oor="http://openoffice.org/2001/registry" xmlns:xs="http://www.w3.org/2001/XMLSchema">
921
- <item oor:path="/org.openoffice.Office.Events/ApplicationEvents/Bindings">
922
- <node oor:name="OnStartApp" oor:op="replace">
923
- <prop oor:name="BindingURL" oor:type="xs:string">
924
- <value>vnd.sun.star.script:JtoFontRegister.py$register?language=Python&amp;location=user</value>
925
- </prop>
926
- </node>
927
- </item>
928
- <item oor:path="/org.openoffice.Office.Common/Security/Scripting">
929
- <prop oor:name="MacroSecurityLevel" oor:op="fuse">
930
- <value>0</value>
931
- </prop>
932
- </item>
933
- </oor:items>
934
- `;
935
- MacOSCoreTextStager = class {
936
- async stage(fonts, tempDir) {
937
- const embeddable = fonts.filter((r) => r.sources.length > 0);
938
- if (embeddable.length === 0) {
939
- return { envOverrides: {}, cleanup: async () => {
940
- } };
941
- }
942
- const fontsDir = path4.join(tempDir, "fonts");
943
- await fs3.mkdir(fontsDir, { recursive: true });
944
- const id = nextStagingId();
945
- const fontPaths = [];
946
- const staged = [];
947
- let serial = 0;
948
- for (const r of embeddable) {
949
- for (const s of r.sources) {
950
- serial += 1;
951
- const suffix = s.italic ? "i" : "r";
952
- const synth = synthesizeFamilyName3(r.family, s.weight, s.italic);
953
- const data = synth.family === r.family ? s.data : rewriteFontFamilyName3(s.data, synth.family);
954
- const name = `${safeFilenamePart(synth.family)}-${s.weight}${suffix}-${id}-${serial}.ttf`;
955
- const full = path4.join(fontsDir, name);
956
- await fs3.writeFile(full, data);
957
- fontPaths.push(full);
958
- staged.push({
959
- family: synth.family,
960
- weight: s.weight,
961
- italic: s.italic,
962
- path: full
963
- });
964
- }
965
- }
966
- if (process.env.JTO_DEBUG_FONTS === "1") {
967
- console.log(
968
- "[jto macos-stager] staged " + staged.length + " font(s) for CT Process-scope registration; JTO_FONT_PATHS has " + fontPaths.length + " entries\n" + staged.map(
969
- (s) => ` ${s.family} (w=${s.weight}${s.italic ? " italic" : ""}) \u2192 ${path4.basename(s.path)}`
970
- ).join("\n")
971
- );
972
- }
973
- const profileUser = path4.join(tempDir, "user-profile", "user");
974
- const scriptsDir = path4.join(profileUser, "Scripts", "python");
975
- await fs3.mkdir(scriptsDir, { recursive: true });
976
- await fs3.writeFile(
977
- path4.join(scriptsDir, "JtoFontRegister.py"),
978
- PYTHON_MACRO
979
- );
980
- await fs3.writeFile(
981
- path4.join(profileUser, "registrymodifications.xcu"),
982
- REGISTRY_MOD_XCU
983
- );
984
- return {
985
- envOverrides: {
986
- // Colon-separated list of staged TTF paths (`:` matches Python's
987
- // `os.pathsep` on macOS). The macro reads this at OnStartApp.
988
- JTO_FONT_PATHS: fontPaths.join(":"),
989
- // Force LibreOffice's Core Graphics backend. Skia on macOS can
990
- // skip Core Text's freshly-registered fonts in some builds.
991
- SAL_DISABLE_SKIA: "1"
992
- },
993
- cleanup: async () => {
994
- }
995
- };
996
- }
997
- };
998
- }
999
- });
1000
-
1001
- // src/server/services/font-staging/index.ts
1002
- function getFontStager(platform = process.platform) {
1003
- const hit = cached.get(platform);
1004
- if (hit) return hit;
1005
- let stager;
1006
- switch (platform) {
1007
- case "win32":
1008
- stager = new WindowsFontStager();
1009
- break;
1010
- case "darwin":
1011
- stager = new MacOSCoreTextStager();
1012
- break;
1013
- case "linux":
1014
- case "freebsd":
1015
- case "openbsd":
1016
- stager = new FontconfigStager();
1017
- break;
1018
- default:
1019
- stager = new NoopFontStager();
1020
- }
1021
- cached.set(platform, stager);
1022
- return stager;
1023
- }
1024
- var cached;
1025
- var init_font_staging = __esm({
1026
- "src/server/services/font-staging/index.ts"() {
1027
- "use strict";
1028
- init_esm_shims();
1029
- init_noop_stager();
1030
- init_fontconfig_stager();
1031
- init_windows_stager();
1032
- init_macos_stager();
1033
- cached = /* @__PURE__ */ new Map();
1034
- }
1035
- });
1036
-
1037
670
  // src/server/services/libreoffice-converter.ts
1038
671
  import { execFile } from "child_process";
1039
- import { promises as fs4 } from "fs";
672
+ import { promises as fs } from "fs";
1040
673
  import os from "os";
1041
- import path5 from "path";
674
+ import path2 from "path";
675
+ import { getFontStager } from "@json-to-office/jto-cli";
1042
676
  function executeFile(binary, args, timeoutMs, envOverrides) {
1043
677
  return new Promise((resolve2, reject) => {
1044
678
  execFile(
@@ -1070,7 +704,7 @@ function toErrorText(value) {
1070
704
  return String(value);
1071
705
  }
1072
706
  function sanitizeBaseName(originalName) {
1073
- const parsed = path5.parse(originalName || "document");
707
+ const parsed = path2.parse(originalName || "document");
1074
708
  return (parsed.name || "document").replace(/[^a-zA-Z0-9._-]/g, "_") || "document";
1075
709
  }
1076
710
  var DEFAULT_CONVERSION_TIMEOUT_MS, BINARY_PROBE_TIMEOUT_MS, MAX_EXEC_BUFFER_BYTES, LibreOfficeError, LibreOfficeBinaryNotFoundError, LibreOfficeTimeoutError, LibreOfficeConversionError, LibreOfficeOutputNotFoundError, LibreOfficeConverterService;
@@ -1079,7 +713,6 @@ var init_libreoffice_converter = __esm({
1079
713
  "use strict";
1080
714
  init_esm_shims();
1081
715
  init_config();
1082
- init_font_staging();
1083
716
  DEFAULT_CONVERSION_TIMEOUT_MS = 3e4;
1084
717
  BINARY_PROBE_TIMEOUT_MS = 5e3;
1085
718
  MAX_EXEC_BUFFER_BYTES = 20 * 1024 * 1024;
@@ -1144,20 +777,20 @@ var init_libreoffice_converter = __esm({
1144
777
  throw new LibreOfficeConversionError("Input file is empty");
1145
778
  }
1146
779
  const binaryPath = await this.resolveBinaryPath();
1147
- const tempDir = await fs4.mkdtemp(
1148
- path5.join(os.tmpdir(), "jto-libreoffice-")
780
+ const tempDir = await fs.mkdtemp(
781
+ path2.join(os.tmpdir(), "jto-libreoffice-")
1149
782
  );
1150
783
  const outputBaseName = sanitizeBaseName(originalName);
1151
784
  const ext = this.format === "pptx" ? ".pptx" : ".docx";
1152
- const inputPath = path5.join(tempDir, `${outputBaseName}${ext}`);
1153
- const pdfPath = path5.join(tempDir, `${outputBaseName}.pdf`);
785
+ const inputPath = path2.join(tempDir, `${outputBaseName}${ext}`);
786
+ const pdfPath = path2.join(tempDir, `${outputBaseName}.pdf`);
1154
787
  const stager = getFontStager();
1155
788
  const fontsToStage = (resolvedFonts ?? []).filter(
1156
789
  (r) => r.sources.length > 0
1157
790
  );
1158
791
  const stageHandle = fontsToStage.length > 0 ? await stager.stage(fontsToStage, tempDir) : null;
1159
792
  try {
1160
- await fs4.writeFile(inputPath, input);
793
+ await fs.writeFile(inputPath, input);
1161
794
  const filterName = this.format === "pptx" ? "impress_pdf_Export" : "writer_pdf_Export";
1162
795
  await this.runConversion(
1163
796
  binaryPath,
@@ -1167,7 +800,7 @@ var init_libreoffice_converter = __esm({
1167
800
  stageHandle?.envOverrides
1168
801
  );
1169
802
  try {
1170
- return await fs4.readFile(pdfPath);
803
+ return await fs.readFile(pdfPath);
1171
804
  } catch (error) {
1172
805
  if (error.code === "ENOENT") {
1173
806
  throw new LibreOfficeOutputNotFoundError(pdfPath);
@@ -1179,7 +812,7 @@ var init_libreoffice_converter = __esm({
1179
812
  await stageHandle.cleanup().catch(() => {
1180
813
  });
1181
814
  }
1182
- await fs4.rm(tempDir, { recursive: true, force: true }).catch(() => {
815
+ await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {
1183
816
  });
1184
817
  }
1185
818
  }
@@ -1206,9 +839,9 @@ var init_libreoffice_converter = __esm({
1206
839
  throw new LibreOfficeBinaryNotFoundError(candidates);
1207
840
  }
1208
841
  async isBinaryAvailable(binaryPath) {
1209
- if (binaryPath.includes(path5.sep)) {
842
+ if (binaryPath.includes(path2.sep)) {
1210
843
  try {
1211
- await fs4.access(binaryPath);
844
+ await fs.access(binaryPath);
1212
845
  } catch {
1213
846
  return false;
1214
847
  }
@@ -1222,7 +855,7 @@ var init_libreoffice_converter = __esm({
1222
855
  }
1223
856
  }
1224
857
  async runConversion(binaryPath, inputPath, outputDir, filterName, envOverrides) {
1225
- const userProfilePath = path5.join(outputDir, "user-profile").replace(/\\/g, "/");
858
+ const userProfilePath = path2.join(outputDir, "user-profile").replace(/\\/g, "/");
1226
859
  const userInstallation = `file:///${userProfilePath.replace(/^\//, "")}`;
1227
860
  const args = [
1228
861
  "--headless",
@@ -1640,36 +1273,36 @@ function isPrivateHost(host) {
1640
1273
  const firstIpv6Group = Number.parseInt(normalized.split(":")[0] || "0", 16);
1641
1274
  return firstIpv6Group >= 64512 && firstIpv6Group <= 65023 || firstIpv6Group >= 65152 && firstIpv6Group <= 65215;
1642
1275
  }
1643
- function assertAllowedUrl(source, path8, allowedHosts) {
1276
+ function assertAllowedUrl(source, path5, allowedHosts) {
1644
1277
  if (source.startsWith("data:")) return;
1645
1278
  let url;
1646
1279
  try {
1647
1280
  url = new URL(source);
1648
1281
  } catch {
1649
1282
  throw new UnsafeOutboundSourceError(
1650
- path8,
1283
+ path5,
1651
1284
  "local and relative file paths are disabled for HTTP requests"
1652
1285
  );
1653
1286
  }
1654
1287
  if (url.protocol !== "https:") {
1655
- throw new UnsafeOutboundSourceError(path8, "only HTTPS URLs are allowed");
1288
+ throw new UnsafeOutboundSourceError(path5, "only HTTPS URLs are allowed");
1656
1289
  }
1657
1290
  if (url.username || url.password) {
1658
1291
  throw new UnsafeOutboundSourceError(
1659
- path8,
1292
+ path5,
1660
1293
  "URLs containing credentials are not allowed"
1661
1294
  );
1662
1295
  }
1663
1296
  const host = url.hostname.toLowerCase().replace(/\.$/, "");
1664
1297
  if (isPrivateHost(host)) {
1665
1298
  throw new UnsafeOutboundSourceError(
1666
- path8,
1299
+ path5,
1667
1300
  `private or local host "${host}" is not allowed`
1668
1301
  );
1669
1302
  }
1670
1303
  if (!allowedHosts.some((pattern) => hostMatches(host, pattern))) {
1671
1304
  throw new UnsafeOutboundSourceError(
1672
- path8,
1305
+ path5,
1673
1306
  `host "${host}" is not in OUTBOUND_HOST_ALLOWLIST`
1674
1307
  );
1675
1308
  }
@@ -1687,13 +1320,13 @@ function extractCssReferences(input) {
1687
1320
  function extractAbsoluteUrls(input) {
1688
1321
  return input.match(/(?:https?|file):\/\/[^\s'"<>)}\]]+/gi) ?? [];
1689
1322
  }
1690
- function assertSafeSvg(svg, path8, allowedHosts) {
1323
+ function assertSafeSvg(svg, path5, allowedHosts) {
1691
1324
  if (/<\s*script\b/i.test(svg) || /<\s*(?:foreignObject|iframe|object|embed)\b/i.test(svg) || /\bon[a-z]+\s*=/i.test(svg)) {
1692
- throw new UnsafeOutboundSourceError(path8, "active SVG content is disabled");
1325
+ throw new UnsafeOutboundSourceError(path5, "active SVG content is disabled");
1693
1326
  }
1694
1327
  if (/<!\s*(?:DOCTYPE|ENTITY)\b/i.test(svg)) {
1695
1328
  throw new UnsafeOutboundSourceError(
1696
- path8,
1329
+ path5,
1697
1330
  "SVG document types and entities are disabled"
1698
1331
  );
1699
1332
  }
@@ -1702,20 +1335,20 @@ function assertSafeSvg(svg, path8, allowedHosts) {
1702
1335
  while (match = refPattern.exec(svg)) {
1703
1336
  const ref = match[2].trim();
1704
1337
  if (!ref || ref.startsWith("#") || ref.startsWith("data:")) continue;
1705
- assertAllowedUrl(ref, path8, allowedHosts);
1338
+ assertAllowedUrl(ref, path5, allowedHosts);
1706
1339
  }
1707
1340
  for (const ref of extractCssReferences(svg)) {
1708
1341
  if (ref.startsWith("#") || ref.startsWith("data:")) continue;
1709
- assertAllowedUrl(ref, path8, allowedHosts);
1342
+ assertAllowedUrl(ref, path5, allowedHosts);
1710
1343
  }
1711
1344
  }
1712
- function assertSafeEncodedImage(source, path8, allowedHosts) {
1345
+ function assertSafeEncodedImage(source, path5, allowedHosts) {
1713
1346
  let candidate = source.trim();
1714
1347
  let declaredSvg = false;
1715
1348
  if (candidate.startsWith("data:")) {
1716
1349
  const comma = candidate.indexOf(",");
1717
1350
  if (comma < 0) {
1718
- throw new UnsafeOutboundSourceError(path8, "malformed image data URL");
1351
+ throw new UnsafeOutboundSourceError(path5, "malformed image data URL");
1719
1352
  }
1720
1353
  const metadata = candidate.slice(5, comma);
1721
1354
  declaredSvg = metadata.split(";")[0].toLowerCase() === "image/svg+xml";
@@ -1724,7 +1357,7 @@ function assertSafeEncodedImage(source, path8, allowedHosts) {
1724
1357
  try {
1725
1358
  candidate = /(?:^|;)base64(?:;|$)/i.test(metadata) ? Buffer.from(payload, "base64").toString("utf8") : decodeURIComponent(payload);
1726
1359
  } catch {
1727
- throw new UnsafeOutboundSourceError(path8, "malformed SVG data URL");
1360
+ throw new UnsafeOutboundSourceError(path5, "malformed SVG data URL");
1728
1361
  }
1729
1362
  } else if (!/<\s*svg\b/i.test(candidate)) {
1730
1363
  candidate = Buffer.from(candidate, "base64").toString("utf8");
@@ -1732,37 +1365,37 @@ function assertSafeEncodedImage(source, path8, allowedHosts) {
1732
1365
  const svgStart = candidate.search(/<\s*svg\b/i);
1733
1366
  if (svgStart < 0) {
1734
1367
  if (declaredSvg) {
1735
- throw new UnsafeOutboundSourceError(path8, "malformed SVG image data");
1368
+ throw new UnsafeOutboundSourceError(path5, "malformed SVG image data");
1736
1369
  }
1737
1370
  return;
1738
1371
  }
1739
- assertSafeSvg(candidate, path8, allowedHosts);
1372
+ assertSafeSvg(candidate, path5, allowedHosts);
1740
1373
  }
1741
- function assertSafeResources(resources, path8, allowedHosts) {
1374
+ function assertSafeResources(resources, path5, allowedHosts) {
1742
1375
  if (typeof resources.js === "string" && resources.js.trim()) {
1743
1376
  throw new UnsafeOutboundSourceError(
1744
- `${path8}.js`,
1377
+ `${path5}.js`,
1745
1378
  "remote renderer JavaScript resources are disabled"
1746
1379
  );
1747
1380
  }
1748
1381
  if (typeof resources.css === "string") {
1749
1382
  for (const ref of extractCssReferences(resources.css)) {
1750
1383
  if (ref.startsWith("data:")) continue;
1751
- assertAllowedUrl(ref, `${path8}.css`, allowedHosts);
1384
+ assertAllowedUrl(ref, `${path5}.css`, allowedHosts);
1752
1385
  }
1753
1386
  }
1754
1387
  if (Array.isArray(resources.files) && resources.files.length > 0) {
1755
1388
  throw new UnsafeOutboundSourceError(
1756
- `${path8}.files`,
1389
+ `${path5}.files`,
1757
1390
  "remote renderer JavaScript resources are disabled"
1758
1391
  );
1759
1392
  }
1760
1393
  }
1761
- function assertNoRemoteReferences(value, path8, allowedHosts, seen) {
1394
+ function assertNoRemoteReferences(value, path5, allowedHosts, seen) {
1762
1395
  if (typeof value === "string") {
1763
1396
  if (/\b(?:fetch|XMLHttpRequest|WebSocket|EventSource)\s*\(/i.test(value) || /\b(?:javascript|file):/i.test(value)) {
1764
1397
  throw new UnsafeOutboundSourceError(
1765
- path8,
1398
+ path5,
1766
1399
  "network-capable JavaScript and file URLs are disabled"
1767
1400
  );
1768
1401
  }
@@ -1771,11 +1404,11 @@ function assertNoRemoteReferences(value, path8, allowedHosts, seen) {
1771
1404
  ...extractCssReferences(value)
1772
1405
  ]) {
1773
1406
  if (url.startsWith("data:")) continue;
1774
- assertAllowedUrl(url, path8, allowedHosts);
1407
+ assertAllowedUrl(url, path5, allowedHosts);
1775
1408
  }
1776
1409
  if (/^\/\//.test(value.trim())) {
1777
1410
  throw new UnsafeOutboundSourceError(
1778
- path8,
1411
+ path5,
1779
1412
  "protocol-relative URLs are disabled"
1780
1413
  );
1781
1414
  }
@@ -1785,20 +1418,20 @@ function assertNoRemoteReferences(value, path8, allowedHosts, seen) {
1785
1418
  seen.add(value);
1786
1419
  if (Array.isArray(value)) {
1787
1420
  value.forEach(
1788
- (entry, index) => assertNoRemoteReferences(entry, `${path8}[${index}]`, allowedHosts, seen)
1421
+ (entry, index) => assertNoRemoteReferences(entry, `${path5}[${index}]`, allowedHosts, seen)
1789
1422
  );
1790
1423
  return;
1791
1424
  }
1792
1425
  for (const [key, child] of Object.entries(value)) {
1793
- assertNoRemoteReferences(child, `${path8}.${key}`, allowedHosts, seen);
1426
+ assertNoRemoteReferences(child, `${path5}.${key}`, allowedHosts, seen);
1794
1427
  }
1795
1428
  }
1796
- function visit(value, path8, containerKey, allowedHosts, seen) {
1429
+ function visit(value, path5, containerKey, allowedHosts, seen) {
1797
1430
  if (!value || typeof value !== "object" || seen.has(value)) return;
1798
1431
  seen.add(value);
1799
1432
  if (Array.isArray(value)) {
1800
1433
  value.forEach(
1801
- (entry, index) => visit(entry, `${path8}[${index}]`, containerKey, allowedHosts, seen)
1434
+ (entry, index) => visit(entry, `${path5}[${index}]`, containerKey, allowedHosts, seen)
1802
1435
  );
1803
1436
  return;
1804
1437
  }
@@ -1806,7 +1439,7 @@ function visit(value, path8, containerKey, allowedHosts, seen) {
1806
1439
  for (const key of Object.keys(record)) {
1807
1440
  if (DANGEROUS_KEYS.has(key)) {
1808
1441
  throw new UnsafeOutboundSourceError(
1809
- `${path8}.${key}`,
1442
+ `${path5}.${key}`,
1810
1443
  "prototype mutation keys are disabled"
1811
1444
  );
1812
1445
  }
@@ -1814,31 +1447,31 @@ function visit(value, path8, containerKey, allowedHosts, seen) {
1814
1447
  const kind = typeof record.kind === "string" ? record.kind : void 0;
1815
1448
  if (kind === "file" && typeof record.path === "string") {
1816
1449
  throw new UnsafeOutboundSourceError(
1817
- `${path8}.path`,
1450
+ `${path5}.path`,
1818
1451
  "local file sources are disabled for HTTP requests"
1819
1452
  );
1820
1453
  }
1821
1454
  if ((kind === "url" || kind === "variable") && typeof record.url === "string") {
1822
- assertAllowedUrl(record.url, `${path8}.url`, allowedHosts);
1455
+ assertAllowedUrl(record.url, `${path5}.url`, allowedHosts);
1823
1456
  }
1824
1457
  const componentName = typeof record.name === "string" ? record.name.toLowerCase() : void 0;
1825
1458
  const props = isRecord(record.props) ? record.props : void 0;
1826
1459
  if (componentName && props) {
1827
1460
  if (COMPONENT_SOURCE_NAMES.has(componentName) && typeof props.path === "string") {
1828
- assertAllowedUrl(props.path, `${path8}.props.path`, allowedHosts);
1829
- assertSafeEncodedImage(props.path, `${path8}.props.path`, allowedHosts);
1461
+ assertAllowedUrl(props.path, `${path5}.props.path`, allowedHosts);
1462
+ assertSafeEncodedImage(props.path, `${path5}.props.path`, allowedHosts);
1830
1463
  }
1831
1464
  if (COMPONENT_SOURCE_NAMES.has(componentName) && typeof props.base64 === "string") {
1832
1465
  assertSafeEncodedImage(
1833
1466
  props.base64,
1834
- `${path8}.props.base64`,
1467
+ `${path5}.props.base64`,
1835
1468
  allowedHosts
1836
1469
  );
1837
1470
  }
1838
1471
  if ((componentName === "highcharts" || componentName === "visual") && typeof props.serverUrl === "string") {
1839
1472
  assertAllowedUrl(
1840
1473
  props.serverUrl,
1841
- `${path8}.props.serverUrl`,
1474
+ `${path5}.props.serverUrl`,
1842
1475
  allowedHosts
1843
1476
  );
1844
1477
  }
@@ -1846,30 +1479,30 @@ function visit(value, path8, containerKey, allowedHosts, seen) {
1846
1479
  if (isRecord(props.resources)) {
1847
1480
  assertSafeResources(
1848
1481
  props.resources,
1849
- `${path8}.props.resources`,
1482
+ `${path5}.props.resources`,
1850
1483
  allowedHosts
1851
1484
  );
1852
1485
  }
1853
1486
  assertNoRemoteReferences(
1854
1487
  props.options,
1855
- `${path8}.props.options`,
1488
+ `${path5}.props.options`,
1856
1489
  allowedHosts,
1857
1490
  /* @__PURE__ */ new WeakSet()
1858
1491
  );
1859
1492
  }
1860
1493
  }
1861
1494
  if (containerKey === "image" && typeof record.path === "string") {
1862
- assertAllowedUrl(record.path, `${path8}.path`, allowedHosts);
1863
- assertSafeEncodedImage(record.path, `${path8}.path`, allowedHosts);
1495
+ assertAllowedUrl(record.path, `${path5}.path`, allowedHosts);
1496
+ assertSafeEncodedImage(record.path, `${path5}.path`, allowedHosts);
1864
1497
  }
1865
1498
  if (typeof record.svg === "string") {
1866
- assertSafeSvg(record.svg, `${path8}.svg`, allowedHosts);
1499
+ assertSafeSvg(record.svg, `${path5}.svg`, allowedHosts);
1867
1500
  }
1868
1501
  if (containerKey === "resources") {
1869
- assertSafeResources(record, path8, allowedHosts);
1502
+ assertSafeResources(record, path5, allowedHosts);
1870
1503
  }
1871
1504
  for (const [key, child] of Object.entries(record)) {
1872
- visit(child, `${path8}.${key}`, key, allowedHosts, seen);
1505
+ visit(child, `${path5}.${key}`, key, allowedHosts, seen);
1873
1506
  }
1874
1507
  }
1875
1508
  function assertSafeOutboundSources(value, policy, rootPath = "request") {
@@ -1890,9 +1523,9 @@ var init_outbound_source_policy = __esm({
1890
1523
  "use strict";
1891
1524
  init_esm_shims();
1892
1525
  UnsafeOutboundSourceError = class extends Error {
1893
- constructor(path8, reason) {
1894
- super(`Unsafe outbound source at ${path8}: ${reason}`);
1895
- this.path = path8;
1526
+ constructor(path5, reason) {
1527
+ super(`Unsafe outbound source at ${path5}: ${reason}`);
1528
+ this.path = path5;
1896
1529
  this.name = "UnsafeOutboundSourceError";
1897
1530
  }
1898
1531
  };
@@ -1902,7 +1535,7 @@ var init_outbound_source_policy = __esm({
1902
1535
  });
1903
1536
 
1904
1537
  // src/server/rasterize-route.ts
1905
- import path6 from "path";
1538
+ import path3 from "path";
1906
1539
  import { Type as Type2 } from "@sinclair/typebox";
1907
1540
  import { bodyLimit } from "hono/body-limit";
1908
1541
  import { HTTPException as HTTPException3 } from "hono/http-exception";
@@ -1911,7 +1544,9 @@ import {
1911
1544
  DEFAULT_VISUAL_DPI,
1912
1545
  MIN_VISUAL_DPI,
1913
1546
  MAX_VISUAL_DPI,
1914
- MAX_RASTERIZE_BATCH_SLIDES
1547
+ MAX_RASTERIZE_BATCH_SLIDES,
1548
+ MAX_RASTERIZE_FONTS,
1549
+ MAX_RASTERIZE_FONT_BYTES
1915
1550
  } from "@json-to-office/shared";
1916
1551
  import {
1917
1552
  createLibreOfficePptxRasterizer,
@@ -1953,11 +1588,23 @@ function assertPixelBudget(slides) {
1953
1588
  });
1954
1589
  }
1955
1590
  }
1591
+ function assertFontBudget(fonts) {
1592
+ if (!fonts?.length) return;
1593
+ const bytes = fonts.reduce(
1594
+ (sum, f) => sum + Math.ceil(f.data.length * 0.75),
1595
+ 0
1596
+ );
1597
+ if (bytes > MAX_RASTERIZE_FONT_BYTES) {
1598
+ throw new HTTPException3(400, {
1599
+ message: "Requested font payload is too large"
1600
+ });
1601
+ }
1602
+ }
1956
1603
  function resolveSafeBaseDir(baseDir) {
1957
1604
  if (baseDir === void 0) return void 0;
1958
- const resolved = path6.resolve(baseDir);
1605
+ const resolved = path3.resolve(baseDir);
1959
1606
  const cwd = process.cwd();
1960
- if (resolved !== cwd && !resolved.startsWith(cwd + path6.sep)) {
1607
+ if (resolved !== cwd && !resolved.startsWith(cwd + path3.sep)) {
1961
1608
  throw new HTTPException3(400, {
1962
1609
  message: "baseDir must be inside the server working directory"
1963
1610
  });
@@ -1980,13 +1627,22 @@ function toHttpException(error) {
1980
1627
  message: "Internal server error during rasterization"
1981
1628
  });
1982
1629
  }
1630
+ function resolveMaxBodyBytes(explicit) {
1631
+ const fromEnv = Number.parseInt(
1632
+ process.env.MAX_RASTERIZE_BODY_SIZE ?? "",
1633
+ 10
1634
+ );
1635
+ const configured = explicit ?? (Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : config.requestLimits.maxBodySize);
1636
+ return Math.min(configured, MAX_RASTERIZE_BODY_CEILING);
1637
+ }
1983
1638
  function registerRasterizeRoute(router, options = {}) {
1984
1639
  const getRasterizer = options.getRasterizer ?? getSharedRasterizer;
1985
1640
  const getBatchRasterizer = options.getBatchRasterizer ?? getSharedBatchRasterizer;
1641
+ const maxBodyBytes = resolveMaxBodyBytes(options.maxBodyBytes);
1986
1642
  const shared = [
1987
1643
  ...options.preMiddleware ?? [],
1988
1644
  bodyLimit({
1989
- maxSize: 32 * 1024 * 1024,
1645
+ maxSize: maxBodyBytes,
1990
1646
  onError: () => {
1991
1647
  throw new HTTPException3(413, { message: "Request body too large" });
1992
1648
  }
@@ -2006,10 +1662,11 @@ function registerRasterizeRoute(router, options = {}) {
2006
1662
  ...shared,
2007
1663
  tbValidator(RasterizeRequestSchema),
2008
1664
  async (c) => {
2009
- const { presentation, dpi, baseDir } = getValidated(c, "json");
1665
+ const { presentation, dpi, baseDir, fonts } = getValidated(c, "json");
2010
1666
  const safeBaseDir = resolveSafeBaseDir(baseDir);
2011
1667
  const effectiveDpi = clampVisualDpi(dpi ?? DEFAULT_VISUAL_DPI);
2012
1668
  assertPixelBudget([{ presentation, dpi: effectiveDpi }]);
1669
+ assertFontBudget(fonts);
2013
1670
  const result = await guard(async () => {
2014
1671
  if (options.sourcePolicy) {
2015
1672
  assertSafeOutboundSources(
@@ -2021,7 +1678,12 @@ function registerRasterizeRoute(router, options = {}) {
2021
1678
  return getRasterizer()({
2022
1679
  presentation,
2023
1680
  dpi: effectiveDpi,
2024
- baseDir: safeBaseDir
1681
+ baseDir: safeBaseDir,
1682
+ // Conditional so a fontless request reaches the engine with no
1683
+ // `fonts` key at all — an `undefined` would still be an absent
1684
+ // digest, but keeping the object identical to the pre-change shape
1685
+ // makes the cache-key equivalence obvious.
1686
+ ...fonts?.length && { fonts }
2025
1687
  });
2026
1688
  });
2027
1689
  return c.json(result);
@@ -2032,13 +1694,14 @@ function registerRasterizeRoute(router, options = {}) {
2032
1694
  ...shared,
2033
1695
  tbValidator(RasterizeBatchRequestSchema),
2034
1696
  async (c) => {
2035
- const { slides, baseDir } = getValidated(c, "json");
1697
+ const { slides, baseDir, fonts } = getValidated(c, "json");
2036
1698
  const safeBaseDir = resolveSafeBaseDir(baseDir);
2037
1699
  const effectiveSlides = slides.map((slide) => ({
2038
1700
  presentation: slide.presentation,
2039
1701
  dpi: clampVisualDpi(slide.dpi ?? DEFAULT_VISUAL_DPI)
2040
1702
  }));
2041
1703
  assertPixelBudget(effectiveSlides);
1704
+ assertFontBudget(fonts);
2042
1705
  const result = await guard(async () => {
2043
1706
  if (options.sourcePolicy) {
2044
1707
  slides.forEach(
@@ -2051,7 +1714,8 @@ function registerRasterizeRoute(router, options = {}) {
2051
1714
  }
2052
1715
  return getBatchRasterizer()({
2053
1716
  slides: effectiveSlides,
2054
- baseDir: safeBaseDir
1717
+ baseDir: safeBaseDir,
1718
+ ...fonts?.length && { fonts }
2055
1719
  });
2056
1720
  });
2057
1721
  return c.json({
@@ -2068,20 +1732,47 @@ function registerRasterizeRoute(router, options = {}) {
2068
1732
  }
2069
1733
  );
2070
1734
  }
2071
- var RasterizeRequestSchema, RasterizeBatchRequestSchema, sharedRasterizer, sharedBatchRasterizer, jsonOnly, DEFAULT_SLIDE_WIDTH_IN, DEFAULT_SLIDE_HEIGHT_IN, MAX_SLIDE_PIXELS, MAX_BATCH_PIXELS;
1735
+ var BASE64_PATTERN, RasterizeFontFaceSchema, FontsSchema, RasterizeRequestSchema, RasterizeBatchRequestSchema, sharedRasterizer, sharedBatchRasterizer, jsonOnly, DEFAULT_SLIDE_WIDTH_IN, DEFAULT_SLIDE_HEIGHT_IN, MAX_SLIDE_PIXELS, MAX_BATCH_PIXELS, MAX_RASTERIZE_BODY_CEILING;
2072
1736
  var init_rasterize_route = __esm({
2073
1737
  "src/server/rasterize-route.ts"() {
2074
1738
  "use strict";
2075
1739
  init_esm_shims();
1740
+ init_config();
2076
1741
  init_typebox_validator();
2077
1742
  init_outbound_source_policy();
1743
+ BASE64_PATTERN = "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$";
1744
+ RasterizeFontFaceSchema = Type2.Object(
1745
+ {
1746
+ family: Type2.String({ minLength: 1, maxLength: 128 }),
1747
+ weight: Type2.Integer({ minimum: 1, maximum: 1e3 }),
1748
+ italic: Type2.Boolean(),
1749
+ data: Type2.String({
1750
+ minLength: 1,
1751
+ maxLength: 4 * 1024 * 1024,
1752
+ pattern: BASE64_PATTERN
1753
+ }),
1754
+ format: Type2.Optional(
1755
+ Type2.Union([
1756
+ Type2.Literal("ttf"),
1757
+ Type2.Literal("otf"),
1758
+ Type2.Literal("woff"),
1759
+ Type2.Literal("woff2")
1760
+ ])
1761
+ )
1762
+ },
1763
+ { additionalProperties: false }
1764
+ );
1765
+ FontsSchema = Type2.Optional(
1766
+ Type2.Array(RasterizeFontFaceSchema, { maxItems: MAX_RASTERIZE_FONTS })
1767
+ );
2078
1768
  RasterizeRequestSchema = Type2.Object(
2079
1769
  {
2080
1770
  presentation: Type2.Object({}, { additionalProperties: true }),
2081
1771
  dpi: Type2.Optional(
2082
1772
  Type2.Number({ minimum: MIN_VISUAL_DPI, maximum: MAX_VISUAL_DPI })
2083
1773
  ),
2084
- baseDir: Type2.Optional(Type2.String())
1774
+ baseDir: Type2.Optional(Type2.String()),
1775
+ fonts: FontsSchema
2085
1776
  },
2086
1777
  { additionalProperties: false }
2087
1778
  );
@@ -2099,7 +1790,13 @@ var init_rasterize_route = __esm({
2099
1790
  ),
2100
1791
  { minItems: 1, maxItems: MAX_RASTERIZE_BATCH_SLIDES }
2101
1792
  ),
2102
- baseDir: Type2.Optional(Type2.String())
1793
+ baseDir: Type2.Optional(Type2.String()),
1794
+ // Request-level, shared by every slide, exactly like `baseDir`. NOT
1795
+ // inside the per-slide object above, whose `additionalProperties: false`
1796
+ // must keep rejecting per-slide fonts: a uniform slide shape is what
1797
+ // lets batch dedupe and the disk-cache key stay identical to the
1798
+ // single-slide path.
1799
+ fonts: FontsSchema
2103
1800
  },
2104
1801
  { additionalProperties: false }
2105
1802
  );
@@ -2116,19 +1813,20 @@ var init_rasterize_route = __esm({
2116
1813
  DEFAULT_SLIDE_HEIGHT_IN = 7.5;
2117
1814
  MAX_SLIDE_PIXELS = 64e6;
2118
1815
  MAX_BATCH_PIXELS = 256e6;
1816
+ MAX_RASTERIZE_BODY_CEILING = 64 * 1024 * 1024;
2119
1817
  }
2120
1818
  });
2121
1819
 
2122
1820
  // src/server/services/template-media-inliner.ts
2123
- import { promises as fs5 } from "fs";
2124
- import path7 from "path";
1821
+ import { promises as fs2 } from "fs";
1822
+ import path4 from "path";
2125
1823
  function isRecord2(value) {
2126
1824
  return typeof value === "object" && value !== null && !Array.isArray(value);
2127
1825
  }
2128
1826
  function isInlineCandidate(source) {
2129
1827
  if (!source || source.includes("\0")) return false;
2130
1828
  if (source.startsWith("data:")) return false;
2131
- if (path7.isAbsolute(source)) return false;
1829
+ if (path4.isAbsolute(source)) return false;
2132
1830
  try {
2133
1831
  new URL(source);
2134
1832
  return false;
@@ -2140,17 +1838,17 @@ async function toDataUrl(source, baseDir, state) {
2140
1838
  let cacheKey = source;
2141
1839
  let result = null;
2142
1840
  try {
2143
- const resolved = await fs5.realpath(path7.resolve(baseDir, source));
1841
+ const resolved = await fs2.realpath(path4.resolve(baseDir, source));
2144
1842
  cacheKey = resolved;
2145
- const cached2 = state.cache.get(cacheKey);
2146
- if (cached2 !== void 0) return cached2;
2147
- const contained = resolved === state.baseDirReal || resolved.startsWith(state.baseDirReal + path7.sep);
1843
+ const cached = state.cache.get(cacheKey);
1844
+ if (cached !== void 0) return cached;
1845
+ const contained = resolved === state.baseDirReal || resolved.startsWith(state.baseDirReal + path4.sep);
2148
1846
  if (contained) {
2149
- const mime = MIME_BY_EXTENSION[path7.extname(resolved).toLowerCase()];
1847
+ const mime = MIME_BY_EXTENSION[path4.extname(resolved).toLowerCase()];
2150
1848
  if (mime) {
2151
- const stat = await fs5.stat(resolved);
1849
+ const stat = await fs2.stat(resolved);
2152
1850
  if (stat.isFile() && stat.size <= state.maxFileBytes && stat.size <= state.totalBudget) {
2153
- const buffer = await fs5.readFile(resolved);
1851
+ const buffer = await fs2.readFile(resolved);
2154
1852
  state.totalBudget -= buffer.length;
2155
1853
  result = `data:${mime};base64,${buffer.toString("base64")}`;
2156
1854
  }
@@ -2198,7 +1896,7 @@ async function inlineTemplateMedia(jsonDefinition, baseDir, options = {}) {
2198
1896
  }
2199
1897
  let baseDirReal;
2200
1898
  try {
2201
- baseDirReal = await fs5.realpath(baseDir);
1899
+ baseDirReal = await fs2.realpath(baseDir);
2202
1900
  } catch {
2203
1901
  return jsonDefinition;
2204
1902
  }
@@ -2242,9 +1940,9 @@ import {
2242
1940
  import { dirname } from "path";
2243
1941
  function createFormatRouter(adapter) {
2244
1942
  const router = new Hono2();
2245
- const assertRequestSources = (value, path8) => {
1943
+ const assertRequestSources = (value, path5) => {
2246
1944
  try {
2247
- assertSafeOutboundSources(value, config.outboundSources, path8);
1945
+ assertSafeOutboundSources(value, config.outboundSources, path5);
2248
1946
  } catch (error) {
2249
1947
  if (error instanceof UnsafeOutboundSourceError) {
2250
1948
  throw new HTTPException4(400, { message: error.message });
@@ -4155,9 +3853,9 @@ var init_unified_server = __esm({
4155
3853
  return await apiApp.fetch(c.req.raw, c.env);
4156
3854
  });
4157
3855
  this.app.use("*", async (c, next) => {
4158
- const path8 = c.req.path;
4159
- if (path8.startsWith("/api")) {
4160
- if (/\.(ts|tsx|js|jsx|css|map)$/.test(path8)) {
3856
+ const path5 = c.req.path;
3857
+ if (path5.startsWith("/api")) {
3858
+ if (/\.(ts|tsx|js|jsx|css|map)$/.test(path5)) {
4161
3859
  return next();
4162
3860
  }
4163
3861
  const response = await apiApp.fetch(c.req.raw, c.env);
@@ -4260,8 +3958,8 @@ var init_unified_server = __esm({
4260
3958
  }
4261
3959
  if (this.viteServer) {
4262
3960
  this.app.use("*", async (c, next) => {
4263
- const path8 = c.req.path;
4264
- if ((path8.startsWith("/api") || path8 === "/health") && !/\.(ts|tsx|js|jsx|css|map)$/.test(path8)) {
3961
+ const path5 = c.req.path;
3962
+ if ((path5.startsWith("/api") || path5 === "/health") && !/\.(ts|tsx|js|jsx|css|map)$/.test(path5)) {
4265
3963
  return next();
4266
3964
  }
4267
3965
  const env = c.env;
@@ -4280,7 +3978,7 @@ var init_unified_server = __esm({
4280
3978
  }
4281
3979
  async setupBuiltClient(clientPath) {
4282
3980
  const { serveStatic } = await import("@hono/node-server/serve-static");
4283
- const fs6 = await import("fs");
3981
+ const fs3 = await import("fs");
4284
3982
  const { extname } = await import("path");
4285
3983
  const mime = await import("mime-types");
4286
3984
  const format = this.adapter.name.replace(/[^a-zA-Z0-9]/g, "");
@@ -4288,7 +3986,7 @@ var init_unified_server = __esm({
4288
3986
  "/assets/*",
4289
3987
  serveStatic({
4290
3988
  root: clientPath,
4291
- rewriteRequestPath: (path8) => path8.replace(/^\/assets/, "/assets")
3989
+ rewriteRequestPath: (path5) => path5.replace(/^\/assets/, "/assets")
4292
3990
  })
4293
3991
  );
4294
3992
  this.app.use("/*", async (c, next) => {
@@ -4297,9 +3995,9 @@ var init_unified_server = __esm({
4297
3995
  const ext = extname(reqPath);
4298
3996
  if (ext) {
4299
3997
  const filePath = resolve(clientPath, reqPath.slice(1));
4300
- if (fs6.existsSync(filePath)) {
3998
+ if (fs3.existsSync(filePath)) {
4301
3999
  const mimeType = mime.lookup(filePath) || "application/octet-stream";
4302
- const content = fs6.readFileSync(filePath);
4000
+ const content = fs3.readFileSync(filePath);
4303
4001
  c.header("Content-Type", mimeType);
4304
4002
  return c.body(content);
4305
4003
  }
@@ -4311,8 +4009,8 @@ var init_unified_server = __esm({
4311
4009
  if (reqPath.startsWith("/api") || reqPath === "/health")
4312
4010
  return c.notFound();
4313
4011
  const indexPath = resolve(clientPath, "index.html");
4314
- if (fs6.existsSync(indexPath)) {
4315
- let html = fs6.readFileSync(indexPath, "utf-8");
4012
+ if (fs3.existsSync(indexPath)) {
4013
+ let html = fs3.readFileSync(indexPath, "utf-8");
4316
4014
  html = html.replace(
4317
4015
  "</head>",
4318
4016
  `<script>window.__JTO_FORMAT__ = '${format}';</script>
@@ -4473,7 +4171,7 @@ function createDevCommand(adapter) {
4473
4171
  }
4474
4172
 
4475
4173
  // src/cli.ts
4476
- var PACKAGE_VERSION = true ? "0.34.0" : "dev-mode";
4174
+ var PACKAGE_VERSION = true ? "0.35.0" : "dev-mode";
4477
4175
  var program = new Command2();
4478
4176
  program.name("jto").description("JSON to Office CLI - Generate .docx and .pptx from JSON").version(PACKAGE_VERSION);
4479
4177
  registerCoreCommands(program, {