pptx-ts 3.7.0 → 4.0.1

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 (136) hide show
  1. package/README.md +116 -269
  2. package/dist/autopage-odPz7zZO.js +536 -0
  3. package/dist/autopage-odPz7zZO.js.map +1 -0
  4. package/dist/binary-string-CKVAYI3M.js +24 -0
  5. package/dist/binary-string-CKVAYI3M.js.map +1 -0
  6. package/dist/body-insets-Dw_BeWD_.js +54 -0
  7. package/dist/body-insets-Dw_BeWD_.js.map +1 -0
  8. package/dist/browser.d.ts +29 -9
  9. package/dist/browser.d.ts.map +1 -1
  10. package/dist/browser.js +41 -14
  11. package/dist/browser.js.map +1 -1
  12. package/dist/clip-Bl01BPcN.d.ts +67 -0
  13. package/dist/clip-Bl01BPcN.d.ts.map +1 -0
  14. package/dist/diagnostics-D8ot4Ei7.d.ts +244 -0
  15. package/dist/diagnostics-D8ot4Ei7.d.ts.map +1 -0
  16. package/dist/embedded-fonts-DarT9aoa.d.ts +20 -0
  17. package/dist/embedded-fonts-DarT9aoa.d.ts.map +1 -0
  18. package/dist/entry-compose-DCnvBZz2.d.ts +1046 -0
  19. package/dist/entry-compose-DCnvBZz2.d.ts.map +1 -0
  20. package/dist/{enums-CvRYniUA.d.ts → enums-BUKTUB6V.d.ts} +21 -13
  21. package/dist/{enums-CvRYniUA.d.ts.map → enums-BUKTUB6V.d.ts.map} +1 -1
  22. package/dist/{errors-Bvln0H-7.js → errors-C4oENPcu.js} +92 -2
  23. package/dist/errors-C4oENPcu.js.map +1 -0
  24. package/dist/families.d.ts +260 -0
  25. package/dist/families.d.ts.map +1 -0
  26. package/dist/families.js +57 -0
  27. package/dist/families.js.map +1 -0
  28. package/dist/{fit-BKs0zxMI.js → fit-Cb7naFj6.js} +249 -167
  29. package/dist/fit-Cb7naFj6.js.map +1 -0
  30. package/dist/font-metrics-v5fn7s7e.d.ts +613 -0
  31. package/dist/font-metrics-v5fn7s7e.d.ts.map +1 -0
  32. package/dist/grid-DikTjFey.js +187 -0
  33. package/dist/grid-DikTjFey.js.map +1 -0
  34. package/dist/{html-dom-B0I0QSbU.js → html-dom-Iuog2wdK.js} +121 -91
  35. package/dist/html-dom-Iuog2wdK.js.map +1 -0
  36. package/dist/html.d.ts +6 -6
  37. package/dist/html.d.ts.map +1 -1
  38. package/dist/html.js +7 -6
  39. package/dist/html.js.map +1 -1
  40. package/dist/image-size-CvN2iCA4.js +1611 -0
  41. package/dist/image-size-CvN2iCA4.js.map +1 -0
  42. package/dist/index.d.ts +24 -10
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +31 -11
  45. package/dist/index.js.map +1 -1
  46. package/dist/inspect.d.ts +40 -31
  47. package/dist/inspect.d.ts.map +1 -1
  48. package/dist/inspect.js +40 -48
  49. package/dist/inspect.js.map +1 -1
  50. package/dist/math.d.ts +7 -5
  51. package/dist/math.d.ts.map +1 -1
  52. package/dist/math.js +16 -7
  53. package/dist/math.js.map +1 -1
  54. package/dist/measure.d.ts +12 -225
  55. package/dist/measure.d.ts.map +1 -1
  56. package/dist/measure.js +3 -3
  57. package/dist/node.d.ts +22 -8
  58. package/dist/node.d.ts.map +1 -1
  59. package/dist/node.js +27 -7
  60. package/dist/node.js.map +1 -1
  61. package/dist/notes-Dh12JAbn.js +3264 -0
  62. package/dist/notes-Dh12JAbn.js.map +1 -0
  63. package/dist/options-internal-WqgMf720.js +43 -0
  64. package/dist/options-internal-WqgMf720.js.map +1 -0
  65. package/dist/{package-CaOLXSNi.d.ts → package-J49Ck6S2.d.ts} +27 -12
  66. package/dist/package-J49Ck6S2.d.ts.map +1 -0
  67. package/dist/playbtn-CUqQdoDf.js +22 -0
  68. package/dist/playbtn-CUqQdoDf.js.map +1 -0
  69. package/dist/{slide-ZqAdVvHV.d.ts → pres-C6qgOFAa.d.ts} +1021 -1423
  70. package/dist/pres-C6qgOFAa.d.ts.map +1 -0
  71. package/dist/{presentation-DrJtj3-C.js → presentation-DGYlAWhl.js} +3208 -2727
  72. package/dist/presentation-DGYlAWhl.js.map +1 -0
  73. package/dist/{presentation-types-CDjScsev.d.ts → presentation-types-8jLQHI4M.d.ts} +1070 -683
  74. package/dist/presentation-types-8jLQHI4M.d.ts.map +1 -0
  75. package/dist/read.d.ts +9 -9
  76. package/dist/read.d.ts.map +1 -1
  77. package/dist/read.js +4 -4
  78. package/dist/script.d.ts +80 -66
  79. package/dist/script.d.ts.map +1 -1
  80. package/dist/script.js +2399 -1139
  81. package/dist/script.js.map +1 -1
  82. package/dist/{shapes-BemroU-z.js → shapes-re0KyQk_.js} +2807 -1609
  83. package/dist/shapes-re0KyQk_.js.map +1 -0
  84. package/dist/slide-Dx2ZeVZ6.d.ts +1256 -0
  85. package/dist/slide-Dx2ZeVZ6.d.ts.map +1 -0
  86. package/dist/slide-size-BalTjFuv.js +88 -0
  87. package/dist/slide-size-BalTjFuv.js.map +1 -0
  88. package/dist/{units-internal-Dg95w_YD.js → table-cell-margins-EVg0EwxT.js} +235 -273
  89. package/dist/table-cell-margins-EVg0EwxT.js.map +1 -0
  90. package/dist/table-dom-BTDP3lEP.js +21 -0
  91. package/dist/table-dom-BTDP3lEP.js.map +1 -0
  92. package/dist/text-anchor-CrYXAfUu.js +176 -0
  93. package/dist/text-anchor-CrYXAfUu.js.map +1 -0
  94. package/dist/text-sN64Otf_.js +3980 -0
  95. package/dist/text-sN64Otf_.js.map +1 -0
  96. package/dist/transition-speed-kUEWDWTx.js +14 -0
  97. package/dist/transition-speed-kUEWDWTx.js.map +1 -0
  98. package/dist/units-internal-XJlf-hOl.js +649 -0
  99. package/dist/units-internal-XJlf-hOl.js.map +1 -0
  100. package/dist/zip-C0leZZMj.d.ts +67 -0
  101. package/dist/zip-C0leZZMj.d.ts.map +1 -0
  102. package/dist/zip-CKd8ihcf.js +177 -0
  103. package/dist/zip-CKd8ihcf.js.map +1 -0
  104. package/dist/zip.d.ts +3 -68
  105. package/dist/zip.js +3 -163
  106. package/dist/zoom-DPUHh85d.js +8865 -0
  107. package/dist/zoom-DPUHh85d.js.map +1 -0
  108. package/package.json +106 -19
  109. package/skills/ts-pptx-upstream/SKILL.md +58 -57
  110. package/dist/autopage-DkjYZUFD.js +0 -423
  111. package/dist/autopage-DkjYZUFD.js.map +0 -1
  112. package/dist/diagnostics-DfBdDHre.d.ts +0 -829
  113. package/dist/diagnostics-DfBdDHre.d.ts.map +0 -1
  114. package/dist/errors-Bvln0H-7.js.map +0 -1
  115. package/dist/errors-PflMPWBn.d.ts +0 -182
  116. package/dist/errors-PflMPWBn.d.ts.map +0 -1
  117. package/dist/fit-BKs0zxMI.js.map +0 -1
  118. package/dist/html-dom-B0I0QSbU.js.map +0 -1
  119. package/dist/namespaces-BVnNu7sg.js +0 -666
  120. package/dist/namespaces-BVnNu7sg.js.map +0 -1
  121. package/dist/notes-CJFJaG6M.js +0 -1724
  122. package/dist/notes-CJFJaG6M.js.map +0 -1
  123. package/dist/package-CaOLXSNi.d.ts.map +0 -1
  124. package/dist/pres-BcBR3CSM.d.ts +0 -202
  125. package/dist/pres-BcBR3CSM.d.ts.map +0 -1
  126. package/dist/presentation-DrJtj3-C.js.map +0 -1
  127. package/dist/presentation-types-CDjScsev.d.ts.map +0 -1
  128. package/dist/shapes-BemroU-z.js.map +0 -1
  129. package/dist/slide-ZqAdVvHV.d.ts.map +0 -1
  130. package/dist/text-B8SLEaZx.js +0 -11327
  131. package/dist/text-B8SLEaZx.js.map +0 -1
  132. package/dist/units-1kRzYShk.js +0 -231
  133. package/dist/units-1kRzYShk.js.map +0 -1
  134. package/dist/units-internal-Dg95w_YD.js.map +0 -1
  135. package/dist/zip.d.ts.map +0 -1
  136. package/dist/zip.js.map +0 -1
