@json-to-office/core-pptx 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -25,7 +25,9 @@ var W = {
25
25
  PLACEHOLDER_NO_POSITION: "PLACEHOLDER_NO_POSITION",
26
26
  THEME_COLOR_FALLBACK: "THEME_COLOR_FALLBACK",
27
27
  UNKNOWN_COLOR: "UNKNOWN_COLOR",
28
- GRID_POSITION_CLAMPED: "GRID_POSITION_CLAMPED"
28
+ GRID_POSITION_CLAMPED: "GRID_POSITION_CLAMPED",
29
+ IMAGE_ZERO_BOX: "IMAGE_ZERO_BOX",
30
+ FONT_UNRESOLVED: "FONT_UNRESOLVED"
29
31
  };
30
32
  function warn(warnings, code, message, extra) {
31
33
  if (warnings) {
@@ -428,6 +430,25 @@ function resolveColor(color, theme, warnings) {
428
430
  return bare;
429
431
  }
430
432
 
433
+ // src/utils/fontAliasContext.ts
434
+ import { synthesizeFamilyName } from "@json-to-office/shared";
435
+ function applyFontWeight(params) {
436
+ if (!params.family) {
437
+ const weight2 = params.fontWeight ?? (params.bold === true ? 700 : void 0);
438
+ return {
439
+ bold: weight2 != null ? weight2 >= 600 : params.bold,
440
+ italic: params.italic
441
+ };
442
+ }
443
+ const weight = params.fontWeight ?? (params.bold === true ? 700 : void 0);
444
+ const synth = synthesizeFamilyName(
445
+ params.family,
446
+ weight,
447
+ params.italic === true
448
+ );
449
+ return { fontFace: synth.family, bold: synth.bold, italic: synth.italic };
450
+ }
451
+
431
452
  // src/components/text.ts
432
453
  function resolvePagePlaceholders(text, ctx) {
433
454
  const { slideNumber, totalSlides, pageNumberFormat } = ctx;
@@ -450,11 +471,27 @@ function renderTextComponent(slide, props, theme, warnings, slideCtx) {
450
471
  }
451
472
  opts.fontSize = props.fontSize ?? style?.fontSize ?? theme.defaults.fontSize;
452
473
  opts.fontFace = props.fontFace ?? style?.fontFace ?? (isHeadingStyle ? theme.fonts.heading : theme.fonts.body);
453
- opts.color = resolveColor(props.color ?? style?.fontColor ?? theme.defaults.fontColor, theme, warnings);
474
+ opts.color = resolveColor(
475
+ props.color ?? style?.fontColor ?? theme.defaults.fontColor,
476
+ theme,
477
+ warnings
478
+ );
454
479
  const bold = props.bold ?? style?.bold;
455
480
  const italic = props.italic ?? style?.italic;
481
+ const fontWeight = props.fontWeight ?? style?.fontWeight;
456
482
  if (bold != null) opts.bold = bold;
457
483
  if (italic != null) opts.italic = italic;
484
+ if (fontWeight != null || bold === true) {
485
+ const w = applyFontWeight({
486
+ family: opts.fontFace,
487
+ fontWeight,
488
+ italic,
489
+ bold
490
+ });
491
+ if (w.fontFace !== void 0) opts.fontFace = w.fontFace;
492
+ if (w.bold !== void 0) opts.bold = w.bold;
493
+ if (w.italic !== void 0) opts.italic = w.italic;
494
+ }
458
495
  if (props.strike) opts.strike = true;
459
496
  if (props.underline !== void 0) {
460
497
  if (typeof props.underline === "boolean") {
@@ -502,7 +539,8 @@ function renderTextComponent(slide, props, theme, warnings, slideCtx) {
502
539
  if (lineSpacing !== void 0) opts.lineSpacing = lineSpacing;
503
540
  const charSpacing = props.charSpacing ?? style?.charSpacing;
504
541
  if (charSpacing !== void 0) opts.charSpacing = charSpacing;
505
- if (props.paraSpaceBefore !== void 0) opts.paraSpaceBefore = props.paraSpaceBefore;
542
+ if (props.paraSpaceBefore !== void 0)
543
+ opts.paraSpaceBefore = props.paraSpaceBefore;
506
544
  const paraSpaceAfter = props.paraSpaceAfter ?? style?.paraSpaceAfter;
507
545
  if (paraSpaceAfter !== void 0) opts.paraSpaceAfter = paraSpaceAfter;
508
546
  if (props.breakLine) opts.breakLine = true;
@@ -516,7 +554,8 @@ import probe from "probe-image-size";
516
554
  function isPrivateUrl(urlStr) {
517
555
  try {
518
556
  const { hostname } = new URL(urlStr);
519
- if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]" || hostname.startsWith("10.") || hostname.startsWith("192.168.") || hostname.startsWith("169.254.") || hostname.endsWith(".local") || hostname.endsWith(".internal")) return true;
557
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]" || hostname.startsWith("10.") || hostname.startsWith("192.168.") || hostname.startsWith("169.254.") || hostname.endsWith(".local") || hostname.endsWith(".internal"))
558
+ return true;
520
559
  if (hostname.startsWith("172.")) {
521
560
  const second = parseInt(hostname.split(".")[1], 10);
522
561
  if (second >= 16 && second <= 31) return true;
@@ -546,30 +585,75 @@ async function probeImageSize(imagePath, warnings) {
546
585
  const result = await probe(createReadStream(resolved));
547
586
  return result ? { width: result.width, height: result.height } : void 0;
548
587
  } catch (err) {
549
- warn(warnings, W.IMAGE_PROBE_FAILED, `Image probe failed: ${err instanceof Error ? err.message : String(err)}`, { component: "image" });
588
+ warn(
589
+ warnings,
590
+ W.IMAGE_PROBE_FAILED,
591
+ `Image probe failed: ${err instanceof Error ? err.message : String(err)}`,
592
+ { component: "image" }
593
+ );
550
594
  return void 0;
551
595
  }
552
596
  }
553
- async function renderImageComponent(slide, props, theme, warnings) {
597
+ function resolveDimension(value, axisLength) {
598
+ if (typeof value === "number") return value;
599
+ if (value.endsWith("%")) {
600
+ const pct = parseFloat(value);
601
+ return !Number.isNaN(pct) && pct >= 0 ? pct / 100 * axisLength : 0;
602
+ }
603
+ const n = Number(value);
604
+ return Number.isNaN(n) ? 0 : n;
605
+ }
606
+ async function renderImageComponent(slide, props, theme, warnings, slideWidth = 10, slideHeight = 7.5) {
554
607
  const opts = {};
608
+ const source = props.path || props.base64;
609
+ if (!source) {
610
+ warn(
611
+ warnings,
612
+ W.IMAGE_NO_SOURCE,
613
+ "Image component missing both path and base64",
614
+ { component: "image" }
615
+ );
616
+ return;
617
+ }
555
618
  if (props.path) {
556
619
  opts.path = props.path;
557
- } else if (props.base64) {
558
- opts.data = props.base64;
559
620
  } else {
560
- warn(warnings, W.IMAGE_NO_SOURCE, "Image component missing both path and base64", { component: "image" });
561
- return;
621
+ opts.data = props.base64;
562
622
  }
563
623
  if (props.x !== void 0) opts.x = props.x;
564
624
  if (props.y !== void 0) opts.y = props.y;
565
625
  if (props.w !== void 0) opts.w = props.w;
566
626
  if (props.h !== void 0) opts.h = props.h;
627
+ const hasW = props.w !== void 0;
628
+ const hasH = props.h !== void 0;
629
+ const needsProbe = hasW !== hasH && !props.sizing || props.sizing?.type === "contain" || props.sizing?.type === "cover";
630
+ const intrinsic = needsProbe ? await probeImageSize(source, warnings) : void 0;
631
+ if (hasW !== hasH && !props.sizing) {
632
+ if (intrinsic && intrinsic.width > 0 && intrinsic.height > 0) {
633
+ const aspect = intrinsic.width / intrinsic.height;
634
+ if (hasW && !hasH) {
635
+ const wInches = resolveDimension(props.w, slideWidth);
636
+ opts.w = wInches;
637
+ opts.h = wInches / aspect;
638
+ } else {
639
+ const hInches = resolveDimension(props.h, slideHeight);
640
+ opts.h = hInches;
641
+ opts.w = hInches * aspect;
642
+ }
643
+ }
644
+ }
567
645
  if (props.sizing && (props.sizing.type === "contain" || props.sizing.type === "cover")) {
568
- const source = props.path || props.base64;
569
- const intrinsic = source ? await probeImageSize(source, warnings) : void 0;
570
- const boxW = Number(props.sizing.w ?? props.w);
571
- const boxH = Number(props.sizing.h ?? props.h);
572
- if (intrinsic && !isNaN(boxW) && !isNaN(boxH)) {
646
+ const boxW = resolveDimension(props.sizing.w ?? props.w ?? 0, slideWidth);
647
+ const boxH = resolveDimension(props.sizing.h ?? props.h ?? 0, slideHeight);
648
+ if (boxW <= 0 || boxH <= 0) {
649
+ warn(
650
+ warnings,
651
+ W.IMAGE_ZERO_BOX,
652
+ `Image sizing box resolved to zero (${boxW}x${boxH})`,
653
+ { component: "image" }
654
+ );
655
+ }
656
+ if (intrinsic && intrinsic.width > 0 && intrinsic.height > 0 && boxW > 0 && boxH > 0) {
573
657
  const imgAspect = intrinsic.width / intrinsic.height;
574
658
  if (props.sizing.type === "contain") {
575
659
  const boxAspect = boxW / boxH;
@@ -581,8 +665,8 @@ async function renderImageComponent(slide, props, theme, warnings) {
581
665
  fitH = boxH;
582
666
  fitW = boxH * imgAspect;
583
667
  }
584
- const baseX = Number(props.x ?? 0);
585
- const baseY = Number(props.y ?? 0);
668
+ const baseX = resolveDimension(props.x ?? 0, slideWidth);
669
+ const baseY = resolveDimension(props.y ?? 0, slideHeight);
586
670
  opts.x = baseX + (boxW - fitW) / 2;
587
671
  opts.y = baseY + (boxH - fitH) / 2;
588
672
  opts.w = fitW;
@@ -598,8 +682,8 @@ async function renderImageComponent(slide, props, theme, warnings) {
598
682
  } else if (props.sizing) {
599
683
  opts.sizing = {
600
684
  ...props.sizing,
601
- w: props.sizing.w ?? props.w,
602
- h: props.sizing.h ?? props.h
685
+ w: resolveDimension(props.sizing.w ?? props.w ?? 0, slideWidth),
686
+ h: resolveDimension(props.sizing.h ?? props.h ?? 0, slideHeight)
603
687
  };
604
688
  }
605
689
  if (props.rotate !== void 0) opts.rotate = props.rotate;
@@ -663,9 +747,16 @@ function buildShapeOpts(props, theme, warnings) {
663
747
  }
664
748
  if (props.line) {
665
749
  opts.line = {};
666
- if (props.line.color) opts.line.color = resolveColor(props.line.color, theme, warnings);
667
- if (props.line.width) opts.line.width = props.line.width;
668
- if (props.line.dashType) opts.line.dashType = props.line.dashType;
750
+ if (props.line.color)
751
+ opts.line.color = resolveColor(
752
+ props.line.color,
753
+ theme,
754
+ warnings
755
+ );
756
+ if (props.line.width)
757
+ opts.line.width = props.line.width;
758
+ if (props.line.dashType)
759
+ opts.line.dashType = props.line.dashType;
669
760
  }
670
761
  if (props.rotate !== void 0) opts.rotate = props.rotate;
671
762
  if (props.rectRadius !== void 0) opts.rectRadius = props.rectRadius;
@@ -685,7 +776,9 @@ function renderShapeComponent(slide, props, theme, pptx, warnings) {
685
776
  const shapeTypeName = SHAPE_TYPE_MAP[props.type] || props.type;
686
777
  const shapeType = pptx.ShapeType[shapeTypeName];
687
778
  if (!shapeType) {
688
- warn(warnings, W.UNKNOWN_SHAPE, `Unknown shape type: ${props.type}`, { component: "shape" });
779
+ warn(warnings, W.UNKNOWN_SHAPE, `Unknown shape type: ${props.type}`, {
780
+ component: "shape"
781
+ });
689
782
  return;
690
783
  }
691
784
  const style = props.style ? theme.styles?.[props.style] : void 0;
@@ -695,32 +788,69 @@ function renderShapeComponent(slide, props, theme, pptx, warnings) {
695
788
  opts.shape = shapeType;
696
789
  opts.fontSize = props.fontSize ?? style?.fontSize ?? theme.defaults.fontSize;
697
790
  opts.fontFace = props.fontFace ?? style?.fontFace ?? (isHeadingStyle ? theme.fonts.heading : theme.fonts.body);
698
- opts.color = resolveColor(props.fontColor ?? style?.fontColor ?? theme.defaults.fontColor, theme, warnings);
791
+ const preAliasFamily = opts.fontFace;
792
+ opts.color = resolveColor(
793
+ props.fontColor ?? style?.fontColor ?? theme.defaults.fontColor,
794
+ theme,
795
+ warnings
796
+ );
699
797
  const bold = props.bold ?? style?.bold;
700
798
  const italic = props.italic ?? style?.italic;
701
- if (bold != null) opts.bold = bold;
702
- if (italic != null) opts.italic = italic;
799
+ const fontWeight = props.fontWeight ?? style?.fontWeight;
703
800
  const charSpacing = props.charSpacing ?? style?.charSpacing;
704
801
  if (charSpacing !== void 0) opts.charSpacing = charSpacing;
705
802
  const align = props.align ?? style?.align;
706
803
  if (align) opts.align = align;
707
804
  opts.valign = props.valign ?? "top";
708
805
  if (Array.isArray(props.text)) {
806
+ if (bold != null) opts.bold = bold;
807
+ if (italic != null) opts.italic = italic;
709
808
  const textSegments = props.text.map((seg) => {
710
809
  const segOpts = {};
711
810
  if (seg.fontSize != null) segOpts.fontSize = seg.fontSize;
712
811
  if (seg.fontFace != null) segOpts.fontFace = seg.fontFace;
713
- if (seg.color != null) segOpts.color = resolveColor(seg.color, theme, warnings);
714
- if (seg.bold != null) segOpts.bold = seg.bold;
715
- if (seg.italic != null) segOpts.italic = seg.italic;
812
+ if (seg.color != null)
813
+ segOpts.color = resolveColor(seg.color, theme, warnings);
716
814
  if (seg.breakLine != null) segOpts.breakLine = seg.breakLine;
717
815
  if (seg.charSpacing != null) segOpts.charSpacing = seg.charSpacing;
718
816
  if (seg.spaceBefore != null) segOpts.paraSpaceBefore = seg.spaceBefore;
719
817
  if (seg.spaceAfter != null) segOpts.paraSpaceAfter = seg.spaceAfter;
818
+ const segWeight = seg.fontWeight;
819
+ const effWeight = segWeight ?? fontWeight;
820
+ const effBold = seg.bold ?? bold;
821
+ const effItalic = seg.italic ?? italic;
822
+ if (effBold != null) segOpts.bold = effBold;
823
+ if (effItalic != null) segOpts.italic = effItalic;
824
+ if (effWeight != null || effBold === true) {
825
+ if (seg.fontFace == null) {
826
+ const w = applyFontWeight({
827
+ family: preAliasFamily,
828
+ fontWeight: effWeight,
829
+ italic: effItalic,
830
+ bold: effBold
831
+ });
832
+ if (w.fontFace !== void 0) segOpts.fontFace = w.fontFace;
833
+ if (w.bold !== void 0) segOpts.bold = w.bold;
834
+ if (w.italic !== void 0) segOpts.italic = w.italic;
835
+ }
836
+ }
720
837
  return { text: seg.text, options: segOpts };
721
838
  });
722
839
  slide.addText(textSegments, opts);
723
840
  } else {
841
+ if (bold != null) opts.bold = bold;
842
+ if (italic != null) opts.italic = italic;
843
+ if (fontWeight != null || bold === true) {
844
+ const w = applyFontWeight({
845
+ family: preAliasFamily,
846
+ fontWeight,
847
+ italic,
848
+ bold
849
+ });
850
+ if (w.fontFace !== void 0) opts.fontFace = w.fontFace;
851
+ if (w.bold !== void 0) opts.bold = w.bold;
852
+ if (w.italic !== void 0) opts.italic = w.italic;
853
+ }
724
854
  slide.addText(props.text, opts);
725
855
  }
726
856
  } else {
@@ -783,14 +913,19 @@ function renderTableComponent(slide, props, theme, pptx, warnings) {
783
913
  return { text: applyTextVariationSelector(cell), options: opts2 };
784
914
  }
785
915
  const cellOpts = {};
786
- if (cell.color) cellOpts.color = resolveColor(cell.color, theme, warnings);
916
+ if (cell.color)
917
+ cellOpts.color = resolveColor(cell.color, theme, warnings);
787
918
  if (bgFill) {
788
919
  const isHeader = rowIndex === 0;
789
920
  if (!isCorner) {
790
921
  const resolvedFill = cell.fill ? resolveColor(cell.fill, theme, warnings) : isHeader ? headerFill : bgFill;
791
922
  cellOpts.fill = { color: resolvedFill };
792
923
  }
793
- cellOpts.border = buildBorderRadiusBorders(rowIndex, colIndex, row.length);
924
+ cellOpts.border = buildBorderRadiusBorders(
925
+ rowIndex,
926
+ colIndex,
927
+ row.length
928
+ );
794
929
  } else if (cell.fill) {
795
930
  cellOpts.fill = { color: resolveColor(cell.fill, theme, warnings) };
796
931
  }
@@ -798,6 +933,17 @@ function renderTableComponent(slide, props, theme, pptx, warnings) {
798
933
  if (cell.fontFace) cellOpts.fontFace = cell.fontFace;
799
934
  if (cell.bold) cellOpts.bold = true;
800
935
  if (cell.italic) cellOpts.italic = true;
936
+ if (cell.fontWeight != null || cell.bold === true) {
937
+ const w = applyFontWeight({
938
+ family: cellOpts.fontFace ?? props.fontFace ?? theme.fonts.body,
939
+ fontWeight: cell.fontWeight,
940
+ italic: cell.italic,
941
+ bold: cell.bold
942
+ });
943
+ if (w.fontFace !== void 0) cellOpts.fontFace = w.fontFace;
944
+ if (w.bold !== void 0) cellOpts.bold = w.bold;
945
+ if (w.italic !== void 0) cellOpts.italic = w.italic;
946
+ }
801
947
  if (cell.align) cellOpts.align = cell.align;
802
948
  if (cell.valign) cellOpts.valign = cell.valign;
803
949
  if (cell.colspan) cellOpts.colspan = cell.colspan;
@@ -820,7 +966,8 @@ function renderTableComponent(slide, props, theme, pptx, warnings) {
820
966
  color: resolveColor(props.border.color ?? "000000", theme, warnings)
821
967
  };
822
968
  }
823
- if (props.fill) opts.fill = { color: resolveColor(props.fill, theme, warnings) };
969
+ if (props.fill)
970
+ opts.fill = { color: resolveColor(props.fill, theme, warnings) };
824
971
  opts.fontSize = props.fontSize ?? theme.defaults.fontSize;
825
972
  opts.fontFace = props.fontFace ?? theme.fonts.body;
826
973
  if (props.color) opts.color = resolveColor(props.color, theme, warnings);
@@ -881,7 +1028,12 @@ function renderTableComponent(slide, props, theme, pptx, warnings) {
881
1028
  }
882
1029
  if (bgFill && borderRadiusTableW !== void 0) {
883
1030
  opts.w = borderRadiusTableW;
884
- opts.border = [{ type: "none" }, { type: "none" }, { type: "none" }, { type: "none" }];
1031
+ opts.border = [
1032
+ { type: "none" },
1033
+ { type: "none" },
1034
+ { type: "none" },
1035
+ { type: "none" }
1036
+ ];
885
1037
  }
886
1038
  slide.addTable(tableRows, opts);
887
1039
  }
@@ -1050,16 +1202,23 @@ function renderChartComponent(slide, props, theme, _pptx, warnings) {
1050
1202
  }
1051
1203
 
1052
1204
  // src/components/index.ts
1053
- async function renderComponent(slide, component, theme, pptx, warnings, slideCtx, services) {
1205
+ async function renderComponent(slide, component, theme, pptx, warnings, ctx) {
1054
1206
  if (component.enabled === false) return;
1055
1207
  const { name, props } = component;
1056
1208
  const p = props;
1057
1209
  switch (name) {
1058
1210
  case "text":
1059
- renderTextComponent(slide, p, theme, warnings, slideCtx);
1211
+ renderTextComponent(slide, p, theme, warnings, ctx?.slideCtx);
1060
1212
  break;
1061
1213
  case "image":
1062
- await renderImageComponent(slide, p, theme, warnings);
1214
+ await renderImageComponent(
1215
+ slide,
1216
+ p,
1217
+ theme,
1218
+ warnings,
1219
+ ctx?.slideWidth,
1220
+ ctx?.slideHeight
1221
+ );
1063
1222
  break;
1064
1223
  case "shape":
1065
1224
  renderShapeComponent(slide, p, theme, pptx, warnings);
@@ -1073,7 +1232,7 @@ async function renderComponent(slide, component, theme, pptx, warnings, slideCtx
1073
1232
  p,
1074
1233
  theme,
1075
1234
  warnings,
1076
- services?.highcharts
1235
+ ctx?.services?.highcharts
1077
1236
  );
1078
1237
  break;
1079
1238
  case "chart":
@@ -1159,6 +1318,12 @@ async function renderPresentation(processed, warnings) {
1159
1318
  totalSlides,
1160
1319
  pageNumberFormat: processed.pageNumberFormat
1161
1320
  };
1321
+ const renderCtx = {
1322
+ slideCtx,
1323
+ services: processed.services,
1324
+ slideWidth: processed.slideWidth,
1325
+ slideHeight: processed.slideHeight
1326
+ };
1162
1327
  const slide = slideData.template ? pptx.addSlide({ masterName: slideData.template }) : pptx.addSlide();
1163
1328
  if (slideData.background) {
1164
1329
  if (slideData.background.color) {
@@ -1198,8 +1363,7 @@ async function renderPresentation(processed, warnings) {
1198
1363
  processed.theme,
1199
1364
  pptx,
1200
1365
  warnings,
1201
- slideCtx,
1202
- processed.services
1366
+ renderCtx
1203
1367
  );
1204
1368
  }
1205
1369
  }
@@ -1217,8 +1381,7 @@ async function renderPresentation(processed, warnings) {
1217
1381
  processed.theme,
1218
1382
  pptx,
1219
1383
  warnings,
1220
- slideCtx,
1221
- processed.services
1384
+ renderCtx
1222
1385
  );
1223
1386
  }
1224
1387
  if (slideData.placeholders) {
@@ -1264,8 +1427,7 @@ async function renderPresentation(processed, warnings) {
1264
1427
  processed.theme,
1265
1428
  pptx,
1266
1429
  warnings,
1267
- slideCtx,
1268
- processed.services
1430
+ renderCtx
1269
1431
  );
1270
1432
  }
1271
1433
  } else {
@@ -1291,8 +1453,7 @@ async function renderPresentation(processed, warnings) {
1291
1453
  processed.theme,
1292
1454
  pptx,
1293
1455
  warnings,
1294
- slideCtx,
1295
- processed.services
1456
+ renderCtx
1296
1457
  );
1297
1458
  } else {
1298
1459
  warn(
@@ -1312,7 +1473,60 @@ async function renderPresentation(processed, warnings) {
1312
1473
  return pptx;
1313
1474
  }
1314
1475
 
1476
+ // src/core/fontResolution.ts
1477
+ import {
1478
+ collectFontNamesFromPptx,
1479
+ validateFontReferences,
1480
+ FontRegistry
1481
+ } from "@json-to-office/shared";
1482
+ import {
1483
+ loadFileFontSource,
1484
+ FontDiskCache,
1485
+ fetchVariableFontSource
1486
+ } from "@json-to-office/shared/fonts/node";
1487
+ async function resolveDocumentFonts(document, theme, warnings, fonts) {
1488
+ const names = /* @__PURE__ */ new Set();
1489
+ for (const n of collectFontNamesFromPptx(document)) names.add(n);
1490
+ for (const n of collectFontNamesFromPptx(theme)) names.add(n);
1491
+ if (names.size === 0) return [];
1492
+ const validation = validateFontReferences({
1493
+ referencedNames: names,
1494
+ registeredEntries: fonts?.extraEntries
1495
+ });
1496
+ if (validation.warnings.length > 0) {
1497
+ if (fonts?.strict) {
1498
+ throw new Error(
1499
+ `Unresolved font references (strict mode):
1500
+ ` + validation.warnings.map((w) => ` - ${w.message}`).join("\n")
1501
+ );
1502
+ }
1503
+ for (const w of validation.warnings) {
1504
+ warn(warnings, W.FONT_UNRESOLVED, w.message, {
1505
+ component: "fontRegistry"
1506
+ });
1507
+ }
1508
+ }
1509
+ if (!fonts?.onResolved) return [];
1510
+ const registry = new FontRegistry({
1511
+ opts: fonts,
1512
+ fileLoader: loadFileFontSource,
1513
+ variableLoader: fetchVariableFontSource,
1514
+ diskCache: fonts?.googleFonts?.cacheDir ? new FontDiskCache(fonts.googleFonts.cacheDir) : void 0
1515
+ });
1516
+ const resolved = await registry.resolveMany(names);
1517
+ for (const r of resolved) {
1518
+ for (const msg of r.warnings) {
1519
+ warn(warnings, W.FONT_UNRESOLVED, msg, {
1520
+ component: "fontRegistry"
1521
+ });
1522
+ }
1523
+ }
1524
+ fonts.onResolved(resolved);
1525
+ return resolved;
1526
+ }
1527
+
1315
1528
  // src/core/generator.ts
1529
+ import { applyExportMode, scopedThemeName } from "@json-to-office/shared";
1316
1530
  function isPresentationComponentDefinition(definition) {
1317
1531
  if (typeof definition !== "object" || definition === null) return false;
1318
1532
  const def = definition;
@@ -1341,7 +1555,47 @@ async function generateBufferWithWarnings(jsonConfig, options) {
1341
1555
  component = jsonConfig;
1342
1556
  }
1343
1557
  const warnings = [];
1344
- const pptx = await generatePresentation(component, options, warnings);
1558
+ const baseThemeName = component.props?.theme ?? "default";
1559
+ let resolvedTheme = options?.customThemes?.[baseThemeName] ?? getPptxTheme(baseThemeName);
1560
+ const mode = applyExportMode({
1561
+ doc: component,
1562
+ theme: resolvedTheme,
1563
+ fonts: options?.fonts
1564
+ });
1565
+ component = mode.doc;
1566
+ resolvedTheme = mode.theme;
1567
+ for (const w of mode.warnings) {
1568
+ warnings.push({
1569
+ code: w.code,
1570
+ message: w.message,
1571
+ component: "fontRegistry"
1572
+ });
1573
+ }
1574
+ await resolveDocumentFonts(
1575
+ component,
1576
+ resolvedTheme,
1577
+ warnings,
1578
+ options?.fonts
1579
+ );
1580
+ const themeName = scopedThemeName(baseThemeName, options?.fonts?.mode);
1581
+ if (themeName !== baseThemeName) {
1582
+ component = {
1583
+ ...component,
1584
+ props: { ...component.props, theme: themeName }
1585
+ };
1586
+ }
1587
+ const effectiveOptions = {
1588
+ ...options,
1589
+ customThemes: {
1590
+ ...options?.customThemes ?? {},
1591
+ [themeName]: resolvedTheme
1592
+ }
1593
+ };
1594
+ const pptx = await generatePresentation(
1595
+ component,
1596
+ effectiveOptions,
1597
+ warnings
1598
+ );
1345
1599
  const data = await pptx.write({ outputType: "nodebuffer" });
1346
1600
  const buffer = await neutralizeTableStyle(data);
1347
1601
  return { buffer, warnings };
@@ -1501,6 +1755,7 @@ async function exportPluginSchema(customComponents, outputPath, options = {}) {
1501
1755
  }
1502
1756
 
1503
1757
  // src/plugin/createPresentationGenerator.ts
1758
+ import { applyExportMode as applyExportMode2, scopedThemeName as scopedThemeName2 } from "@json-to-office/shared";
1504
1759
  var MEDIUM_STYLE_2_ACCENT_12 = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}";
1505
1760
  var NO_STYLE_NO_GRID2 = "{2D5ABB26-0587-4C30-8999-92F81FD0307C}";
1506
1761
  async function neutralizeTableStyle2(buffer) {
@@ -1624,7 +1879,8 @@ function createBuilderImpl(state) {
1624
1879
  theme: state.theme,
1625
1880
  customThemes: state.customThemes,
1626
1881
  debug: state.debug,
1627
- services: state.services
1882
+ services: state.services,
1883
+ fonts: state.fonts
1628
1884
  };
1629
1885
  return createBuilderImpl(
1630
1886
  newState
@@ -1636,20 +1892,42 @@ function createBuilderImpl(state) {
1636
1892
  if (!internalDocument || internalDocument.name !== "pptx") {
1637
1893
  throw new Error("Top-level component must be a pptx component");
1638
1894
  }
1639
- const themeName = typeof state.theme === "string" ? state.theme : internalDocument.props.theme ?? "default";
1640
- const resolvedTheme = typeof state.theme === "object" ? state.theme : state.customThemes?.[themeName] ?? getPptxTheme(themeName);
1895
+ const baseThemeName = typeof state.theme === "string" ? state.theme : internalDocument.props.theme ?? "default";
1896
+ let resolvedTheme = typeof state.theme === "object" ? state.theme : state.customThemes?.[baseThemeName] ?? getPptxTheme(baseThemeName);
1641
1897
  const warnings = [];
1642
- const processedChildren = internalDocument.children ? await processAllSlides(
1643
- internalDocument.children,
1644
- warnings,
1645
- resolvedTheme
1646
- ) : [];
1647
- const processedDocument = {
1648
- ...internalDocument,
1898
+ const mode = applyExportMode2({
1899
+ doc: internalDocument,
1900
+ theme: resolvedTheme,
1901
+ fonts: state.fonts
1902
+ });
1903
+ resolvedTheme = mode.theme;
1904
+ for (const w of mode.warnings) {
1905
+ warnings.push({
1906
+ code: w.code,
1907
+ message: w.message,
1908
+ component: "fontRegistry"
1909
+ });
1910
+ }
1911
+ const processedChildren = mode.doc.children ? await processAllSlides(mode.doc.children, warnings, resolvedTheme) : [];
1912
+ const themeName = scopedThemeName2(baseThemeName, state.fonts?.mode);
1913
+ const docWithScopedTheme = themeName !== baseThemeName ? {
1914
+ ...mode.doc,
1915
+ props: { ...mode.doc.props, theme: themeName },
1649
1916
  children: processedChildren
1917
+ } : { ...mode.doc, children: processedChildren };
1918
+ const processedDocument = docWithScopedTheme;
1919
+ await resolveDocumentFonts(
1920
+ processedDocument,
1921
+ resolvedTheme,
1922
+ warnings,
1923
+ state.fonts
1924
+ );
1925
+ const effectiveCustomThemes = {
1926
+ ...state.customThemes ?? {},
1927
+ [themeName]: resolvedTheme
1650
1928
  };
1651
1929
  const processed = processPresentation(processedDocument, {
1652
- customThemes: state.customThemes,
1930
+ customThemes: effectiveCustomThemes,
1653
1931
  services: state.services
1654
1932
  });
1655
1933
  const pptx = await renderPresentation(processed, warnings);
@@ -1761,7 +2039,8 @@ function createPresentationGenerator(options = {}) {
1761
2039
  theme: options.theme,
1762
2040
  customThemes: options.customThemes,
1763
2041
  debug: options.debug ?? false,
1764
- services: options.services
2042
+ services: options.services,
2043
+ fonts: options.fonts
1765
2044
  };
1766
2045
  return createBuilderImpl(initialState);
1767
2046
  }