@@ -1,6 +1,9 @@
1
- import { d as emuToInches, i as EMU_PER_POINT, x as warn } from "./units-1kRzYShk.js";
2
- import { n as InvalidOptionError, o as UnsupportedFeatureError, r as MediaError } from "./errors-Bvln0H-7.js";
3
- import { M as LINEH_MODIFIER, a as inch2Emu, i as getSmartParseNumber, s as marginToEmu, u as resolveTableColWidthsEmu, y as DEF_CELL_MARGIN_IN } from "./units-internal-Dg95w_YD.js";
1
+ import { l as warn, n as InvalidOptionError, o as UnsupportedFeatureError, r as MediaError } from "./errors-C4oENPcu.js";
2
+ import { _ as resolveCellMarginsInches, b as resolveTableColWidthsEmu, d as marginToEmu, s as getSmartParseNumber, t as autoPageLineHeightEmu, v as resolveInsetsEmu, x as resolveTableRowHeightEmu } from "./units-internal-XJlf-hOl.js";
3
+ import { D as emuToInches, I as EMU_PER_POINT } from "./table-cell-margins-EVg0EwxT.js";
4
+ import { t as pickDefined } from "./options-internal-WqgMf720.js";
5
+ import { t as BODY_INSET_DEFAULTS_EMU } from "./body-insets-Dw_BeWD_.js";
6
+ import { n as tableColCount, r as walkTableGrid } from "./grid-DikTjFey.js";
4
7
  //#region src/measure/font-collection.ts
5
8
  /**
6
9
  * Font **collection** (`.ttc` / `.otc`) support for the write-side metrics.
@@ -272,7 +275,7 @@ function extractFontFace(data, index) {
272
275
  * Width is summed from **raw `hmtx` advances with no kerning/shaping** (GPOS/GSUB
273
276
  * are deliberately ignored): kerning almost always narrows a line, so raw advances
274
277
  * over-estimate width — the conservative direction (shrink a touch too much, never
275
- * overflow). See `docs/measured-text-fit.md` ("Font metrics provider").
278
+ * overflow). See `docs/contributing/design/text-fit.md` ("Font metrics provider").
276
279
  */
277
280
  var OpentypeFontMetrics = class {
278
281
  font;
@@ -342,7 +345,7 @@ let heuristicSingleton;
342
345
  /**
343
346
  * Shared, font-independent `FontMetrics` used when a named face has no registered
344
347
  * metrics but the deck has opted into measured fit (some other face is registered).
345
- * Approximate but conservative — see {@link heuristicCharRatio}. A deck with no
348
+ * Approximate but conservative — see `heuristicCharRatio`. A deck with no
346
349
  * registered metrics at all does not engage measured fit, so this never fires there.
347
350
  */
348
351
  function getHeuristicFontMetrics() {
@@ -386,6 +389,7 @@ var FontMetricsRegistry = class {
386
389
  return `${face.toLowerCase()}\0${bold ? 1 : 0}\0${italic ? 1 : 0}`;
387
390
  }
388
391
  set(face, metrics, opts) {
392
+ if (typeof face !== "string" || face.trim() === "") throw new InvalidOptionError("font/missing-typeface", `registerFontMetrics: \`face\` must be the non-empty family name your fontFace options use, got ${JSON.stringify(face)}.`);
389
393
  this.map.set(this.key(face, !!opts?.bold, !!opts?.italic), metrics);
390
394
  }
391
395
  /** True if any variant of `face` has registered metrics. */
@@ -446,9 +450,9 @@ function makeRegistryResolver(registry, onHeuristic) {
446
450
  * fallback and charges the registered font's `.notdef` advance instead, which is a single
447
451
  * flat number unrelated to the glyph that actually paints. Unlike the other approximations
448
452
  * here it is **not** conservative in a fixed direction: a `.notdef` wider than the real
449
- * glyph over-reports (a phantom line — safe), a narrower one under-reports and can drop a
453
+ * glyph over-reports (a phantom line, which is safe), a narrower one under-reports and can drop a
450
454
  * line, which is the overflow direction the resize bake has no safety net for. So it is
451
- * surfaced rather than absorbed — see `docs/measured-text-fit.md` ("No font fallback").
455
+ * surfaced rather than absorbed. See `docs/contributing/design/text-fit.md` ("Font fallback").
452
456
  *
453
457
  * Runs whose face has no registered metrics at all are skipped: they measure through the
454
458
  * cmap-less heuristic, which has no coverage to report, and the caller already flags them
@@ -481,12 +485,13 @@ function collectUncoveredCodepoints(paragraphs, registry, into) {
481
485
  * Calibrated from `autofit-calibration.json` (Deck 1, `autofit-line-metrics`):
482
486
  * the per-line `cy` delta is a **font-independent** 1.2117× at 12/18/32 pt across
483
487
  * {Aptos, Aptos SemiBold, Calibri, Tahoma, Arial}, and PowerPoint vs LibreOffice
484
- * agree to ≤ 0.041 pt. See `test/read/fixtures/README.md` → "Findings" #1–#2.
488
+ * agree to ≤ 0.041 pt. See "Findings behind the constants" in
489
+ * `docs/contributing/design/text-fit.md`.
485
490
  */
486
491
  const SINGLE_LINE_PITCH = 1.2117;
487
- /** PowerPoint's `fontScale` search grid: discrete 2.5% steps (Findings #3). */
492
+ /** PowerPoint's `fontScale` search grid: discrete 2.5% steps (`docs/contributing/design/text-fit.md`). */
488
493
  const FONT_SCALE_STEP_PCT = 2.5;
489
- /** Floor for the shrink search (see `docs/measured-text-fit.md` → Solvers). */
494
+ /** Floor for the shrink search (see `docs/contributing/design/text-fit.md` → Wrap simulator and solvers). */
490
495
  const MIN_FONT_SCALE_PCT = 25;
491
496
  /**
492
497
  * Conservative safety factors applied by the shrink solver (not by the pure
@@ -526,7 +531,7 @@ const isWhitespace = (ch) => ch === " " || ch === " " || ch === "\xA0" || /\s/.t
526
531
  * Known gap: no kinsoku (line-break prohibition) rules. PowerPoint will not start
527
532
  * a line with `。` or `、` and hangs them past the right inset instead; this model
528
533
  * breaks before them. That is a widest-line difference, not a line-count one, and
529
- * `docs/measured-text-fit.md` records it.
534
+ * `docs/contributing/design/text-fit.md` records it.
530
535
  */
531
536
  function isCjkBreakCharacter(ch) {
532
537
  const cp = ch.codePointAt(0) ?? 0;
@@ -593,8 +598,9 @@ function tokenizeParagraph(para, resolve, fontScalePct, widthSafety) {
593
598
  * laid-out line width. `maxLineWidthPt` tracks the running line width and records
594
599
  * it whenever a line completes (a wrap or the final line), so callers can measure
595
600
  * the actual text extent, not just the box width. Trailing whitespace counts
596
- * toward width (Findings #7), which slightly over-reports — the conservative
597
- * direction, consistent with the wrap model erring wide.
601
+ * toward width, as PowerPoint counts it, which slightly over-reports — the
602
+ * conservative direction, consistent with the wrap model erring wide. The
603
+ * PowerPoint behaviour this follows is tabled in `docs/contributing/design/text-fit.md`.
598
604
  */
599
605
  function countLines(tokens, innerWidthPt) {
600
606
  let lines = 1;
@@ -637,7 +643,7 @@ function countLines(tokens, innerWidthPt) {
637
643
  maxLineWidthPt
638
644
  };
639
645
  }
640
- /** Max run size in a paragraph (line height follows the tallest run — Findings #7). */
646
+ /** Max run size in a paragraph (PowerPoint's line height follows the tallest run). */
641
647
  function maxRunSizePt(para) {
642
648
  let max = 0;
643
649
  for (const run of para.runs) if (run.sizePt > max) max = run.sizePt;
@@ -651,7 +657,7 @@ function maxRunSizePt(para) {
651
657
  * and the public layout-time measure API.
652
658
  */
653
659
  function measureLayout(paragraphs, innerWidthPt, resolve, fontScalePct, lnSpcReductionPct, widthSafety = 1) {
654
- if (innerWidthPt <= 0) return null;
660
+ if (!(innerWidthPt > 0)) return null;
655
661
  let total = 0;
656
662
  let lineCount = 0;
657
663
  let widestLineWidthPt = 0;
@@ -681,14 +687,6 @@ function measureHeightPt(paragraphs, innerWidthPt, resolve, fontScalePct, lnSpcR
681
687
  return result === null ? null : result.heightPt;
682
688
  }
683
689
  /**
684
- * Inner height (points) the text needs at full size — the value to bake into the
685
- * shape's `a:ext/@cy` for `fit: 'resize'` (`spAutoFit`). Unlike shrink, resize has
686
- * **no safety net**: an under-estimate overflows (there is no text scaling
687
- * fallback), so this errs **tall** — width is inflated (earlier wrap ⇒ more lines)
688
- * and the laid-out height by the calibrated height factor, so the computed `cy` is
689
- * ≥ PowerPoint's and ≥ the LibreOffice-rendered height across the resize oracle.
690
- */
691
- /**
692
690
  * Width used for wrap layout: the inner box width when wrapping, or unbounded for a
693
691
  * non-wrapping frame (`wrap:false`) so each paragraph lays out as a single line. A
694
692
  * non-wrapping frame's width constraint is enforced separately, against the widest
@@ -697,6 +695,14 @@ function measureHeightPt(paragraphs, innerWidthPt, resolve, fontScalePct, lnSpcR
697
695
  function layoutWidthPt(box) {
698
696
  return box.wrap === false ? Infinity : box.innerWidthPt;
699
697
  }
698
+ /**
699
+ * Inner height (points) the text needs at full size — the value to bake into the
700
+ * shape's `a:ext/@cy` for `fit: 'resize'` (`spAutoFit`). Unlike shrink, resize has
701
+ * **no safety net**: an under-estimate overflows (there is no text scaling
702
+ * fallback), so this errs **tall** — width is inflated (earlier wrap ⇒ more lines)
703
+ * and the laid-out height by the calibrated height factor, so the computed `cy` is
704
+ * ≥ PowerPoint's and ≥ the LibreOffice-rendered height across the resize oracle.
705
+ */
700
706
  function solveResize(paragraphs, box, resolve) {
701
707
  const h = measureHeightPt(paragraphs, layoutWidthPt(box), resolve, 100, 0, WIDTH_SAFETY_FACTOR);
702
708
  if (h === null) return { kind: "unmeasurable" };
@@ -705,6 +711,16 @@ function solveResize(paragraphs, box, resolve) {
705
711
  neededInnerHeightPt: h * HEIGHT_SAFETY_FACTOR
706
712
  };
707
713
  }
714
+ /**
715
+ * Largest `fontScale` (on PowerPoint's 2.5% grid) at which the text fits the box.
716
+ * Width and height both re-measured per scale. Conservative: because the model
717
+ * over-estimates, the returned scale is ≤ PowerPoint's and the text never overflows.
718
+ *
719
+ * `lnSpcReduction` is left at 0 — dropping `fontScale` alone is provably
720
+ * conservative (PowerPoint trades line-spacing reduction to keep the font *larger*,
721
+ * so a reduction-free fit is always ≤ PowerPoint's scale). The reduction ramp
722
+ * PowerPoint applies is recorded in `docs/contributing/design/text-fit.md`.
723
+ */
708
724
  function solveShrink(paragraphs, box, resolve) {
709
725
  const width = layoutWidthPt(box);
710
726
  const noWrap = box.wrap === false;
@@ -743,8 +759,6 @@ function solveShrink(paragraphs, box, resolve) {
743
759
  * `ext.cy` for a given anchor. Everything here is about *reading* an authored object; the solving
744
760
  * and the rewriting live in the siblings (`table-fit.ts`, `fit.ts`).
745
761
  */
746
- const DEF_INS_LR_EMU = 91440;
747
- const DEF_INS_TB_EMU = 45720;
748
762
  const CRLF_RE = /\r*\n/g;
749
763
  /** Normalize `slideObj.text` (string | TextProps | TextProps[]) to a run list. */
750
764
  function normalizeRuns(obj) {
@@ -758,12 +772,12 @@ function normalizeRuns(obj) {
758
772
  if (!Array.isArray(text) && typeof text === "object" && "text" in text) {
759
773
  const t = text;
760
774
  return [{
761
- text: t.text,
775
+ ...pickDefined(t, ["text"]),
762
776
  options: t.options ?? opts
763
777
  }];
764
778
  }
765
779
  if (Array.isArray(text)) return text.map((t) => ({
766
- text: t.text,
780
+ ...pickDefined(t, ["text"]),
767
781
  options: t.options ?? opts
768
782
  }));
769
783
  return [];
@@ -778,7 +792,7 @@ function extractParagraphs(obj) {
778
792
  /**
779
793
  * Convert a run list (+ box-level default options) into a measurable
780
794
  * `FitParagraph[]`, or null if empty. The single converter shared by the
781
- * export-time pass ({@link extractParagraphs}) and the public layout-time
795
+ * export-time pass (`extractParagraphs`) and the public layout-time
782
796
  * `measureText` API, so a layout-time prediction and the baked export never drift.
783
797
  */
784
798
  function buildFitParagraphs(runs, opts) {
@@ -840,27 +854,35 @@ function buildFitParagraphs(runs, opts) {
840
854
  if (cur.length > 0) paras.push(toPara(cur, curParaOpts));
841
855
  return paras.length > 0 ? paras : null;
842
856
  }
843
- /** Resolve text-frame insets (EMU): explicit `_bodyProp` (from `inset`) → `margin` → PowerPoint defaults. */
844
- function resolveInsetsEmu(opts) {
857
+ /**
858
+ * Resolve a text frame's insets (EMU): explicit `_bodyProp` (from `inset`) → `margin` →
859
+ * PowerPoint's defaults.
860
+ *
861
+ * Named for the question it answers, which is not the one `units-internal.resolveInsetsEmu`
862
+ * answers: that one takes a `margin` alone and returns `null` when the caller stated none, so
863
+ * an emitter can leave the attributes off. This one takes the whole option bag, consults
864
+ * `_bodyProp` first, and always produces four numbers, because a solver has to measure against
865
+ * something. Two exported functions with one name, imported from two places, was a coin-flip
866
+ * for anyone reading a call site.
867
+ *
868
+ * The `[T, R, B, L]` shuffle and the defaults both come from the modules that own them rather
869
+ * than being spelled again here.
870
+ */
871
+ function resolveBodyInsetsEmu(opts) {
845
872
  const bp = opts._bodyProp ?? {};
846
- const margin = opts.margin;
847
873
  let lIns = bp.lIns;
848
874
  let rIns = bp.rIns;
849
875
  let tIns = bp.tIns;
850
876
  let bIns = bp.bIns;
851
- if (lIns == null && rIns == null && tIns == null && bIns == null && margin != null) {
852
- if (Array.isArray(margin)) {
853
- tIns = marginToEmu(margin[0] ?? 0);
854
- rIns = marginToEmu(margin[1] ?? 0);
855
- bIns = marginToEmu(margin[2] ?? 0);
856
- lIns = marginToEmu(margin[3] ?? 0);
857
- } else if (typeof margin === "number") lIns = rIns = tIns = bIns = marginToEmu(margin);
877
+ if (lIns == null && rIns == null && tIns == null && bIns == null) {
878
+ const insets = resolveInsetsEmu(opts.margin);
879
+ if (insets) ({l: lIns, r: rIns, t: tIns, b: bIns} = insets);
858
880
  }
859
881
  return {
860
- lIns: lIns ?? DEF_INS_LR_EMU,
861
- rIns: rIns ?? DEF_INS_LR_EMU,
862
- tIns: tIns ?? DEF_INS_TB_EMU,
863
- bIns: bIns ?? DEF_INS_TB_EMU
882
+ lIns: lIns ?? BODY_INSET_DEFAULTS_EMU.left,
883
+ rIns: rIns ?? BODY_INSET_DEFAULTS_EMU.right,
884
+ tIns: tIns ?? BODY_INSET_DEFAULTS_EMU.top,
885
+ bIns: bIns ?? BODY_INSET_DEFAULTS_EMU.bottom
864
886
  };
865
887
  }
866
888
  /**
@@ -878,7 +900,7 @@ function computeBox(obj, presLayout) {
878
900
  const wEmu = getSmartParseNumber(opts.w, "X", presLayout);
879
901
  const hEmu = getSmartParseNumber(opts.h, "Y", presLayout);
880
902
  if (!(wEmu > 0) || !(hEmu > 0)) return null;
881
- const { lIns, rIns, tIns, bIns } = resolveInsetsEmu(opts);
903
+ const { lIns, rIns, tIns, bIns } = resolveBodyInsetsEmu(opts);
882
904
  const wrap = (opts._bodyProp ?? {}).wrap !== false;
883
905
  const innerWidthPt = (wEmu - lIns - rIns) / EMU_PER_POINT;
884
906
  const innerHeightPt = (hEmu - tIns - bIns) / EMU_PER_POINT;
@@ -897,46 +919,85 @@ function anchorTopShareOfDelta(opts) {
897
919
  if (anchor === "b") return 1;
898
920
  return .5;
899
921
  }
900
- /** Text/format options that a cell inherits from the table when it sets none itself (mirrors `gen/slide/objects/table.ts`). */
901
922
  //#endregion
902
- //#region src/measure/table-fit.ts
923
+ //#region src/gen/table/cell-inherit.ts
903
924
  /**
904
- * ts-pptx: measuring a table's row heights and cell fits
925
+ * ts-pptx: what a table cell inherits from its table.
905
926
  *
906
- * A table is measured cell by cell against its resolved grid: each cell inherits the table's text
907
- * options where it sets none, gets its own margins resolved to EMU insets, and is laid out inside
908
- * the column span it actually occupies. A merged region is measured once, at its origin, across
909
- * the columns it covers — which is what `walkTableGrid` exists to hand out.
927
+ * A cell that states nothing takes the table's value, and two paths resolve that: the emitter,
928
+ * building the bag it hands to the text-body writer, and the measured-fit pass, building the
929
+ * effective text it lays out. They inherited **different lists** -- the measure side named
930
+ * `italic`, `charSpacing`, `lineSpacing` and `lineSpacingMultiple` that the emitter did not,
931
+ * while its docstring claimed to mirror the emitter's -- so a table-level `italic` was measured
932
+ * with italic metrics and emitted upright, and on `fit: 'shrink'` a table-level line spacing
933
+ * made the solver compute a taller layout and bake a smaller font that the cell then rendered
934
+ * at default spacing. `pptx.tableLayout()` reported geometry the file disagreed with.
935
+ *
936
+ * So the text keys are one list, shared. The second list is the emitter's alone, and is a
937
+ * statement rather than an omission: those keys either paint (`fill`, `color`, `border`,
938
+ * `underline`) or are not modelled by the fitter (`textDirection`, which does change how text
939
+ * lays out -- the measured-fit pass does not read it on a cell either way, and widening the
940
+ * fitter to handle vertical cell text is its own change).
941
+ */
942
+ /**
943
+ * The keys both paths inherit: everything that decides how a cell's text is laid out.
944
+ *
945
+ * `margin` and `valign` are here rather than below because the fitter reads them -- insets
946
+ * shrink the box the text has to fit, and the anchor decides where a short line sits in it.
910
947
  */
911
- const CELL_INHERIT_KEYS = [
948
+ const CELL_INHERITED_TEXT_KEYS = [
949
+ "align",
950
+ "bold",
951
+ "charSpacing",
912
952
  "fontFace",
913
953
  "fontSize",
914
- "bold",
915
954
  "italic",
916
- "charSpacing",
917
- "align",
918
955
  "lineSpacing",
919
956
  "lineSpacingMultiple",
920
- "valign",
921
- "margin"
957
+ "margin",
958
+ "valign"
959
+ ];
960
+ /** The keys only the emitter inherits: they paint, or the fitter does not model them. */
961
+ const CELL_INHERITED_EMIT_KEYS = [
962
+ "border",
963
+ "color",
964
+ "fill",
965
+ "textDirection",
966
+ "underline"
922
967
  ];
923
- /** Effective cell options: the cell's own values, with table-level values filled in where unset. */
968
+ /** Every key the emitter inherits from the table onto a cell. */
969
+ const CELL_INHERITED_KEYS = [...CELL_INHERITED_TEXT_KEYS, ...CELL_INHERITED_EMIT_KEYS];
970
+ //#endregion
971
+ //#region src/measure/table-fit.ts
972
+ /**
973
+ * ts-pptx: measuring a table's row heights and cell fits
974
+ *
975
+ * A table is measured cell by cell against its resolved grid: each cell inherits the table's text
976
+ * options where it sets none, gets its own margins resolved to EMU insets, and is laid out inside
977
+ * the column span it actually occupies. A merged region is measured once, at its origin, across
978
+ * the columns it covers — which is what `walkTableGrid` exists to hand out.
979
+ */
980
+ /**
981
+ * Effective cell options: the cell's own values, with table-level values filled in where unset.
982
+ *
983
+ * The keys are `CELL_INHERITED_TEXT_KEYS` (`gen/table/cell-inherit.ts`), the half of the
984
+ * emitter's list that decides how text lays out. This used to be its own list, claiming in its
985
+ * docstring to mirror the emitter's while naming four keys the emitter did not.
986
+ *
987
+ * PowerPoint has no text-autofit for a table cell — `a:tcPr` carries no autofit, and the app
988
+ * ignores a `normAutofit` inside one; rows auto-grow instead. So a cell's `fit: 'shrink'` is
989
+ * honoured by baking a *reduced literal font size* onto its runs, which both PowerPoint and
990
+ * LibreOffice render identically with no edit or resize. That is what makes these options worth
991
+ * resolving here: the shrink is computed against the cell's effective text, not the table's.
992
+ */
924
993
  function effectiveCellOpts(cellOpts, tableOpts) {
925
994
  const merged = { ...cellOpts };
926
- for (const k of CELL_INHERIT_KEYS) if (merged[k] === void 0 && tableOpts[k] !== void 0) merged[k] = tableOpts[k];
995
+ for (const k of CELL_INHERITED_TEXT_KEYS) if (merged[k] === void 0 && tableOpts[k] !== void 0) merged[k] = tableOpts[k];
927
996
  return merged;
928
997
  }
929
998
  /** Resolve a cell's margins to EMU insets, mirroring `gen/slide/objects/table.ts` (array is `[T,R,B,L]`, inches; see `marginToEmu`). */
930
999
  function resolveCellInsetsEmu(margin) {
931
- let m = margin === 0 || margin ? margin : DEF_CELL_MARGIN_IN;
932
- if (typeof m === "number") m = [
933
- m,
934
- m,
935
- m,
936
- m
937
- ];
938
- if (!Array.isArray(m) || m.length !== 4 || m.some((v) => typeof v !== "number" || !isFinite(v))) m = DEF_CELL_MARGIN_IN;
939
- const arr = m;
1000
+ const arr = resolveCellMarginsInches(margin);
940
1001
  return {
941
1002
  marT: marginToEmu(arr[0]),
942
1003
  marR: marginToEmu(arr[1]),
@@ -945,12 +1006,17 @@ function resolveCellInsetsEmu(margin) {
945
1006
  };
946
1007
  }
947
1008
  /**
948
- * Bake a reduced font size onto a cell's runs by factor `f` (< 1). Clones every
949
- * options object before mutating: a plain-string cell shares the table's `opt`
950
- * object (`gen/define/`), so in-place mutation would corrupt every other such cell.
1009
+ * Bake a reduced font size onto a cell's runs by factor `f` (< 1). The cell's options and runs are
1010
+ * replaced with scaled copies, never edited in place. The cell itself is the stored slide model,
1011
+ * so `applyMeasuredFit` puts the authored options and runs back once the write is built.
1012
+ *
1013
+ * The shrunk size never goes below {@link MIN_FONT_SIZE_PT}, the emitter's own bound. It used to
1014
+ * have no minimum, so a 1pt cell taken to the 25% scale floor was baked as 0.2pt and
1015
+ * `clampFontSizeSz` then warned that "fontSize 0.2 is outside the valid range" -- a value the
1016
+ * caller never stated and cannot act on. Their input was valid; only our scaling was out of range.
951
1017
  */
952
1018
  function scaleCellFontSizes(cell, eff, f) {
953
- const shrink = (sizePt) => Math.floor(sizePt * f * 10) / 10;
1019
+ const shrink = (sizePt) => Math.max(1, Math.floor(sizePt * f * 10) / 10);
954
1020
  const baseSize = Number(eff.fontSize ?? 12);
955
1021
  cell.options = {
956
1022
  ...cell.options,
@@ -964,46 +1030,33 @@ function scaleCellFontSizes(cell, eff, f) {
964
1030
  }
965
1031
  } : run);
966
1032
  }
967
- /** Grid column count of a table (sums the first row's colspans), mirroring `gen/slide/objects/table.ts`. */
968
- function tableColCount(rows) {
969
- const first = rows[0];
970
- return first ? first.reduce((n, c) => n + (Number(c?.options?.colspan) || 1), 0) : 0;
971
- }
972
1033
  /**
973
- * Walk a table's cell grid in row-major order, resolving each authored cell to its
974
- * grid origin (`row`/`col`) and clamped colspan/rowspan. Tracks rowspan occupancy so
975
- * a cell never lands beneath one spanned from above (grid build). This is
976
- * the single traversal shared by the measured-fit shrink pass and
977
- * {@link computeTableLayout}, so cell placement cannot drift between them.
1034
+ * Resolve a table's grid the one way every consumer has to see it.
1035
+ *
1036
+ * Three sites derived these four things: `pptx.tableLayout()`, the export-time measured-fit
1037
+ * pass, and the table emitter (the height alone). They already disagreed — only the fit pass
1038
+ * read `cy`, the EMU height the auto-pager and the fit pass itself stamp onto the options, so
1039
+ * `addTable(rows, { cy })` with no `h` gave a file whose rows are pinned and a
1040
+ * `pptx.tableLayout()` that reported every row auto-height. That is the same drift the one
1041
+ * reading of `rowH` closed, arriving through a second option, which is why the answer is a
1042
+ * shared resolver rather than a third correction.
1043
+ *
1044
+ * `w` defaults to `75%` of the slide, matching what the emitter falls back to.
1045
+ * @param rows - the table's authored rows
1046
+ * @param opts - the table's options
1047
+ * @param presLayout - the presentation layout, for resolving percentages
978
1048
  */
979
- function* walkTableGrid(rows, numCols) {
980
- const occupied = new Array(numCols).fill(0);
981
- for (let r = 0; r < rows.length; r++) {
982
- const row = rows[r];
983
- if (!row) continue;
984
- let col = 0;
985
- for (const cell of row) {
986
- while (col < numCols && (occupied[col] ?? 0) > 0) col++;
987
- if (col >= numCols) break;
988
- const colspan = Math.max(1, Number(cell?.options?.colspan) || 1);
989
- const rowspan = Math.min(Math.max(1, Number(cell?.options?.rowspan) || 1), rows.length - r);
990
- const colStart = col;
991
- const colEnd = Math.min(colStart + colspan, numCols);
992
- for (let c = colStart; c < colEnd; c++) occupied[c] = rowspan;
993
- col = colEnd;
994
- yield {
995
- cell,
996
- row: r,
997
- col: colStart,
998
- rowSpan: rowspan,
999
- colSpan: colEnd - colStart
1000
- };
1001
- }
1002
- for (let c = 0; c < numCols; c++) {
1003
- const cur = occupied[c] ?? 0;
1004
- if (cur > 0) occupied[c] = cur - 1;
1005
- }
1006
- }
1049
+ function resolveTableGridEmu(rows, opts, presLayout) {
1050
+ const numRows = rows.length;
1051
+ const numCols = tableColCount(rows);
1052
+ const cxEmu = opts.w != null ? getSmartParseNumber(opts.w, "X", presLayout) : getSmartParseNumber("75%", "X", presLayout);
1053
+ const tableHeightEmu = opts.h != null ? getSmartParseNumber(opts.h, "Y", presLayout) : typeof opts.cy === "number" ? opts.cy : 0;
1054
+ return {
1055
+ numCols,
1056
+ colWidthsEmu: resolveTableColWidthsEmu(opts.colW, cxEmu, numCols),
1057
+ tableHeightEmu,
1058
+ rowHeightEmu: (rowIndex) => resolveTableRowHeightEmu(opts.rowH, rowIndex, tableHeightEmu, numRows)
1059
+ };
1007
1060
  }
1008
1061
  /**
1009
1062
  * Compute per-cell geometry (inches) for a table laid out at `opts.x`/`y`/`w` — the
@@ -1024,31 +1077,21 @@ function computeTableLayout(rows, opts, presLayout, registry) {
1024
1077
  };
1025
1078
  if (!rows || rows.length === 0 || !rows[0]) return empty;
1026
1079
  const numRows = rows.length;
1027
- const numCols = tableColCount(rows);
1080
+ const grid = resolveTableGridEmu(rows, opts, presLayout);
1081
+ const numCols = grid.numCols;
1028
1082
  if (!(numCols > 0)) return empty;
1029
1083
  const o = opts;
1030
1084
  const tableXEmu = opts.x != null ? getSmartParseNumber(opts.x, "X", presLayout) : 0;
1031
1085
  const tableYEmu = opts.y != null ? getSmartParseNumber(opts.y, "Y", presLayout) : 0;
1032
- const cxEmu = opts.w != null ? getSmartParseNumber(opts.w, "X", presLayout) : getSmartParseNumber("75%", "X", presLayout);
1033
- const colWidthsEmu = resolveTableColWidthsEmu(opts.colW, cxEmu, numCols);
1086
+ const colWidthsEmu = grid.colWidthsEmu;
1034
1087
  const colXEmu = new Array(numCols + 1).fill(0);
1035
1088
  for (let c = 0; c < numCols; c++) colXEmu[c + 1] = (colXEmu[c] ?? 0) + (colWidthsEmu[c] ?? 0);
1036
- const tableHeightEmu = opts.h != null ? getSmartParseNumber(opts.h, "Y", presLayout) : 0;
1037
- const explicitRowHEmu = (r) => {
1038
- if (Array.isArray(opts.rowH)) {
1039
- const v = opts.rowH[r];
1040
- if (typeof v === "number" && isFinite(v)) return inch2Emu(v);
1041
- return tableHeightEmu > 0 ? Math.round(tableHeightEmu / numRows) : null;
1042
- }
1043
- if (opts.rowH != null && !isNaN(Number(opts.rowH))) return inch2Emu(Number(opts.rowH));
1044
- if (tableHeightEmu > 0) return Math.round(tableHeightEmu / numRows);
1045
- return null;
1046
- };
1089
+ const explicitRowHEmu = grid.rowHeightEmu;
1047
1090
  const resolve = makeRegistryResolver(registry);
1048
- const defLineEmu = inch2Emu(12 * LINEH_MODIFIER / 100);
1091
+ const defLineEmu = autoPageLineHeightEmu(12);
1049
1092
  const estimateContentHeightEmu = (cell, eff, innerWidthPt) => {
1050
1093
  const fontSizePt = Number(eff.fontSize ?? 12) || 12;
1051
- const oneLineEmu = inch2Emu(fontSizePt * LINEH_MODIFIER / 100);
1094
+ const oneLineEmu = autoPageLineHeightEmu(fontSizePt);
1052
1095
  if (!(innerWidthPt > 0)) return oneLineEmu;
1053
1096
  const paragraphs = extractParagraphs({
1054
1097
  text: cell.text,
@@ -1087,7 +1130,10 @@ function computeTableLayout(rows, opts, presLayout, registry) {
1087
1130
  if (contentEmu > (rowHeightsEmu[p.row] ?? 0)) rowHeightsEmu[p.row] = contentEmu;
1088
1131
  }
1089
1132
  }
1090
- for (let r = 0; r < numRows; r++) if ((rowHeightsEmu[r] ?? 0) <= 0) rowHeightsEmu[r] = defLineEmu;
1133
+ for (let r = 0; r < numRows; r++) if ((rowHeightsEmu[r] ?? 0) <= 0) {
1134
+ rowHeightsEmu[r] = defLineEmu;
1135
+ rowExact[r] = false;
1136
+ }
1091
1137
  const rowYEmu = new Array(numRows + 1).fill(0);
1092
1138
  for (let r = 0; r < numRows; r++) rowYEmu[r + 1] = (rowYEmu[r] ?? 0) + (rowHeightsEmu[r] ?? 0);
1093
1139
  return {
@@ -1112,28 +1158,21 @@ function computeTableLayout(rows, opts, presLayout, registry) {
1112
1158
  heightExact: rowExact.every(Boolean)
1113
1159
  };
1114
1160
  }
1115
- /**
1116
- * Build the `MetricsResolver` both the export pass and `measureText` use, so they
1117
- * agree run-for-run: exact registered metrics → conservative heuristic for any
1118
- * **named** face without exact metrics → `undefined` only for an unnamed
1119
- * (theme-default) face that cannot be guessed. `onHeuristic` is called with each
1120
- * named face that fell back to the heuristic (for the export pass's warn-once).
1121
- */
1122
1161
  //#endregion
1123
1162
  //#region src/measure/fit.ts
1124
1163
  /** Map the public {@link MeasureTextOptions} onto the internal run-option shape. */
1125
1164
  function measureOptsToRunOpts(opts) {
1126
- return {
1127
- fontSize: opts.fontSize,
1128
- fontFace: opts.fontFace,
1129
- bold: opts.bold,
1130
- italic: opts.italic,
1131
- charSpacing: opts.charSpacing,
1132
- lineSpacing: opts.lineSpacing,
1133
- lineSpacingMultiple: opts.lineSpacingMultiple,
1134
- paraSpaceBefore: opts.paraSpaceBefore,
1135
- paraSpaceAfter: opts.paraSpaceAfter
1136
- };
1165
+ return pickDefined(opts, [
1166
+ "fontSize",
1167
+ "fontFace",
1168
+ "bold",
1169
+ "italic",
1170
+ "charSpacing",
1171
+ "lineSpacing",
1172
+ "lineSpacingMultiple",
1173
+ "paraSpaceBefore",
1174
+ "paraSpaceAfter"
1175
+ ]);
1137
1176
  }
1138
1177
  /** A fresh result each call: the arrays are caller-owned, so they cannot be shared. */
1139
1178
  const unmeasurable = () => ({
@@ -1165,11 +1204,11 @@ function formatCodepoints(cps) {
1165
1204
  * Layout-time text measurement against registered metrics — the public engine
1166
1205
  * behind `pptx.measureText()`. Uses the **same** calibrated wrap model, resolver
1167
1206
  * semantics, and conservative safety factors as the export-time bake
1168
- * ({@link applyMeasuredFit} / {@link solveResize} / {@link solveShrink}), so a
1207
+ * (`applyMeasuredFit` / {@link solveResize} / {@link solveShrink}), so a
1169
1208
  * layout-time prediction matches the value the export would bake **for any deck
1170
1209
  * that opted into measured fit (i.e. registered at least one face)**.
1171
1210
  *
1172
- * With an **empty** registry the two intentionally diverge: {@link applyMeasuredFit}
1211
+ * With an **empty** registry the two intentionally diverge: `applyMeasuredFit`
1173
1212
  * treats "no metrics" as "not opted in" and bakes nothing, while this returns
1174
1213
  * heuristic numbers so the API is useful with zero setup. Check
1175
1214
  * `approximatedFaces` if that distinction matters.
@@ -1182,9 +1221,13 @@ function formatCodepoints(cps) {
1182
1221
  * rather than tall — there is no font fallback here, only the font's `.notdef` advance.
1183
1222
  */
1184
1223
  function measureText(registry, text, opts) {
1224
+ const inset = opts.insetIn ?? 0;
1225
+ requireFiniteInches(opts.wIn, "wIn", "measureText");
1226
+ requireFiniteInches(inset, "insetIn", "measureText");
1227
+ if (opts.fontSize !== void 0 && !(Number.isFinite(opts.fontSize) && opts.fontSize > 0)) throw new InvalidOptionError("font/size-not-positive", `measureText: \`fontSize\` must be a positive number of points, got ${String(opts.fontSize)}.`);
1228
+ if (!(opts.wIn > 2 * inset)) throw new InvalidOptionError("coord/not-positive", `measureText: \`wIn\` ${opts.wIn} leaves no width inside an inset of ${inset} on each side.`);
1185
1229
  const paragraphs = buildFitParagraphs(typeof text === "string" || typeof text === "number" ? [{ text: String(text) }] : Array.isArray(text) ? text : [], measureOptsToRunOpts(opts));
1186
1230
  if (!paragraphs) return unmeasurable();
1187
- const inset = opts.insetIn ?? 0;
1188
1231
  const innerWidthPt = (opts.wIn - 2 * inset) * 72;
1189
1232
  const heuristicFaces = /* @__PURE__ */ new Set();
1190
1233
  const resolve = makeRegistryResolver(registry, (face) => heuristicFaces.add(face));
@@ -1200,8 +1243,12 @@ function measureText(registry, text, opts) {
1200
1243
  measurable: true,
1201
1244
  approximatedFaces: [...heuristicFaces],
1202
1245
  uncoveredCodepoints: flattenCodepoints(uncoveredByFace),
1203
- fitsBox: (hIn) => heightPt <= hIn * 72,
1246
+ fitsBox: (hIn) => {
1247
+ requireBoxHeight(hIn, "fitsBox");
1248
+ return heightPt <= hIn * 72;
1249
+ },
1204
1250
  shrinkScaleFor: (hIn) => {
1251
+ requireBoxHeight(hIn, "shrinkScaleFor");
1205
1252
  const box = {
1206
1253
  innerWidthPt,
1207
1254
  innerHeightPt: hIn * 72
@@ -1212,6 +1259,20 @@ function measureText(registry, text, opts) {
1212
1259
  }
1213
1260
  };
1214
1261
  }
1262
+ /** Refuse a measure that is not a finite number of inches, naming the option and the call. */
1263
+ function requireFiniteInches(value, name, call) {
1264
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new InvalidOptionError("coord/non-finite", `${call}: \`${name}\` must be a finite number of inches; got ${String(value)}.`);
1265
+ }
1266
+ /**
1267
+ * Refuse a box height a fit cannot be tested against: not a finite number, or not above 0. A `NaN`
1268
+ * height made `fitsBox` answer `false` and `shrinkScaleFor` answer `100`, both as if measured.
1269
+ * @param hIn - the caller's inner box height, in inches
1270
+ * @param call - the method it was passed to, opening the message
1271
+ */
1272
+ function requireBoxHeight(hIn, call) {
1273
+ requireFiniteInches(hIn, "hIn", call);
1274
+ if (!(hIn > 0)) throw new InvalidOptionError("coord/not-positive", `${call}: \`hIn\` must be greater than 0; got ${String(hIn)}.`);
1275
+ }
1215
1276
  /**
1216
1277
  * Apply measured fit across every slide. For each text box that opts in via
1217
1278
  * `fit:'shrink'` or `fit:'resize'` and whose font has registered metrics, this
@@ -1225,9 +1286,20 @@ function measureText(registry, text, opts) {
1225
1286
  * Safe to call with an empty registry (no-op). Warns once if any opted-in box could
1226
1287
  * not be measured (missing metrics), and once if one held code points its registered
1227
1288
  * face has no glyph for, so neither kind of overflow is silently ignored.
1289
+ *
1290
+ * Every rewrite lands on the stored slide model, because that is what the emitters read.
1291
+ * The returned function puts each authored value back; call it once the XML is built. Left
1292
+ * baked, a second write found a text box's object-form `fit` and skipped it, and measured a
1293
+ * resized box or a shrunk cell from this write's result, so metrics registered in between
1294
+ * never reached those shapes.
1295
+ * @returns the step that restores every value this pass rewrote
1228
1296
  */
1229
1297
  function applyMeasuredFit(slides, registry) {
1230
- if (registry.size === 0) return;
1298
+ const restores = [];
1299
+ const restore = () => {
1300
+ for (const undo of restores.splice(0).reverse()) undo();
1301
+ };
1302
+ if (registry.size === 0) return restore;
1231
1303
  const heuristicFaces = /* @__PURE__ */ new Set();
1232
1304
  const resolve = makeRegistryResolver(registry, (face) => heuristicFaces.add(face));
1233
1305
  const unmeasuredShrink = /* @__PURE__ */ new Set();
@@ -1248,18 +1320,9 @@ function applyMeasuredFit(slides, registry) {
1248
1320
  const rows = tableObj.arrTabRows;
1249
1321
  if (!rows || rows.length === 0 || !rows[0]) return;
1250
1322
  const tableOpts = tableObj.options ?? {};
1251
- const numRows = rows.length;
1252
- const numCols = tableColCount(rows);
1323
+ const { numCols, colWidthsEmu, rowHeightEmu: pinnedRowHeightEmu } = resolveTableGridEmu(rows, tableOpts, layout);
1253
1324
  if (!(numCols > 0)) return;
1254
- const cxEmu = tableOpts.w != null ? getSmartParseNumber(tableOpts.w, "X", layout) : getSmartParseNumber("75%", "X", layout);
1255
- const colWidthsEmu = resolveTableColWidthsEmu(tableOpts.colW, cxEmu, numCols);
1256
- const tableHeightEmu = tableOpts.h != null ? getSmartParseNumber(tableOpts.h, "Y", layout) : typeof tableOpts.cy === "number" ? tableOpts.cy : 0;
1257
- const rowHeightEmu = (rIdx) => {
1258
- if (Array.isArray(tableOpts.rowH) && tableOpts.rowH[rIdx]) return inch2Emu(Number(tableOpts.rowH[rIdx]));
1259
- if (tableOpts.rowH != null && !Array.isArray(tableOpts.rowH) && !isNaN(Number(tableOpts.rowH))) return inch2Emu(Number(tableOpts.rowH));
1260
- if (tableHeightEmu > 0) return Math.round(tableHeightEmu / numRows);
1261
- return 0;
1262
- };
1325
+ const rowHeightEmu = (rIdx) => pinnedRowHeightEmu(rIdx) ?? 0;
1263
1326
  for (const { cell, row: r, col: colStart, colSpan, rowSpan } of walkTableGrid(rows, numCols)) {
1264
1327
  const colEnd = colStart + colSpan;
1265
1328
  const cellOpts = cell?.options ?? {};
@@ -1294,7 +1357,10 @@ function applyMeasuredFit(slides, registry) {
1294
1357
  }, resolve);
1295
1358
  if (outcome.kind === "shrink") {
1296
1359
  const f = outcome.result.fontScalePct / 100;
1297
- if (f < 1) scaleCellFontSizes(cell, eff, f);
1360
+ if (f < 1) {
1361
+ restores.push(restorer(cell, "options"), restorer(cell, "text"));
1362
+ scaleCellFontSizes(cell, eff, f);
1363
+ }
1298
1364
  } else if (outcome.kind === "unmeasurable") collectUnmeasured(paragraphs, unmeasuredShrink);
1299
1365
  }
1300
1366
  };
@@ -1321,21 +1387,24 @@ function applyMeasuredFit(slides, registry) {
1321
1387
  const outcome = solveShrink(paragraphs, box, resolve);
1322
1388
  if (outcome.kind === "shrink") {
1323
1389
  const { fontScalePct, lnSpcReductionPct } = outcome.result;
1324
- options.fit = {
1390
+ const shrink = {
1325
1391
  type: "shrink",
1326
- fontScale: fontScalePct,
1327
- lnSpcReduction: lnSpcReductionPct || void 0
1392
+ fontScale: fontScalePct
1328
1393
  };
1394
+ if (lnSpcReductionPct) shrink.lnSpcReduction = lnSpcReductionPct;
1395
+ restores.push(restorer(options, "fit"));
1396
+ options.fit = shrink;
1329
1397
  } else if (outcome.kind === "unmeasurable") collectUnmeasured(paragraphs, unmeasuredShrink);
1330
1398
  } else {
1331
1399
  const outcome = solveResize(paragraphs, box, resolve);
1332
1400
  if (outcome.kind === "resize") {
1333
1401
  const opts = obj.options;
1334
- const { tIns, bIns } = resolveInsetsEmu(opts);
1402
+ const { tIns, bIns } = resolveBodyInsetsEmu(opts);
1335
1403
  const oldHeightEmu = getSmartParseNumber(opts.h, "Y", layout);
1336
1404
  const newHeightEmu = Math.round(outcome.neededInnerHeightPt * EMU_PER_POINT) + tIns + bIns;
1337
1405
  const oldYEmu = getSmartParseNumber(opts.y, "Y", layout);
1338
1406
  const shiftEmu = Math.round((newHeightEmu - oldHeightEmu) * anchorTopShareOfDelta(opts));
1407
+ restores.push(restorer(opts, "h"), restorer(opts, "y"));
1339
1408
  opts.h = `${newHeightEmu}emu`;
1340
1409
  if (shiftEmu !== 0) opts.y = `${oldYEmu - shiftEmu}emu`;
1341
1410
  } else collectUnmeasured(paragraphs, unmeasuredResize);
@@ -1349,8 +1418,21 @@ function applyMeasuredFit(slides, registry) {
1349
1418
  warn("measure/uncovered-codepoints", `Measured fit charged the .notdef advance for code point(s) the registered face does not cover (${perFace}) — PowerPoint renders those from a substituted font, so the measured width can be wrong in EITHER direction. Too narrow drops a line and the text overflows the baked size, the one case measured fit does not err safe. Register a face that covers them, or set an explicit fontFace on those runs.`);
1350
1419
  }
1351
1420
  if (heuristicFaces.size > 0) warn("measure/heuristic-metrics", `Note: measured fit used a conservative average-advance estimate for font(s) [${[...heuristicFaces].join(", ")}] — no exact metrics registered. Fit is approximate (may shrink/grow more than necessary). Call pptx.registerFontMetrics(face, fontFilePathOrBytes) for an exact fit.`);
1421
+ return restore;
1422
+ }
1423
+ /**
1424
+ * The step that puts `target[key]` back as it stands now, an absent key included, so a restored
1425
+ * option bag is the authored one rather than one holding an `undefined` the caller never wrote.
1426
+ */
1427
+ function restorer(target, key) {
1428
+ const had = Object.hasOwn(target, key);
1429
+ const value = target[key];
1430
+ return () => {
1431
+ if (had) target[key] = value;
1432
+ else delete target[key];
1433
+ };
1352
1434
  }
1353
1435
  //#endregion
1354
- export { parseFontMetrics as _, FONT_SCALE_STEP_PCT as a, SINGLE_LINE_PITCH as c, measureLayout as d, solveResize as f, makeRegistryResolver as g, getHeuristicFontMetrics as h, buildFitParagraphs as i, WIDTH_SAFETY_FACTOR as l, FontMetricsRegistry as m, measureText as n, HEIGHT_SAFETY_FACTOR as o, solveShrink as p, computeTableLayout as r, MIN_FONT_SCALE_PCT as s, applyMeasuredFit as t, measureHeightPt as u, isFontCollection as v, listFontFaces as y };
1436
+ export { getHeuristicFontMetrics as _, CELL_INHERITED_KEYS as a, isFontCollection as b, HEIGHT_SAFETY_FACTOR as c, WIDTH_SAFETY_FACTOR as d, measureHeightPt as f, FontMetricsRegistry as g, solveShrink as h, computeTableLayout as i, MIN_FONT_SCALE_PCT as l, solveResize as m, measureText as n, buildFitParagraphs as o, measureLayout as p, requireBoxHeight as r, FONT_SCALE_STEP_PCT as s, applyMeasuredFit as t, SINGLE_LINE_PITCH as u, makeRegistryResolver as v, listFontFaces as x, parseFontMetrics as y };
1355
1437
 
1356
- //# sourceMappingURL=fit-BKs0zxMI.js.map
1438
+ //# sourceMappingURL=fit-Cb7naFj6.js.map