@office-kit/pptx-preview 0.11.0 → 0.13.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.
@@ -1,4 +1,255 @@
1
- import { getEffectiveColorMap, getGroupChildren, getGroupTransform, getParagraphAlignment, getParagraphBullet, getParagraphBulletImageBytes, getParagraphBulletStyle, getParagraphIndent, getParagraphLevel, getParagraphPropertiesEffective, getParagraphSpacing, getPresentationFonts, getPresentationTheme, getShapeAdjustValues, getShapeAltTitle, getShapeBodyPrEffective, getShapeBoundsResolved, getShapeChartSpec, getShapeClickAction, getShapeCustomGeometry, getShapeDescription, getShapeEffectsEffective, getShapeFillColorResolved, getShapeFillEffective, getShapeFillOpacity, getShapeFlip, getShapeGradientFill, getShapeGradientFillEffective, getShapeHyperlink, getShapeHyperlinkTooltip, getShapeImageBiLevelThreshold, getShapeImageBrightness, getShapeImageBytes, getShapeImageContrast, getShapeImageCrop, getShapeImageDuotone, getShapeImageFillBytes, getShapeImageFormat, getShapeImageLinkUrl, getShapeImageOpacity, getShapeImagePartName, getShapeKind, getShapeName, getShapeParagraphCount, getShapeParagraphElements, getShapePatternFill, getShapePlaceholderType, getShapePreset, getShapeRotation, getShapeRunClickAction, getShapeRunFormat, getShapeRunFormatEffective, getShapeRunHyperlink, getShapeRunHyperlinkTooltip, getShapeStrokeArrow, getShapeStrokeCap, getShapeStrokeColorResolved, getShapeStrokeCompound, getShapeStrokeDash, getShapeStrokeEffective, getShapeStrokeJoin, getShapeStrokeOpacity, getShapeTextAnchor, getShapeTextAutoFitParams, getShapeTextBodyRotationDeg, getShapeTextColumns, getShapeTextDirection, getShapeTextMargins, getShapeXmlString, getSlideBackground, getSlideBackgroundGradientFill, getSlideBackgroundImageBytes, getSlideBackgroundPatternFill, getSlideIndex, getSlideLayout, getSlideLayoutBackground, getSlideLayoutBackgroundGradientFill, getSlideLayoutBackgroundImageBytes, getSlideLayoutBackgroundPatternFill, getSlideLayoutShapes, getSlideMasterBackground, getSlideMasterBackgroundGradientFill, getSlideMasterBackgroundImageBytes, getSlideMasterBackgroundPatternFill, getSlideMasterShapes, getSlideShapes, getSlideSize, getSlides, getTableCellAnchor, getTableCellBorders, getTableCellFill, getTableCellMargins, getTableCellParagraphs, getTableCellSpan, getTableCells, getTableColumnWidths, getTableDimensions, getTableRowHeights, getTableStyleFlags, isChartShape, isParagraphBulletPicture, isShapeImageGrayscale, isShapePlaceholder, isShapeTextBox, isTableShape, resolveDeckBodyTextColor } from "@office-kit/pptx";
1
+ import { getEffectiveColorMap, getGroupChildren, getGroupTransform, getParagraphAlignment, getParagraphBullet, getParagraphBulletImageBytes, getParagraphBulletStyle, getParagraphIndent, getParagraphLevel, getParagraphPropertiesEffective, getParagraphSpacing, getPresentationFirstSlideNumber, getPresentationFonts, getPresentationTheme, getShapeAdjustValues, getShapeAltTitle, getShapeBodyPrEffective, getShapeBounds, getShapeBoundsResolved, getShapeChartSpec, getShapeClickAction, getShapeCustomGeometry, getShapeDescription, getShapeEffectsEffective, getShapeFillColorResolved, getShapeFillEffective, getShapeFillOpacity, getShapeFlip, getShapeGradientFill, getShapeGradientFillEffective, getShapeHyperlinkTooltip, getShapeId, getShapeImageBiLevelThreshold, getShapeImageBrightness, getShapeImageBytes, getShapeImageContrast, getShapeImageCrop, getShapeImageDuotone, getShapeImageFillBytes, getShapeImageFillLayout, getShapeImageFormat, getShapeImageIntrinsicSize, getShapeImageLinkUrl, getShapeImageOpacity, getShapeImagePartName, getShapeKind, getShapeName, getShapeParagraphCount, getShapeParagraphElements, getShapePatternFill, getShapePlaceholderType, getShapePreset, getShapeRotation, getShapeRunClickAction, getShapeRunFormat, getShapeRunFormatEffective, getShapeRunHyperlink, getShapeRunHyperlinkTooltip, getShapeStrokeArrow, getShapeStrokeCap, getShapeStrokeColorResolved, getShapeStrokeCompound, getShapeStrokeDash, getShapeStrokeEffective, getShapeStrokeJoin, getShapeStrokeOpacity, getShapeTextAnchor, getShapeTextAutoFit, getShapeTextAutoFitParams, getShapeTextBodyRotationDeg, getShapeTextColumns, getShapeTextDirection, getShapeTextMargins, getShapeXmlString, getSlideBackground, getSlideBackgroundGradientFill, getSlideBackgroundImageBytes, getSlideBackgroundImageCrop, getSlideBackgroundImageFillLayout, getSlideBackgroundImageIntrinsicSize, getSlideBackgroundImageOpacity, getSlideBackgroundPatternFill, getSlideIndex, getSlideLayout, getSlideLayoutBackground, getSlideLayoutBackgroundGradientFill, getSlideLayoutBackgroundImageBytes, getSlideLayoutBackgroundPatternFill, getSlideLayoutShapes, getSlideMasterBackground, getSlideMasterBackgroundGradientFill, getSlideMasterBackgroundImageBytes, getSlideMasterBackgroundPatternFill, getSlideMasterShapes, getSlideShapes, getSlideSize, getSlides, getTableCellAnchor, getTableCellAppearanceEffective, getTableCellMargins, getTableCellParagraphs, getTableCellRunFormatEffective, getTableCellSpan, getTableCellTextDirection, getTableCells, getTableColumnWidths, getTableDimensions, getTableRowHeights, isChartShape, isParagraphBulletPicture, isShapeHidden, isShapeImageGrayscale, isShapePlaceholder, isShapeTextBox, isSlideBackgroundGraphicsHidden, isSlideLayoutBackgroundGraphicsHidden, isTableShape, resolveDeckBodyTextColor } from "@office-kit/pptx";
2
+ //#region src/text-body-rect.ts
3
+ const presetTextRect = (preset) => {
4
+ switch (preset) {
5
+ case "triangle": return {
6
+ l: .25,
7
+ t: .5,
8
+ r: .75,
9
+ b: 1
10
+ };
11
+ case "diamond": return {
12
+ l: .25,
13
+ t: .25,
14
+ r: .75,
15
+ b: .75
16
+ };
17
+ case "pentagon": return {
18
+ l: .191,
19
+ t: .236,
20
+ r: .809,
21
+ b: 1
22
+ };
23
+ case "star5": return {
24
+ l: .309,
25
+ t: .382,
26
+ r: .691,
27
+ b: .764
28
+ };
29
+ case "leftRightArrow": return {
30
+ l: .18,
31
+ t: .35,
32
+ r: .82,
33
+ b: .65
34
+ };
35
+ default: return null;
36
+ }
37
+ };
38
+ /**
39
+ * The text rectangle a custom-geometry shape states for itself, as fractions
40
+ * of its own extent, or `null` for a preset shape and for a custom one that
41
+ * states none. `<a:rect>` is written in EMU against the shape's extents, so
42
+ * dividing by them here is what lets the result travel with a shape that a
43
+ * group has scaled.
44
+ */
45
+ function shapeCustomTextRect(custom, extent) {
46
+ const rect = custom?.textRect;
47
+ if (!rect || extent === null || extent.w <= 0 || extent.h <= 0) return null;
48
+ return {
49
+ l: rect.l / extent.w,
50
+ t: rect.t / extent.h,
51
+ r: rect.r / extent.w,
52
+ b: rect.b / extent.h
53
+ };
54
+ }
55
+ /**
56
+ * Text layout rectangle used by the preview, including preset geometry and body
57
+ * insets. All coordinates and margins must use the same unit (normally EMU).
58
+ * Preset regions follow the preview's current geometry approximations. When
59
+ * margins collapse a preset region, retain that region without margins.
60
+ *
61
+ * A custom-geometry shape states its own rectangle in `<a:custGeom><a:rect>`.
62
+ * Pass it as `custom`, as fractions of the shape's extent so that it scales
63
+ * with `bounds` the way a preset region does, and it wins: unlike the table
64
+ * above it is not an approximation of anything — it is what the file says.
65
+ */
66
+ function resolveTextBodyRect(preset, bounds, margins, custom) {
67
+ const region = custom ?? presetTextRect(preset);
68
+ const x = bounds.x + (region?.l ?? 0) * bounds.w;
69
+ const y = bounds.y + (region?.t ?? 0) * bounds.h;
70
+ const w = ((region?.r ?? 1) - (region?.l ?? 0)) * bounds.w;
71
+ const h = ((region?.b ?? 1) - (region?.t ?? 0)) * bounds.h;
72
+ const innerW = w - margins.left - margins.right;
73
+ const innerH = h - margins.top - margins.bottom;
74
+ if (region && (innerW <= 0 || innerH <= 0)) return {
75
+ x,
76
+ y,
77
+ w,
78
+ h
79
+ };
80
+ return {
81
+ x: x + margins.left,
82
+ y: y + margins.top,
83
+ w: innerW,
84
+ h: innerH
85
+ };
86
+ }
87
+ //#endregion
88
+ //#region src/text-body-style.ts
89
+ /** Mac PowerPoint uses zero spacing when `spcCol` is absent. */
90
+ const DEFAULT_COLUMN_GAP_PX = 0;
91
+ const EMU_PER_PX$2 = 9525;
92
+ const NONE = {
93
+ declarations: "",
94
+ transform: ""
95
+ };
96
+ /**
97
+ * `<a:bodyPr vert="…"/>` as CSS. `writing-mode` is the right primitive for
98
+ * the East-Asian and Mongolian cases, and `text-orientation` for the WordArt
99
+ * ones, which stack upright glyphs. `vert270` reads bottom-to-top, which no
100
+ * writing-mode expresses on its own — hence the half turn. Its columns still
101
+ * follow the OOXML vertical-rl progression before that half turn; using
102
+ * vertical-lr here reverses the column order relative to the SVG renderer.
103
+ */
104
+ function verticalTextStyle(vert) {
105
+ switch (vert) {
106
+ case "vert":
107
+ case "eaVert": return {
108
+ declarations: "writing-mode:vertical-rl",
109
+ transform: ""
110
+ };
111
+ case "mongolianVert": return {
112
+ declarations: "writing-mode:vertical-lr",
113
+ transform: ""
114
+ };
115
+ case "vert270": return {
116
+ declarations: "writing-mode:vertical-rl",
117
+ transform: "rotate(180deg)"
118
+ };
119
+ case "wordArtVert": return {
120
+ declarations: "writing-mode:vertical-rl;text-orientation:upright",
121
+ transform: ""
122
+ };
123
+ case "wordArtVertRtl": return {
124
+ declarations: "writing-mode:vertical-rl;text-orientation:upright;direction:rtl",
125
+ transform: ""
126
+ };
127
+ case null: return NONE;
128
+ }
129
+ }
130
+ /**
131
+ * `<a:bodyPr numCol="N" spcCol="EMU"/>` as CSS declarations, without a
132
+ * trailing `;`. Empty below two columns, which is the same as not splitting
133
+ * the body.
134
+ */
135
+ function textColumnsStyle(cols) {
136
+ if (!cols || cols.count < 2) return "";
137
+ const gapPx = cols.gapEmu !== void 0 ? (cols.gapEmu / EMU_PER_PX$2).toFixed(2) : DEFAULT_COLUMN_GAP_PX;
138
+ return `column-count:${cols.count};column-gap:${gapPx}px`;
139
+ }
140
+ //#endregion
141
+ //#region src/text-underline-style.ts
142
+ const escapeXml$2 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
143
+ /**
144
+ * DrawingML underline as CSS for an inline decoration box. Pass a resolved
145
+ * CSS color for image-based patterns. For `words`, apply only to non-space
146
+ * text segments; keep strikethrough on a separate outer box.
147
+ */
148
+ function textUnderlineStyle(underline, color, includeDecorationColor = true) {
149
+ if (underline === void 0 || underline === false || underline === "none") return "";
150
+ const heavy = underline === "heavy" || String(underline).endsWith("Heavy");
151
+ const thickness = heavy ? ";text-decoration-thickness:0.1em" : "";
152
+ const decorationColor = includeDecorationColor ? `;text-decoration-color:${escapeXml$2(color)}` : "";
153
+ if (underline === "dbl") return `text-decoration:underline;text-decoration-style:double${decorationColor}`;
154
+ if (underline === "wavy" || underline === "wavyHeavy") return `text-decoration:underline;text-decoration-style:wavy${decorationColor}${thickness}`;
155
+ if (!(typeof underline === "string" && (underline.startsWith("dash") || underline.startsWith("dotted") || underline.startsWith("dotDash") || underline.startsWith("dotDotDash") || underline === "wavyDbl"))) return `text-decoration:underline${decorationColor}${thickness}`;
156
+ const stroke = heavy ? 2 : 1;
157
+ const dash = underline.startsWith("dotted") ? "1 2" : underline === "dash" || underline === "dashHeavy" ? "4 2" : underline.startsWith("dashLong") ? "8 3" : underline.startsWith("dotDotDash") ? "5 2 1 2 1 2" : "5 2 1 2";
158
+ const width = underline === "wavyDbl" ? 8 : underline.startsWith("dotted") ? 3 : underline === "dash" || underline === "dashHeavy" ? 6 : underline.startsWith("dashLong") ? 11 : underline.startsWith("dotDotDash") ? 13 : 10;
159
+ const path = underline === "wavyDbl" ? "<path d=\"M0 1 Q2 -1 4 1 T8 1 M0 4 Q2 2 4 4 T8 4\"/>" : `<path d="M0 3 H${width}" stroke-dasharray="${dash}"/>`;
160
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="6" viewBox="0 0 ${width} 6"><g fill="none" stroke="${escapeXml$2(color)}" stroke-width="${stroke}">${path}</g></svg>`;
161
+ return `text-decoration:none;background-image:url('data:image/svg+xml,${encodeURIComponent(svg).replaceAll("'", "%27")}');background-repeat:repeat-x;background-size:${width / 20}em 0.3em;background-position:0 100%;box-decoration-break:clone;-webkit-box-decoration-break:clone`;
162
+ }
163
+ //#endregion
164
+ //#region src/paragraph-number-labels.ts
165
+ const bulletAutoNumType = (style) => {
166
+ if (style === "number") return "arabicPeriod";
167
+ if (style !== null && typeof style === "object" && "autoNum" in style) return style.autoNum ?? null;
168
+ return null;
169
+ };
170
+ const toRoman = (n) => {
171
+ if (n <= 0) return String(n);
172
+ const map = [
173
+ [1e3, "M"],
174
+ [900, "CM"],
175
+ [500, "D"],
176
+ [400, "CD"],
177
+ [100, "C"],
178
+ [90, "XC"],
179
+ [50, "L"],
180
+ [40, "XL"],
181
+ [10, "X"],
182
+ [9, "IX"],
183
+ [5, "V"],
184
+ [4, "IV"],
185
+ [1, "I"]
186
+ ];
187
+ let out = "";
188
+ let r = n;
189
+ for (const [v, s] of map) while (r >= v) {
190
+ out += s;
191
+ r -= v;
192
+ }
193
+ return out;
194
+ };
195
+ const toAlpha = (n) => {
196
+ if (n <= 0) return String(n);
197
+ let r = n;
198
+ let out = "";
199
+ while (r > 0) {
200
+ r -= 1;
201
+ out = String.fromCharCode(65 + r % 26) + out;
202
+ r = Math.floor(r / 26);
203
+ }
204
+ return out;
205
+ };
206
+ const formatAutoNum = (token, n) => {
207
+ const arabic = String(n);
208
+ switch (token) {
209
+ case "arabicPlain": return arabic;
210
+ case "arabicPeriod": return `${arabic}.`;
211
+ case "arabicParenR": return `${arabic})`;
212
+ case "arabicParenBoth": return `(${arabic})`;
213
+ case "romanUcPeriod": return `${toRoman(n)}.`;
214
+ case "romanLcPeriod": return `${toRoman(n).toLowerCase()}.`;
215
+ case "romanUcParenR": return `${toRoman(n)})`;
216
+ case "romanLcParenR": return `${toRoman(n).toLowerCase()})`;
217
+ case "romanUcParenBoth": return `(${toRoman(n)})`;
218
+ case "romanLcParenBoth": return `(${toRoman(n).toLowerCase()})`;
219
+ case "alphaUcPeriod": return `${toAlpha(n)}.`;
220
+ case "alphaLcPeriod": return `${toAlpha(n).toLowerCase()}.`;
221
+ case "alphaUcParenR": return `${toAlpha(n)})`;
222
+ case "alphaLcParenR": return `${toAlpha(n).toLowerCase()})`;
223
+ case "alphaUcParenBoth": return `(${toAlpha(n)})`;
224
+ case "alphaLcParenBoth": return `(${toAlpha(n).toLowerCase()})`;
225
+ default: return `${arabic}.`;
226
+ }
227
+ };
228
+ /** Resolve list numbering consistently for rendered slides and editing surfaces. */
229
+ const paragraphNumberLabels = (paraData) => {
230
+ const numberLabels = Array.from({ length: paraData.length }, () => null);
231
+ {
232
+ const counters = [];
233
+ const types = [];
234
+ for (let i = 0; i < paraData.length; i++) {
235
+ const para = paraData[i];
236
+ const num = bulletAutoNumType(para.bulletStyle);
237
+ const level = Math.max(0, para.level);
238
+ for (let l = num === null ? level : level + 1; l < counters.length; l++) {
239
+ counters[l] = 0;
240
+ types[l] = null;
241
+ }
242
+ if (num === null) continue;
243
+ if (types[level] !== num) {
244
+ counters[level] = 1;
245
+ types[level] = num;
246
+ } else counters[level] = (counters[level] ?? 0) + 1;
247
+ numberLabels[i] = formatAutoNum(num, counters[level]);
248
+ }
249
+ }
250
+ return numberLabels;
251
+ };
252
+ //#endregion
2
253
  //#region src/emf.ts
3
254
  const WORD_BYTES = 4;
4
255
  const SHORT_BYTES = 2;
@@ -275,6 +526,8 @@ const FALLBACK_DESCENT = .22;
275
526
  const FALLBACK_LINEGAP = .08;
276
527
  const BASELINE_LEADING_DROP = .036;
277
528
  const GRID_NUDGE_X = -.75;
529
+ const EMU_PER_PX$1 = 9525;
530
+ let reflectionNamespace = 0;
278
531
  const EAST_ASIAN_CHAR = /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩]/;
279
532
  const CLOSING_PUNCT = /^[、。,.・)」』】〉》”’!?:;ー〜…,.!?:;)\]}]+$/;
280
533
  const OPENING_PUNCT = /^[(「『【〈《“‘([{]+$/;
@@ -303,14 +556,16 @@ const specOf = (piece) => ({
303
556
  sizePx: piece.sizePx,
304
557
  bold: piece.bold,
305
558
  italic: piece.italic,
306
- letterSpacingPx: piece.letterSpacingPx
559
+ letterSpacingPx: piece.letterSpacingPx,
560
+ kerning: piece.kerning ?? true
307
561
  });
308
562
  const bulletSpec = (b) => ({
309
563
  family: b.family,
310
564
  sizePx: b.sizePx,
311
565
  bold: false,
312
566
  italic: false,
313
- letterSpacingPx: 0
567
+ letterSpacingPx: 0,
568
+ kerning: true
314
569
  });
315
570
  const escapeXml$1 = (s) => s.replace(/[&<>"]/g, (c) => ({
316
571
  "&": "&amp;",
@@ -325,7 +580,7 @@ const fmt = (n) => {
325
580
  const layoutCore = (input, measure) => {
326
581
  const widthCache = /* @__PURE__ */ new Map();
327
582
  const metricCache = /* @__PURE__ */ new Map();
328
- const key = (text, s) => `${s.family}|${s.sizePx}|${s.bold}|${s.italic}|${s.letterSpacingPx}|${text}`;
583
+ const key = (text, s) => `${s.family}|${s.sizePx}|${s.bold}|${s.italic}|${s.letterSpacingPx}|${s.kerning ?? true}|${text}`;
329
584
  const mWidth = (text, s) => {
330
585
  const k = key(text, s);
331
586
  let w = widthCache.get(k);
@@ -368,8 +623,49 @@ const layoutCore = (input, measure) => {
368
623
  const bulletLead = bullet ? bullet.imageHref ? bullet.sizePx + mWidth(" ", bulletSpec(bullet)) : mWidth(`${bullet.text} `, bulletSpec(bullet)) : 0;
369
624
  const avail = Math.max(1, wrapRight - wrapLeft);
370
625
  const tokens = [];
626
+ let wordParts = [];
627
+ const makeToken = (text, piece, continuesSegment = false) => ({
628
+ text,
629
+ piece,
630
+ isSpace: false,
631
+ isBreak: false,
632
+ continuesSegment,
633
+ width: mWidth(text, {
634
+ ...specOf(piece),
635
+ sizePx: renderedSizePxOf(piece)
636
+ })
637
+ });
638
+ const flushWord = () => {
639
+ if (wordParts.length === 0) return;
640
+ const word = wordParts.map((part) => part.text).join("");
641
+ let partIndex = 0;
642
+ let partOffset = 0;
643
+ for (const segment of splitEastAsianBreakables(word)) {
644
+ const fragments = [];
645
+ let remaining = segment.length;
646
+ let width = 0;
647
+ while (remaining > 0) {
648
+ const part = wordParts[partIndex];
649
+ const length = Math.min(remaining, part.text.length - partOffset);
650
+ const token = makeToken(part.text.slice(partOffset, partOffset + length), part.piece, fragments.length > 0);
651
+ fragments.push(token);
652
+ width += token.width;
653
+ remaining -= length;
654
+ partOffset += length;
655
+ if (partOffset === part.text.length) {
656
+ partIndex++;
657
+ partOffset = 0;
658
+ }
659
+ }
660
+ const canBreak = EAST_ASIAN_CHAR.test(segment) ? width > avail - bulletLead : para.latinLineBreak === true || width > avail - bulletLead;
661
+ if (input.wrap && canBreak && [...segment].length > 1) for (const fragment of fragments) for (const ch of fragment.text) tokens.push(makeToken(ch, fragment.piece));
662
+ else for (const fragment of fragments) tokens.push(fragment);
663
+ }
664
+ wordParts = [];
665
+ };
371
666
  for (const piece of para.pieces) {
372
667
  if (piece.isBreak) {
668
+ flushWord();
373
669
  tokens.push({
374
670
  text: "",
375
671
  piece,
@@ -379,28 +675,51 @@ const layoutCore = (input, measure) => {
379
675
  });
380
676
  continue;
381
677
  }
382
- for (const word of piece.text.match(/\s+|\S+/g) ?? []) {
383
- const isSpace = /^\s+$/.test(word);
384
- for (const seg of isSpace ? [word] : splitEastAsianBreakables(word)) {
385
- const w = mWidth(seg, specOf(piece));
386
- if (input.wrap && !isSpace && w > avail - bulletLead && [...seg].length > 1) for (const ch of seg) tokens.push({
387
- text: ch,
388
- piece,
389
- isSpace: false,
390
- isBreak: false,
391
- width: mWidth(ch, specOf(piece))
392
- });
393
- else tokens.push({
394
- text: seg,
395
- piece,
396
- isSpace,
397
- isBreak: false,
398
- width: w
678
+ for (const word of piece.text.match(/\t|[^\S\t]+|\S+/g) ?? []) {
679
+ if (!/^\s+$/.test(word)) {
680
+ wordParts.push({
681
+ text: word,
682
+ piece
399
683
  });
684
+ continue;
685
+ }
686
+ flushWord();
687
+ if (word === " ") tokens.push({
688
+ text: "",
689
+ piece,
690
+ isSpace: true,
691
+ isBreak: false,
692
+ isTab: true,
693
+ width: 0
694
+ });
695
+ else tokens.push({
696
+ ...makeToken(word, piece),
697
+ isSpace: true
698
+ });
699
+ }
700
+ }
701
+ flushWord();
702
+ let fieldWidth = 0;
703
+ let decimalWidth = 0;
704
+ for (let ti = tokens.length - 1; ti >= 0; ti--) {
705
+ const token = tokens[ti];
706
+ if (token.isTab || token.isBreak) {
707
+ if (token.isTab) {
708
+ token.tabFieldWidth = fieldWidth;
709
+ token.tabDecimalWidth = decimalWidth;
400
710
  }
711
+ fieldWidth = decimalWidth = 0;
712
+ } else {
713
+ fieldWidth += token.width;
714
+ const decimal = token.text.indexOf(".");
715
+ decimalWidth = decimal < 0 ? decimalWidth + token.width : mWidth(token.text.slice(0, decimal), {
716
+ ...specOf(token.piece),
717
+ sizePx: renderedSizePxOf(token.piece)
718
+ });
401
719
  }
402
720
  }
403
- const wrapped = wrapTokens(tokens, input.wrap, wrapRight - firstLeft - bulletLead, avail);
721
+ for (const token of tokens) if (token.piece.highlightHex) token.highlightMetrics = mMetrics(token.piece);
722
+ const wrapped = wrapTokens(tokens, input.wrap, wrapRight - firstLeft - bulletLead, avail, para, firstLeft + bulletLead - wrapLeft);
404
723
  const paraLines = wrapped.length > 0 ? wrapped : [[]];
405
724
  for (let li = 0; li < paraLines.length; li++) {
406
725
  const toks = paraLines[li];
@@ -414,6 +733,13 @@ const layoutCore = (input, measure) => {
414
733
  if (m.d > descent) descent = m.d;
415
734
  if (m.g > lineGap) lineGap = m.g;
416
735
  }
736
+ const emptyLineBreak = toks.find((token) => token.isBreak && token.piece.sizePx > 0);
737
+ if (ascent === 0 && emptyLineBreak) {
738
+ const metrics = mMetrics(emptyLineBreak.piece);
739
+ ascent = metrics.a;
740
+ descent = metrics.d;
741
+ lineGap = metrics.g;
742
+ }
417
743
  if (ascent === 0) {
418
744
  ascent = para.fallbackSizePx * FALLBACK_ASCENT;
419
745
  descent = para.fallbackSizePx * FALLBACK_DESCENT;
@@ -482,20 +808,130 @@ const layoutCore = (input, measure) => {
482
808
  };
483
809
  const columns = vert === "none" ? input.columns ?? null : null;
484
810
  const { placements, requiredH } = columns && columns.count >= 2 ? placeColumns(frame, columns, input.anchor, buildLines) : placeSingle(frame, input.anchor, buildLines);
811
+ let anchorShift = 0;
812
+ if (input.anchorCentered && placements.length) {
813
+ let left = Infinity;
814
+ let right = -Infinity;
815
+ for (const { line, dx } of placements) {
816
+ let end = line.tokens.length;
817
+ while (end > 0 && (line.tokens[end - 1].isSpace || line.tokens[end - 1].isBreak)) end--;
818
+ let width = 0;
819
+ for (let i = 0; i < end; i++) if (!line.tokens[i].isBreak) width += line.tokens[i].width;
820
+ if (end) {
821
+ const x = line.anchorX + dx - (line.textAnchor === "end" ? width : line.textAnchor === "middle" ? width / 2 : 0);
822
+ left = Math.min(left, x);
823
+ right = Math.max(right, x + width);
824
+ }
825
+ if (line.bullet) {
826
+ const { x, b } = line.bullet;
827
+ left = Math.min(left, x + dx);
828
+ right = Math.max(right, x + dx + (b.imageHref ? b.sizePx : mWidth(b.text, bulletSpec(b))));
829
+ }
830
+ }
831
+ if (left !== Infinity) {
832
+ const shift = frame.x + frame.w / 2 - (left + right) / 2;
833
+ anchorShift = shift;
834
+ for (let i = 0; i < placements.length; i++) {
835
+ const placement = placements[i];
836
+ placements[i] = {
837
+ ...placement,
838
+ dx: placement.dx + shift
839
+ };
840
+ }
841
+ }
842
+ }
485
843
  return {
486
844
  placements,
487
845
  requiredH,
488
846
  vert,
489
847
  cx,
490
- cy
848
+ cy,
849
+ anchorShift
491
850
  };
492
851
  };
493
852
  const layoutTextSvg = (input, measure) => {
494
- const { placements, vert, cx, cy } = layoutCore(input, measure);
495
- const body = emitPlacements(placements);
853
+ const core = layoutCore(input, measure);
854
+ const { vert, cx, cy } = core;
855
+ const { defs, placements } = resolveFillPaints(core.placements);
856
+ const body = defs + emitPlacements(placements, input.effectsOnly === true, input.effectKind ?? "all");
496
857
  if (vert === "none" || vert === "upright") return body;
497
858
  return `<g transform="rotate(${vert === "cw90" ? 90 : 270} ${fmt(cx)} ${fmt(cy)})">${body}</g>`;
498
859
  };
860
+ const lineContentExtent = (line, dx) => {
861
+ const toks = [...line.tokens];
862
+ while (toks.length > 0 && (toks[toks.length - 1].isSpace || toks[toks.length - 1].isBreak)) toks.pop();
863
+ const content = toks.filter((t) => !t.isBreak);
864
+ if (content.length === 0) return null;
865
+ const width = content.reduce((sum, t) => sum + t.width, 0);
866
+ return {
867
+ left: line.anchorX + dx + GRID_NUDGE_X - (line.textAnchor === "middle" ? width / 2 : line.textAnchor === "end" ? width : 0),
868
+ width
869
+ };
870
+ };
871
+ const resolveFillPaints = (placements) => {
872
+ if (!placements.some(({ line }) => line.tokens.some((t) => t.piece.fillPaint))) return {
873
+ defs: "",
874
+ placements
875
+ };
876
+ let left = Number.POSITIVE_INFINITY;
877
+ let top = Number.POSITIVE_INFINITY;
878
+ let right = Number.NEGATIVE_INFINITY;
879
+ let bottom = Number.NEGATIVE_INFINITY;
880
+ for (const { line, baselineY, dx } of placements) {
881
+ const extent = lineContentExtent(line, dx);
882
+ if (extent === null) continue;
883
+ left = Math.min(left, extent.left);
884
+ right = Math.max(right, extent.left + extent.width);
885
+ top = Math.min(top, baselineY - line.ascent);
886
+ bottom = Math.max(bottom, baselineY + line.descent);
887
+ }
888
+ if (!(right > left && bottom > top)) return {
889
+ defs: "",
890
+ placements
891
+ };
892
+ const bounds = {
893
+ x: left,
894
+ y: top,
895
+ w: right - left,
896
+ h: bottom - top
897
+ };
898
+ const painted = /* @__PURE__ */ new Map();
899
+ let defs = "";
900
+ const pieces = /* @__PURE__ */ new Map();
901
+ const repaint = (piece) => {
902
+ const paint = piece.fillPaint;
903
+ if (paint === void 0) return piece;
904
+ let next = pieces.get(piece);
905
+ if (next) return next;
906
+ let fill = painted.get(paint);
907
+ if (fill === void 0) {
908
+ const built = paint(bounds);
909
+ defs += built.defs;
910
+ fill = built.fill;
911
+ painted.set(paint, fill);
912
+ }
913
+ next = {
914
+ ...piece,
915
+ fillHex: fill
916
+ };
917
+ pieces.set(piece, next);
918
+ return next;
919
+ };
920
+ const repainted = placements.map((placement) => ({
921
+ ...placement,
922
+ line: {
923
+ ...placement.line,
924
+ tokens: placement.line.tokens.map((token) => token.piece.fillPaint ? {
925
+ ...token,
926
+ piece: repaint(token.piece)
927
+ } : token)
928
+ }
929
+ }));
930
+ return {
931
+ defs,
932
+ placements: repainted
933
+ };
934
+ };
499
935
  /** Content height (px) the body would occupy at the given input's font sizes —
500
936
  * for a single column the block height, for multi-column the tallest filled
501
937
  * column. The SVG normAutofit path uses this to pick a shrink scale. */
@@ -558,17 +994,40 @@ const placeColumns = (frame, columns, anchor, buildLines) => {
558
994
  requiredH: tallest
559
995
  };
560
996
  };
561
- const emitPlacements = (placements) => {
562
- const parts = [];
997
+ /**
998
+ * Draws the placed lines, with each paragraph's own lines — its bullet
999
+ * included — inside a `<g data-pptx-paragraph>`.
1000
+ *
1001
+ * The index is the one `<p:bldP build="p">` counts in, so a player revealing a
1002
+ * text body one paragraph at a time has a single element to show or hide. A
1003
+ * paragraph that wraps into the next column appears as a second group under
1004
+ * the same index, because its lines are drawn where that column is.
1005
+ */
1006
+ const emitPlacements = (placements, effectsOnly = false, effectKind = "all") => {
1007
+ const paragraphs = [];
1008
+ let parts = [];
1009
+ let paraIndex = null;
1010
+ const close = () => {
1011
+ if (paraIndex !== null && parts.length > 0) paragraphs.push(`<g data-pptx-paragraph="${paraIndex}">${parts.join("")}</g>`);
1012
+ parts = [];
1013
+ };
1014
+ let effectIndex = 0;
1015
+ const namespace = reflectionNamespace++;
1016
+ const nextEffectId = (kind) => `text-${kind}-${namespace}-${effectIndex++}`;
563
1017
  for (const { line, baselineY, dx } of placements) {
564
- if (line.bullet) {
1018
+ if (line.paraIndex !== paraIndex) {
1019
+ close();
1020
+ paraIndex = line.paraIndex;
1021
+ }
1022
+ if (line.bullet && !effectsOnly) {
565
1023
  const b = line.bullet.b;
566
1024
  if (b.imageHref) parts.push(`<image x="${fmt(line.bullet.x + dx + GRID_NUDGE_X)}" y="${fmt(baselineY - b.sizePx)}" width="${fmt(b.sizePx)}" height="${fmt(b.sizePx)}" href="${b.imageHref}" xlink:href="${b.imageHref}" preserveAspectRatio="xMidYMid meet"/>`);
567
1025
  else parts.push(`<text x="${fmt(line.bullet.x + dx + GRID_NUDGE_X)}" y="${fmt(baselineY)}" font-family="${escapeXml$1(b.family)}" font-size="${fmt(b.sizePx)}" fill="${b.fillHex}" xml:space="preserve">${escapeXml$1(b.text)}</text>`);
568
1026
  }
569
- parts.push(emitLine(line, baselineY, dx));
1027
+ parts.push(emitLine(line, baselineY, dx, line.descent, (kind) => nextEffectId(kind), effectsOnly, effectKind));
570
1028
  }
571
- return parts.join("");
1029
+ close();
1030
+ return paragraphs.join("");
572
1031
  };
573
1032
  const topPad = (line) => {
574
1033
  const lead = line.advance - (line.ascent + line.descent);
@@ -582,64 +1041,296 @@ const lineAdvance = (line, para) => {
582
1041
  else adv = natural;
583
1042
  return adv * para.lineAdvanceScale;
584
1043
  };
585
- const emitLine = (line, baselineY, dx) => {
1044
+ const emitLine = (line, baselineY, dx, descent, nextEffectId, effectsOnly, effectKind) => {
586
1045
  const toks = [...line.tokens];
587
1046
  while (toks.length > 0 && (toks[toks.length - 1].isSpace || toks[toks.length - 1].isBreak)) toks.pop();
588
1047
  const content = toks.filter((t) => !t.isBreak);
589
1048
  if (content.length === 0) return "";
590
1049
  const groups = groupTokens(content);
591
- const tspans = groups.map((g) => tspan(g)).join("");
1050
+ const tspans = groups.map((g) => {
1051
+ const span = tspan(g);
1052
+ if (!g.piece.href) return span;
1053
+ const target = g.piece.href.startsWith("#") ? "" : " target=\"_blank\" rel=\"noopener noreferrer\"";
1054
+ const title = g.piece.hrefTip ? `<title>${escapeXml$1(g.piece.hrefTip)}</title>` : "";
1055
+ return `<a href="${escapeXml$1(g.piece.href)}"${target}>${title}${span}</a>`;
1056
+ }).join("");
592
1057
  if (tspans === "") return "";
593
1058
  const x0 = line.anchorX + dx + GRID_NUDGE_X;
594
- return `<text x="${fmt(x0)}" y="${fmt(baselineY)}" text-anchor="${line.textAnchor}" xml:space="preserve">${tspans}</text>` + emitWavyUnderlines(groups, line.textAnchor, x0, baselineY);
1059
+ const text = `<text x="${fmt(x0)}" y="${fmt(baselineY)}" text-anchor="${line.textAnchor}" xml:space="preserve">${tspans}</text>`;
1060
+ if (effectsOnly && effectKind === "fill") {
1061
+ const painted = groups.map((g) => g.piece.fillPaint ? g : {
1062
+ ...g,
1063
+ piece: unpaintedPiece(g.piece)
1064
+ });
1065
+ if (!painted.some((g) => g.piece.fillPaint)) return "";
1066
+ const glyphs = `<text x="${fmt(x0)}" y="${fmt(baselineY)}" text-anchor="${line.textAnchor}" xml:space="preserve" aria-hidden="true" pointer-events="none">${painted.map(tspan).join("")}</text>`;
1067
+ return emitTextOuterEffects(painted, line.textAnchor, x0, baselineY, nextEffectId) + glyphs + emitTextDecorations(painted, line.textAnchor, x0, baselineY);
1068
+ }
1069
+ const reflections = effectKind === "reflection" || effectKind === "all" ? emitTextReflections(groups, line.textAnchor, x0, baselineY, descent, nextEffectId) : "";
1070
+ const outerEffects = effectKind === "outer" || effectKind === "all" ? emitTextOuterEffects(groups, line.textAnchor, x0, baselineY, nextEffectId) : "";
1071
+ const innerShadows = effectKind === "innerShadow" || effectKind === "all" ? emitTextInnerShadows(groups, line.textAnchor, x0, baselineY, nextEffectId) : "";
1072
+ if (effectsOnly) return outerEffects + reflections + innerShadows;
1073
+ return emitHighlights(groups, line.textAnchor, x0, baselineY) + outerEffects + reflections + text + innerShadows + emitTextDecorations(groups, line.textAnchor, x0, baselineY);
1074
+ };
1075
+ const unpaintedPiece = (piece) => {
1076
+ const { outlineHex: _outline, shadow: _shadow, glow: _glow, ...rest } = piece;
1077
+ return {
1078
+ ...rest,
1079
+ fillHex: "none",
1080
+ underline: "none",
1081
+ strike: false
1082
+ };
595
1083
  };
596
1084
  const groupTokens = (toks) => {
597
1085
  const groups = [];
598
1086
  for (const t of toks) {
599
1087
  if (t.isBreak) continue;
600
1088
  const last = groups[groups.length - 1];
601
- if (last && samePiece(last.piece, t.piece)) {
1089
+ if (last && !last.isTab && !t.isTab && samePiece(last.piece, t.piece)) {
602
1090
  last.text += t.text;
603
1091
  last.width += t.width;
1092
+ last.parts.push({
1093
+ text: t.text,
1094
+ width: t.width
1095
+ });
604
1096
  } else groups.push({
605
1097
  text: t.text,
1098
+ parts: [{
1099
+ text: t.text,
1100
+ width: t.width
1101
+ }],
1102
+ isTab: t.isTab === true,
606
1103
  piece: t.piece,
607
- width: t.width
1104
+ width: t.width,
1105
+ ...t.highlightMetrics ? { highlightMetrics: t.highlightMetrics } : {}
608
1106
  });
609
1107
  }
610
1108
  return groups;
611
1109
  };
612
- const SUPERSCRIPT_SHIFT_RATIO = .33;
613
- const SUBSCRIPT_SHIFT_RATIO = .16;
614
- const baselineShiftPxOf = (p) => p.superSub === 1 ? p.sizePx * SUPERSCRIPT_SHIFT_RATIO : p.superSub === -1 ? -p.sizePx * SUBSCRIPT_SHIFT_RATIO : 0;
1110
+ const emitTextReflections = (groups, textAnchor, x0, baselineY, descent, nextEffectId) => {
1111
+ const totalWidth = groups.reduce((sum, group) => sum + group.width, 0);
1112
+ let cursor = x0 - (textAnchor === "middle" ? totalWidth / 2 : textAnchor === "end" ? totalWidth : 0);
1113
+ const parts = [];
1114
+ for (const group of groups) {
1115
+ const reflection = group.piece.reflection;
1116
+ if (!reflection || group.width <= 0 || group.isTab) {
1117
+ cursor += group.width;
1118
+ continue;
1119
+ }
1120
+ const scaleY = reflection.scaleY ?? 1;
1121
+ const distPx = (reflection.offsetEmu ?? 0) / EMU_PER_PX$1;
1122
+ const contactY = baselineY + descent;
1123
+ const angleRad = (reflection.angleDeg ?? 0) * Math.PI / 180;
1124
+ const offsetX = distPx * Math.cos(angleRad);
1125
+ const offsetY = distPx * Math.sin(angleRad);
1126
+ const startA = Math.max(0, Math.min(1, reflection.startOpacity ?? 1));
1127
+ const endA = Math.max(0, Math.min(1, reflection.opacity ?? 0));
1128
+ const endPosition = Math.max(0, Math.min(1, reflection.endPosition ?? 1));
1129
+ const startPosition = Math.max(0, Math.min(endPosition, reflection.startPosition ?? 0));
1130
+ const id = nextEffectId("reflection");
1131
+ const gradientId = `${id}-gradient`;
1132
+ const maskId = `${id}-mask`;
1133
+ const filterId = `${id}-blur`;
1134
+ const blurPx = Math.max(0, (reflection.blurEmu ?? 0) / EMU_PER_PX$1 / 2);
1135
+ const fadeDirection = reflection.fadeDirection;
1136
+ const defs = `<defs><linearGradient id="${gradientId}" ${fadeDirection === void 0 || fadeDirection === 90 ? "x1=\"0\" y1=\"1\" x2=\"0\" y2=\"0\"" : (() => {
1137
+ const radians = fadeDirection * Math.PI / 180;
1138
+ const dx = Math.cos(radians) / 2;
1139
+ const dy = Math.sin(radians) / 2;
1140
+ return `x1="${(.5 + dx).toFixed(3)}" y1="${(.5 + dy).toFixed(3)}" x2="${(.5 - dx).toFixed(3)}" y2="${(.5 - dy).toFixed(3)}"`;
1141
+ })()}><stop offset="0" stop-color="#fff" stop-opacity="${startA.toFixed(3)}"/><stop offset="${startPosition.toFixed(3)}" stop-color="#fff" stop-opacity="${startA.toFixed(3)}"/><stop offset="${endPosition.toFixed(3)}" stop-color="#fff" stop-opacity="${endA.toFixed(3)}"/><stop offset="1" stop-color="#fff" stop-opacity="${endA.toFixed(3)}"/></linearGradient><mask id="${maskId}" maskContentUnits="objectBoundingBox"><rect width="1" height="1" fill="url(#${gradientId})"/></mask>` + (blurPx > 0 ? `<filter id="${filterId}" x="-25%" y="-25%" width="150%" height="150%"><feGaussianBlur stdDeviation="${fmt(blurPx)}"/></filter>` : "") + `</defs>`;
1142
+ const filter = blurPx > 0 ? ` filter="url(#${filterId})"` : "";
1143
+ const scaleX = reflection.scaleX ?? 1;
1144
+ const alignment = reflection.alignment ?? "b";
1145
+ const anchorX = alignment === "l" || alignment === "tl" || alignment === "bl" ? cursor : alignment === "r" || alignment === "tr" || alignment === "br" ? cursor + group.width : cursor + group.width / 2;
1146
+ const anchorY = alignment === "tl" || alignment === "t" || alignment === "tr" ? baselineY - group.piece.sizePx : alignment === "l" || alignment === "ctr" || alignment === "r" ? baselineY - group.piece.sizePx / 2 : contactY;
1147
+ const skew = (reflection.skewX ?? 0) !== 0 || (reflection.skewY ?? 0) !== 0 ? ` skewX(${fmt(reflection.skewX ?? 0)}) skewY(${fmt(reflection.skewY ?? 0)})` : "";
1148
+ const transform = `translate(${fmt(anchorX + offsetX)} ${fmt(anchorY + offsetY)})${skew} scale(${fmt(scaleX)} ${fmt(scaleY)}) translate(${fmt(-anchorX)} ${fmt(-anchorY)})`;
1149
+ const glyph = `<text x="${fmt(cursor)}" y="${fmt(baselineY)}" text-anchor="start" xml:space="preserve">${tspan(group)}</text>`;
1150
+ parts.push(`${defs}<g transform="${transform}" mask="url(#${maskId})"${filter} data-pptx-reflection="text" aria-hidden="true" pointer-events="none">${glyph}</g>`);
1151
+ cursor += group.width;
1152
+ }
1153
+ return parts.join("");
1154
+ };
1155
+ const emitTextOuterEffects = (groups, textAnchor, x0, baselineY, nextId) => {
1156
+ const totalWidth = groups.reduce((sum, group) => sum + group.width, 0);
1157
+ let cursor = x0 - (textAnchor === "middle" ? totalWidth / 2 : textAnchor === "end" ? totalWidth : 0);
1158
+ const parts = [];
1159
+ for (const group of groups) {
1160
+ const shadow = group.piece.shadow;
1161
+ const glow = group.piece.glow;
1162
+ if (group.width <= 0 || group.isTab || !shadow && !glow) {
1163
+ cursor += group.width;
1164
+ continue;
1165
+ }
1166
+ const glyph = `<text x="${fmt(cursor)}" y="${fmt(baselineY)}" text-anchor="start" xml:space="preserve">${tspan(group)}</text>`;
1167
+ if (shadow) {
1168
+ const id = nextId("outer-shadow");
1169
+ const angle = (shadow.angleDeg ?? 45) * Math.PI / 180;
1170
+ const distance = (shadow.offsetEmu ?? 38100) / EMU_PER_PX$1;
1171
+ const blur = Math.max(0, (shadow.blurEmu ?? 50800) / EMU_PER_PX$1 / 2);
1172
+ const glyphSize = renderedSizePxOf(group.piece);
1173
+ const pad = Math.max(1, distance + blur * 3);
1174
+ const color = shadow.color ?? "#000000";
1175
+ const opacity = Math.max(0, Math.min(1, shadow.opacity ?? 1));
1176
+ const defs = `<defs><filter id="${id}" filterUnits="userSpaceOnUse" x="${fmt(cursor - pad)}" y="${fmt(baselineY - glyphSize * 1.5 - pad)}" width="${fmt(group.width + pad * 2)}" height="${fmt(glyphSize * 2 + pad * 2)}"><feGaussianBlur in="SourceAlpha" stdDeviation="${fmt(blur)}" result="shadowBlur"/><feOffset in="shadowBlur" dx="${fmt(Math.cos(angle) * distance)}" dy="${fmt(Math.sin(angle) * distance)}" result="shadowOffset"/><feFlood flood-color="${escapeXml$1(color)}" flood-opacity="${opacity.toFixed(3)}" result="shadowColor"/><feComposite in="shadowColor" in2="shadowOffset" operator="in"/></filter></defs>`;
1177
+ parts.push(`${defs}${glyph.replace("<text ", `<text filter="url(#${id})" aria-hidden="true" pointer-events="none" `)}`);
1178
+ }
1179
+ if (glow) {
1180
+ const id = nextId("glow");
1181
+ const radius = Math.max(0, (glow.radiusEmu ?? 63500) / EMU_PER_PX$1);
1182
+ const glyphSize = renderedSizePxOf(group.piece);
1183
+ const pad = Math.max(1, radius * 2);
1184
+ const color = glow.color ?? "#FFFF00";
1185
+ const opacity = Math.max(0, Math.min(1, glow.opacity ?? 1));
1186
+ const defs = `<defs><filter id="${id}" filterUnits="userSpaceOnUse" x="${fmt(cursor - pad)}" y="${fmt(baselineY - glyphSize * 1.5 - pad)}" width="${fmt(group.width + pad * 2)}" height="${fmt(glyphSize * 2 + pad * 2)}"><feMorphology in="SourceAlpha" operator="dilate" radius="${fmt(radius / 2)}" result="glowExpanded"/><feGaussianBlur in="glowExpanded" stdDeviation="${fmt(radius / 2)}" result="glowBlur"/><feFlood flood-color="${escapeXml$1(color)}" flood-opacity="${opacity.toFixed(3)}" result="glowColor"/><feComposite in="glowColor" in2="glowBlur" operator="in"/></filter></defs>`;
1187
+ parts.push(`${defs}${glyph.replace("<text ", `<text filter="url(#${id})" aria-hidden="true" pointer-events="none" `)}`);
1188
+ }
1189
+ cursor += group.width;
1190
+ }
1191
+ return parts.join("");
1192
+ };
1193
+ const emitTextInnerShadows = (groups, textAnchor, x0, baselineY, nextId) => {
1194
+ const totalWidth = groups.reduce((sum, group) => sum + group.width, 0);
1195
+ let cursor = x0 - (textAnchor === "middle" ? totalWidth / 2 : textAnchor === "end" ? totalWidth : 0);
1196
+ const parts = [];
1197
+ for (const group of groups) {
1198
+ const shadow = group.piece.innerShadow;
1199
+ if (!shadow || group.width <= 0 || group.isTab) {
1200
+ cursor += group.width;
1201
+ continue;
1202
+ }
1203
+ const id = nextId("inner-shadow");
1204
+ const angle = (shadow.angleDeg ?? 45) * Math.PI / 180;
1205
+ const dx = (shadow.offsetEmu ?? 38100) / EMU_PER_PX$1 * Math.cos(angle);
1206
+ const dy = (shadow.offsetEmu ?? 38100) / EMU_PER_PX$1 * Math.sin(angle);
1207
+ const blurPx = Math.max(0, (shadow.blurEmu ?? 50800) / EMU_PER_PX$1 / 2);
1208
+ const color = shadow.color ?? "#000000";
1209
+ const opacity = Math.max(0, Math.min(1, shadow.opacity ?? 1));
1210
+ const defs = `<defs><filter id="${id}" x="-25%" y="-25%" width="150%" height="150%"><feGaussianBlur in="SourceAlpha" stdDeviation="${fmt(blurPx)}" result="innerBlur"/><feOffset in="innerBlur" dx="${fmt(dx)}" dy="${fmt(dy)}" result="innerOff"/><feComposite in="innerOff" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="innerMask"/><feFlood flood-color="${escapeXml$1(color)}" flood-opacity="${opacity.toFixed(3)}" result="innerColor"/><feComposite in="innerColor" in2="innerMask" operator="in" result="innerOut"/><feComposite in="innerOut" in2="SourceAlpha" operator="in"/></filter></defs>`;
1211
+ const glyph = `<text x="${fmt(cursor)}" y="${fmt(baselineY)}" text-anchor="start" xml:space="preserve" filter="url(#${id})" aria-hidden="true" pointer-events="none">${tspan(group)}</text>`;
1212
+ parts.push(defs + glyph);
1213
+ cursor += group.width;
1214
+ }
1215
+ return parts.join("");
1216
+ };
1217
+ const baselineShiftPxOf = (p) => p.sizePx * p.baseline;
615
1218
  const SUPER_SUB_SIZE_RATIO = .65;
616
- const renderedSizePxOf = (p) => p.superSub !== 0 ? p.sizePx * SUPER_SUB_SIZE_RATIO : p.sizePx;
617
- const emitWavyUnderlines = (groups, textAnchor, x0, baselineY) => {
618
- if (!groups.some((g) => g.piece.underline === "wavy")) return "";
1219
+ const SMALL_CAPS_LOWERCASE_RATIO = .8;
1220
+ const renderedSizePxOf = (p) => p.sizePx * (p.baseline !== 0 ? SUPER_SUB_SIZE_RATIO : 1) * (p.smallCaps === true ? SMALL_CAPS_LOWERCASE_RATIO : 1);
1221
+ const underlineStrokeWidth = (piece) => {
1222
+ const size = renderedSizePxOf(piece);
1223
+ const heavy = piece.underline === "heavy" || piece.underline.endsWith("Heavy");
1224
+ return Math.max(heavy ? 1 : .6, size * (heavy ? .09 : .06));
1225
+ };
1226
+ const underlineDashArray = (piece) => {
1227
+ const underline = piece.underline;
1228
+ const scale = renderedSizePxOf(piece) / 10;
1229
+ const pattern = (values) => values.map((value) => fmt(value * scale)).join(" ");
1230
+ switch (underline) {
1231
+ case "dotted":
1232
+ case "dottedHeavy": return pattern([1.5, 3]);
1233
+ case "dash":
1234
+ case "dashHeavy": return pattern([5, 3]);
1235
+ case "dashLong":
1236
+ case "dashLongHeavy": return pattern([9, 3]);
1237
+ case "dotDash":
1238
+ case "dotDashHeavy": return pattern([
1239
+ 1.5,
1240
+ 3,
1241
+ 6,
1242
+ 3
1243
+ ]);
1244
+ case "dotDotDash":
1245
+ case "dotDotDashHeavy": return pattern([
1246
+ 1.5,
1247
+ 3,
1248
+ 1.5,
1249
+ 3,
1250
+ 6,
1251
+ 3
1252
+ ]);
1253
+ default: return null;
1254
+ }
1255
+ };
1256
+ const underlineSegments = (group, x) => {
1257
+ if (group.piece.underline !== "words" || !/\s/.test(group.text)) return [{
1258
+ x,
1259
+ width: group.width
1260
+ }];
1261
+ const segments = [];
1262
+ let offset = 0;
1263
+ for (const part of group.parts) {
1264
+ if (!/^\s+$/u.test(part.text) && part.text.length > 0) segments.push({
1265
+ x: x + offset,
1266
+ width: part.width
1267
+ });
1268
+ offset += part.width;
1269
+ }
1270
+ return segments;
1271
+ };
1272
+ const STRIKE_CENTER_EM = .3;
1273
+ const STRIKE_THICKNESS_EM = .05;
1274
+ const MIN_STRIKE_THICKNESS_PX = .6;
1275
+ const emitTextDecorations = (groups, textAnchor, x0, baselineY) => {
619
1276
  const totalWidth = groups.reduce((sum, g) => sum + g.width, 0);
620
1277
  let cursor = textAnchor === "middle" ? x0 - totalWidth / 2 : textAnchor === "end" ? x0 - totalWidth : x0;
621
1278
  const parts = [];
622
1279
  for (const g of groups) {
623
- if (g.piece.underline === "wavy" && g.width > 0) {
624
- const y = baselineY - baselineShiftPxOf(g.piece);
625
- parts.push(wavyPath(cursor, cursor + g.width, y, g.piece));
1280
+ if (g.piece.strike === "double" && !g.isTab && g.width > 0) {
1281
+ const size = renderedSizePxOf(g.piece);
1282
+ const center = baselineY - baselineShiftPxOf(g.piece) - size * STRIKE_CENTER_EM;
1283
+ const stroke = Math.max(MIN_STRIKE_THICKNESS_PX, size * STRIKE_THICKNESS_EM);
1284
+ for (const offset of [-stroke, stroke]) parts.push(`<line x1="${fmt(cursor)}" x2="${fmt(cursor + g.width)}" y1="${fmt(center + offset)}" y2="${fmt(center + offset)}" stroke="${g.piece.fillHex}" stroke-width="${fmt(stroke)}"/>`);
1285
+ }
1286
+ if (g.piece.underline !== "none" && g.width > 0 && (g.piece.underline !== "sng" || g.piece.underlineHex !== void 0)) {
1287
+ const size = renderedSizePxOf(g.piece);
1288
+ const baseline = baselineY - baselineShiftPxOf(g.piece);
1289
+ const y = baseline + size * WAVY_BASELINE_OFFSET_RATIO;
1290
+ const dash = underlineDashArray(g.piece);
1291
+ for (const segment of underlineSegments(g, cursor)) if (g.piece.underline.startsWith("wavy")) if (g.piece.underline === "wavyDbl") {
1292
+ const offset = size * .08;
1293
+ parts.push(wavyPath(segment.x, segment.x + segment.width, baseline, g.piece, -offset));
1294
+ parts.push(wavyPath(segment.x, segment.x + segment.width, baseline, g.piece, offset));
1295
+ } else parts.push(wavyPath(segment.x, segment.x + segment.width, baseline, g.piece));
1296
+ else {
1297
+ const stroke = underlineStrokeWidth(g.piece);
1298
+ const offsets = g.piece.underline === "dbl" ? [-size * .08, size * .08] : [0];
1299
+ for (const offset of offsets) {
1300
+ const dashAttr = dash === null ? "" : ` stroke-dasharray="${dash}"`;
1301
+ parts.push(`<line x1="${fmt(segment.x)}" x2="${fmt(segment.x + segment.width)}" y1="${fmt(y + offset)}" y2="${fmt(y + offset)}" stroke="${g.piece.underlineHex ?? g.piece.fillHex}" stroke-width="${fmt(stroke)}"${dashAttr}/>`);
1302
+ }
1303
+ }
626
1304
  }
627
1305
  cursor += g.width;
628
1306
  }
629
1307
  return parts.join("");
630
1308
  };
1309
+ const emitHighlights = (groups, anchor, x0, baselineY) => {
1310
+ const width = groups.reduce((sum, group) => sum + group.width, 0);
1311
+ let x = x0 - (anchor === "middle" ? width / 2 : anchor === "end" ? width : 0);
1312
+ const backgrounds = [];
1313
+ for (const group of groups) {
1314
+ const metrics = group.highlightMetrics;
1315
+ if (group.piece.highlightHex && metrics && group.width > 0) {
1316
+ const scale = (group.piece.baseline === 0 ? 1 : SUPER_SUB_SIZE_RATIO) * (group.piece.smallCaps === true ? SMALL_CAPS_LOWERCASE_RATIO : 1);
1317
+ const y = baselineY - baselineShiftPxOf(group.piece) - metrics.a * scale;
1318
+ backgrounds.push(`<rect x="${fmt(x)}" y="${fmt(y)}" width="${fmt(group.width)}" height="${fmt((metrics.a + metrics.d) * scale)}" fill="${escapeXml$1(group.piece.highlightHex)}"/>`);
1319
+ }
1320
+ x += group.width;
1321
+ }
1322
+ return backgrounds.join("");
1323
+ };
631
1324
  const WAVY_AMPLITUDE_RATIO = .045;
632
1325
  const WAVY_AMPLITUDE_MIN_PX = .6;
633
1326
  const WAVY_PERIOD_RATIO = .18;
634
1327
  const WAVY_PERIOD_MIN_PX = 2;
635
1328
  const WAVY_BASELINE_OFFSET_RATIO = .12;
636
- const WAVY_STROKE_WIDTH_RATIO = .06;
637
- const WAVY_STROKE_WIDTH_MIN_PX = .6;
638
- const wavyPath = (x1, x2, baselineY, piece) => {
1329
+ const wavyPath = (x1, x2, baselineY, piece, yOffset = 0) => {
639
1330
  const size = renderedSizePxOf(piece);
640
1331
  const amp = Math.max(WAVY_AMPLITUDE_MIN_PX, size * WAVY_AMPLITUDE_RATIO);
641
1332
  const period = Math.max(WAVY_PERIOD_MIN_PX, size * WAVY_PERIOD_RATIO);
642
- const y = baselineY + size * WAVY_BASELINE_OFFSET_RATIO;
1333
+ const y = baselineY + size * WAVY_BASELINE_OFFSET_RATIO + yOffset;
643
1334
  let d = `M${fmt(x1)} ${fmt(y)}`;
644
1335
  let cx = x1;
645
1336
  let up = true;
@@ -650,11 +1341,25 @@ const wavyPath = (x1, x2, baselineY, piece) => {
650
1341
  cx = midX;
651
1342
  up = !up;
652
1343
  }
653
- const strokeWidth = Math.max(WAVY_STROKE_WIDTH_MIN_PX, size * WAVY_STROKE_WIDTH_RATIO);
654
- return `<path d="${d}" stroke="${piece.fillHex}" stroke-width="${fmt(strokeWidth)}" fill="none"/>`;
1344
+ const strokeWidth = underlineStrokeWidth(piece);
1345
+ return `<path d="${d}" stroke="${piece.underlineHex ?? piece.fillHex}" stroke-width="${fmt(strokeWidth)}" fill="none"/>`;
1346
+ };
1347
+ const sameReflection = (a, b) => a === void 0 === (b === void 0) && a?.blurEmu === b?.blurEmu && a?.offsetEmu === b?.offsetEmu && a?.angleDeg === b?.angleDeg && a?.opacity === b?.opacity && a?.startOpacity === b?.startOpacity && a?.startPosition === b?.startPosition && a?.endPosition === b?.endPosition && a?.fadeDirection === b?.fadeDirection && a?.scaleX === b?.scaleX && a?.scaleY === b?.scaleY && a?.skewX === b?.skewX && a?.skewY === b?.skewY && a?.alignment === b?.alignment && a?.rotateWithShape === b?.rotateWithShape;
1348
+ const sameInnerShadow = (a, b) => {
1349
+ if (a === void 0 || b === void 0) return a === b;
1350
+ return a.color === b.color && a.blurEmu === b.blurEmu && a.offsetEmu === b.offsetEmu && a.angleDeg === b.angleDeg && a.opacity === b.opacity;
655
1351
  };
656
- const samePiece = (a, b) => a.family === b.family && a.sizePx === b.sizePx && a.bold === b.bold && a.italic === b.italic && a.letterSpacingPx === b.letterSpacingPx && a.fillHex === b.fillHex && a.underline === b.underline && a.strike === b.strike && a.superSub === b.superSub && a.href === b.href;
1352
+ const sameShadow = (a, b) => {
1353
+ if (a === void 0 || b === void 0) return a === b;
1354
+ return a.color === b.color && a.blurEmu === b.blurEmu && a.offsetEmu === b.offsetEmu && a.angleDeg === b.angleDeg && a.opacity === b.opacity;
1355
+ };
1356
+ const sameGlow = (a, b) => {
1357
+ if (a === void 0 || b === void 0) return a === b;
1358
+ return a.color === b.color && a.radiusEmu === b.radiusEmu && a.opacity === b.opacity;
1359
+ };
1360
+ const samePiece = (a, b) => a.family === b.family && a.sizePx === b.sizePx && a.bold === b.bold && a.italic === b.italic && a.letterSpacingPx === b.letterSpacingPx && a.kerning === b.kerning && a.fillHex === b.fillHex && a.outlineHex === b.outlineHex && a.outlineWidthPx === b.outlineWidthPx && a.underlineHex === b.underlineHex && a.highlightHex === b.highlightHex && a.underline === b.underline && a.strike === b.strike && a.baseline === b.baseline && a.smallCaps === b.smallCaps && sameShadow(a.shadow, b.shadow) && sameGlow(a.glow, b.glow) && sameInnerShadow(a.innerShadow, b.innerShadow) && sameReflection(a.reflection, b.reflection) && a.href === b.href && a.hrefTip === b.hrefTip;
657
1361
  const tspan = (g) => {
1362
+ if (g.isTab) return `<tspan dx="${fmt(g.width)}">&#8203;</tspan>`;
658
1363
  const p = g.piece;
659
1364
  const sizePx = renderedSizePxOf(p);
660
1365
  const attrs = [
@@ -664,15 +1369,21 @@ const tspan = (g) => {
664
1369
  ];
665
1370
  if (p.bold) attrs.push("font-weight=\"700\"");
666
1371
  if (p.italic) attrs.push("font-style=\"italic\"");
667
- const deco = [];
668
- if (p.underline === "single") deco.push("underline");
669
- if (p.strike) deco.push("line-through");
670
- if (deco.length) attrs.push(`text-decoration="${deco.join(" ")}"`);
1372
+ if (p.kerning !== void 0) attrs.push(`font-kerning="${p.kerning ? "normal" : "none"}"`);
1373
+ if (p.outlineHex !== void 0 && (p.outlineWidthPx ?? 0) > 0) {
1374
+ attrs.push(`stroke="${p.outlineHex}"`);
1375
+ attrs.push(`stroke-width="${fmt(p.outlineWidthPx)}"`);
1376
+ attrs.push("paint-order=\"stroke fill\"");
1377
+ }
1378
+ const decorations = [];
1379
+ if (p.underline === "sng" && p.underlineHex === void 0) decorations.push("underline");
1380
+ if (p.strike === true) decorations.push("line-through");
1381
+ if (decorations.length) attrs.push(`text-decoration="${decorations.join(" ")}"`);
671
1382
  if (p.letterSpacingPx !== 0) attrs.push(`letter-spacing="${fmt(p.letterSpacingPx)}"`);
672
- if (p.superSub !== 0) attrs.push(`baseline-shift="${fmt(baselineShiftPxOf(p))}"`);
1383
+ if (p.baseline !== 0) attrs.push(`baseline-shift="${fmt(baselineShiftPxOf(p))}"`);
673
1384
  return `<tspan ${attrs.join(" ")}>${escapeXml$1(g.text)}</tspan>`;
674
1385
  };
675
- const wrapTokens = (tokens, wrap, firstAvail, avail) => {
1386
+ const wrapTokens = (tokens, wrap, firstAvail, avail, para, firstOffset) => {
676
1387
  const lines = [];
677
1388
  let cur = [];
678
1389
  let lineW = 0;
@@ -682,19 +1393,41 @@ const wrapTokens = (tokens, wrap, firstAvail, avail) => {
682
1393
  while (cur.length > 0 && (cur[cur.length - 1].isSpace || cur[cur.length - 1].isBreak)) cur.pop();
683
1394
  };
684
1395
  const close = () => {
1396
+ const endingBreak = cur[cur.length - 1];
685
1397
  trimTrailing();
1398
+ if (cur.length === 0 && endingBreak?.isBreak) cur.push(endingBreak);
686
1399
  lines.push(cur);
687
1400
  cur = [];
688
1401
  lineW = 0;
689
1402
  trailingSpaceW = 0;
690
1403
  first = false;
691
1404
  };
692
- for (const tok of tokens) {
1405
+ const wordWidths = tokens.map((token) => token.width);
1406
+ for (let index = tokens.length - 2; index >= 0; index--) if (tokens[index + 1].continuesSegment) wordWidths[index] += wordWidths[index + 1];
1407
+ for (let index = 0; index < tokens.length; index++) {
1408
+ const tok = tokens[index];
693
1409
  if (tok.isBreak) {
694
1410
  cur.push(tok);
695
1411
  close();
696
1412
  continue;
697
1413
  }
1414
+ if (tok.isTab) {
1415
+ const position = lineW + (first ? firstOffset : 0);
1416
+ const stops = para.tabStops ?? [];
1417
+ let lo = 0;
1418
+ let hi = stops.length;
1419
+ while (lo < hi) {
1420
+ const mid = lo + hi >>> 1;
1421
+ if (stops[mid].positionPx <= position + .01) lo = mid + 1;
1422
+ else hi = mid;
1423
+ }
1424
+ const stop = stops[lo];
1425
+ const interval = para.defaultTabSizePx ?? 96;
1426
+ const next = stop?.positionPx ?? (interval > 0 ? (Math.floor(position / interval) + 1) * interval : position);
1427
+ const alignment = stop?.alignment ?? "left";
1428
+ const offset = alignment === "center" ? (tok.tabFieldWidth ?? 0) / 2 : alignment === "right" ? tok.tabFieldWidth ?? 0 : alignment === "decimal" ? tok.tabDecimalWidth ?? 0 : 0;
1429
+ tok.width = Math.max(0, next - offset - position);
1430
+ }
698
1431
  if (tok.isSpace) {
699
1432
  cur.push(tok);
700
1433
  lineW += tok.width;
@@ -703,7 +1436,7 @@ const wrapTokens = (tokens, wrap, firstAvail, avail) => {
703
1436
  }
704
1437
  const limit = first ? firstAvail : avail;
705
1438
  const hasContent = lineW - trailingSpaceW > 0;
706
- if (wrap && hasContent && lineW + tok.width > limit + .5) {
1439
+ if (wrap && hasContent && !tok.continuesSegment && lineW + wordWidths[index] > limit + .5) {
707
1440
  close();
708
1441
  cur.push(tok);
709
1442
  lineW = tok.width;
@@ -718,7 +1451,46 @@ const wrapTokens = (tokens, wrap, firstAvail, avail) => {
718
1451
  return lines;
719
1452
  };
720
1453
  //#endregion
1454
+ //#region src/browser-measure.ts
1455
+ /** Custom tab alignment needs the same font advances as the browser paints. */
1456
+ function browserTextMeasurer() {
1457
+ if (typeof document === "undefined") return void 0;
1458
+ const context = document.createElement("canvas").getContext("2d");
1459
+ if (!context) return void 0;
1460
+ return (text, spec) => {
1461
+ context.font = `${spec.italic ? "italic" : "normal"} ${spec.bold ? "bold" : "normal"} ${spec.sizePx}px ${spec.family}`;
1462
+ context.fontKerning = spec.kerning === false ? "none" : "normal";
1463
+ context.letterSpacing = `${spec.letterSpacingPx}px`;
1464
+ const metrics = context.measureText(text);
1465
+ return {
1466
+ widthPx: metrics.width,
1467
+ ascentPx: metrics.fontBoundingBoxAscent,
1468
+ descentPx: metrics.fontBoundingBoxDescent,
1469
+ lineGapPx: 0,
1470
+ inkAscentPx: metrics.actualBoundingBoxAscent,
1471
+ inkDescentPx: metrics.actualBoundingBoxDescent
1472
+ };
1473
+ };
1474
+ }
1475
+ //#endregion
721
1476
  //#region src/render-slide.ts
1477
+ const clickActionHref = (pres, action) => {
1478
+ if (!action) return void 0;
1479
+ if (action.kind === "url") return action.url;
1480
+ if (action.kind === "customShow") return `#pptx-custom-show?id=${action.id}&return=${action.returnToShow}`;
1481
+ if (action.kind === "endShow") return "#pptx-end-show";
1482
+ if (action.kind === "lastSlideViewed") return "#pptx-last-slide-viewed";
1483
+ if (action.kind === "slide") {
1484
+ const index = getSlideIndex(pres, action.slide);
1485
+ return index >= 0 ? `#slide-${index + 1}` : void 0;
1486
+ }
1487
+ return {
1488
+ nextSlide: "#pptx-next-slide",
1489
+ prevSlide: "#pptx-prev-slide",
1490
+ firstSlide: "#pptx-first-slide",
1491
+ lastSlide: "#pptx-last-slide"
1492
+ }[action.kind];
1493
+ };
722
1494
  const DEFAULT_SIZE = {
723
1495
  width: 12192e3,
724
1496
  height: 6858e3
@@ -728,6 +1500,7 @@ const PX_PER_PT = 96 / 72;
728
1500
  const DEFAULT_BODY_PT = 18;
729
1501
  const DEFAULT_TITLE_PT = 44;
730
1502
  const DEFAULT_FONT = "Calibri, 'Helvetica Neue', Arial, sans-serif";
1503
+ const browserFontFamily = (family) => family ? `${JSON.stringify(family)}, ${DEFAULT_FONT}` : DEFAULT_FONT;
731
1504
  const DEFAULT_BULLET_FONT = "Arial";
732
1505
  const DEFAULT_INSET_X = 91440;
733
1506
  const DEFAULT_INSET_Y = 45720;
@@ -770,7 +1543,7 @@ const mimeFromPartName = (name) => {
770
1543
  if (dot < 0) return null;
771
1544
  return EXT_TO_MIME[name.slice(dot + 1).toLowerCase()] ?? null;
772
1545
  };
773
- const renderPicture = (shape, pres, x, y, w, h, transform, textOverlay, bytes, format) => {
1546
+ const renderPicture = (shape, pres, x, y, w, h, transform, textOverlay, bytes, format, outline) => {
774
1547
  let mime = null;
775
1548
  if (bytes && format) mime = imageMime[format] ?? null;
776
1549
  if (bytes && !mime) {
@@ -784,69 +1557,137 @@ const renderPicture = (shape, pres, x, y, w, h, transform, textOverlay, bytes, f
784
1557
  if (bytes && mime) {
785
1558
  const dataUrl = `data:${mime};base64,${u8ToBase64(bytes)}`;
786
1559
  const crop = getShapeImageCrop(shape);
787
- let imgX = x, imgY = y, imgW = w, imgH = h;
1560
+ const layout = getShapeImageFillLayout(shape);
1561
+ const stretch = layout?.mode === "stretch" ? layout : null;
1562
+ const fillL = stretch?.left ?? 0;
1563
+ const fillT = stretch?.top ?? 0;
1564
+ const fillR = stretch?.right ?? 0;
1565
+ const fillB = stretch?.bottom ?? 0;
1566
+ const hasFillOffsets = fillL !== 0 || fillT !== 0 || fillR !== 0 || fillB !== 0;
1567
+ let imgX = x + w * fillL, imgY = y + h * fillT, imgW = Math.max(0, w * (1 - fillL - fillR)), imgH = Math.max(0, h * (1 - fillT - fillB));
788
1568
  let clipDef = "";
789
1569
  let clipAttr = "";
790
1570
  const cropL = crop?.left ?? 0;
791
1571
  const cropT = crop?.top ?? 0;
792
1572
  const cropR = crop?.right ?? 0;
793
1573
  const cropB = crop?.bottom ?? 0;
794
- if (cropL > 0 || cropT > 0 || cropR > 0 || cropB > 0) {
1574
+ if (cropL !== 0 || cropT !== 0 || cropR !== 0 || cropB !== 0) {
795
1575
  const scaleX = 1 / Math.max(.001, 1 - cropL - cropR);
796
1576
  const scaleY = 1 / Math.max(.001, 1 - cropT - cropB);
797
- imgW = w * scaleX;
798
- imgH = h * scaleY;
799
- imgX = x - imgW * cropL;
800
- imgY = y - imgH * cropT;
1577
+ imgW *= scaleX;
1578
+ imgH *= scaleY;
1579
+ imgX -= imgW * cropL;
1580
+ imgY -= imgH * cropT;
1581
+ }
1582
+ const preset = getShapePreset(shape) ?? "rect";
1583
+ if (crop || hasFillOffsets || preset !== "rect") {
801
1584
  const clipId = mintId();
802
- clipDef = `<defs><clipPath id="${clipId}"><rect x="${E(x)}" y="${E(y)}" width="${E(w)}" height="${E(h)}"/></clipPath></defs>`;
1585
+ clipDef = `<defs><clipPath id="${clipId}">${pictureClipGeometry(shape, preset, x, y, w, h)}</clipPath></defs>`;
803
1586
  clipAttr = ` clip-path="url(#${clipId})"`;
804
1587
  }
805
1588
  const brightness = getShapeImageBrightness(shape) ?? 0;
806
- const contrast = getShapeImageContrast(shape) ?? 1;
1589
+ const contrast = getShapeImageContrast(shape) ?? 0;
807
1590
  const opacity = getShapeImageOpacity(shape) ?? 1;
808
1591
  const grayscale = isShapeImageGrayscale(shape);
809
1592
  const biLevel = getShapeImageBiLevelThreshold(shape);
810
1593
  const duotone = getShapeImageDuotone(pres, shape);
811
1594
  let filterAttr = "";
812
- if (brightness !== 0 || contrast !== 1 || grayscale || biLevel !== null || duotone && (duotone.firstColor || duotone.secondColor)) {
1595
+ if (brightness !== 0 || contrast !== 0 || grayscale || biLevel !== null || duotone && (duotone.firstColor || duotone.secondColor)) {
813
1596
  const fid = mintId();
814
1597
  const prims = [];
815
- if (brightness !== 0 || contrast !== 1) prims.push(`<feComponentTransfer><feFuncR type="linear" slope="${contrast}" intercept="${brightness}"/><feFuncG type="linear" slope="${contrast}" intercept="${brightness}"/><feFuncB type="linear" slope="${contrast}" intercept="${brightness}"/></feComponentTransfer>`);
1598
+ if (brightness !== 0 || contrast !== 0) {
1599
+ const midpoint = 128 / 255;
1600
+ const msoBrightness = Math.max(-1, Math.min(1, brightness));
1601
+ const slope = contrast >= 0 ? 128 / (128 - 127 * Math.min(1, contrast)) : (128 + 127 * Math.max(-1, contrast)) / 128;
1602
+ const intercept = (msoBrightness / 2 - midpoint) * slope + midpoint + msoBrightness / 2;
1603
+ prims.push(`<feComponentTransfer color-interpolation-filters="sRGB"><feFuncR type="linear" slope="${slope}" intercept="${intercept}"/><feFuncG type="linear" slope="${slope}" intercept="${intercept}"/><feFuncB type="linear" slope="${slope}" intercept="${intercept}"/></feComponentTransfer>`);
1604
+ }
816
1605
  if (grayscale) prims.push(`<feColorMatrix type="matrix" values="0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0"/>`);
817
1606
  if (duotone && duotone.firstColor && duotone.secondColor) {
818
1607
  const [r1, g1, b1] = hexChannels(duotone.firstColor);
819
1608
  const [r2, g2, b2] = hexChannels(duotone.secondColor);
820
- const steps = 16;
821
- const tR = [];
822
- const tG = [];
823
- const tB = [];
824
- for (let i = 0; i < steps; i++) {
825
- const t = i / (steps - 1);
826
- tR.push(((r1 + (r2 - r1) * t) / 255).toFixed(4));
827
- tG.push(((g1 + (g2 - g1) * t) / 255).toFixed(4));
828
- tB.push(((b1 + (b2 - b1) * t) / 255).toFixed(4));
829
- }
830
- prims.push(`<feColorMatrix type="matrix" values="0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0"/>`, `<feComponentTransfer><feFuncR type="table" tableValues="${tR.join(" ")}"/><feFuncG type="table" tableValues="${tG.join(" ")}"/><feFuncB type="table" tableValues="${tB.join(" ")}"/></feComponentTransfer>`);
1609
+ prims.push(`<feColorMatrix color-interpolation-filters="sRGB" type="matrix" values="0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0"/>`, `<feComponentTransfer color-interpolation-filters="sRGB"><feFuncR type="table" tableValues="${r1 / 255} ${r2 / 255}"/><feFuncG type="table" tableValues="${g1 / 255} ${g2 / 255}"/><feFuncB type="table" tableValues="${b1 / 255} ${b2 / 255}"/></feComponentTransfer>`);
831
1610
  }
832
1611
  if (biLevel !== null) {
833
1612
  const t = biLevel / 100;
834
- const steps = 32;
835
- const vals = [];
836
- for (let i = 0; i < steps; i++) vals.push(i / (steps - 1) >= t ? "1" : "0");
837
- const tableStr = vals.join(" ");
838
- prims.push(`<feComponentTransfer><feFuncR type="discrete" tableValues="${tableStr}"/><feFuncG type="discrete" tableValues="${tableStr}"/><feFuncB type="discrete" tableValues="${tableStr}"/></feComponentTransfer>`);
1613
+ prims.push(`<feColorMatrix type="matrix" values="0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0"/>`);
1614
+ const shift = .5 - t;
1615
+ prims.push(`<feComponentTransfer><feFuncR type="linear" slope="1" intercept="${shift}"/><feFuncG type="linear" slope="1" intercept="${shift}"/><feFuncB type="linear" slope="1" intercept="${shift}"/></feComponentTransfer>`, "<feComponentTransfer><feFuncR type=\"discrete\" tableValues=\"0 1\"/><feFuncG type=\"discrete\" tableValues=\"0 1\"/><feFuncB type=\"discrete\" tableValues=\"0 1\"/></feComponentTransfer>");
839
1616
  }
840
1617
  clipDef += `<defs><filter id="${fid}">${prims.join("")}</filter></defs>`;
841
1618
  filterAttr = ` filter="url(#${fid})"`;
842
1619
  }
843
1620
  const opacityAttr = opacity !== 1 ? ` opacity="${opacity.toFixed(3)}"` : "";
844
- return `${clipDef}<g${transform}${clipAttr}><image x="${E(imgX)}" y="${E(imgY)}" width="${E(imgW)}" height="${E(imgH)}" href="${dataUrl}" xlink:href="${dataUrl}" preserveAspectRatio="none"${filterAttr}${opacityAttr}/></g><g${transform}>${textOverlay}</g>`;
1621
+ const border = outline.stroke !== "none" && outline.strokeWidth > 0 ? `<g${transform} fill="none" stroke="${outline.stroke}" stroke-width="${E(outline.strokeWidth)}"${outline.strokeAttrs ? ` ${outline.strokeAttrs}` : ""}>${pictureClipGeometry(shape, preset, x, y, w, h)}</g>` : "";
1622
+ if (layout?.mode === "tile") {
1623
+ const intrinsic = getShapeImageIntrinsicSize(shape);
1624
+ if (intrinsic) {
1625
+ const pattern = imageTilePattern(dataUrl, layout, intrinsic, x, y, w, h, cropL, cropT, cropR, cropB);
1626
+ const geometry = pictureClipGeometry(shape, preset, x, y, w, h);
1627
+ return `${clipDef}${pattern.defs}<g${transform} fill="${pattern.fill}"${filterAttr}${opacityAttr}>${geometry}</g>${border}<g${transform}>${textOverlay}</g>`;
1628
+ }
1629
+ }
1630
+ return `${clipDef}<g${transform}${clipAttr}><image x="${E(imgX)}" y="${E(imgY)}" width="${E(imgW)}" height="${E(imgH)}" href="${dataUrl}" xlink:href="${dataUrl}" preserveAspectRatio="none"${filterAttr}${opacityAttr}/></g>${border}<g${transform}>${textOverlay}</g>`;
845
1631
  }
846
1632
  const linkUrl = getShapeImageLinkUrl(shape);
847
1633
  const label = !bytes ? linkUrl ? `picture (link: ${linkUrl.length > 48 ? linkUrl.slice(0, 45) + "…" : linkUrl})` : "picture (no bytes)" : `picture (${format ?? "unknown"}${bytes ? `, ${bytes.byteLength} B` : ""})`;
848
1634
  return `<g data-pptx-fallback="image"${transform}><rect x="${E(x)}" y="${E(y)}" width="${E(w)}" height="${E(h)}" fill="#F3F4F6" stroke="#9CA3AF" stroke-width="${E(9525)}" stroke-dasharray="${E(5e4)},${E(3e4)}"/>${renderPicturePlaceholderLabel(x, y, w, h, label)}${textOverlay}</g>`;
849
1635
  };
1636
+ const imageTilePattern = (dataUrl, layout, intrinsic, x, y, w, h, cropL = 0, cropT = 0, cropR = 0, cropB = 0) => {
1637
+ const sourceW = intrinsic.width * (layout.scaleX ?? 1);
1638
+ const sourceH = intrinsic.height * (layout.scaleY ?? 1);
1639
+ const tileW = sourceW * (1 - cropL - cropR);
1640
+ const tileH = sourceH * (1 - cropT - cropB);
1641
+ if (tileW <= 0 || tileH <= 0) return {
1642
+ defs: "",
1643
+ fill: "none"
1644
+ };
1645
+ const alignment = layout.alignment ?? "tl";
1646
+ const horizontal = [
1647
+ "t",
1648
+ "ctr",
1649
+ "b"
1650
+ ].includes(alignment) ? .5 : [
1651
+ "tr",
1652
+ "r",
1653
+ "br"
1654
+ ].includes(alignment) ? 1 : 0;
1655
+ const vertical = [
1656
+ "l",
1657
+ "ctr",
1658
+ "r"
1659
+ ].includes(alignment) ? .5 : [
1660
+ "bl",
1661
+ "b",
1662
+ "br"
1663
+ ].includes(alignment) ? 1 : 0;
1664
+ const tileX = x + (w - tileW) * horizontal + (layout.offsetX ?? 0);
1665
+ const tileY = y + (h - tileH) * vertical + (layout.offsetY ?? 0);
1666
+ const mirrorX = layout.flip === "x" || layout.flip === "xy";
1667
+ const mirrorY = layout.flip === "y" || layout.flip === "xy";
1668
+ const patternId = mintId();
1669
+ const images = [];
1670
+ for (let row = 0; row < (mirrorY ? 2 : 1); row++) for (let col = 0; col < (mirrorX ? 2 : 1); col++) {
1671
+ const reflection = col || row ? ` transform="translate(${E(col * 2 * tileW)} ${E(row * 2 * tileH)}) scale(${col ? -1 : 1} ${row ? -1 : 1})"` : "";
1672
+ images.push(`<g${reflection}><svg width="${E(tileW)}" height="${E(tileH)}" overflow="hidden"><image x="${E(-sourceW * cropL)}" y="${E(-sourceH * cropT)}" width="${E(sourceW)}" height="${E(sourceH)}" href="${dataUrl}" xlink:href="${dataUrl}" preserveAspectRatio="none"/></svg></g>`);
1673
+ }
1674
+ return {
1675
+ defs: `<defs><pattern id="${patternId}" patternUnits="userSpaceOnUse" x="${E(tileX)}" y="${E(tileY)}" width="${E(tileW * (mirrorX ? 2 : 1))}" height="${E(tileH * (mirrorY ? 2 : 1))}">${images.join("")}</pattern></defs>`,
1676
+ fill: `url(#${patternId})`
1677
+ };
1678
+ };
1679
+ const pictureClipGeometry = (shape, preset, x, y, w, h) => {
1680
+ if (preset === "ellipse") return `<ellipse cx="${E(x + w / 2)}" cy="${E(y + h / 2)}" rx="${E(w / 2)}" ry="${E(h / 2)}"/>`;
1681
+ if (preset === "roundRect") {
1682
+ const radius = E(Math.min(w, h) * Math.max(0, Math.min(.5, (getShapeAdjustValues(shape).adj ?? 16667) / 1e5)));
1683
+ return `<rect x="${E(x)}" y="${E(y)}" width="${E(w)}" height="${E(h)}" rx="${radius}" ry="${radius}"/>`;
1684
+ }
1685
+ const path = PRESET_PATHS[preset];
1686
+ if (path) return `<path d="${path(x / EMU_PER_PX, y / EMU_PER_PX, w / EMU_PER_PX, h / EMU_PER_PX)}" clip-rule="evenodd"/>`;
1687
+ const points = PRESET_POINTS[preset];
1688
+ if (points) return `<polygon points="${points(w / EMU_PER_PX, h / EMU_PER_PX).map(([nx, ny]) => `${E(x + nx * w)},${E(y + ny * h)}`).join(" ")}"/>`;
1689
+ return `<rect x="${E(x)}" y="${E(y)}" width="${E(w)}" height="${E(h)}"/>`;
1690
+ };
850
1691
  const renderPicturePlaceholderLabel = (x, y, w, h, text) => {
851
1692
  const cx = x + w / 2;
852
1693
  const cy = y + h / 2;
@@ -917,10 +1758,88 @@ let nextDefId = 0;
917
1758
  const mintId = () => `pkdef-${(nextDefId++).toString(36)}`;
918
1759
  let activeColorMap = null;
919
1760
  let activeDeckTextColor = "#000000";
920
- const gradientDef = (grad, theme) => {
1761
+ let activeSlideNumber = "1";
1762
+ const gradientDef = (grad, theme, transform = "", box) => {
921
1763
  const id = mintId();
1764
+ const units = box ? "userSpaceOnUse" : "objectBoundingBox";
1765
+ if (box) transform = ` gradientTransform="matrix(${[
1766
+ box.w,
1767
+ 0,
1768
+ 0,
1769
+ box.h,
1770
+ box.x,
1771
+ box.y
1772
+ ].map((value) => value.toFixed(4)).join(" ")})"`;
922
1773
  const orderedStops = [...grad.stops].sort((a, b) => a.offset - b.offset);
923
- const stops = orderedStops.map((s) => `<stop offset="${s.offset.toFixed(4)}" stop-color="${resolveColor(s.color, theme, "#E5E7EB")}"/>`).join("");
1774
+ const stops = orderedStops.map((s) => `<stop offset="${s.offset.toFixed(4)}" stop-color="${s.resolvedColor ?? resolveColor(s.color, theme, "#E5E7EB")}"${s.opacity !== void 0 ? ` stop-opacity="${s.opacity}"` : ""}/>`).join("");
1775
+ if (grad.path === "rect") {
1776
+ const focus = grad.focus ?? {
1777
+ left: .5,
1778
+ top: .5,
1779
+ right: .5,
1780
+ bottom: .5
1781
+ };
1782
+ const tile = grad.tileRect ?? {
1783
+ left: 0,
1784
+ top: 0,
1785
+ right: 0,
1786
+ bottom: 0
1787
+ };
1788
+ const l = tile.left;
1789
+ const t = tile.top;
1790
+ const r = 1 - tile.right;
1791
+ const b = 1 - tile.bottom;
1792
+ const fl = focus.left;
1793
+ const ft = focus.top;
1794
+ const fr = 1 - focus.right;
1795
+ const fb = 1 - focus.bottom;
1796
+ if (l <= fl && fl <= fr && fr <= r && t <= ft && ft <= fb && fb <= b && l < r && t < b) {
1797
+ const sides = [
1798
+ {
1799
+ points: `${l},${t} ${r},${t} ${fr},${ft} ${fl},${ft}`,
1800
+ x1: l,
1801
+ y1: t,
1802
+ x2: l,
1803
+ y2: ft
1804
+ },
1805
+ {
1806
+ points: `${r},${t} ${r},${b} ${fr},${fb} ${fr},${ft}`,
1807
+ x1: r,
1808
+ y1: t,
1809
+ x2: fr,
1810
+ y2: t
1811
+ },
1812
+ {
1813
+ points: `${r},${b} ${l},${b} ${fl},${fb} ${fr},${fb}`,
1814
+ x1: l,
1815
+ y1: b,
1816
+ x2: l,
1817
+ y2: fb
1818
+ },
1819
+ {
1820
+ points: `${l},${b} ${l},${t} ${fl},${ft} ${fl},${fb}`,
1821
+ x1: l,
1822
+ y1: t,
1823
+ x2: fl,
1824
+ y2: t
1825
+ }
1826
+ ];
1827
+ let gradients = "";
1828
+ let polygons = "";
1829
+ for (const side of sides) {
1830
+ if (side.x1 === side.x2 && side.y1 === side.y2) continue;
1831
+ const sideId = mintId();
1832
+ gradients += `<linearGradient id="${sideId}" gradientUnits="userSpaceOnUse" x1="${side.x2}" y1="${side.y2}" x2="${side.x1}" y2="${side.y1}">${stops}</linearGradient>`;
1833
+ polygons += `<polygon points="${side.points}" fill="url(#${sideId})"/>`;
1834
+ }
1835
+ const first = orderedStops[0];
1836
+ const center = first && fr > fl && fb > ft ? `<rect x="${fl}" y="${ft}" width="${fr - fl}" height="${fb - ft}" fill="${first.resolvedColor ?? resolveColor(first.color, theme, "#E5E7EB")}"${first.opacity !== void 0 ? ` fill-opacity="${first.opacity}"` : ""}/>` : "";
1837
+ return {
1838
+ defs: `<defs>${gradients}<pattern id="${id}" patternUnits="${units}" patternContentUnits="${units}" x="${l}" y="${t}" width="${r - l}" height="${b - t}"${transform.replace("gradientTransform", "patternTransform")}><g shape-rendering="crispEdges" transform="translate(${-l} ${-t})">${polygons}${center}</g></pattern></defs>`,
1839
+ fillAttr: `url(#${id})`
1840
+ };
1841
+ }
1842
+ }
924
1843
  if (grad.path === "circle" || grad.path === "rect" || grad.path === "shape") {
925
1844
  const focus = grad.focus ?? {
926
1845
  left: .5,
@@ -928,11 +1847,10 @@ const gradientDef = (grad, theme) => {
928
1847
  right: .5,
929
1848
  bottom: .5
930
1849
  };
931
- const cx = (focus.left + focus.right) / 2;
932
- const cy = (focus.top + focus.bottom) / 2;
933
- const reversed = orderedStops.slice().reverse().map((s) => `<stop offset="${(1 - s.offset).toFixed(4)}" stop-color="${resolveColor(s.color, theme, "#E5E7EB")}"/>`).join("");
1850
+ const cx = (focus.left + 1 - focus.right) / 2;
1851
+ const cy = (focus.top + 1 - focus.bottom) / 2;
934
1852
  return {
935
- defs: `<defs><radialGradient id="${id}" gradientUnits="objectBoundingBox" cx="${cx.toFixed(4)}" cy="${cy.toFixed(4)}" r="${Math.max(.5, Math.max(cx, cy, 1 - cx, 1 - cy)).toFixed(4)}">${reversed}</radialGradient></defs>`,
1853
+ defs: `<defs><radialGradient id="${id}" gradientUnits="${units}"${box ? transform : ""} cx="${cx.toFixed(4)}" cy="${cy.toFixed(4)}" r="${Math.max(.5, Math.max(cx, cy, 1 - cx, 1 - cy)).toFixed(4)}">${stops}</radialGradient></defs>`,
936
1854
  fillAttr: `url(#${id})`
937
1855
  };
938
1856
  }
@@ -944,7 +1862,7 @@ const gradientDef = (grad, theme) => {
944
1862
  const x2 = .5 + dx;
945
1863
  const y2 = .5 + dy;
946
1864
  return {
947
- defs: `<defs><linearGradient id="${id}" gradientUnits="objectBoundingBox" x1="${x1.toFixed(4)}" y1="${y1.toFixed(4)}" x2="${x2.toFixed(4)}" y2="${y2.toFixed(4)}">${stops}</linearGradient></defs>`,
1865
+ defs: `<defs><linearGradient id="${id}" gradientUnits="${units}"${transform} x1="${x1.toFixed(4)}" y1="${y1.toFixed(4)}" x2="${x2.toFixed(4)}" y2="${y2.toFixed(4)}">${stops}</linearGradient></defs>`,
948
1866
  fillAttr: `url(#${id})`
949
1867
  };
950
1868
  };
@@ -1020,6 +1938,24 @@ const patternDef = (pat) => {
1020
1938
  fillAttr: `url(#${id})`
1021
1939
  };
1022
1940
  };
1941
+ const textFillPaint = (fill, theme) => (box) => {
1942
+ if (fill.kind === "pattern") {
1943
+ const built = patternDef({
1944
+ preset: fill.preset,
1945
+ foreground: resolveColor(fill.foreground, theme, "#000000"),
1946
+ background: resolveColor(fill.background, theme, "#FFFFFF")
1947
+ });
1948
+ return {
1949
+ defs: built.defs,
1950
+ fill: built.fillAttr
1951
+ };
1952
+ }
1953
+ const built = gradientDef(fill, theme, "", box);
1954
+ return {
1955
+ defs: built.defs,
1956
+ fill: built.fillAttr
1957
+ };
1958
+ };
1023
1959
  const DASH_PATTERNS = {
1024
1960
  solid: "",
1025
1961
  dot: "1 3",
@@ -1068,13 +2004,39 @@ const paint = (shape, fill, stroke, theme, isPlaceholder, pres) => {
1068
2004
  let resolved = null;
1069
2005
  if (shape && pres) resolved = getShapeFillColorResolved(pres, shape);
1070
2006
  fillColor = resolved ?? resolveColor(fill.color, theme, "#E5E7EB");
1071
- const opacity = shape ? getShapeFillOpacity(shape) : null;
2007
+ const opacity = shape ? getShapeFillOpacity(shape, pres) : null;
1072
2008
  if (opacity !== null && opacity < 1) fillAttrs = ` fill-opacity="${opacity.toFixed(3)}"`;
1073
2009
  } else if (fill.kind === "none") fillColor = "none";
1074
2010
  else if (fill.kind === "gradient") {
1075
2011
  const grad = shape ? pres ? getShapeGradientFillEffective(pres, shape) : getShapeGradientFill(shape) : null;
1076
2012
  if (grad) {
1077
- const built = gradientDef(grad, theme);
2013
+ let transform = "";
2014
+ const bounds = shape && (pres ? getShapeBoundsResolved(pres, shape) : getShapeBounds(shape));
2015
+ if (grad.rotateWithShape === false && shape && bounds && bounds.w > 0 && bounds.h > 0) {
2016
+ const angle = getShapeRotation(shape) * Math.PI / 180;
2017
+ const cos = Math.cos(angle);
2018
+ const sin = Math.sin(angle);
2019
+ const w = bounds.w;
2020
+ const h = bounds.h;
2021
+ const rotatedW = Math.abs(cos) * w + Math.abs(sin) * h;
2022
+ const rotatedH = Math.abs(sin) * w + Math.abs(cos) * h;
2023
+ const flip = getShapeFlip(shape);
2024
+ const sx = flip?.horizontal ? -1 : 1;
2025
+ const sy = flip?.vertical ? -1 : 1;
2026
+ const a = sx * cos * rotatedW / w;
2027
+ const b = -sy * sin * rotatedW / h;
2028
+ const c = sx * sin * rotatedH / w;
2029
+ const d = sy * cos * rotatedH / h;
2030
+ transform = ` gradientTransform="matrix(${[
2031
+ a,
2032
+ b,
2033
+ c,
2034
+ d,
2035
+ (1 - a - c) / 2,
2036
+ (1 - b - d) / 2
2037
+ ].map((value) => value.toFixed(6)).join(" ")})"`;
2038
+ }
2039
+ const built = gradientDef(grad, theme, transform);
1078
2040
  defs = built.defs;
1079
2041
  fillColor = built.fillAttr;
1080
2042
  } else fillColor = (shape && pres ? getShapeFillColorResolved(pres, shape) : null) ?? "none";
@@ -1097,9 +2059,9 @@ const paint = (shape, fill, stroke, theme, isPlaceholder, pres) => {
1097
2059
  strokeColor = resolved ?? resolveColor(stroke.color, theme, "#9CA3AF");
1098
2060
  strokeWidth = stroke.widthEmu ?? 9525;
1099
2061
  if (shape) {
1100
- const opacity = getShapeStrokeOpacity(shape);
2062
+ const opacity = getShapeStrokeOpacity(shape, pres);
1101
2063
  if (opacity !== null && opacity < 1) strokeAttrParts.push(`stroke-opacity="${opacity.toFixed(3)}"`);
1102
- const dash = getShapeStrokeDash(shape);
2064
+ const dash = getShapeStrokeDash(shape, pres);
1103
2065
  if (dash && dash !== "solid") {
1104
2066
  const pattern = DASH_PATTERNS[dash];
1105
2067
  if (pattern) {
@@ -1108,17 +2070,17 @@ const paint = (shape, fill, stroke, theme, isPlaceholder, pres) => {
1108
2070
  strokeAttrParts.push(`stroke-dasharray="${arr}"`);
1109
2071
  }
1110
2072
  }
1111
- const cap = getShapeStrokeCap(shape);
2073
+ const cap = getShapeStrokeCap(shape, pres);
1112
2074
  if (cap === "rnd") strokeAttrParts.push("stroke-linecap=\"round\"");
1113
2075
  else if (cap === "sq") strokeAttrParts.push("stroke-linecap=\"square\"");
1114
2076
  else if (cap === "flat") strokeAttrParts.push("stroke-linecap=\"butt\"");
1115
- const join = getShapeStrokeJoin(shape);
2077
+ const join = getShapeStrokeJoin(shape, pres);
1116
2078
  if (join === "round") strokeAttrParts.push("stroke-linejoin=\"round\"");
1117
2079
  else if (join === "bevel") strokeAttrParts.push("stroke-linejoin=\"bevel\"");
1118
2080
  else if (join === "miter") strokeAttrParts.push("stroke-linejoin=\"miter\"");
1119
- if (getShapeStrokeCompound(shape) === "dbl") strokeWidth = Math.max(strokeWidth, 19050);
1120
- const head = getShapeStrokeArrow(shape, "head");
1121
- const tail = getShapeStrokeArrow(shape, "tail");
2081
+ if (getShapeStrokeCompound(shape, pres) === "dbl") strokeWidth = Math.max(strokeWidth, 19050);
2082
+ const head = getShapeStrokeArrow(shape, "head", pres);
2083
+ const tail = getShapeStrokeArrow(shape, "tail", pres);
1122
2084
  if (head && head.type !== "none") {
1123
2085
  const m = buildArrowMarker(head.type, head.width, head.length, strokeColor, "auto-start-reverse");
1124
2086
  defs += m.def;
@@ -2045,116 +3007,86 @@ const ALIGNMENT_TO_CSS = {
2045
3007
  right: "right",
2046
3008
  justify: "justify"
2047
3009
  };
2048
- const ANCHOR_TO_CSS = {
2049
- top: "flex-start",
2050
- center: "center",
2051
- bottom: "flex-end"
2052
- };
2053
- const placeholderDefaultPt = (phType) => {
2054
- if (phType === "title" || phType === "ctrTitle") return DEFAULT_TITLE_PT;
2055
- if (phType === "subTitle") return 32;
2056
- if (phType === "ftr" || phType === "dt" || phType === "sldNum") return 12;
2057
- return DEFAULT_BODY_PT;
2058
- };
2059
- const bulletChar = (level) => level <= 0 ? "•" : level === 1 ? "◦" : "▪";
2060
- const bulletAutoNumType = (style) => {
2061
- if (style === "number") return "arabicPeriod";
2062
- if (style !== null && typeof style === "object" && "autoNum" in style) return style.autoNum ?? null;
2063
- return null;
2064
- };
2065
- const toRoman = (n) => {
2066
- if (n <= 0) return String(n);
2067
- const map = [
2068
- [1e3, "M"],
2069
- [900, "CM"],
2070
- [500, "D"],
2071
- [400, "CD"],
2072
- [100, "C"],
2073
- [90, "XC"],
2074
- [50, "L"],
2075
- [40, "XL"],
2076
- [10, "X"],
2077
- [9, "IX"],
2078
- [5, "V"],
2079
- [4, "IV"],
2080
- [1, "I"]
2081
- ];
2082
- let out = "";
2083
- let r = n;
2084
- for (const [v, s] of map) while (r >= v) {
2085
- out += s;
2086
- r -= v;
2087
- }
2088
- return out;
2089
- };
2090
- const toAlpha = (n) => {
2091
- if (n <= 0) return String(n);
2092
- let r = n;
2093
- let out = "";
2094
- while (r > 0) {
2095
- r -= 1;
2096
- out = String.fromCharCode(65 + r % 26) + out;
2097
- r = Math.floor(r / 26);
2098
- }
2099
- return out;
2100
- };
2101
- const formatAutoNum = (token, n) => {
2102
- const arabic = String(n);
2103
- switch (token) {
2104
- case "arabicPlain": return arabic;
2105
- case "arabicPeriod": return `${arabic}.`;
2106
- case "arabicParenR": return `${arabic})`;
2107
- case "arabicParenBoth": return `(${arabic})`;
2108
- case "romanUcPeriod": return `${toRoman(n)}.`;
2109
- case "romanLcPeriod": return `${toRoman(n).toLowerCase()}.`;
2110
- case "romanUcParenR": return `${toRoman(n)})`;
2111
- case "romanLcParenR": return `${toRoman(n).toLowerCase()})`;
2112
- case "romanUcParenBoth": return `(${toRoman(n)})`;
2113
- case "romanLcParenBoth": return `(${toRoman(n).toLowerCase()})`;
2114
- case "alphaUcPeriod": return `${toAlpha(n)}.`;
2115
- case "alphaLcPeriod": return `${toAlpha(n).toLowerCase()}.`;
2116
- case "alphaUcParenR": return `${toAlpha(n)})`;
2117
- case "alphaLcParenR": return `${toAlpha(n).toLowerCase()})`;
2118
- case "alphaUcParenBoth": return `(${toAlpha(n)})`;
2119
- case "alphaLcParenBoth": return `(${toAlpha(n).toLowerCase()})`;
2120
- default: return `${arabic}.`;
2121
- }
3010
+ const ANCHOR_TO_CSS = {
3011
+ top: "flex-start",
3012
+ center: "center",
3013
+ bottom: "flex-end"
3014
+ };
3015
+ const placeholderDefaultPt = (phType) => {
3016
+ if (phType === "title" || phType === "ctrTitle") return DEFAULT_TITLE_PT;
3017
+ if (phType === "subTitle") return 32;
3018
+ if (phType === "ftr" || phType === "dt" || phType === "sldNum") return 12;
3019
+ return DEFAULT_BODY_PT;
3020
+ };
3021
+ const bulletChar = (level) => level <= 0 ? "•" : level === 1 ? "◦" : "▪";
3022
+ const cssColorWithOpacity = (hex, opacity) => {
3023
+ if (opacity === void 0 || opacity >= 1) return hex;
3024
+ const match = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
3025
+ if (!match) return hex;
3026
+ const [r, g, b] = [
3027
+ match[1],
3028
+ match[2],
3029
+ match[3]
3030
+ ].map((part) => Number.parseInt(part, 16));
3031
+ return `rgba(${r},${g},${b},${Math.max(0, opacity).toFixed(3)})`;
2122
3032
  };
2123
- const renderRun = (text, format, theme, effectivePt, _wasDefault = false) => {
3033
+ const renderRun = (text, format, theme, effectivePt, defaultColor, _wasDefault = false, suppressLineHeight = false, lineHeightOverride = void 0) => {
2124
3034
  if (text === "") return "";
3035
+ if (format?.textFill) {
3036
+ const { textFill: _fill, outline: _outline, shadow: _shadow, glow: _glow, underline: _underline, strike: _strike, ...layout } = format;
3037
+ return renderRun(text, {
3038
+ ...layout,
3039
+ color: "transparent"
3040
+ }, theme, effectivePt, defaultColor, _wasDefault, suppressLineHeight, lineHeightOverride);
3041
+ }
2125
3042
  const styles = [];
2126
3043
  styles.push(`font-size:${(effectivePt * PX_PER_PT).toFixed(2)}px`);
2127
- styles.push(`line-height:1.05`);
3044
+ styles.push(suppressLineHeight ? "line-height:0;vertical-align:top" : lineHeightOverride !== void 0 ? `line-height:${lineHeightOverride}` : "line-height:1.05");
2128
3045
  if (format?.font) styles.push(`font-family:${escapeXml(format.font)}, ${DEFAULT_FONT}`);
2129
3046
  if (format?.bold) styles.push("font-weight:700");
2130
3047
  if (format?.italic) styles.push("font-style:italic");
2131
3048
  const underline = format?.underline;
2132
3049
  const strike = format?.strike;
2133
3050
  const hasUnderline = underline !== void 0 && underline !== false && underline !== "none";
2134
- const hasStrike = strike !== void 0 && strike !== false && strike !== "noStrike";
2135
- const isWavyUnderline = typeof underline === "string" && underline.startsWith("wavy");
2136
- const nestedWavyUnderline = hasUnderline && hasStrike && isWavyUnderline;
2137
- if (nestedWavyUnderline) styles.push("text-decoration:line-through");
2138
- else if (hasUnderline && hasStrike) styles.push("text-decoration:underline line-through");
2139
- else if (hasUnderline) {
2140
- styles.push("text-decoration:underline");
2141
- if (isWavyUnderline) styles.push("text-decoration-style:wavy");
2142
- } else if (hasStrike) styles.push("text-decoration:line-through");
3051
+ if (strike !== void 0 && strike !== false && strike !== "noStrike") styles.push(`text-decoration:line-through${strike === "dblStrike" ? " double" : ""}`);
3052
+ const underlineCss = hasUnderline ? textUnderlineStyle(underline, resolveColor(format?.underlineColor ?? format?.color, theme, defaultColor), format?.underlineColor !== void 0 && format.underlineColor !== null) : "";
2143
3053
  if (format?.color !== void 0 && format.color !== null) styles.push(`color:${resolveColor(format.color, theme, "#000000")}`);
2144
3054
  if (format?.spc !== void 0 && format.spc !== 0) {
2145
3055
  const trackingPx = format.spc / 100 * PX_PER_PT;
2146
3056
  styles.push(`letter-spacing:${trackingPx.toFixed(3)}px`);
2147
3057
  }
2148
3058
  if (format?.baseline !== void 0 && format.baseline !== 0) {
2149
- const direction = format.baseline > 0 ? "super" : "sub";
2150
- styles.push(`vertical-align:${direction}`);
2151
- styles.push("font-size:0.65em");
3059
+ styles.push(`vertical-align:${(format.baseline * effectivePt * PX_PER_PT).toFixed(2)}px`);
3060
+ styles.push(`font-size:${(effectivePt * PX_PER_PT * .65).toFixed(2)}px`);
2152
3061
  }
2153
3062
  if (format?.cap === "all") styles.push("text-transform:uppercase");
2154
3063
  else if (format?.cap === "small") styles.push("font-variant:small-caps");
3064
+ if (format?.kern !== void 0) styles.push(`font-kerning:${format.kern > 0 && effectivePt >= format.kern / 100 ? "normal" : "none"}`);
2155
3065
  if (format?.highlight !== void 0 && format.highlight !== null) styles.push(`background-color:${resolveColor(format.highlight, theme, "#FFFF00")}`);
3066
+ const outline = format?.outline;
3067
+ if (outline && outline.color) {
3068
+ const widthPx = (outline.widthEmu ?? 9525) / EMU_PER_PX;
3069
+ styles.push(`-webkit-text-stroke:${widthPx.toFixed(2)}px ${resolveColor(outline.color, theme, "#000000")}`);
3070
+ styles.push("paint-order:stroke fill");
3071
+ }
3072
+ const textShadows = [];
3073
+ const glow = format?.glow;
3074
+ if (glow?.color) {
3075
+ const radiusPx = (glow.radiusEmu ?? 63500) / EMU_PER_PX;
3076
+ const color = cssColorWithOpacity(resolveColor(glow.color, theme, "#FFFF00"), glow.opacity);
3077
+ textShadows.push(`0 0 ${radiusPx.toFixed(2)}px ${color}`);
3078
+ }
3079
+ const shadow = format?.shadow;
3080
+ if (shadow) {
3081
+ const distancePx = (shadow.offsetEmu ?? 38100) / EMU_PER_PX;
3082
+ const radians = (shadow.angleDeg ?? 45) * Math.PI / 180;
3083
+ const blurPx = (shadow.blurEmu ?? 50800) / EMU_PER_PX;
3084
+ const color = cssColorWithOpacity(resolveColor(shadow.color ?? "#000000", theme, "#000000"), shadow.opacity);
3085
+ textShadows.push(`${(Math.cos(radians) * distancePx).toFixed(2)}px ${(Math.sin(radians) * distancePx).toFixed(2)}px ${blurPx.toFixed(2)}px ${color}`);
3086
+ }
3087
+ if (textShadows.length) styles.push(`text-shadow:${textShadows.join(",")}`);
2156
3088
  const html = text.split("\n").map((part) => escapeXml(part)).join("<br/>");
2157
- const content = nestedWavyUnderline ? `<span style="text-decoration:underline;text-decoration-style:wavy">${html}</span>` : html;
3089
+ const content = !hasUnderline ? html : underline === "words" ? text.split(/(\s+)/u).map((part) => /^\s+$/u.test(part) ? escapeXml(part).replaceAll("\n", "<br/>") : part === "" ? "" : `<span style="${underlineCss}">${escapeXml(part)}</span>`).join("") : `<span style="${underlineCss}">${html}</span>`;
2158
3090
  return `<span style="${styles.join(";")}">${content}</span>`;
2159
3091
  };
2160
3092
  const LINE_HEIGHT = 1.05;
@@ -2163,13 +3095,33 @@ const AUTOFIT_STEP = .05;
2163
3095
  const underlineStyleOf = (fmt) => {
2164
3096
  const u = fmt?.underline;
2165
3097
  if (u === void 0 || u === false || u === "none") return "none";
2166
- if (typeof u === "string" && u.startsWith("wavy")) return "wavy";
2167
- return "single";
3098
+ switch (u) {
3099
+ case "words":
3100
+ case "sng":
3101
+ case "dbl":
3102
+ case "heavy":
3103
+ case "dotted":
3104
+ case "dottedHeavy":
3105
+ case "dash":
3106
+ case "dashHeavy":
3107
+ case "dashLong":
3108
+ case "dashLongHeavy":
3109
+ case "dotDash":
3110
+ case "dotDashHeavy":
3111
+ case "dotDotDash":
3112
+ case "dotDotDashHeavy":
3113
+ case "wavy":
3114
+ case "wavyHeavy":
3115
+ case "wavyDbl": return u;
3116
+ default: return "sng";
3117
+ }
2168
3118
  };
2169
- const hasStrikeFmt = (fmt) => {
3119
+ const strikeStyleOf = (fmt) => {
2170
3120
  const s = fmt?.strike;
3121
+ if (s === "dblStrike") return "double";
2171
3122
  return s !== void 0 && s !== false && s !== "noStrike";
2172
3123
  };
3124
+ const hasPaintedTextFill = (run) => !!run.fmt?.textFill && !run.href;
2173
3125
  const alignOf = (a) => a === "center" || a === "right" || a === "justify" ? a : "left";
2174
3126
  const verticalLayoutOf = (vert) => {
2175
3127
  switch (vert) {
@@ -2182,13 +3134,44 @@ const verticalLayoutOf = (vert) => {
2182
3134
  case null: return "none";
2183
3135
  }
2184
3136
  };
3137
+ const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
3138
+ const graphemesOf = (text) => Array.from(graphemeSegmenter.segment(text), ({ segment }) => segment);
3139
+ const isSmallCapsLowercase = (grapheme) => grapheme.toLowerCase() === grapheme && grapheme.toUpperCase() !== grapheme;
3140
+ const smallCapsParts = (text, base) => {
3141
+ const parts = [];
3142
+ let current = "";
3143
+ let currentSmall = false;
3144
+ const flush = () => {
3145
+ if (current === "") return;
3146
+ parts.push({
3147
+ ...base,
3148
+ text: current,
3149
+ ...currentSmall ? { smallCaps: true } : {}
3150
+ });
3151
+ current = "";
3152
+ };
3153
+ for (const grapheme of graphemesOf(text)) {
3154
+ const small = isSmallCapsLowercase(grapheme);
3155
+ if (current !== "" && small !== currentSmall) flush();
3156
+ currentSmall = small;
3157
+ current += grapheme.toUpperCase();
3158
+ }
3159
+ flush();
3160
+ return parts;
3161
+ };
2185
3162
  const buildSvgTextInput = (a) => {
2186
3163
  const scale = a.autoFitScale;
2187
3164
  const paragraphs = a.paraData.map((para, pi) => {
2188
3165
  const pieces = [];
2189
3166
  for (const run of para.runs) {
2190
- if (run.text === "\n" && run.fmt === null) {
2191
- pieces.push(breakPiece());
3167
+ if (run.text === "\n") {
3168
+ pieces.push({
3169
+ ...breakPiece(),
3170
+ family: (a.resolveFamily ?? substituteFamily)(run.fmt?.font ?? a.themeFace),
3171
+ sizePx: run.sizePt * scale * PX_PER_PT,
3172
+ bold: run.fmt?.bold ?? false,
3173
+ italic: run.fmt?.italic ?? false
3174
+ });
2192
3175
  continue;
2193
3176
  }
2194
3177
  let fmt = run.fmt;
@@ -2203,36 +3186,63 @@ const buildSvgTextInput = (a) => {
2203
3186
  const family = (a.resolveFamily ?? substituteFamily)(fmt?.font ?? a.themeFace);
2204
3187
  const sizePx = run.sizePt * scale * PX_PER_PT;
2205
3188
  const fillHex = fmt?.color !== void 0 && fmt.color !== null ? resolveColor(fmt.color, a.theme, "#000000") : a.defaultColor;
2206
- const superSub = fmt?.baseline !== void 0 && fmt.baseline !== 0 ? fmt.baseline > 0 ? 1 : -1 : 0;
3189
+ const baseline = fmt?.baseline ?? 0;
2207
3190
  const letterSpacingPx = fmt?.spc !== void 0 && fmt.spc !== 0 ? fmt.spc / 100 * PX_PER_PT : 0;
2208
- const caps = fmt?.cap === "all" || fmt?.cap === "small";
3191
+ const kerning = fmt?.kern === void 0 ? true : fmt.kern > 0 && sizePx / PX_PER_PT >= fmt.kern / 100;
3192
+ const cap = fmt?.cap;
3193
+ const caps = cap === "all" || cap === "small";
2209
3194
  const base = {
2210
3195
  family,
2211
3196
  sizePx,
2212
3197
  bold: fmt?.bold ?? false,
2213
3198
  italic: fmt?.italic ?? false,
2214
3199
  letterSpacingPx,
3200
+ kerning,
2215
3201
  fillHex,
3202
+ ...fmt?.textFill && !run.href ? { fillPaint: textFillPaint(fmt.textFill, a.theme) } : {},
3203
+ ...fmt?.underlineColor !== void 0 && fmt.underlineColor !== null ? { underlineHex: resolveColor(fmt.underlineColor, a.theme, fillHex) } : {},
2216
3204
  underline: underlineStyleOf(fmt),
2217
- strike: hasStrikeFmt(fmt),
2218
- superSub,
2219
- href: run.href ?? null
3205
+ strike: strikeStyleOf(fmt),
3206
+ ...fmt?.highlight ? { highlightHex: resolveColor(fmt.highlight, a.theme, "#FFFF00") } : {},
3207
+ ...fmt?.outline?.color ? {
3208
+ outlineHex: resolveColor(fmt.outline.color, a.theme, "#000000"),
3209
+ outlineWidthPx: (fmt.outline.widthEmu ?? 9525) / EMU_PER_PX
3210
+ } : {},
3211
+ ...fmt?.shadow ? { shadow: {
3212
+ ...fmt.shadow,
3213
+ ...fmt.shadow.color ? { color: resolveColor(fmt.shadow.color, a.theme, "#000000") } : {}
3214
+ } } : {},
3215
+ ...fmt?.glow ? { glow: {
3216
+ ...fmt.glow,
3217
+ color: resolveColor(fmt.glow.color, a.theme, "#FFFF00")
3218
+ } } : {},
3219
+ baseline,
3220
+ ...fmt?.reflection ? { reflection: fmt.reflection } : {},
3221
+ ...fmt?.innerShadow ? { innerShadow: {
3222
+ ...fmt.innerShadow,
3223
+ ...fmt.innerShadow.color ? { color: resolveColor(fmt.innerShadow.color, a.theme, "#000000") } : {}
3224
+ } } : {},
3225
+ href: run.href ?? null,
3226
+ ...run.hrefTip !== void 0 ? { hrefTip: run.hrefTip } : {}
2220
3227
  };
2221
3228
  const segs = run.text.split("\n");
2222
3229
  segs.forEach((seg, i) => {
2223
3230
  const segText = caps ? seg.toUpperCase() : seg;
2224
- if (a.vert === "upright") for (const g of Array.from(segText)) {
3231
+ const segPieces = cap === "small" ? smallCapsParts(seg, base) : [{
3232
+ ...base,
3233
+ text: segText
3234
+ }];
3235
+ if (a.vert === "upright") for (const segPiece of segPieces) for (const g of graphemesOf(segPiece.text)) {
2225
3236
  const last = pieces[pieces.length - 1];
2226
3237
  if (last !== void 0 && !last.isBreak) pieces.push(breakPiece());
2227
3238
  pieces.push({
2228
- ...base,
3239
+ ...segPiece,
2229
3240
  text: g,
2230
3241
  isBreak: false
2231
3242
  });
2232
3243
  }
2233
- else pieces.push({
2234
- ...base,
2235
- text: segText,
3244
+ else for (const segPiece of segPieces) pieces.push({
3245
+ ...segPiece,
2236
3246
  isBreak: false
2237
3247
  });
2238
3248
  if (i < segs.length - 1) pieces.push(breakPiece());
@@ -2251,7 +3261,13 @@ const buildSvgTextInput = (a) => {
2251
3261
  px: para.lineSpacing.value * PX_PER_PT * scale
2252
3262
  } : null;
2253
3263
  return {
3264
+ latinLineBreak: para.latinLineBreak,
2254
3265
  align: alignOf(para.align),
3266
+ tabStops: (para.tabStops ?? []).map((stop) => ({
3267
+ positionPx: stop.positionEmu / EMU_PER_PX * scale,
3268
+ alignment: stop.alignment
3269
+ })),
3270
+ defaultTabSizePx: (para.defaultTabSizeEmu ?? 914400) / EMU_PER_PX * scale,
2255
3271
  marLpx,
2256
3272
  marRpx,
2257
3273
  firstIndentPx,
@@ -2261,7 +3277,7 @@ const buildSvgTextInput = (a) => {
2261
3277
  lineAdvanceScale: a.lineHeightScale,
2262
3278
  bullet: buildBullet(a, para, pi),
2263
3279
  pieces,
2264
- fallbackSizePx: a.defaultPt * scale * PX_PER_PT
3280
+ fallbackSizePx: (para.emptySizePt ?? a.defaultPt) * scale * PX_PER_PT
2265
3281
  };
2266
3282
  });
2267
3283
  return {
@@ -2270,10 +3286,13 @@ const buildSvgTextInput = (a) => {
2270
3286
  boxWpx: a.innerW / EMU_PER_PX,
2271
3287
  boxHpx: a.innerH / EMU_PER_PX,
2272
3288
  anchor: a.anchor,
3289
+ anchorCentered: a.anchorCentered ?? false,
2273
3290
  wrap: a.wrap,
2274
3291
  paragraphs,
2275
3292
  vert: a.vert,
2276
- columns: a.columns
3293
+ columns: a.columns,
3294
+ ...a.effectsOnly ? { effectsOnly: true } : {},
3295
+ ...a.effectKind ? { effectKind: a.effectKind } : {}
2277
3296
  };
2278
3297
  };
2279
3298
  const buildAndLayoutSvgText = (a) => layoutTextSvg(buildSvgTextInput(a), a.measure);
@@ -2287,7 +3306,7 @@ const breakPiece = () => ({
2287
3306
  fillHex: "#000000",
2288
3307
  underline: "none",
2289
3308
  strike: false,
2290
- superSub: 0,
3309
+ baseline: 0,
2291
3310
  href: null,
2292
3311
  isBreak: true
2293
3312
  });
@@ -2299,9 +3318,10 @@ const buildBullet = (a, para, pi) => {
2299
3318
  const baseSizePx = (para.runs.find((r) => r.text !== "\n" && r.text !== "")?.sizePt ?? a.defaultPt) * PX_PER_PT * a.autoFitScale;
2300
3319
  const sizePx = para.bulletDetail.sizePct !== null ? baseSizePx * para.bulletDetail.sizePct : para.bulletDetail.sizePts !== null ? para.bulletDetail.sizePts * PX_PER_PT * a.autoFitScale : baseSizePx;
2301
3320
  const fillHex = bulletFillOf(para, a.theme, a.defaultColor);
3321
+ const bulletFont = para.bulletDetail.fontFollowText ? para.runs.find((r) => r.text !== "\n" && r.text !== "")?.fmt?.font ?? DEFAULT_BULLET_FONT : para.bulletDetail.font ?? DEFAULT_BULLET_FONT;
2302
3322
  return {
2303
3323
  text: char,
2304
- family: (a.resolveFamily ?? substituteFamily)(para.bulletDetail.font ?? DEFAULT_BULLET_FONT),
3324
+ family: (a.resolveFamily ?? substituteFamily)(bulletFont),
2305
3325
  sizePx,
2306
3326
  fillHex,
2307
3327
  ...para.bulletImageHref ? { imageHref: para.bulletImageHref } : {}
@@ -2312,42 +3332,7 @@ const bulletFillOf = (para, theme, defaultColor) => {
2312
3332
  const runColor = para.runs.find((r) => r.text !== "\n" && r.text !== "")?.fmt?.color;
2313
3333
  return runColor === void 0 || runColor === null ? defaultColor : resolveColor(runColor, theme, defaultColor);
2314
3334
  };
2315
- const presetTextRect = (preset) => {
2316
- switch (preset) {
2317
- case "triangle": return {
2318
- l: .25,
2319
- t: .5,
2320
- r: .75,
2321
- b: 1
2322
- };
2323
- case "diamond": return {
2324
- l: .25,
2325
- t: .25,
2326
- r: .75,
2327
- b: .75
2328
- };
2329
- case "pentagon": return {
2330
- l: .191,
2331
- t: .236,
2332
- r: .809,
2333
- b: 1
2334
- };
2335
- case "star5": return {
2336
- l: .309,
2337
- t: .382,
2338
- r: .691,
2339
- b: .764
2340
- };
2341
- case "leftRightArrow": return {
2342
- l: .18,
2343
- t: .35,
2344
- r: .82,
2345
- b: .65
2346
- };
2347
- default: return null;
2348
- }
2349
- };
2350
- const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defaultColor) => {
3335
+ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defaultColor, useSvgTextRect = false) => {
2351
3336
  let paragraphCount;
2352
3337
  try {
2353
3338
  paragraphCount = getShapeParagraphCount(shape);
@@ -2365,6 +3350,10 @@ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defau
2365
3350
  } catch {
2366
3351
  effectiveBody = {
2367
3352
  anchor: getShapeTextAnchor(shape),
3353
+ anchorCentered: null,
3354
+ autoFit: getShapeTextAutoFit(shape),
3355
+ autoFitParams: getShapeTextAutoFitParams(shape),
3356
+ columns: getShapeTextColumns(shape),
2368
3357
  wrap: null,
2369
3358
  vert: getShapeTextDirection(shape),
2370
3359
  margins: getShapeTextMargins(shape) ?? {
@@ -2384,32 +3373,23 @@ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defau
2384
3373
  const tIns = margins.top ?? DEFAULT_INSET_Y;
2385
3374
  const rIns = margins.right ?? DEFAULT_INSET_X;
2386
3375
  const bIns = margins.bottom ?? DEFAULT_INSET_Y;
2387
- const pRect = presetTextRect(getShapePreset(shape));
2388
- const rectX = pRect ? bounds.x + pRect.l * bounds.w : bounds.x;
2389
- const rectY = pRect ? bounds.y + pRect.t * bounds.h : bounds.y;
2390
- const rectW = pRect ? (pRect.r - pRect.l) * bounds.w : bounds.w;
2391
- const rectH = pRect ? (pRect.b - pRect.t) * bounds.h : bounds.h;
2392
- let innerX = rectX + lIns;
2393
- let innerY = rectY + tIns;
2394
- let innerW = rectW - lIns - rIns;
2395
- let innerH = rectH - tIns - bIns;
2396
- if (pRect && (innerW <= 0 || innerH <= 0)) {
2397
- innerX = rectX;
2398
- innerY = rectY;
2399
- innerW = rectW;
2400
- innerH = rectH;
2401
- }
3376
+ const { x: innerX, y: innerY, w: innerW, h: innerH } = resolveTextBodyRect(getShapePreset(shape), bounds, {
3377
+ left: lIns,
3378
+ top: tIns,
3379
+ right: rIns,
3380
+ bottom: bIns
3381
+ }, shapeCustomTextRect(getShapeCustomGeometry(shape), getShapeBounds(shape)));
2402
3382
  if (innerW <= 0 || innerH <= 0) return null;
2403
- const svgTextRect = (v) => v === "none" || v === "upright" ? {
2404
- x: innerX,
2405
- y: innerY,
2406
- w: innerW,
2407
- h: innerH
2408
- } : {
3383
+ const svgTextRect = (v) => useSvgTextRect && v !== "none" && v !== "upright" ? {
2409
3384
  x: bounds.x + tIns,
2410
3385
  y: bounds.y + lIns,
2411
3386
  w: Math.max(0, bounds.w - tIns - bIns),
2412
3387
  h: Math.max(0, bounds.h - lIns - rIns)
3388
+ } : {
3389
+ x: innerX,
3390
+ y: innerY,
3391
+ w: innerW,
3392
+ h: innerH
2413
3393
  };
2414
3394
  const paraData = [];
2415
3395
  let hasAnyText = false;
@@ -2434,15 +3414,12 @@ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defau
2434
3414
  const align = effective.align ?? defaultAlign;
2435
3415
  const level = effective.level;
2436
3416
  const bulletStyle = getParagraphBullet(shape, p) ?? effective.bullet;
2437
- let bulletDetail = {
2438
- color: null,
2439
- sizePct: null,
2440
- sizePts: null,
2441
- font: null
3417
+ const bulletDetail = effective.bulletDetail ?? {
3418
+ ...getParagraphBulletStyle(pres, shape, p),
3419
+ colorFollowText: false,
3420
+ sizeFollowText: false,
3421
+ fontFollowText: false
2442
3422
  };
2443
- try {
2444
- bulletDetail = getParagraphBulletStyle(pres, shape, p);
2445
- } catch {}
2446
3423
  let bulletIsPicture = false;
2447
3424
  try {
2448
3425
  bulletIsPicture = isParagraphBulletPicture(shape, p);
@@ -2479,16 +3456,19 @@ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defau
2479
3456
  elements = [];
2480
3457
  }
2481
3458
  let rIdx = 0;
3459
+ let fieldIndex = 0;
3460
+ let breakIndex = 0;
2482
3461
  for (const el of elements) {
2483
3462
  if (el.kind === "br") {
3463
+ const fmt = getShapeRunFormatEffective(pres, shape, p, { breakIndex: breakIndex++ });
2484
3464
  runs.push({
2485
3465
  text: "\n",
2486
- fmt: null,
2487
- sizePt: defaultPt
3466
+ fmt,
3467
+ sizePt: fmt.size ?? defaultPt
2488
3468
  });
2489
3469
  continue;
2490
3470
  }
2491
- const txt = el.text;
3471
+ const txt = el.kind === "fld" && el.type === "slidenum" ? activeSlideNumber : el.text;
2492
3472
  let fmt = el.format;
2493
3473
  let href;
2494
3474
  let hrefTip;
@@ -2500,19 +3480,13 @@ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defau
2500
3480
  }
2501
3481
  try {
2502
3482
  href = getShapeRunHyperlink(shape, p, rIdx) ?? void 0;
2503
- if (!href) {
2504
- const act = getShapeRunClickAction(shape, p, rIdx);
2505
- if (act?.kind === "slide") {
2506
- const idx = getSlideIndex(pres, act.slide);
2507
- if (idx >= 0) href = `#slide-${idx + 1}`;
2508
- } else if (act?.kind === "url") href = act.url;
2509
- }
3483
+ if (!href) href = clickActionHref(pres, getShapeRunClickAction(shape, p, rIdx));
2510
3484
  if (href) hrefTip = getShapeRunHyperlinkTooltip(shape, p, rIdx) ?? void 0;
2511
3485
  } catch {
2512
3486
  href = void 0;
2513
3487
  }
2514
3488
  rIdx++;
2515
- }
3489
+ } else fmt = getShapeRunFormatEffective(pres, shape, p, { fieldIndex: fieldIndex++ });
2516
3490
  const sizePt = fmt?.size ?? defaultPt;
2517
3491
  if (txt) hasAnyText = true;
2518
3492
  runs.push({
@@ -2524,6 +3498,10 @@ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defau
2524
3498
  });
2525
3499
  }
2526
3500
  paraData.push({
3501
+ ...runs.length === 0 ? { emptySizePt: getShapeRunFormatEffective(pres, shape, p, null).size ?? defaultPt } : {},
3502
+ latinLineBreak: effective.latinLineBreak,
3503
+ tabStops: effective.tabStops,
3504
+ defaultTabSizeEmu: effective.defaultTabSizeEmu,
2527
3505
  align,
2528
3506
  level,
2529
3507
  bulletStyle,
@@ -2538,36 +3516,17 @@ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defau
2538
3516
  });
2539
3517
  }
2540
3518
  if (!hasAnyText) return null;
2541
- const authoredAutofit = getShapeTextAutoFitParams(shape);
3519
+ const authoredAutofit = effectiveBody.autoFitParams;
2542
3520
  let autoFitScale = authoredAutofit?.fontScale ?? 1;
2543
3521
  const lineHeightScale = 1 - (authoredAutofit?.lnSpcReduction ?? 0);
2544
3522
  LINE_HEIGHT * lineHeightScale;
2545
- const numberLabels = Array.from({ length: paraData.length }, () => null);
2546
- {
2547
- const counters = [];
2548
- const types = [];
2549
- for (let i = 0; i < paraData.length; i++) {
2550
- const para = paraData[i];
2551
- const num = bulletAutoNumType(para.bulletStyle);
2552
- const level = Math.max(0, para.level);
2553
- for (let l = num === null ? level : level + 1; l < counters.length; l++) {
2554
- counters[l] = 0;
2555
- types[l] = null;
2556
- }
2557
- if (num === null) continue;
2558
- if (types[level] !== num) {
2559
- counters[level] = 1;
2560
- types[level] = num;
2561
- } else counters[level] = (counters[level] ?? 0) + 1;
2562
- numberLabels[i] = formatAutoNum(num, counters[level]);
2563
- }
2564
- }
2565
- if (authoredAutofit && autoFitScale === 1) {
3523
+ const numberLabels = paragraphNumberLabels(paraData);
3524
+ if (authoredAutofit && autoFitScale === 1 && getShapeTextAutoFit(shape) === "normal") {
2566
3525
  const fitVert = verticalLayoutOf(effectiveBody.vert ?? getShapeTextDirection(shape));
2567
- const fitCols = getShapeTextColumns(shape);
3526
+ const fitCols = effectiveBody.columns;
2568
3527
  const fitColumns = fitVert === "none" && fitCols && fitCols.count >= 2 ? {
2569
3528
  count: fitCols.count,
2570
- gapPx: fitCols.gapEmu !== void 0 ? fitCols.gapEmu / EMU_PER_PX : 12
3529
+ gapPx: fitCols.gapEmu !== void 0 ? fitCols.gapEmu / EMU_PER_PX : 0
2571
3530
  } : null;
2572
3531
  const fitRect = svgTextRect(fitVert);
2573
3532
  const fitBoxPx = (fitVert === "cw90" || fitVert === "cw270" ? fitRect.w : fitRect.h) / EMU_PER_PX;
@@ -2582,6 +3541,7 @@ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defau
2582
3541
  themeFace,
2583
3542
  defaultColor,
2584
3543
  anchor: anchor === "center" || anchor === "bottom" ? anchor : "top",
3544
+ anchorCentered: effectiveBody.anchorCentered ?? false,
2585
3545
  wrap: effectiveBody.wrap !== "none",
2586
3546
  innerX: fitRect.x,
2587
3547
  innerY: fitRect.y,
@@ -2601,10 +3561,55 @@ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defau
2601
3561
  }
2602
3562
  autoFitScale = Math.max(AUTOFIT_FLOOR, s);
2603
3563
  }
3564
+ let anchorOffset = {
3565
+ x: 0,
3566
+ y: 0
3567
+ };
3568
+ if (effectiveBody.anchorCentered) {
3569
+ const vert = verticalLayoutOf(effectiveBody.vert ?? getShapeTextDirection(shape));
3570
+ const rect = svgTextRect(vert);
3571
+ const cols = effectiveBody.columns;
3572
+ const { anchorShift } = layoutCore(buildSvgTextInput({
3573
+ pres,
3574
+ shape,
3575
+ theme,
3576
+ paraData,
3577
+ numberLabels,
3578
+ lineHeightScale,
3579
+ defaultPt,
3580
+ themeFace,
3581
+ defaultColor,
3582
+ anchor,
3583
+ anchorCentered: true,
3584
+ wrap: effectiveBody.wrap !== "none",
3585
+ innerX: rect.x,
3586
+ innerY: rect.y,
3587
+ innerW: rect.w,
3588
+ innerH: rect.h,
3589
+ measure,
3590
+ vert,
3591
+ autoFitScale,
3592
+ columns: vert === "none" && cols && cols.count >= 2 ? {
3593
+ count: cols.count,
3594
+ gapPx: cols.gapEmu !== void 0 ? cols.gapEmu / EMU_PER_PX : 0
3595
+ } : null
3596
+ }), measure);
3597
+ anchorOffset = vert === "cw90" ? {
3598
+ x: 0,
3599
+ y: anchorShift
3600
+ } : vert === "cw270" ? {
3601
+ x: 0,
3602
+ y: -anchorShift
3603
+ } : {
3604
+ x: anchorShift,
3605
+ y: 0
3606
+ };
3607
+ }
2604
3608
  return {
2605
3609
  paraData,
2606
3610
  numberLabels,
2607
3611
  authoredAutofit,
3612
+ anchorOffset,
2608
3613
  autoFitScale,
2609
3614
  lineHeightScale,
2610
3615
  defaultPt,
@@ -2619,31 +3624,29 @@ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defau
2619
3624
  svgTextRect
2620
3625
  };
2621
3626
  };
2622
- const renderTextBody = (pres, shape, bounds, theme, phType, ctx) => {
2623
- const model = resolveTextBodyModel(pres, shape, bounds, theme, phType, ctx.measure, activeDeckTextColor);
2624
- if (model === null) return "";
2625
- const { paraData, numberLabels, authoredAutofit, autoFitScale, defaultPt, themeFace, effectiveDefaultFont, effectiveBody, anchor, innerX, innerY, innerW, innerH, svgTextRect } = model;
2626
- const defaultColor = activeDeckTextColor;
3627
+ const renderHtmlParagraphs = (paraData, numberLabels, theme, autoFitScale, defaultPt, defaultColor, wrap = true) => {
2627
3628
  const paragraphs = [];
2628
3629
  for (let pi = 0; pi < paraData.length; pi++) {
2629
3630
  const para = paraData[pi];
2630
- const runHtmls = para.runs.map((run) => {
3631
+ const runHtmls = para.runs.map((run, runIndex) => {
2631
3632
  let runFmt = run.fmt;
2632
3633
  if (run.href) {
2633
3634
  const hlinkColor = theme ? normalizeHex(theme.hyperlink) : "#0563C1";
3635
+ const { textFill: _linkFill, ...linkFmt } = runFmt ?? {};
2634
3636
  runFmt = {
2635
- ...runFmt,
3637
+ ...linkFmt,
2636
3638
  color: hlinkColor,
2637
3639
  underline: runFmt?.underline ?? true
2638
3640
  };
2639
3641
  }
2640
- const span = renderRun(run.text, runFmt, theme, run.sizePt * autoFitScale, run.fmt?.size === void 0);
3642
+ const span = renderRun(run.text, runFmt, theme, run.sizePt * autoFitScale, defaultColor, run.fmt?.size === void 0, run.text === "\n" && /[^\n]$/.test(para.runs[runIndex - 1]?.text ?? ""), para.lineSpacing?.kind === "pct" ? para.lineSpacing.value.toFixed(3) : para.lineSpacing?.kind === "pts" ? "0" : void 0);
2641
3643
  if (!run.href) return span;
2642
3644
  const targetAttrs = run.href.startsWith("#") ? "" : " target=\"_blank\" rel=\"noopener noreferrer\"";
2643
3645
  const titleAttr = run.hrefTip ? ` title="${escapeXml(run.hrefTip)}"` : "";
2644
3646
  return `<a href="${escapeXml(run.href)}"${targetAttrs}${titleAttr} style="color:inherit;text-decoration:inherit">${span}</a>`;
2645
3647
  });
2646
- let lineHeightCss = "";
3648
+ const firstRunPt = para.runs.find((run) => run.text !== "\n" && run.text !== "")?.sizePt ?? defaultPt;
3649
+ let lineHeightCss = para.emptySizePt !== void 0 ? `line-height:${LINE_HEIGHT}` : "";
2647
3650
  if (para.lineSpacing?.kind === "pct") lineHeightCss = `line-height:${para.lineSpacing.value.toFixed(3)}`;
2648
3651
  else if (para.lineSpacing?.kind === "pts") lineHeightCss = `line-height:${(para.lineSpacing.value * PX_PER_PT * autoFitScale).toFixed(2)}px`;
2649
3652
  const marginTopCss = para.spcBefPts !== null && para.spcBefPts > 0 ? `margin-top:${(para.spcBefPts * PX_PER_PT * autoFitScale).toFixed(2)}px` : "";
@@ -2652,7 +3655,12 @@ const renderTextBody = (pres, shape, bounds, theme, phType, ctx) => {
2652
3655
  const rightPx = para.indent.rightEmu !== null ? para.indent.rightEmu / EMU_PER_PX * autoFitScale : 0;
2653
3656
  const firstLinePx = para.indent.firstLineEmu !== null ? para.indent.firstLineEmu / EMU_PER_PX * autoFitScale : 0;
2654
3657
  const pStyles = [
2655
- marginTopCss || (marginBottomCss ? "" : "margin:0"),
3658
+ "margin:0",
3659
+ para.lineSpacing?.kind === "pts" ? `font-size:${(firstRunPt * PX_PER_PT * autoFitScale).toFixed(2)}px` : "font-size:0",
3660
+ ...para.lineSpacing?.kind === "pts" && para.runs[0]?.fmt?.font ? [`font-family:${escapeXml(para.runs[0].fmt.font)}, ${DEFAULT_FONT}`] : [],
3661
+ ...para.emptySizePt !== void 0 ? [`font-size:${(para.emptySizePt * PX_PER_PT * autoFitScale).toFixed(2)}px`] : [],
3662
+ ...para.runs.some((run) => run.text.includes(" ")) ? [`white-space:${wrap ? "pre-wrap" : "pre"}`, `tab-size:${((para.defaultTabSizeEmu ?? 914400) / EMU_PER_PX * autoFitScale).toFixed(2)}px`] : [],
3663
+ marginTopCss,
2656
3664
  marginBottomCss,
2657
3665
  "padding:0",
2658
3666
  `text-align:${ALIGNMENT_TO_CSS[para.align] ?? "left"}`,
@@ -2664,8 +3672,8 @@ const renderTextBody = (pres, shape, bounds, theme, phType, ctx) => {
2664
3672
  let prefix = "";
2665
3673
  const explicitChar = para.bulletStyle !== null && typeof para.bulletStyle === "object" && "char" in para.bulletStyle ? para.bulletStyle.char : null;
2666
3674
  const numberLabel = numberLabels[pi];
2667
- const showBullet = para.bulletStyle === "bullet" || explicitChar !== null || numberLabel !== null || para.bulletIsPicture || para.bulletStyle !== "none" && para.level > 0;
2668
- const baseBulletPx = (para.runs.find((r) => r.text !== "\n" && r.text !== "")?.sizePt ?? defaultPt) * PX_PER_PT * autoFitScale;
3675
+ const showBullet = para.runs.some((run) => run.text.length > 0) && (para.bulletStyle === "bullet" || explicitChar !== null || numberLabel !== null || para.bulletIsPicture || para.bulletStyle !== "none" && para.level > 0);
3676
+ const baseBulletPx = firstRunPt * PX_PER_PT * autoFitScale;
2669
3677
  if (showBullet && para.bulletImageHref) {
2670
3678
  const bulletPx = para.bulletDetail.sizePct !== null ? baseBulletPx * para.bulletDetail.sizePct : para.bulletDetail.sizePts !== null ? para.bulletDetail.sizePts * PX_PER_PT * autoFitScale : baseBulletPx;
2671
3679
  const imgStyles = [
@@ -2680,22 +3688,32 @@ const renderTextBody = (pres, shape, bounds, theme, phType, ctx) => {
2680
3688
  const char = para.bulletIsPicture ? "■" : numberLabel ?? explicitChar ?? bulletChar(para.level);
2681
3689
  const bulletStyles = [`margin-right:${(.4 * defaultPt * PX_PER_PT * autoFitScale).toFixed(2)}px`];
2682
3690
  bulletStyles.push(`color:${bulletFillOf(para, theme, defaultColor)}`);
2683
- if (para.bulletDetail.sizePct !== null) bulletStyles.push(`font-size:${(para.bulletDetail.sizePct * 100).toFixed(1)}%`);
3691
+ if (para.bulletDetail.sizePct !== null) bulletStyles.push(`font-size:${(baseBulletPx * para.bulletDetail.sizePct).toFixed(2)}px`);
2684
3692
  else if (para.bulletDetail.sizePts !== null) bulletStyles.push(`font-size:${(para.bulletDetail.sizePts * PX_PER_PT * autoFitScale).toFixed(2)}px`);
2685
3693
  else bulletStyles.push(`font-size:${baseBulletPx.toFixed(2)}px`);
2686
- bulletStyles.push(para.bulletDetail.font ? `font-family:${escapeXml(para.bulletDetail.font)}, ${DEFAULT_FONT}` : `font-family:${DEFAULT_BULLET_FONT}, ${DEFAULT_FONT}`);
3694
+ const bulletFont = para.bulletDetail.fontFollowText ? para.runs.find((r) => r.text !== "\n" && r.text !== "")?.fmt?.font ?? DEFAULT_BULLET_FONT : para.bulletDetail.font ?? DEFAULT_BULLET_FONT;
3695
+ bulletStyles.push(`font-family:${escapeXml(bulletFont)}, ${DEFAULT_FONT}`);
2687
3696
  prefix = `<span style="${bulletStyles.join(";")}">${escapeXml(char)}</span>`;
2688
3697
  }
2689
- paragraphs.push(`<p style="${pStyles.join(";")}">${prefix}${runHtmls.join("") || "&#8203;"}</p>`);
3698
+ paragraphs.push(`<p data-pptx-paragraph="${pi}" style="${pStyles.join(";")}">${prefix}${runHtmls.join("") || "&#8203;"}</p>`);
2690
3699
  }
3700
+ return paragraphs;
3701
+ };
3702
+ const renderTextBody = (pres, shape, bounds, theme, phType, ctx, output = "body") => {
3703
+ const model = resolveTextBodyModel(pres, shape, bounds, theme, phType, ctx.measure, activeDeckTextColor, ctx.mode === "svg" && output === "body");
3704
+ if (model === null) return "";
3705
+ const { paraData, numberLabels, authoredAutofit, autoFitScale, defaultPt, themeFace, effectiveDefaultFont, effectiveBody, anchor, innerX, innerY, innerW, innerH, svgTextRect } = model;
3706
+ const defaultColor = activeDeckTextColor;
3707
+ const paragraphs = renderHtmlParagraphs(paraData, numberLabels, theme, autoFitScale, defaultPt, defaultColor, effectiveBody.wrap !== "none");
2691
3708
  const justify = ANCHOR_TO_CSS[anchor] ?? "flex-start";
2692
- if (ctx.mode === "svg") {
3709
+ const customTabs = paraData.some((para) => para.tabStops?.length && para.runs.some((run) => run.text.includes(" ")));
3710
+ if (ctx.mode === "svg" || customTabs) {
2693
3711
  const svgLineScale = 1 - (authoredAutofit?.lnSpcReduction ?? 0);
2694
3712
  const svgVert = verticalLayoutOf(effectiveBody.vert ?? getShapeTextDirection(shape));
2695
- const svgCols = getShapeTextColumns(shape);
3713
+ const svgCols = effectiveBody.columns;
2696
3714
  const svgColumns = svgVert === "none" && svgCols && svgCols.count >= 2 ? {
2697
3715
  count: svgCols.count,
2698
- gapPx: svgCols.gapEmu !== void 0 ? svgCols.gapEmu / EMU_PER_PX : 12
3716
+ gapPx: svgCols.gapEmu !== void 0 ? svgCols.gapEmu / EMU_PER_PX : 0
2699
3717
  } : null;
2700
3718
  const { x: vInnerX, y: vInnerY, w: vInnerW, h: vInnerH } = svgTextRect(svgVert);
2701
3719
  if (vInnerW <= 0 || vInnerH <= 0) return "";
@@ -2710,15 +3728,46 @@ const renderTextBody = (pres, shape, bounds, theme, phType, ctx) => {
2710
3728
  themeFace,
2711
3729
  defaultColor,
2712
3730
  anchor: anchor === "center" || anchor === "bottom" ? anchor : "top",
3731
+ anchorCentered: effectiveBody.anchorCentered ?? false,
2713
3732
  wrap: effectiveBody.wrap !== "none",
2714
3733
  innerX: vInnerX,
2715
3734
  innerY: vInnerY,
2716
3735
  innerW: vInnerW,
2717
3736
  innerH: vInnerH,
2718
- measure: ctx.measure,
3737
+ measure: ctx.mode === "svg" ? ctx.measure : browserTextMeasurer() ?? ctx.measure,
3738
+ ...ctx.mode === "foreignObject" ? { resolveFamily: browserFontFamily } : {},
2719
3739
  vert: svgVert,
2720
3740
  columns: svgColumns
2721
3741
  };
3742
+ if (output === "effects") {
3743
+ const effectArgs = {
3744
+ ...svgArgsBase,
3745
+ autoFitScale: authoredAutofit ? autoFitScale : 1,
3746
+ effectsOnly: true,
3747
+ resolveFamily: browserFontFamily
3748
+ };
3749
+ const effects = [
3750
+ paraData.some((para) => para.runs.some((run) => run.fmt?.reflection != null)) ? buildAndLayoutSvgText({
3751
+ ...effectArgs,
3752
+ effectKind: "reflection"
3753
+ }) : "",
3754
+ paraData.some((para) => para.runs.some(hasPaintedTextFill)) ? buildAndLayoutSvgText({
3755
+ ...effectArgs,
3756
+ effectKind: "fill"
3757
+ }) : "",
3758
+ paraData.some((para) => para.runs.some((run) => run.fmt?.innerShadow != null)) ? buildAndLayoutSvgText({
3759
+ ...effectArgs,
3760
+ effectKind: "innerShadow"
3761
+ }) : ""
3762
+ ].join("");
3763
+ const bodyRotDegSvg = getShapeTextBodyRotationDeg(shape);
3764
+ if (bodyRotDegSvg !== null && bodyRotDegSvg !== 0) {
3765
+ const pivotX = vInnerX + vInnerW / 2;
3766
+ const pivotY = vInnerY + vInnerH / 2;
3767
+ return `<g transform="rotate(${bodyRotDegSvg} ${E(pivotX)} ${E(pivotY)})">${effects}</g>`;
3768
+ }
3769
+ return effects;
3770
+ }
2722
3771
  const svgScale = authoredAutofit ? autoFitScale : 1;
2723
3772
  const svgInner = buildAndLayoutSvgText({
2724
3773
  ...svgArgsBase,
@@ -2732,31 +3781,144 @@ const renderTextBody = (pres, shape, bounds, theme, phType, ctx) => {
2732
3781
  }
2733
3782
  return svgInner;
2734
3783
  }
2735
- const vert = effectiveBody.vert ?? getShapeTextDirection(shape);
2736
- let writingMode = "";
2737
- let extraTransform = "";
2738
- if (vert === "vert" || vert === "eaVert") writingMode = "writing-mode:vertical-rl";
2739
- else if (vert === "vert270" || vert === "mongolianVert") {
2740
- writingMode = "writing-mode:vertical-lr";
2741
- if (vert === "vert270") extraTransform = ";transform:rotate(180deg)";
2742
- } else if (vert === "wordArtVert") writingMode = "writing-mode:vertical-rl;text-orientation:upright";
2743
- else if (vert === "wordArtVertRtl") writingMode = "writing-mode:vertical-rl;text-orientation:upright;direction:rtl";
2744
- const cols = getShapeTextColumns(shape);
2745
- let colStyles = "";
2746
- if (cols && cols.count >= 2) {
2747
- const gapPx = cols.gapEmu !== void 0 ? (cols.gapEmu / EMU_PER_PX).toFixed(2) : "12";
2748
- colStyles = `;column-count:${cols.count};column-gap:${gapPx}px`;
2749
- }
3784
+ const vertical = verticalTextStyle(effectiveBody.vert ?? getShapeTextDirection(shape));
3785
+ const writingMode = vertical.declarations;
3786
+ const extraTransform = vertical.transform ? `;transform:${vertical.transform}` : "";
3787
+ const columns = textColumnsStyle(effectiveBody.columns);
3788
+ const colStyles = columns ? `;${columns}` : "";
2750
3789
  const vertStyles = (writingMode ? `;${writingMode}${extraTransform}` : "") + colStyles;
2751
- const body = `<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;flex-direction:column;justify-content:${justify};width:100%;height:100%;box-sizing:border-box;overflow:visible;font-family:${effectiveDefaultFont};color:${defaultColor};${effectiveBody.wrap === "none" ? "white-space:nowrap" : "word-break:break-word"}${vertStyles}">${paragraphs.join("")}</div>`;
3790
+ const wrapStyle = effectiveBody.wrap === "none" ? "white-space:nowrap" : "word-break:break-word";
3791
+ const content = paragraphs.join("");
3792
+ const body = `<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;flex-direction:column;justify-content:${justify};width:100%;height:100%;box-sizing:border-box;overflow:visible;font-family:${effectiveDefaultFont};color:${defaultColor};${`translate:${model.anchorOffset.x}px ${model.anchorOffset.y}px;`}${wrapStyle}${vertStyles}">${content}</div>`;
2752
3793
  const foreign = `<foreignObject x="${E(innerX)}" y="${E(innerY)}" width="${E(innerW)}" height="${E(innerH)}" overflow="visible">${body}</foreignObject>`;
3794
+ const hasCharacterEffect = paraData.some((para) => para.runs.some((run) => run.fmt?.reflection !== void 0 && run.fmt.reflection !== null || run.fmt?.innerShadow !== void 0 && run.fmt.innerShadow !== null || hasPaintedTextFill(run)));
3795
+ let reflectionOverlay = "";
3796
+ let fillOverlay = "";
3797
+ let innerShadowOverlay = "";
3798
+ if (hasCharacterEffect) {
3799
+ const svgVert = verticalLayoutOf(effectiveBody.vert ?? getShapeTextDirection(shape));
3800
+ const { x: rX, y: rY, w: rW, h: rH } = svgTextRect(svgVert);
3801
+ const effectArgs = {
3802
+ pres,
3803
+ shape,
3804
+ theme,
3805
+ paraData,
3806
+ numberLabels,
3807
+ autoFitScale: authoredAutofit ? autoFitScale : 1,
3808
+ lineHeightScale: 1 - (authoredAutofit?.lnSpcReduction ?? 0),
3809
+ defaultPt,
3810
+ themeFace,
3811
+ defaultColor,
3812
+ anchor: anchor === "center" || anchor === "bottom" ? anchor : "top",
3813
+ anchorCentered: effectiveBody.anchorCentered ?? false,
3814
+ wrap: effectiveBody.wrap !== "none",
3815
+ innerX: rX,
3816
+ innerY: rY,
3817
+ innerW: rW,
3818
+ innerH: rH,
3819
+ measure: browserTextMeasurer() ?? ctx.measure,
3820
+ vert: svgVert,
3821
+ columns: null,
3822
+ effectsOnly: true,
3823
+ resolveFamily: browserFontFamily
3824
+ };
3825
+ const hasCharacterReflection = paraData.some((para) => para.runs.some((run) => run.fmt?.reflection !== void 0 && run.fmt.reflection !== null));
3826
+ const hasCharacterInnerShadow = paraData.some((para) => para.runs.some((run) => run.fmt?.innerShadow !== void 0 && run.fmt.innerShadow !== null));
3827
+ if (hasCharacterReflection) reflectionOverlay = buildAndLayoutSvgText({
3828
+ ...effectArgs,
3829
+ effectKind: "reflection"
3830
+ });
3831
+ if (paraData.some((para) => para.runs.some(hasPaintedTextFill))) fillOverlay = buildAndLayoutSvgText({
3832
+ ...effectArgs,
3833
+ effectKind: "fill"
3834
+ });
3835
+ if (hasCharacterInnerShadow) innerShadowOverlay = buildAndLayoutSvgText({
3836
+ ...effectArgs,
3837
+ effectKind: "innerShadow"
3838
+ });
3839
+ }
2753
3840
  const bodyRotDeg = getShapeTextBodyRotationDeg(shape);
2754
3841
  if (bodyRotDeg !== null && bodyRotDeg !== 0) {
2755
3842
  const pivotX = innerX + innerW / 2;
2756
3843
  const pivotY = innerY + innerH / 2;
2757
- return `<g transform="rotate(${bodyRotDeg} ${E(pivotX)} ${E(pivotY)})">${foreign}</g>`;
3844
+ const content = output === "effects" ? `${reflectionOverlay}${fillOverlay}${innerShadowOverlay}` : `${reflectionOverlay}${foreign}${fillOverlay}${innerShadowOverlay}`;
3845
+ return `<g transform="rotate(${bodyRotDeg} ${E(pivotX)} ${E(pivotY)})">${content}</g>`;
2758
3846
  }
2759
- return foreign;
3847
+ return output === "effects" ? reflectionOverlay + fillOverlay + innerShadowOverlay : reflectionOverlay + foreign + fillOverlay + innerShadowOverlay;
3848
+ };
3849
+ /**
3850
+ * Paint only character reflection/inner-shadow effects for an editable text
3851
+ * body. The caller supplies the local bounds of the editable box in EMUs. For
3852
+ * a table, `cell` selects a zero-based row/column and bounds describe that cell.
3853
+ * Body rotation and vertical text handling remain canonical here; the editor only
3854
+ * supplies the outer shape/group transform around this SVG.
3855
+ */
3856
+ const renderTextEffectsSvg = (pres, slide, shape, bounds, opts = {}) => {
3857
+ if (opts.cell) return renderTableCellEffectsSvgInternal(pres, slide, shape, opts.cell, bounds, opts);
3858
+ activeColorMap = getEffectiveColorMap(slide);
3859
+ activeDeckTextColor = resolveDeckBodyTextColor(slide) ?? "#000000";
3860
+ const theme = getPresentationTheme(pres);
3861
+ const localBounds = {
3862
+ x: 0,
3863
+ y: 0,
3864
+ w: bounds.w,
3865
+ h: bounds.h
3866
+ };
3867
+ const ctx = {
3868
+ groupScale: {
3869
+ sx: 1,
3870
+ sy: 1
3871
+ },
3872
+ groupReflected: false,
3873
+ mode: "foreignObject",
3874
+ measure: opts.measureText ?? browserTextMeasurer() ?? defaultMeasurer,
3875
+ ownShapes: true,
3876
+ background: {
3877
+ id: "",
3878
+ width: localBounds.w / EMU_PER_PX,
3879
+ height: localBounds.h / EMU_PER_PX
3880
+ },
3881
+ inverseGroupTransform: ""
3882
+ };
3883
+ const body = renderTextBody(pres, shape, localBounds, theme, getShapePlaceholderType(shape), ctx, "effects");
3884
+ if (!body) return "";
3885
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${E(localBounds.w)} ${E(localBounds.h)}" width="100%" height="100%" overflow="visible" aria-hidden="true">${body}</svg>`;
3886
+ };
3887
+ /** Paint only character effects for an editable table cell. */
3888
+ const renderTableCellEffectsSvgInternal = (pres, slide, shape, cellPosition, bounds, opts = {}) => {
3889
+ activeColorMap = getEffectiveColorMap(slide);
3890
+ activeDeckTextColor = resolveDeckBodyTextColor(slide) ?? "#000000";
3891
+ const cell = getTableCells(shape)[cellPosition.row]?.[cellPosition.col];
3892
+ if (!cell) return "";
3893
+ const paragraphs = getTableCellParagraphs(cell);
3894
+ const theme = getPresentationTheme(pres);
3895
+ const margins = getTableCellMargins(cell);
3896
+ const anchor = getTableCellAnchor(cell) ?? "top";
3897
+ const localBounds = {
3898
+ x: 0,
3899
+ y: 0,
3900
+ w: bounds.w,
3901
+ h: bounds.h
3902
+ };
3903
+ const ctx = {
3904
+ groupScale: {
3905
+ sx: 1,
3906
+ sy: 1
3907
+ },
3908
+ groupReflected: false,
3909
+ mode: "foreignObject",
3910
+ measure: opts.measureText ?? browserTextMeasurer() ?? defaultMeasurer,
3911
+ ownShapes: true,
3912
+ background: {
3913
+ id: "",
3914
+ width: localBounds.w / EMU_PER_PX,
3915
+ height: localBounds.h / EMU_PER_PX
3916
+ },
3917
+ inverseGroupTransform: ""
3918
+ };
3919
+ const body = renderTableCellText(cell, paragraphs, 0, 0, localBounds.w, localBounds.h, activeDeckTextColor, pres, shape, theme, getPresentationFonts(pres)?.minorLatin ?? null, ctx, anchor, margins, true);
3920
+ if (!body) return "";
3921
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${E(localBounds.w)} ${E(localBounds.h)}" width="100%" height="100%" overflow="visible" aria-hidden="true">${body}</svg>`;
2760
3922
  };
2761
3923
  const E = (n) => (n / EMU_PER_PX).toFixed(2);
2762
3924
  const px = (n) => n.toFixed(2);
@@ -2781,6 +3943,16 @@ const accentSequence = (theme) => {
2781
3943
  return hexes.length > 0 ? hexes : fallbacks;
2782
3944
  };
2783
3945
  const DEFAULT_CHART_TITLE_PT = 13;
3946
+ const chartText = (f, x, y, attrs, content) => {
3947
+ if (f.reflected) {
3948
+ const reflection = `translate(${px(2 * x)} 0) scale(-1 1)`;
3949
+ const rotation = /transform="([^"]*)"/.exec(attrs);
3950
+ attrs = rotation ? attrs.replace(rotation[0], `transform="${rotation[1]} ${reflection}"`) : `${attrs} transform="${reflection}"`;
3951
+ const anchor = /text-anchor="(start|middle|end)"/.exec(attrs);
3952
+ attrs = anchor ? attrs.replace(anchor[0], `text-anchor="${anchor[1] === "start" ? "end" : anchor[1] === "end" ? "start" : "middle"}"`) : `${attrs} text-anchor="end"`;
3953
+ }
3954
+ return `<text x="${px(x)}" y="${px(y)}" ${attrs}>${content}</text>`;
3955
+ };
2784
3956
  const layoutChart = (xEmu, yEmu, wEmu, hEmu, hasTitle, hasAxes, titleOverlay = false, legendOverlay = false, hasLegend = true, titlePx = DEFAULT_CHART_TITLE_PT * PX_PER_PT, plotAreaLayout) => {
2785
3957
  const x = xEmu / EMU_PER_PX;
2786
3958
  const y = yEmu / EMU_PER_PX;
@@ -2814,14 +3986,25 @@ const niceStep = (range, target = 5) => {
2814
3986
  const m = rawStep / base;
2815
3987
  return (m < 1.5 ? 1 : m < 3 ? 2 : m < 7 ? 5 : 10) * base;
2816
3988
  };
3989
+ const MAX_AXIS_TICKS = 1e3;
3990
+ const intervalTicks = (min, max, step) => {
3991
+ if (!(step > 0) || !Number.isFinite(step)) return [];
3992
+ const start = Math.ceil(min / step) * step;
3993
+ const count = Math.floor((max - start) / step + 1e-9) + 1;
3994
+ if (!Number.isFinite(count) || count <= 0 || count > MAX_AXIS_TICKS) return [];
3995
+ const ticks = [];
3996
+ for (let i = 0; i < count; i++) {
3997
+ const value = start + i * step;
3998
+ if (Number.isFinite(value) && value !== ticks.at(-1)) ticks.push(value);
3999
+ }
4000
+ return ticks;
4001
+ };
2817
4002
  const niceTicks = (min, max, target = 5) => {
2818
4003
  const range = max - min;
2819
4004
  if (range <= 0) return [min];
2820
4005
  const step = niceStep(range, target);
2821
- const start = Math.ceil(min / step) * step;
2822
- const ticks = [];
2823
- for (let v = start; v <= max + step / 2; v += step) ticks.push(v);
2824
- return ticks;
4006
+ const ticks = intervalTicks(min, max + step / 2, step);
4007
+ return ticks.length > 0 ? ticks : [min, max];
2825
4008
  };
2826
4009
  const formatTick = (v) => {
2827
4010
  if (v === 0) return "0";
@@ -2901,12 +4084,8 @@ const axisTickAttrs = (style) => {
2901
4084
  return `font-family="sans-serif" font-size="${chartFontPx(sz)}" fill="${fill}"${weight}${italic}`;
2902
4085
  };
2903
4086
  const renderValueAxis = (f, axis) => {
2904
- const ticks = axis.majorUnit ? (() => {
2905
- const out = [];
2906
- const start = Math.ceil(axis.min / axis.majorUnit) * axis.majorUnit;
2907
- for (let t = start; t <= axis.max + 1e-9; t += axis.majorUnit) out.push(t);
2908
- return out.length > 0 ? out : niceTicks(axis.min, axis.max);
2909
- })() : niceTicks(axis.min, axis.max);
4087
+ const authoredTicks = axis.majorUnit ? intervalTicks(axis.min, axis.max, axis.majorUnit) : [];
4088
+ const ticks = authoredTicks.length > 0 ? authoredTicks : niceTicks(axis.min, axis.max);
2910
4089
  const out = [];
2911
4090
  const range = axis.max - axis.min || 1;
2912
4091
  const fmtTick = (t) => axis.percent ? `${Math.round(t * 100)}%` : formatAxisLabel(axis.displayUnits ? t / DISPLAY_UNIT_DIVISOR[axis.displayUnits] : t, axis.numberFormat);
@@ -2915,6 +4094,26 @@ const renderValueAxis = (f, axis) => {
2915
4094
  const axisColor = axis.lineColor ?? DEFAULT_AXIS_COLOR;
2916
4095
  const tickMark = axis.majorTickMark ?? "out";
2917
4096
  const tickLen = AXIS_TICK_LEN;
4097
+ if (axis.minorGridlines) {
4098
+ const automaticMinorUnit = (ticks.length > 1 ? ticks[1] - ticks[0] : range) / 5;
4099
+ const authoredMinorTicks = axis.minorUnit ? intervalTicks(axis.min, axis.max, axis.minorUnit) : [];
4100
+ const minorTicks = authoredMinorTicks.length > 0 ? authoredMinorTicks : intervalTicks(axis.min, axis.max, automaticMinorUnit);
4101
+ const positionKey = (value) => ((value - axis.min) / range).toFixed(10);
4102
+ const majorPositions = new Set(ticks.map(positionKey));
4103
+ const stroke = axis.minorGridlineColor ?? DEFAULT_GRID_COLOR;
4104
+ const width = axis.minorGridlineWidthEmu === void 0 ? .5 : axis.minorGridlineWidthEmu / EMU_PER_PX;
4105
+ for (const t of minorTicks) {
4106
+ if (majorPositions.has(positionKey(t))) continue;
4107
+ const fraction = (t - axis.min) / range;
4108
+ if (axis.orientation === "vertical") {
4109
+ const y = f.plotY + f.plotH - fraction * f.plotH;
4110
+ out.push(`<line data-chart-gridline="minor" x1="${px(f.plotX)}" y1="${px(y)}" x2="${px(f.plotX + f.plotW)}" y2="${px(y)}" stroke="${stroke}" stroke-width="${width}"/>`);
4111
+ } else {
4112
+ const x = f.plotX + fraction * f.plotW;
4113
+ out.push(`<line data-chart-gridline="minor" x1="${px(x)}" y1="${px(f.plotY)}" x2="${px(x)}" y2="${px(f.plotY + f.plotH)}" stroke="${stroke}" stroke-width="${width}"/>`);
4114
+ }
4115
+ }
4116
+ }
2918
4117
  for (const t of ticks) if (axis.orientation === "vertical") {
2919
4118
  const yp = f.plotY + f.plotH - (t - axis.min) / range * f.plotH;
2920
4119
  const onRight = axis.side === "right";
@@ -2930,7 +4129,7 @@ const renderValueAxis = (f, axis) => {
2930
4129
  const labelX = onRight ? edgeX + 4 : edgeX - 4;
2931
4130
  const rot = axis.labelRotationDeg ?? 0;
2932
4131
  const transform = rot ? ` transform="rotate(${rot} ${px(labelX)} ${px(yp)})"` : "";
2933
- out.push(`<text x="${px(labelX)}" y="${px(yp)}" text-anchor="${onRight ? "start" : "end"}" dominant-baseline="middle" ${axisTickAttrs(axis.labelStyle)}${transform}>${escapeXml(fmtTick(t))}</text>`);
4132
+ out.push(chartText(f, labelX, yp, `text-anchor="${onRight ? "start" : "end"}" dominant-baseline="middle" ${axisTickAttrs(axis.labelStyle)}${transform}`, `${escapeXml(fmtTick(t))}`));
2934
4133
  } else {
2935
4134
  const xp = f.plotX + (t - axis.min) / range * f.plotW;
2936
4135
  if (showGrid) out.push(`<line x1="${px(xp)}" y1="${px(f.plotY)}" x2="${px(xp)}" y2="${px(f.plotY + f.plotH)}" stroke="${gridStroke}" stroke-width="0.5"/>`);
@@ -2943,15 +4142,15 @@ const renderValueAxis = (f, axis) => {
2943
4142
  const horizLabelY = f.plotY + f.plotH + 12;
2944
4143
  const rotH = axis.labelRotationDeg ?? 0;
2945
4144
  const transformH = rotH ? ` transform="rotate(${rotH} ${px(xp)} ${px(horizLabelY)})"` : "";
2946
- out.push(`<text x="${px(xp)}" y="${px(horizLabelY)}" text-anchor="middle" dominant-baseline="middle" ${axisTickAttrs(axis.labelStyle)}${transformH}>${escapeXml(fmtTick(t))}</text>`);
4145
+ out.push(chartText(f, xp, horizLabelY, `text-anchor="middle" dominant-baseline="middle" ${axisTickAttrs(axis.labelStyle)}${transformH}`, `${escapeXml(fmtTick(t))}`));
2947
4146
  }
2948
4147
  if (axis.displayUnits) {
2949
4148
  const lbl = DISPLAY_UNIT_LABEL[axis.displayUnits];
2950
4149
  if (axis.orientation === "vertical") {
2951
4150
  const lblX = f.plotX - 26;
2952
4151
  const lblY = f.plotY + f.plotH / 2;
2953
- out.push(`<text x="${px(lblX)}" y="${px(lblY)}" text-anchor="middle" font-family="sans-serif" font-size="9" fill="#6B7280" font-style="italic" transform="rotate(-90 ${px(lblX)} ${px(lblY)})">${escapeXml(lbl)}</text>`);
2954
- } else out.push(`<text x="${px(f.plotX + f.plotW)}" y="${px(f.plotY + f.plotH + 22)}" text-anchor="end" font-family="sans-serif" font-size="9" fill="#6B7280" font-style="italic">${escapeXml(lbl)}</text>`);
4152
+ out.push(chartText(f, lblX, lblY, `text-anchor="middle" font-family="sans-serif" font-size="9" fill="#6B7280" font-style="italic" transform="rotate(-90 ${px(lblX)} ${px(lblY)})"`, `${escapeXml(lbl)}`));
4153
+ } else out.push(chartText(f, f.plotX + f.plotW, f.plotY + f.plotH + 22, `text-anchor="end" font-family="sans-serif" font-size="9" fill="#6B7280" font-style="italic"`, `${escapeXml(lbl)}`));
2955
4154
  }
2956
4155
  if (!axis.lineHidden) if (axis.orientation === "vertical") out.push(`<line x1="${px(f.plotX)}" y1="${px(f.plotY)}" x2="${px(f.plotX)}" y2="${px(f.plotY + f.plotH)}" stroke="${axisColor}" stroke-width="1"/>`);
2957
4156
  else {
@@ -2975,7 +4174,7 @@ const renderCategoryAxis = (f, orientation, cats, pointCount, skip = 1, labelSty
2975
4174
  const truncated = labels[i] !== void 0 && labels[i].length > truncLen ? `${labels[i].slice(0, truncLen - 2)}…` : labels[i] ?? "";
2976
4175
  const transform = labelRotationDeg && labelRotationDeg !== 0 ? ` transform="rotate(${labelRotationDeg} ${px(cx)} ${px(cy)})"` : "";
2977
4176
  const anchor = labelAlign === "l" ? "start" : labelAlign === "r" ? "end" : labelAlign === "ctr" ? "middle" : labelRotationDeg && labelRotationDeg > 0 ? "end" : labelRotationDeg && labelRotationDeg < 0 ? "start" : "middle";
2978
- out.push(`<text x="${px(cx)}" y="${px(cy)}" text-anchor="${anchor}" dominant-baseline="middle" ${axisTickAttrs(labelStyle)}${transform}>${escapeXml(truncated)}</text>`);
4177
+ out.push(chartText(f, cx, cy, `text-anchor="${anchor}" dominant-baseline="middle" ${axisTickAttrs(labelStyle)}${transform}`, `${escapeXml(truncated)}`));
2979
4178
  }
2980
4179
  } else {
2981
4180
  const step = pointCount > 0 ? f.plotH / pointCount : 0;
@@ -2986,7 +4185,7 @@ const renderCategoryAxis = (f, orientation, cats, pointCount, skip = 1, labelSty
2986
4185
  const lx = f.plotX - 4;
2987
4186
  const truncated = labels[i] !== void 0 && labels[i].length > truncLen ? `${labels[i].slice(0, truncLen - 2)}…` : labels[i] ?? "";
2988
4187
  const transform = labelRotationDeg && labelRotationDeg !== 0 ? ` transform="rotate(${labelRotationDeg} ${px(lx)} ${px(cy)})"` : "";
2989
- out.push(`<text x="${px(lx)}" y="${px(cy)}" text-anchor="end" dominant-baseline="middle" ${axisTickAttrs(labelStyle)}${transform}>${escapeXml(truncated)}</text>`);
4188
+ out.push(chartText(f, lx, cy, `text-anchor="end" dominant-baseline="middle" ${axisTickAttrs(labelStyle)}${transform}`, `${escapeXml(truncated)}`));
2990
4189
  }
2991
4190
  }
2992
4191
  const axisColor = lineColor ?? DEFAULT_AXIS_COLOR;
@@ -3060,7 +4259,7 @@ const renderChartTitle = (f, title, style) => {
3060
4259
  const fill = style?.color ?? "#1F2937";
3061
4260
  const weight = style?.bold === false ? "400" : "600";
3062
4261
  const fontStyleAttr = style?.italic ? " font-style=\"italic\"" : "";
3063
- return `<text x="${px(f.x + f.w / 2)}" y="${px(f.titleY)}" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif" font-size="${chartFontPx(sz)}" fill="${fill}" font-weight="${weight}"${fontStyleAttr}>${escapeXml(title)}</text>`;
4262
+ return chartText(f, f.x + f.w / 2, f.titleY, `text-anchor="middle" dominant-baseline="middle" font-family="sans-serif" font-size="${chartFontPx(sz)}" fill="${fill}" font-weight="${weight}"${fontStyleAttr}`, `${escapeXml(title)}`);
3064
4263
  };
3065
4264
  const FULLWIDTH_CHAR_PATTERN = /[\u1100-\u115F\u2E80-\u303E\u3041-\u33FF\u3400-\u4DBF\u4E00-\u9FFF\uA000-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6]/u;
3066
4265
  const approxLegendTextPx = (text, fontPx) => {
@@ -3095,7 +4294,7 @@ const renderChartLegend = (f, names, colors, position = "b", textStyle, markers)
3095
4294
  const rowY = position === "b" ? f.legendY : f.y + 12;
3096
4295
  let cursor = f.x + Math.max(0, (f.w - naturalTotal * scale) / 2);
3097
4296
  for (let i = 0; i < names.length; i++) {
3098
- out.push(swatch(i, cursor, rowY - 4), `<text x="${px(cursor + swatchGapPx * scale)}" y="${px(rowY)}" dominant-baseline="middle" ${effAttrs}>${escapeXml(names[i] ?? `Series ${i + 1}`)}</text>`);
4297
+ out.push(swatch(i, cursor, rowY - 4), chartText(f, cursor + swatchGapPx * scale, rowY, `dominant-baseline="middle" ${effAttrs}`, `${escapeXml(names[i] ?? `Series ${i + 1}`)}`));
3099
4298
  cursor += (swatchGapPx + (labelWidths[i] ?? 0) + itemGapPx) * scale;
3100
4299
  }
3101
4300
  return out.join("");
@@ -3108,7 +4307,7 @@ const renderChartLegend = (f, names, colors, position = "b", textStyle, markers)
3108
4307
  const xCol = position === "l" ? f.x + 6 : f.x + f.w - rightColW;
3109
4308
  for (let i = 0; i < names.length; i++) {
3110
4309
  const yp = yStart + i * lineH;
3111
- out.push(swatch(i, xCol, yp - 4), `<text x="${px(xCol + 14)}" y="${px(yp + 4)}" dominant-baseline="middle" ${textAttrs}>${escapeXml(names[i] ?? `Series ${i + 1}`)}</text>`);
4310
+ out.push(swatch(i, xCol, yp - 4), chartText(f, xCol + 14, yp + 4, `dominant-baseline="middle" ${textAttrs}`, `${escapeXml(names[i] ?? `Series ${i + 1}`)}`));
3112
4311
  }
3113
4312
  return out.join("");
3114
4313
  };
@@ -3120,6 +4319,50 @@ const pointCount = (spec) => {
3120
4319
  };
3121
4320
  const chartFillOpacityAttr = (opacity = 1) => opacity === 1 ? "" : ` fill-opacity="${opacity.toFixed(3)}"`;
3122
4321
  const chartPointBaseColor = (spec, colors, seriesIndex, pointIndex) => spec.series[seriesIndex]?.pointColors?.[pointIndex] ?? (spec.varyColors && spec.series.length === 1 ? colors[pointIndex % colors.length] : spec.series[seriesIndex]?.color ?? colors[seriesIndex % colors.length]);
4322
+ const columnLabelLayout = (y, h, v, pos) => {
4323
+ let coordinate;
4324
+ let fill = "#374151";
4325
+ if (pos === "ctr") {
4326
+ coordinate = y + h / 2 + 3;
4327
+ fill = "#FFFFFF";
4328
+ } else if (pos === "inEnd") {
4329
+ coordinate = v >= 0 ? y + 9 : y + h - 3;
4330
+ fill = "#FFFFFF";
4331
+ } else if (pos === "inBase") {
4332
+ coordinate = v >= 0 ? y + h - 3 : y + 9;
4333
+ fill = "#FFFFFF";
4334
+ } else coordinate = v >= 0 ? y - 2 : y + h + 9;
4335
+ return {
4336
+ coordinate,
4337
+ fill
4338
+ };
4339
+ };
4340
+ const barLabelLayout = (x, w, v, pos) => {
4341
+ let coordinate;
4342
+ let anchor;
4343
+ let fill = "#374151";
4344
+ if (pos === "ctr") {
4345
+ coordinate = x + w / 2;
4346
+ anchor = "middle";
4347
+ fill = "#FFFFFF";
4348
+ } else if (pos === "inEnd") {
4349
+ coordinate = v >= 0 ? x + w - 4 : x + 4;
4350
+ anchor = v >= 0 ? "end" : "start";
4351
+ fill = "#FFFFFF";
4352
+ } else if (pos === "inBase") {
4353
+ coordinate = v >= 0 ? x + 4 : x + w - 4;
4354
+ anchor = v >= 0 ? "start" : "end";
4355
+ fill = "#FFFFFF";
4356
+ } else {
4357
+ coordinate = v >= 0 ? x + w + 2 : x - 2;
4358
+ anchor = v >= 0 ? "start" : "end";
4359
+ }
4360
+ return {
4361
+ coordinate,
4362
+ anchor,
4363
+ fill
4364
+ };
4365
+ };
3123
4366
  const renderColumnChart = (f, spec, colors) => {
3124
4367
  const N = pointCount(spec);
3125
4368
  if (N === 0 || spec.series.length === 0) return "";
@@ -3135,7 +4378,6 @@ const renderColumnChart = (f, spec, colors) => {
3135
4378
  const clusterUnitsC = isStacked ? 1 : 1 + (Sc - 1) * (1 - overlapPctC);
3136
4379
  const barW = groupW / Math.max(.5, clusterUnitsC + gapPctC);
3137
4380
  const baseY = f.plotY + f.plotH - (0 - min) / range * f.plotH;
3138
- const showLabelFor = (s) => spec.series[s]?.dataLabels?.showValue ?? spec.dataLabels?.showValue ?? false;
3139
4381
  const out = [];
3140
4382
  for (let c = 0; c < N; c++) if (isStacked) {
3141
4383
  let total = 0;
@@ -3156,10 +4398,10 @@ const renderColumnChart = (f, spec, colors) => {
3156
4398
  const y1 = f.plotY + f.plotH - (Math.min(stackedTop, stackedBase) - min) / range * f.plotH;
3157
4399
  const h = Math.abs(y1 - y0);
3158
4400
  out.push(`<rect x="${px(x0)}" y="${px(y0)}" width="${px(barW)}" height="${px(h)}" fill="${chartPointBaseColor(spec, colors, s, c)}"${chartFillOpacityAttr(spec.series[s]?.fillOpacity)}/>`);
3159
- if (showLabelFor(s) && Math.abs(v) > 0) {
3160
- const labelY = (y0 + y1) / 2 + 3;
3161
- const labelText = isPercent ? `${Math.round(v * 100)}%` : formatDataLabelValue(spec, s, v);
3162
- out.push(`<text x="${px(x0 + barW / 2)}" y="${px(labelY)}" text-anchor="middle" font-family="sans-serif" font-size="9" fill="#FFFFFF" font-weight="600">${labelText}</text>`);
4401
+ const labelText = cartesianDataLabelText(spec, s, c, spec.series[s]?.values[c] ?? 0);
4402
+ if (labelText) {
4403
+ const { coordinate: labelY, fill } = columnLabelLayout(y0, h, v, chartPointLabelOptions(spec, s, c).position ?? "ctr");
4404
+ out.push(chartText(f, x0 + barW / 2, labelY, `text-anchor="middle" ${dataLabelTextAttrs(spec, s, fill, 9, true, c)}`, `${escapeXml(labelText)}`));
3163
4405
  }
3164
4406
  if (v >= 0) posAcc = stackedTop;
3165
4407
  else negAcc = stackedTop;
@@ -3177,21 +4419,10 @@ const renderColumnChart = (f, spec, colors) => {
3177
4419
  const baseColor = chartPointBaseColor(spec, colors, s, c);
3178
4420
  const fillColor = v < 0 && spec.series[s]?.invertIfNegative ? mixHex(baseColor, "#000000", .55) : baseColor;
3179
4421
  out.push(`<rect x="${px(x0)}" y="${px(y0)}" width="${px(barW)}" height="${px(h)}" fill="${fillColor}"${chartFillOpacityAttr(spec.series[s]?.fillOpacity)}/>`);
3180
- if (showLabelFor(s)) {
3181
- const pos = spec.series[s]?.dataLabels?.position ?? spec.dataLabels?.position;
3182
- let labelY;
3183
- let fill = "#374151";
3184
- if (pos === "ctr") {
3185
- labelY = y0 + h / 2 + 3;
3186
- fill = "#FFFFFF";
3187
- } else if (pos === "inEnd") {
3188
- labelY = v >= 0 ? y0 + 9 : y0 + h - 3;
3189
- fill = "#FFFFFF";
3190
- } else if (pos === "inBase") {
3191
- labelY = v >= 0 ? y0 + h - 3 : y0 + 9;
3192
- fill = "#FFFFFF";
3193
- } else labelY = v >= 0 ? y0 - 2 : y0 + h + 9;
3194
- out.push(`<text x="${px(x0 + barW / 2)}" y="${px(labelY)}" text-anchor="middle" ${dataLabelTextAttrs(spec, s, fill)}>${formatDataLabelValue(spec, s, v)}</text>`);
4422
+ const labelText = cartesianDataLabelText(spec, s, c, v);
4423
+ if (labelText) {
4424
+ const { coordinate: labelY, fill } = columnLabelLayout(y0, h, v, chartPointLabelOptions(spec, s, c).position);
4425
+ out.push(chartText(f, x0 + barW / 2, labelY, `text-anchor="middle" ${dataLabelTextAttrs(spec, s, fill, 9, false, c)}`, `${escapeXml(labelText)}`));
3195
4426
  }
3196
4427
  }
3197
4428
  }
@@ -3212,11 +4443,11 @@ const renderColumnChart = (f, spec, colors) => {
3212
4443
  ys.push(cy);
3213
4444
  }
3214
4445
  if (xs.length < 2) continue;
3215
- out.push(trendlinePath(xs, ys, series.trendline, tlColor));
4446
+ out.push(trendlinePath(f, xs, ys, series.trendline, tlColor));
3216
4447
  }
3217
4448
  return out.join("");
3218
4449
  };
3219
- const trendlinePath = (xs, ys, tl, color) => {
4450
+ const trendlinePath = (f, xs, ys, tl, color) => {
3220
4451
  const n = xs.length;
3221
4452
  const stepX = n >= 2 ? (xs[n - 1] - xs[0]) / (n - 1) : 0;
3222
4453
  const extendBefore = (tl.backward ?? 0) * stepX;
@@ -3267,7 +4498,7 @@ const trendlinePath = (xs, ys, tl, color) => {
3267
4498
  const path = `<path d="${pts.map(([x, y], i) => `${i === 0 ? "M" : "L"}${px(x)},${px(y)}`).join(" ")}" fill="none" stroke="${color}" stroke-width="1.5" stroke-dasharray="6 3" stroke-linecap="round"/>`;
3268
4499
  if (tl.name !== void 0 && tl.name.length > 0) {
3269
4500
  const [lx, ly] = pts[pts.length - 1];
3270
- return path + `<text x="${px(lx + 4)}" y="${px(ly)}" dominant-baseline="middle" font-family="sans-serif" font-size="9" fill="${color}">${escapeXml(tl.name)}</text>`;
4501
+ return path + chartText(f, lx + 4, ly, `dominant-baseline="middle" font-family="sans-serif" font-size="9" fill="${color}"`, `${escapeXml(tl.name)}`);
3271
4502
  }
3272
4503
  return path;
3273
4504
  };
@@ -3352,12 +4583,28 @@ const formatChartValue = (v) => {
3352
4583
  if (abs >= 10) return v.toFixed(1);
3353
4584
  return v.toFixed(2).replace(/\.?0+$/, "");
3354
4585
  };
3355
- const formatDataLabelValue = (spec, seriesIdx, v) => {
3356
- const nf = spec.series[seriesIdx]?.dataLabels?.numberFormat ?? spec.dataLabels?.numberFormat;
4586
+ const chartPointLabelOptions = (spec, seriesIdx, pointIdx) => ({
4587
+ ...spec.dataLabels,
4588
+ ...spec.series[seriesIdx]?.dataLabels,
4589
+ ...spec.series[seriesIdx]?.pointDataLabels?.[pointIdx]
4590
+ });
4591
+ const cartesianDataLabelText = (spec, seriesIdx, pointIdx, value) => {
4592
+ const options = chartPointLabelOptions(spec, seriesIdx, pointIdx);
4593
+ if (options.text !== void 0) return options.text;
4594
+ const parts = [];
4595
+ const name = spec.series[seriesIdx]?.name;
4596
+ const category = spec.categories[pointIdx];
4597
+ if (options.showSeriesName && name) parts.push(name);
4598
+ if (options.showCategory && category) parts.push(category);
4599
+ if (options.showValue) parts.push(formatDataLabelValue(spec, seriesIdx, value, pointIdx));
4600
+ return parts.join(options.separator ?? " ");
4601
+ };
4602
+ const formatDataLabelValue = (spec, seriesIdx, v, pointIdx) => {
4603
+ const nf = (pointIdx === void 0 ? void 0 : spec.series[seriesIdx]?.pointDataLabels?.[pointIdx])?.numberFormat ?? spec.series[seriesIdx]?.dataLabels?.numberFormat ?? spec.dataLabels?.numberFormat;
3357
4604
  return nf ? formatAxisLabel(v, nf) : formatChartValue(v);
3358
4605
  };
3359
- const dataLabelTextAttrs = (spec, seriesIdx, fallbackFill, fallbackSizePt = 9, fallbackBold = false) => {
3360
- const style = spec.series[seriesIdx]?.dataLabels?.textStyle ?? spec.dataLabels?.textStyle;
4606
+ const dataLabelTextAttrs = (spec, seriesIdx, fallbackFill, fallbackSizePt = 9, fallbackBold = false, pointIdx) => {
4607
+ const style = (pointIdx === void 0 ? void 0 : spec.series[seriesIdx]?.pointDataLabels?.[pointIdx])?.textStyle ?? spec.series[seriesIdx]?.dataLabels?.textStyle ?? spec.dataLabels?.textStyle;
3361
4608
  const sz = style?.sizePt ?? fallbackSizePt;
3362
4609
  const fill = style?.color ?? fallbackFill;
3363
4610
  const weight = style?.bold ?? fallbackBold ? " font-weight=\"600\"" : "";
@@ -3379,7 +4626,6 @@ const renderBarChart = (f, spec, colors) => {
3379
4626
  const clusterUnitsB = isStacked ? 1 : 1 + (Sb - 1) * (1 - overlapPctB);
3380
4627
  const barH = groupH / Math.max(.5, clusterUnitsB + gapPctB);
3381
4628
  const baseX = f.plotX + (0 - min) / range * f.plotW;
3382
- const showLabelForBar = (s) => spec.series[s]?.dataLabels?.showValue ?? spec.dataLabels?.showValue ?? false;
3383
4629
  const out = [];
3384
4630
  for (let c = 0; c < N; c++) if (isStacked) {
3385
4631
  let total = 0;
@@ -3399,10 +4645,10 @@ const renderBarChart = (f, spec, colors) => {
3399
4645
  const x1 = f.plotX + (Math.max(base, stackedTop) - min) / range * f.plotW;
3400
4646
  const w = Math.abs(x1 - x0);
3401
4647
  out.push(`<rect x="${px(x0)}" y="${px(y0)}" width="${px(w)}" height="${px(barH)}" fill="${chartPointBaseColor(spec, colors, s, c)}"${chartFillOpacityAttr(spec.series[s]?.fillOpacity)}/>`);
3402
- if (showLabelForBar(s) && Math.abs(v) > 0) {
3403
- const labelX = (x0 + x1) / 2;
3404
- const labelText = isPercent ? `${Math.round(v * 100)}%` : formatDataLabelValue(spec, s, v);
3405
- out.push(`<text x="${px(labelX)}" y="${px(y0 + barH / 2 + 3)}" text-anchor="middle" font-family="sans-serif" font-size="9" fill="#FFFFFF" font-weight="600">${labelText}</text>`);
4648
+ const labelText = cartesianDataLabelText(spec, s, c, spec.series[s]?.values[c] ?? 0);
4649
+ if (labelText) {
4650
+ const { coordinate: labelX, anchor, fill } = barLabelLayout(x0, w, v, chartPointLabelOptions(spec, s, c).position ?? "ctr");
4651
+ out.push(chartText(f, labelX, y0 + barH / 2 + 3, `text-anchor="${anchor}" ${dataLabelTextAttrs(spec, s, fill, 9, true, c)}`, `${escapeXml(labelText)}`));
3406
4652
  }
3407
4653
  if (v >= 0) posAcc = stackedTop;
3408
4654
  else negAcc = stackedTop;
@@ -3420,28 +4666,10 @@ const renderBarChart = (f, spec, colors) => {
3420
4666
  const baseColor = chartPointBaseColor(spec, colors, s, c);
3421
4667
  const fillColor = v < 0 && spec.series[s]?.invertIfNegative ? mixHex(baseColor, "#000000", .55) : baseColor;
3422
4668
  out.push(`<rect x="${px(x0)}" y="${px(y0)}" width="${px(w)}" height="${px(barH)}" fill="${fillColor}"${chartFillOpacityAttr(spec.series[s]?.fillOpacity)}/>`);
3423
- if (showLabelForBar(s)) {
3424
- const pos = spec.series[s]?.dataLabels?.position ?? spec.dataLabels?.position;
3425
- let labelX;
3426
- let anchor;
3427
- let fill = "#374151";
3428
- if (pos === "ctr") {
3429
- labelX = x0 + w / 2;
3430
- anchor = "middle";
3431
- fill = "#FFFFFF";
3432
- } else if (pos === "inEnd") {
3433
- labelX = v >= 0 ? x0 + w - 4 : x0 + 4;
3434
- anchor = v >= 0 ? "end" : "start";
3435
- fill = "#FFFFFF";
3436
- } else if (pos === "inBase") {
3437
- labelX = v >= 0 ? x0 + 4 : x0 + w - 4;
3438
- anchor = v >= 0 ? "start" : "end";
3439
- fill = "#FFFFFF";
3440
- } else {
3441
- labelX = v >= 0 ? x0 + w + 2 : x0 - 2;
3442
- anchor = v >= 0 ? "start" : "end";
3443
- }
3444
- out.push(`<text x="${px(labelX)}" y="${px(y0 + barH / 2 + 3)}" text-anchor="${anchor}" ${dataLabelTextAttrs(spec, s, fill)}>${formatDataLabelValue(spec, s, v)}</text>`);
4669
+ const labelText = cartesianDataLabelText(spec, s, c, v);
4670
+ if (labelText) {
4671
+ const { coordinate: labelX, anchor, fill } = barLabelLayout(x0, w, v, chartPointLabelOptions(spec, s, c).position);
4672
+ out.push(chartText(f, labelX, y0 + barH / 2 + 3, `text-anchor="${anchor}" ${dataLabelTextAttrs(spec, s, fill, 9, false, c)}`, `${escapeXml(labelText)}`));
3445
4673
  }
3446
4674
  }
3447
4675
  }
@@ -3495,36 +4723,36 @@ const renderLineChart = (f, spec, colors, fill) => {
3495
4723
  basePtsRaw.push([xp, yBase]);
3496
4724
  if (isStacked) accumulated[c] = top;
3497
4725
  }
3498
- const pts = dba === "span" ? ptsRaw.filter((p) => p !== null) : ptsRaw.filter((p) => p !== null);
3499
- const basePts = dba === "span" ? basePtsRaw.filter((p) => p !== null) : basePtsRaw.filter((p) => p !== null);
3500
- const dPath = series.smooth && pts.length > 2 ? smoothPath(pts) : (() => {
3501
- let path = "";
3502
- let starting = true;
3503
- for (const p of ptsRaw) {
3504
- if (p === null) {
3505
- if (dba === "gap") starting = true;
3506
- continue;
3507
- }
3508
- path += `${starting ? "M" : "L"}${px(p[0])},${px(p[1])} `;
3509
- starting = false;
3510
- }
3511
- return path.trim();
3512
- })();
4726
+ const pts = ptsRaw.filter((p) => p !== null);
4727
+ const basePts = basePtsRaw.filter((p) => p !== null);
4728
+ const segments = [];
4729
+ let segment = [];
4730
+ for (const point of ptsRaw) if (point !== null) segment.push(point);
4731
+ else if (dba === "gap" && segment.length > 0) {
4732
+ segments.push(segment);
4733
+ segment = [];
4734
+ }
4735
+ if (segment.length > 0) segments.push(segment);
4736
+ const paths = segments.map((points) => series.smooth && points.length > 2 ? smoothPath(points) : points.map(([x, y], i) => `${i === 0 ? "M" : "L"}${px(x)},${px(y)}`).join(" "));
4737
+ const dPath = paths.join(" ");
4738
+ let offset = 0;
4739
+ const areaPath = fill ? segments.map((points, i) => {
4740
+ const back = basePts.slice(offset, offset + points.length).reverse().map(([x, y]) => `L${px(x)},${px(y)}`).join(" ");
4741
+ offset += points.length;
4742
+ return `${paths[i]} ${back} Z`;
4743
+ }).join(" ") : "";
3513
4744
  perSeries.push({
3514
4745
  s,
3515
4746
  series,
3516
4747
  color,
3517
4748
  ptsRaw,
3518
4749
  pts,
3519
- basePts,
4750
+ areaPath,
3520
4751
  dPath
3521
4752
  });
3522
4753
  }
3523
4754
  const paintOrder = fill && !isStacked ? perSeries.slice().reverse() : perSeries;
3524
- if (fill) for (const { color, basePts, dPath } of paintOrder) {
3525
- const back = basePts.slice().reverse().map(([xp, yp]) => `L${px(xp)},${px(yp)}`).join(" ");
3526
- out.push(`<path d="${dPath} ${back} Z" fill="${color}" stroke="none"/>`);
3527
- }
4755
+ if (fill) for (const { color, areaPath } of paintOrder) out.push(`<path d="${areaPath}" fill="${color}" stroke="none"/>`);
3528
4756
  for (const { s, series, color, ptsRaw, pts, dPath } of paintOrder) {
3529
4757
  const lineWPx = series.lineWidthEmu ? Math.max(.3, series.lineWidthEmu / EMU_PER_PX) : 1.5;
3530
4758
  const dashAttr = series.lineDash ? (() => {
@@ -3534,55 +4762,26 @@ const renderLineChart = (f, spec, colors, fill) => {
3534
4762
  return ` stroke-dasharray="${pat.split(" ").map((n) => (Number.parseFloat(n) * sw).toFixed(2)).join(" ")}"`;
3535
4763
  })() : "";
3536
4764
  out.push(`<path d="${dPath}" fill="none" stroke="${series.lineColor ?? color}" stroke-width="${lineWPx.toFixed(2)}" stroke-linejoin="round" stroke-linecap="round"${dashAttr}/>`);
3537
- if (!isStacked) {
3538
- const explicitSymbol = series.markerSymbol !== void 0 && series.markerSymbol !== "auto" && series.markerSymbol !== "none";
3539
- if (series.markerSymbol !== "none" && !fill && (spec.lineMarkers === true || explicitSymbol)) {
3540
- const symbol = autoMarkerSymbol(series.markerSymbol, s);
3541
- const size = series.markerSizePt ?? 5;
3542
- const r = Math.max(1, size * .5);
3543
- for (const [xp, yp] of pts) out.push(seriesMarker(symbol, xp, yp, r, ...markerColors(series, color)));
3544
- }
4765
+ const explicitSymbol = series.markerSymbol !== void 0 && series.markerSymbol !== "auto" && series.markerSymbol !== "none";
4766
+ if (series.markerSymbol !== "none" && !fill && (spec.lineMarkers === true || explicitSymbol)) {
4767
+ const symbol = autoMarkerSymbol(series.markerSymbol, s);
4768
+ const size = series.markerSizePt ?? 5;
4769
+ const r = Math.max(1, size * .5);
4770
+ for (const [xp, yp] of pts) out.push(seriesMarker(symbol, xp, yp, r, ...markerColors(series, color)));
3545
4771
  }
3546
- if (series.dataLabels?.showValue ?? spec.dataLabels?.showValue ?? false) {
3547
- const lblPos = series.dataLabels?.position ?? spec.dataLabels?.position;
3548
- const computeAttrs = (xp, yp) => {
3549
- switch (lblPos) {
3550
- case "ctr": return {
3551
- x: xp,
3552
- y: yp + 3,
3553
- anchor: "middle"
3554
- };
3555
- case "b": return {
3556
- x: xp,
3557
- y: yp + 13,
3558
- anchor: "middle"
3559
- };
3560
- case "l": return {
3561
- x: xp - 6,
3562
- y: yp + 3,
3563
- anchor: "end"
3564
- };
3565
- case "r": return {
3566
- x: xp + 6,
3567
- y: yp + 3,
3568
- anchor: "start"
3569
- };
3570
- default: return {
3571
- x: xp,
3572
- y: yp - 5,
3573
- anchor: "middle"
3574
- };
3575
- }
3576
- };
3577
- for (let c = 0; c < N; c++) {
3578
- const p = ptsRaw[c];
3579
- if (p == null) continue;
3580
- const v = series.values[c];
3581
- if (v === null || v === void 0 || !Number.isFinite(v)) continue;
3582
- const [xp, yp] = p;
3583
- const { x: lx, y: ly, anchor } = computeAttrs(xp, yp);
3584
- out.push(`<text x="${px(lx)}" y="${px(ly)}" text-anchor="${anchor}" ${dataLabelTextAttrs(spec, s, "#374151")}>${formatDataLabelValue(spec, s, v)}</text>`);
3585
- }
4772
+ for (let c = 0; c < N; c++) {
4773
+ const p = ptsRaw[c];
4774
+ if (p == null) continue;
4775
+ const v = series.values[c];
4776
+ if (v == null || !Number.isFinite(v)) continue;
4777
+ const labelText = cartesianDataLabelText(spec, s, c, v);
4778
+ if (!labelText) continue;
4779
+ const [xp, yp] = p;
4780
+ const pos = chartPointLabelOptions(spec, s, c).position;
4781
+ const lx = pos === "l" ? xp - 6 : pos === "r" ? xp + 6 : xp;
4782
+ const ly = pos === "b" ? yp + 13 : pos === "ctr" || pos === "l" || pos === "r" ? yp + 3 : yp - 5;
4783
+ const anchor = pos === "l" ? "end" : pos === "r" ? "start" : "middle";
4784
+ out.push(chartText(f, lx, ly, `text-anchor="${anchor}" ${dataLabelTextAttrs(spec, s, "#374151", 9, false, c)}`, `${escapeXml(labelText)}`));
3586
4785
  }
3587
4786
  if (!isStacked && series.trendline) {
3588
4787
  const finiteXs = [];
@@ -3593,7 +4792,7 @@ const renderLineChart = (f, spec, colors, fill) => {
3593
4792
  }
3594
4793
  if (finiteXs.length >= 2) {
3595
4794
  const tlColor = series.trendline.color ?? color;
3596
- out.push(trendlinePath(finiteXs, finiteYs, series.trendline, tlColor));
4795
+ out.push(trendlinePath(f, finiteXs, finiteYs, series.trendline, tlColor));
3597
4796
  }
3598
4797
  }
3599
4798
  }
@@ -3665,7 +4864,8 @@ const renderPieChart = (f, spec, colors, doughnut) => {
3665
4864
  out.push(`<path d="${d}" fill="${color}" stroke="#FFFFFF" stroke-width="0.6"/>`);
3666
4865
  }
3667
4866
  const labelMid = (start + end) / 2;
3668
- const pos = spec.series[0]?.dataLabels?.position ?? spec.dataLabels?.position;
4867
+ const labelOptions = chartPointLabelOptions(spec, 0, i);
4868
+ const pos = labelOptions.position;
3669
4869
  let labelR;
3670
4870
  let labelFill = "#FFFFFF";
3671
4871
  if (pos === "inEnd") labelR = radius - 12;
@@ -3676,13 +4876,15 @@ const renderPieChart = (f, spec, colors, doughnut) => {
3676
4876
  const labelX = sx + labelR * Math.cos(labelMid);
3677
4877
  const labelY = sy + labelR * Math.sin(labelMid);
3678
4878
  const labels = [];
3679
- if (spec.dataLabels?.showCategory) {
4879
+ if (labelOptions.showSeriesName && series.name) labels.push(series.name);
4880
+ if (labelOptions.showCategory) {
3680
4881
  const catLabel = spec.categories[i];
3681
4882
  if (catLabel) labels.push(catLabel);
3682
4883
  }
3683
- if (spec.dataLabels?.showValue) labels.push(formatDataLabelValue(spec, 0, v));
3684
- if (spec.dataLabels?.showPercent) labels.push(`${(v / total * 100).toFixed(0)}%`);
3685
- if (labels.length > 0) out.push(`<text x="${px(labelX)}" y="${px(labelY)}" text-anchor="middle" dominant-baseline="middle" ${dataLabelTextAttrs(spec, 0, labelFill, 10, true)}>${escapeXml(labels.join(spec.series[0]?.dataLabels?.separator ?? spec.dataLabels?.separator ?? " "))}</text>`);
4884
+ if (labelOptions.showValue) labels.push(formatDataLabelValue(spec, 0, v, i));
4885
+ if (labelOptions.showPercent) labels.push(`${(v / total * 100).toFixed(0)}%`);
4886
+ const labelText = labelOptions.text ?? labels.join(labelOptions.separator ?? " ");
4887
+ if (labelText) out.push(chartText(f, labelX, labelY, `text-anchor="middle" dominant-baseline="middle" ${dataLabelTextAttrs(spec, 0, labelFill, 10, true, i)}`, `${escapeXml(labelText)}`));
3686
4888
  }
3687
4889
  return out.join("");
3688
4890
  };
@@ -3860,7 +5062,7 @@ const renderRadarChart = (f, spec, colors) => {
3860
5062
  ring.push(`${px(cx + rr * Math.cos(a))},${px(cy + rr * Math.sin(a))}`);
3861
5063
  }
3862
5064
  out.push(`<polygon points="${ring.join(" ")}" fill="none" stroke="#E5E7EB" stroke-width="0.5"/>`);
3863
- out.push(`<text x="${px(cx + 3)}" y="${px(cy - rr)}" dominant-baseline="middle" font-family="sans-serif" font-size="8" fill="#9CA3AF">${escapeXml(formatTick(t))}</text>`);
5065
+ out.push(chartText(f, cx + 3, cy - rr, `dominant-baseline="middle" font-family="sans-serif" font-size="8" fill="#9CA3AF"`, `${escapeXml(formatTick(t))}`));
3864
5066
  }
3865
5067
  for (let i = 0; i < N; i++) {
3866
5068
  const a = angleAt(i);
@@ -3871,7 +5073,7 @@ const renderRadarChart = (f, spec, colors) => {
3871
5073
  const cosA = Math.cos(a);
3872
5074
  const anchor = Math.abs(cosA) < .3 ? "middle" : cosA > 0 ? "start" : "end";
3873
5075
  const label = cat.length > 12 ? `${cat.slice(0, 11)}…` : cat;
3874
- out.push(`<text x="${px(lx)}" y="${px(ly)}" text-anchor="${anchor}" dominant-baseline="middle" ${axisTickAttrs(spec.categoryAxisLabelStyle)}>${escapeXml(label)}</text>`);
5076
+ out.push(chartText(f, lx, ly, `text-anchor="${anchor}" dominant-baseline="middle" ${axisTickAttrs(spec.categoryAxisLabelStyle)}`, `${escapeXml(label)}`));
3875
5077
  }
3876
5078
  const filled = spec.radarStyle === "filled";
3877
5079
  const showMarker = spec.radarStyle === "marker";
@@ -3899,7 +5101,7 @@ const renderRadarChart = (f, spec, colors) => {
3899
5101
  }
3900
5102
  return out.join("");
3901
5103
  };
3902
- const renderChart = (shape, x, y, w, h, transform, theme) => {
5104
+ const renderChart = (shape, x, y, w, h, transform, theme, groupReflected) => {
3903
5105
  let spec = null;
3904
5106
  try {
3905
5107
  spec = getShapeChartSpec(shape);
@@ -3919,7 +5121,11 @@ const renderChart = (shape, x, y, w, h, transform, theme) => {
3919
5121
  const isCartesian = spec.kind === "column" || spec.kind === "bar" || spec.kind === "line" || spec.kind === "area";
3920
5122
  const hasAxes = isCartesian || spec.kind === "scatter" || spec.kind === "bubble";
3921
5123
  const hasLegend = spec.legend !== void 0 && spec.legend.position !== null;
3922
- const f = layoutChart(x, y, w, h, !!spec.title, hasAxes, spec.titleOverlay ?? false, spec.legend?.overlay ?? false, hasLegend, (spec.titleStyle?.sizePt ?? DEFAULT_CHART_TITLE_PT) * PX_PER_PT, spec.plotAreaLayout);
5124
+ const flip = getShapeFlip(shape);
5125
+ const f = {
5126
+ reflected: groupReflected !== Boolean(flip && flip.horizontal !== flip.vertical),
5127
+ ...layoutChart(x, y, w, h, !!spec.title, hasAxes, spec.titleOverlay ?? false, spec.legend?.overlay ?? false, hasLegend, (spec.titleStyle?.sizePt ?? DEFAULT_CHART_TITLE_PT) * PX_PER_PT, spec.plotAreaLayout)
5128
+ };
3923
5129
  const allNamesForLegend = spec.kind === "pie" || spec.kind === "doughnut" ? Array.from(spec.categories) : spec.series.map((s) => s.name);
3924
5130
  const allColorsForLegend = spec.kind === "pie" || spec.kind === "doughnut" ? spec.categories.map((_, i) => spec.series[0]?.pointColors?.[i] ?? spec.series[0]?.color ?? colors[i % colors.length] ?? "#888") : spec.series.map((s, i) => s.color ?? colors[i % colors.length] ?? "#888");
3925
5131
  const hiddenSet = new Set(spec.legend?.hiddenIndices ?? []);
@@ -3954,6 +5160,10 @@ const renderChart = (shape, x, y, w, h, transform, theme) => {
3954
5160
  min: primaryScale.min,
3955
5161
  max: primaryScale.max,
3956
5162
  majorUnit: spec.valueAxis?.majorUnit ?? primaryScale.step,
5163
+ ...spec.valueAxis?.minorUnit !== void 0 ? { minorUnit: spec.valueAxis.minorUnit } : {},
5164
+ ...spec.valueAxisMinorGridlines !== void 0 ? { minorGridlines: spec.valueAxisMinorGridlines } : {},
5165
+ ...spec.valueAxisMinorGridlineColor !== void 0 ? { minorGridlineColor: spec.valueAxisMinorGridlineColor } : {},
5166
+ ...spec.valueAxisMinorGridlineWidthEmu !== void 0 ? { minorGridlineWidthEmu: spec.valueAxisMinorGridlineWidthEmu } : {},
3957
5167
  ...spec.valueAxisLineHidden !== void 0 ? { lineHidden: spec.valueAxisLineHidden } : {},
3958
5168
  ...spec.valueAxisLineColor !== void 0 ? { lineColor: spec.valueAxisLineColor } : {},
3959
5169
  ...spec.valueAxisMajorTickMark !== void 0 ? { majorTickMark: spec.valueAxisMajorTickMark } : {},
@@ -4005,6 +5215,10 @@ const renderChart = (shape, x, y, w, h, transform, theme) => {
4005
5215
  const majorUnit = spec.valueAxis?.majorUnit ?? step;
4006
5216
  const numberFormat = spec.valueAxis?.numberFormat;
4007
5217
  const axisExtras = {
5218
+ ...spec.valueAxis?.minorUnit !== void 0 ? { minorUnit: spec.valueAxis.minorUnit } : {},
5219
+ ...spec.valueAxisMinorGridlines !== void 0 ? { minorGridlines: spec.valueAxisMinorGridlines } : {},
5220
+ ...spec.valueAxisMinorGridlineColor !== void 0 ? { minorGridlineColor: spec.valueAxisMinorGridlineColor } : {},
5221
+ ...spec.valueAxisMinorGridlineWidthEmu !== void 0 ? { minorGridlineWidthEmu: spec.valueAxisMinorGridlineWidthEmu } : {},
4008
5222
  ...spec.valueAxisLineHidden !== void 0 ? { lineHidden: spec.valueAxisLineHidden } : {},
4009
5223
  ...majorUnit !== void 0 ? { majorUnit } : {},
4010
5224
  ...numberFormat !== void 0 ? { numberFormat } : {},
@@ -4062,7 +5276,7 @@ const renderChart = (shape, x, y, w, h, transform, theme) => {
4062
5276
  break;
4063
5277
  default: return null;
4064
5278
  }
4065
- const emptyHint = finiteCount === 0 ? `<text x="${px(f.plotX + f.plotW / 2)}" y="${px(f.plotY + f.plotH / 2)}" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif" font-size="12" fill="#9CA3AF">${escapeXml(`chart (${spec.kind}) — no data`)}</text>` : "";
5279
+ const emptyHint = finiteCount === 0 ? chartText(f, f.plotX + f.plotW / 2, f.plotY + f.plotH / 2, `text-anchor="middle" dominant-baseline="middle" font-family="sans-serif" font-size="12" fill="#9CA3AF"`, `${escapeXml(`chart (${spec.kind}) — no data`)}`) : "";
4066
5280
  const axisTitleAttrs = (style) => {
4067
5281
  const sz = style?.sizePt ?? 11;
4068
5282
  const fill = style?.color ?? "#374151";
@@ -4071,12 +5285,12 @@ const renderChart = (shape, x, y, w, h, transform, theme) => {
4071
5285
  return `font-family="sans-serif" font-size="${chartFontPx(sz)}" fill="${fill}" font-weight="${weight}"${italicAttr}`;
4072
5286
  };
4073
5287
  const valueAxisTitleRot = spec.valueAxisTitleRotationDeg ?? -90;
4074
- const valueAxisTitleSvg = spec.valueAxisTitle ? `<text x="${px(f.plotX - 26)}" y="${px(f.plotY + f.plotH / 2)}" text-anchor="middle" ${axisTitleAttrs(spec.valueAxisTitleStyle)} transform="rotate(${valueAxisTitleRot} ${px(f.plotX - 26)} ${px(f.plotY + f.plotH / 2)})">${escapeXml(spec.valueAxisTitle)}</text>` : "";
5288
+ const valueAxisTitleSvg = spec.valueAxisTitle ? chartText(f, f.plotX - 26, f.plotY + f.plotH / 2, `text-anchor="middle" ${axisTitleAttrs(spec.valueAxisTitleStyle)} transform="rotate(${valueAxisTitleRot} ${px(f.plotX - 26)} ${px(f.plotY + f.plotH / 2)})"`, `${escapeXml(spec.valueAxisTitle)}`) : "";
4075
5289
  const catTitleRot = spec.categoryAxisTitleRotationDeg ?? 0;
4076
5290
  const catTitleCx = f.plotX + f.plotW / 2;
4077
5291
  const catTitleCy = f.plotY + f.plotH + 28;
4078
5292
  const catTitleTransform = catTitleRot !== 0 ? ` transform="rotate(${catTitleRot} ${px(catTitleCx)} ${px(catTitleCy)})"` : "";
4079
- const categoryAxisTitleSvg = spec.categoryAxisTitle ? `<text x="${px(catTitleCx)}" y="${px(catTitleCy)}" text-anchor="middle" ${axisTitleAttrs(spec.categoryAxisTitleStyle)}${catTitleTransform}>${escapeXml(spec.categoryAxisTitle)}</text>` : "";
5293
+ const categoryAxisTitleSvg = spec.categoryAxisTitle ? chartText(f, catTitleCx, catTitleCy, `text-anchor="middle" ${axisTitleAttrs(spec.categoryAxisTitleStyle)}${catTitleTransform}`, `${escapeXml(spec.categoryAxisTitle)}`) : "";
4080
5294
  return [
4081
5295
  `<g${transform}>`,
4082
5296
  `<rect x="${px(f.x)}" y="${px(f.y)}" width="${px(f.w)}" height="${px(f.h)}" fill="${spec.chartAreaFill ?? "#FFFFFF"}" stroke="${spec.chartAreaStrokeColor ?? "none"}" stroke-width="0.6"${spec.roundedCorners ? " rx=\"6\" ry=\"6\"" : ""}/>`,
@@ -4091,72 +5305,90 @@ const renderChart = (shape, x, y, w, h, transform, theme) => {
4091
5305
  "</g>"
4092
5306
  ].join("");
4093
5307
  };
4094
- const cellParaData = (paragraphs) => {
5308
+ const cellParaData = (paragraphs, cell, pres) => {
4095
5309
  let hasText = false;
4096
5310
  return {
4097
- paraData: paragraphs.map((para) => {
5311
+ paraData: paragraphs.map((para, index) => {
5312
+ const properties = getParagraphPropertiesEffective(pres, cell, index);
5313
+ const bulletIsPicture = isParagraphBulletPicture(cell, index);
5314
+ const bulletImageBytes = bulletIsPicture ? getParagraphBulletImageBytes(cell, index) : null;
4098
5315
  const runs = [];
5316
+ let rIdx = 0;
5317
+ let fieldIndex = 0;
5318
+ let breakIndex = 0;
4099
5319
  for (const el of para.elements) {
4100
5320
  if (el.kind === "br") {
5321
+ const fmt = getTableCellRunFormatEffective(pres, cell, index, { breakIndex: breakIndex++ });
4101
5322
  runs.push({
4102
5323
  text: "\n",
4103
- fmt: null,
4104
- sizePt: DEFAULT_BODY_PT
5324
+ fmt,
5325
+ sizePt: fmt.size ?? DEFAULT_BODY_PT
4105
5326
  });
4106
5327
  continue;
4107
5328
  }
4108
5329
  if (el.text.trim()) hasText = true;
5330
+ const href = el.clickAction ? clickActionHref(pres, el.clickAction) : null;
5331
+ const fmt = getTableCellRunFormatEffective(pres, cell, index, el.kind === "r" ? rIdx++ : { fieldIndex: fieldIndex++ });
4109
5332
  runs.push({
4110
5333
  text: el.text,
4111
- fmt: el.format,
4112
- sizePt: el.format?.size ?? DEFAULT_BODY_PT
5334
+ fmt,
5335
+ sizePt: fmt?.size ?? DEFAULT_BODY_PT,
5336
+ ...href ? {
5337
+ href,
5338
+ ...el.tooltip !== void 0 ? { hrefTip: el.tooltip } : {}
5339
+ } : {}
4113
5340
  });
4114
5341
  }
4115
5342
  return {
4116
- align: para.align ?? "left",
4117
- level: 0,
4118
- bulletStyle: null,
4119
- bulletDetail: {
4120
- color: null,
4121
- sizePct: null,
4122
- sizePts: null,
4123
- font: null
5343
+ ...runs.length === 0 ? { emptySizePt: getTableCellRunFormatEffective(pres, cell, index, null).size ?? DEFAULT_BODY_PT } : {},
5344
+ latinLineBreak: properties.latinLineBreak,
5345
+ tabStops: properties.tabStops,
5346
+ defaultTabSizeEmu: properties.defaultTabSizeEmu,
5347
+ align: properties.align ?? "left",
5348
+ level: properties.level,
5349
+ bulletStyle: properties.bullet,
5350
+ bulletDetail: properties.bulletDetail ?? {
5351
+ ...getParagraphBulletStyle(pres, cell, index),
5352
+ colorFollowText: false,
5353
+ sizeFollowText: false,
5354
+ fontFollowText: false
4124
5355
  },
4125
- bulletIsPicture: false,
4126
- bulletImageHref: null,
5356
+ bulletIsPicture,
5357
+ bulletImageHref: bulletImageBytes ? bytesToDataUrl(bulletImageBytes) : null,
4127
5358
  runs,
4128
- lineSpacing: null,
4129
- spcBefPts: null,
4130
- spcAftPts: null,
5359
+ lineSpacing: properties.lineSpacing,
5360
+ spcBefPts: properties.spcBefPts,
5361
+ spcAftPts: properties.spcAftPts,
4131
5362
  indent: {
4132
- leftEmu: null,
4133
- rightEmu: null,
4134
- firstLineEmu: null
5363
+ leftEmu: properties.marL,
5364
+ rightEmu: properties.marR,
5365
+ firstLineEmu: properties.indent
4135
5366
  }
4136
5367
  };
4137
5368
  }),
4138
5369
  hasText
4139
5370
  };
4140
5371
  };
4141
- const renderTableCellText = (paragraphs, cx, cy, cw, ch, color, pres, shape, theme, themeFace, ctx, vAnchor, margins) => {
4142
- const { paraData, hasText } = cellParaData(paragraphs);
5372
+ const renderTableCellText = (cell, paragraphs, cx, cy, cw, ch, color, pres, shape, theme, themeFace, ctx, vAnchor, margins, effectsOnly = false) => {
5373
+ const { paraData, hasText } = cellParaData(paragraphs, cell, pres);
4143
5374
  if (!hasText) return "";
4144
- const defaultPadPx = 4;
4145
- const padL = margins.left !== null ? margins.left / EMU_PER_PX : defaultPadPx;
4146
- const padR = margins.right !== null ? margins.right / EMU_PER_PX : defaultPadPx;
4147
- const padT = margins.top !== null ? margins.top / EMU_PER_PX : defaultPadPx;
4148
- const padB = margins.bottom !== null ? margins.bottom / EMU_PER_PX : defaultPadPx;
5375
+ const numberLabels = paragraphNumberLabels(paraData);
5376
+ const padL = (margins.left ?? 91440) / EMU_PER_PX;
5377
+ const padR = (margins.right ?? 91440) / EMU_PER_PX;
5378
+ const padT = (margins.top ?? 45720) / EMU_PER_PX;
5379
+ const padB = (margins.bottom ?? 45720) / EMU_PER_PX;
4149
5380
  const innerX = cx + padL;
4150
5381
  const innerY = cy + padT;
4151
5382
  const innerW = Math.max(0, cw - padL - padR);
4152
5383
  const innerH = Math.max(0, ch - padT - padB);
4153
5384
  if (innerW <= 0 || innerH <= 0) return "";
4154
- if (ctx.mode === "svg") return buildAndLayoutSvgText({
5385
+ const customTabs = paraData.some((para) => para.tabStops?.length && para.runs.some((run) => run.text.includes(" ")));
5386
+ const svgArgs = {
4155
5387
  pres,
4156
5388
  shape,
4157
5389
  theme,
4158
5390
  paraData,
4159
- numberLabels: paraData.map(() => null),
5391
+ numberLabels,
4160
5392
  autoFitScale: 1,
4161
5393
  lineHeightScale: 1,
4162
5394
  defaultPt: DEFAULT_BODY_PT,
@@ -4168,17 +5400,37 @@ const renderTableCellText = (paragraphs, cx, cy, cw, ch, color, pres, shape, the
4168
5400
  innerY: innerY * EMU_PER_PX,
4169
5401
  innerW: innerW * EMU_PER_PX,
4170
5402
  innerH: innerH * EMU_PER_PX,
4171
- measure: ctx.measure,
4172
- vert: "none",
4173
- columns: null
4174
- });
5403
+ measure: ctx.mode === "svg" ? ctx.measure : browserTextMeasurer() ?? ctx.measure,
5404
+ ...ctx.mode === "foreignObject" ? { resolveFamily: browserFontFamily } : {},
5405
+ vert: verticalLayoutOf(getTableCellTextDirection(cell)),
5406
+ columns: null,
5407
+ ...effectsOnly ? { effectsOnly: true } : {}
5408
+ };
5409
+ const fillOverlay = () => paraData.some((para) => para.runs.some(hasPaintedTextFill)) ? buildAndLayoutSvgText({
5410
+ ...svgArgs,
5411
+ effectsOnly: true,
5412
+ effectKind: "fill"
5413
+ }) : "";
5414
+ if (ctx.mode === "svg" || customTabs || effectsOnly) {
5415
+ if (effectsOnly) {
5416
+ const reflection = paraData.some((para) => para.runs.some((run) => run.fmt?.reflection != null)) ? buildAndLayoutSvgText({
5417
+ ...svgArgs,
5418
+ effectKind: "reflection"
5419
+ }) : "";
5420
+ const innerShadow = paraData.some((para) => para.runs.some((run) => run.fmt?.innerShadow != null)) ? buildAndLayoutSvgText({
5421
+ ...svgArgs,
5422
+ effectKind: "innerShadow"
5423
+ }) : "";
5424
+ return reflection + fillOverlay() + innerShadow;
5425
+ }
5426
+ return buildAndLayoutSvgText(svgArgs);
5427
+ }
5428
+ const vertical = verticalTextStyle(getTableCellTextDirection(cell));
5429
+ const directionStyle = `${vertical.declarations}${vertical.transform ? `;transform:${vertical.transform}` : ""}`;
4175
5430
  const justify = vAnchor === "top" ? "flex-start" : vAnchor === "bottom" ? "flex-end" : "center";
4176
5431
  const familyFont = themeFace ? `${escapeXml(themeFace)}, ${DEFAULT_FONT}` : DEFAULT_FONT;
4177
- const body = paraData.map((para) => {
4178
- const runHtml = para.runs.map((run) => renderRun(run.text, run.fmt, theme, run.sizePt, run.fmt?.size === void 0)).join("");
4179
- return `<p style="margin:0;padding:0;text-align:${ALIGNMENT_TO_CSS[para.align] ?? "left"};line-height:1.2">${runHtml || "&#8203;"}</p>`;
4180
- }).join("");
4181
- return `<foreignObject x="${px(innerX)}" y="${px(innerY)}" width="${px(innerW)}" height="${px(innerH)}"><div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;flex-direction:column;justify-content:${justify};width:100%;height:100%;box-sizing:border-box;overflow:hidden;font-family:${familyFont};color:${color};word-break:break-word">${body}</div></foreignObject>`;
5432
+ const body = renderHtmlParagraphs(paraData, numberLabels, theme, 1, DEFAULT_BODY_PT, color).join("");
5433
+ return `<foreignObject x="${px(innerX)}" y="${px(innerY)}" width="${px(innerW)}" height="${px(innerH)}"><div xmlns="http://www.w3.org/1999/xhtml" style="display:flex;flex-direction:column;justify-content:${justify};width:100%;height:100%;box-sizing:border-box;overflow:hidden;line-height:${LINE_HEIGHT};font-family:${familyFont};color:${color};word-break:break-word;${directionStyle}">${body}</div></foreignObject>${fillOverlay()}`;
4182
5434
  };
4183
5435
  const renderTable = (shape, pres, x, y, w, h, transform, theme, ctx) => {
4184
5436
  let dims;
@@ -4194,6 +5446,8 @@ const renderTable = (shape, pres, x, y, w, h, transform, theme, ctx) => {
4194
5446
  return null;
4195
5447
  }
4196
5448
  if (dims.rows === 0 || dims.cols === 0) return null;
5449
+ const flip = getShapeFlip(shape);
5450
+ const textReflected = ctx.groupReflected !== Boolean(flip && flip.horizontal !== flip.vertical);
4197
5451
  const xPx = x / EMU_PER_PX;
4198
5452
  const yPx = y / EMU_PER_PX;
4199
5453
  const widthsPx = widths.map((w0) => w0 / EMU_PER_PX);
@@ -4206,17 +5460,12 @@ const renderTable = (shape, pres, x, y, w, h, transform, theme, ctx) => {
4206
5460
  for (let c = 0; c < widthsPx.length; c++) colXs.push((colXs[c] ?? xPx) + (widthsPx[c] ?? 0) * wScale);
4207
5461
  const rowYs = [yPx];
4208
5462
  for (let r = 0; r < heightsPx.length; r++) rowYs.push((rowYs[r] ?? yPx) + (heightsPx[r] ?? 0) * hScale);
4209
- const flags = getTableStyleFlags(shape);
4210
- const accent = theme ? normalizeHex(theme.accent1) : "#4472C4";
4211
- const headerFill = accent;
4212
- const bandFill = mixHex(accent, "#FFFFFF", .12);
4213
- const bandFill2 = mixHex(accent, "#FFFFFF", .27);
4214
5463
  const textColor = activeDeckTextColor;
4215
5464
  const tableThemeFace = getPresentationFonts(pres)?.minorLatin ?? null;
4216
5465
  const out = [];
4217
5466
  out.push(`<g${transform}>`);
4218
- out.push(`<rect x="${px(xPx)}" y="${px(yPx)}" width="${px((colXs[widthsPx.length] ?? xPx) - xPx)}" height="${px((rowYs[heightsPx.length] ?? yPx) - yPx)}" fill="#FFFFFF"/>`);
4219
5467
  const borderEdges = [];
5468
+ const borderEdgeCandidates = /* @__PURE__ */ new Map();
4220
5469
  for (let r = 0; r < dims.rows; r++) for (let c = 0; c < dims.cols; c++) {
4221
5470
  const cell = cells[r]?.[c];
4222
5471
  if (!cell) continue;
@@ -4229,19 +5478,16 @@ const renderTable = (shape, pres, x, y, w, h, transform, theme, ctx) => {
4229
5478
  const endRow = Math.min(dims.rows, r + span.rowSpan);
4230
5479
  const cw = (colXs[endCol] ?? cx) - cx;
4231
5480
  const ch = (rowYs[endRow] ?? cy) - cy;
4232
- const fill = getTableCellFill(cell);
5481
+ const appearance = getTableCellAppearanceEffective(pres, typedCell);
5482
+ const fill = appearance.fill;
4233
5483
  let resolvedFill;
4234
- if (fill) resolvedFill = resolveColor(fill, theme, "#FFFFFF");
4235
- else if (flags.firstRow && r === 0) resolvedFill = headerFill;
4236
- else if (flags.lastRow && r === dims.rows - 1) resolvedFill = headerFill;
4237
- else if (flags.firstCol && c === 0) resolvedFill = bandFill;
4238
- else if (flags.lastCol && c === dims.cols - 1) resolvedFill = bandFill;
4239
- else if (flags.bandRow) resolvedFill = (r - (flags.firstRow ? 1 : 0)) % 2 === 0 ? bandFill : bandFill2;
4240
- else if (flags.bandCol) resolvedFill = (c - (flags.firstCol ? 1 : 0)) % 2 === 0 ? bandFill : bandFill2;
5484
+ if (fill.kind === "solid") resolvedFill = resolveColor(fill.color, theme, "#FFFFFF");
5485
+ else if (fill.kind === "none") resolvedFill = "none";
5486
+ else if (fill.kind !== "inherit") resolvedFill = "none";
4241
5487
  else resolvedFill = "none";
4242
- const cellTextColor = resolvedFill === headerFill ? "#FFFFFF" : textColor;
4243
- out.push(`<rect x="${px(cx)}" y="${px(cy)}" width="${px(cw)}" height="${px(ch)}" fill="${resolvedFill}"/>`);
4244
- const borders = getTableCellBorders(pres, typedCell);
5488
+ const cellTextColor = textColor;
5489
+ out.push(`<g data-pptx-cell="${r},${c}"><rect x="${px(cx)}" y="${px(cy)}" width="${px(cw)}" height="${px(ch)}" fill="${resolvedFill}"/>`);
5490
+ const borders = appearance.borders;
4245
5491
  const dashAttr = (dash, widthPx) => {
4246
5492
  if (!dash || dash === "solid") return "";
4247
5493
  const pat = DASH_PATTERNS[dash];
@@ -4251,9 +5497,22 @@ const renderTable = (shape, pres, x, y, w, h, transform, theme, ctx) => {
4251
5497
  const edge = (side, x1, y1, x2, y2) => {
4252
5498
  const b = borders[side];
4253
5499
  if (!b) return;
5500
+ const key = `${Math.round(Math.min(x1, x2) * 100)},${Math.round(Math.min(y1, y2) * 100)},${Math.round(Math.max(x1, x2) * 100)},${Math.round(Math.max(y1, y2) * 100)}`;
4254
5501
  const sw = b.widthEmu ? Math.max(.4, b.widthEmu / EMU_PER_PX) : .5;
4255
5502
  const col = b.color ?? "#9CA3AF";
4256
- borderEdges.push(`<line x1="${px(x1)}" y1="${px(y1)}" x2="${px(x2)}" y2="${px(y2)}" stroke="${col}" stroke-width="${px(sw)}"${dashAttr(b.dash, sw)}/>`);
5503
+ const line = `<line x1="${px(x1)}" y1="${px(y1)}" x2="${px(x2)}" y2="${px(y2)}" stroke="${col}" stroke-width="${px(sw)}"${dashAttr(b.dash, sw)}/>`;
5504
+ const existing = borderEdgeCandidates.get(key);
5505
+ if (existing) {
5506
+ if (sw <= existing.width) return;
5507
+ borderEdges[existing.index] = line;
5508
+ existing.width = sw;
5509
+ return;
5510
+ }
5511
+ borderEdgeCandidates.set(key, {
5512
+ index: borderEdges.length,
5513
+ width: sw
5514
+ });
5515
+ borderEdges.push(line);
4257
5516
  };
4258
5517
  edge("left", cx, cy, cx, cy + ch);
4259
5518
  edge("right", cx + cw, cy, cx + cw, cy + ch);
@@ -4267,15 +5526,8 @@ const renderTable = (shape, pres, x, y, w, h, transform, theme, ctx) => {
4267
5526
  const sw = borders.blToTr.widthEmu ? Math.max(.4, borders.blToTr.widthEmu / EMU_PER_PX) : .5;
4268
5527
  borderEdges.push(`<line x1="${px(cx)}" y1="${px(cy + ch)}" x2="${px(cx + cw)}" y2="${px(cy)}" stroke="${borders.blToTr.color ?? "#9CA3AF"}" stroke-width="${px(sw)}"${dashAttr(borders.blToTr.dash, sw)}/>`);
4269
5528
  }
4270
- const defaultColor = "#9CA3AF";
4271
- if (!borders.left) borderEdges.push(`<line x1="${px(cx)}" y1="${px(cy)}" x2="${px(cx)}" y2="${px(cy + ch)}" stroke="${defaultColor}" stroke-width="0.4" opacity="0.6"/>`);
4272
- if (!borders.right) borderEdges.push(`<line x1="${px(cx + cw)}" y1="${px(cy)}" x2="${px(cx + cw)}" y2="${px(cy + ch)}" stroke="${defaultColor}" stroke-width="0.4" opacity="0.6"/>`);
4273
- if (!borders.top) borderEdges.push(`<line x1="${px(cx)}" y1="${px(cy)}" x2="${px(cx + cw)}" y2="${px(cy)}" stroke="${defaultColor}" stroke-width="0.4" opacity="0.6"/>`);
4274
- if (!borders.bottom) borderEdges.push(`<line x1="${px(cx)}" y1="${px(cy + ch)}" x2="${px(cx + cw)}" y2="${px(cy + ch)}" stroke="${defaultColor}" stroke-width="0.4" opacity="0.6"/>`);
4275
- const cellParagraphs = getTableCellParagraphs(cell);
4276
- const vAnchor = getTableCellAnchor(typedCell) ?? "top";
4277
- const cellMargins = getTableCellMargins(typedCell);
4278
- out.push(renderTableCellText(cellParagraphs, cx, cy, cw, ch, cellTextColor, pres, shape, theme, tableThemeFace, ctx, vAnchor, cellMargins));
5529
+ const cellText = renderTableCellText(typedCell, getTableCellParagraphs(cell), cx, cy, cw, ch, cellTextColor, pres, shape, theme, tableThemeFace, ctx, getTableCellAnchor(typedCell) ?? "top", getTableCellMargins(typedCell));
5530
+ out.push(textReflected && cellText ? `<g transform="translate(${px(2 * cx + cw)} 0) scale(-1 1)">${cellText}</g>` : cellText, "</g>");
4279
5531
  }
4280
5532
  out.push(borderEdges.join(""));
4281
5533
  out.push("</g>");
@@ -4397,7 +5649,7 @@ const customGeometryToSvg = (geom, x, y, w, h, fill, fillExtra, stroke, strokeWi
4397
5649
  }
4398
5650
  return out.join("");
4399
5651
  };
4400
- const renderShape = (shape, pres, theme, ctx) => {
5652
+ const renderShapeContent = (shape, pres, theme, ctx) => {
4401
5653
  const bounds = getShapeBoundsResolved(pres, shape);
4402
5654
  if (!bounds) return "";
4403
5655
  const x = bounds.x;
@@ -4422,7 +5674,10 @@ const renderShape = (shape, pres, theme, ctx) => {
4422
5674
  if (flip.vertical) transforms.push(`translate(0 ${E(2 * cy)}) scale(1 -1)`);
4423
5675
  const transform = transforms.length > 0 ? ` transform="${transforms.join(" ")}"` : "";
4424
5676
  const textRotation = (flip.vertical ? rotation + 180 : rotation) % 360;
4425
- const textTransform = textRotation !== 0 ? ` transform="rotate(${textRotation} ${E(cx)} ${E(cy)})"` : "";
5677
+ const textTransforms = [];
5678
+ if (textRotation !== 0) textTransforms.push(`rotate(${textRotation} ${E(cx)} ${E(cy)})`);
5679
+ if (ctx.groupReflected) textTransforms.push(`translate(${E(2 * cx)} 0) scale(-1 1)`);
5680
+ const textTransform = textTransforms.length > 0 ? ` transform="${textTransforms.join(" ")}"` : "";
4426
5681
  const { sx: gsx, sy: gsy } = ctx.groupScale;
4427
5682
  const groupScaled = gsx !== 1 || gsy !== 1;
4428
5683
  const textBounds = groupScaled ? {
@@ -4438,8 +5693,8 @@ const renderShape = (shape, pres, theme, ctx) => {
4438
5693
  };
4439
5694
  const rawTextOverlay = kind === "shape" || kind === "graphicFrame" ? renderTextBody(pres, shape, textBounds, theme, phType, ctx) : "";
4440
5695
  const textOverlay = rawTextOverlay !== "" && groupScaled ? `<g transform="scale(${(1 / gsx).toFixed(6)} ${(1 / gsy).toFixed(6)})">${rawTextOverlay}</g>` : rawTextOverlay;
4441
- if (kind === "picture") return renderPicture(shape, pres, x, y, w, h, transform, textOverlay, getShapeImageBytes(shape), getShapeImageFormat(shape));
4442
- if (kind === "shape" && fill.kind === "image") return renderPicture(shape, pres, x, y, w, h, transform, textOverlay, getShapeImageFillBytes(shape), getShapeImageFormat(shape));
5696
+ if (kind === "picture") return renderPicture(shape, pres, x, y, w, h, transform, textOverlay, getShapeImageBytes(shape), getShapeImageFormat(shape), paint(shape, fill, stroke, theme, false, pres));
5697
+ if (kind === "shape" && fill.kind === "image") return renderPicture(shape, pres, x, y, w, h, transform, textOverlay, getShapeImageFillBytes(shape), getShapeImageFormat(shape), paint(shape, fill, stroke, theme, false, pres));
4443
5698
  if (kind === "connector") {
4444
5699
  const p = paint(shape, fill, stroke, theme, false, pres);
4445
5700
  const sw = p.strokeWidth || 19050;
@@ -4494,20 +5749,24 @@ const renderShape = (shape, pres, theme, ctx) => {
4494
5749
  const children = getGroupChildren(shape);
4495
5750
  if (children.length === 0) return "";
4496
5751
  const tParts = [];
5752
+ const inverseParts = [];
4497
5753
  let groupScaleX = 1;
4498
5754
  let groupScaleY = 1;
4499
5755
  if (xform && rotation !== 0) {
4500
5756
  const cxG = (xform.outer.x + xform.outer.w / 2) / EMU_PER_PX;
4501
5757
  const cyG = (xform.outer.y + xform.outer.h / 2) / EMU_PER_PX;
4502
5758
  tParts.push(`rotate(${rotation} ${cxG.toFixed(2)} ${cyG.toFixed(2)})`);
5759
+ inverseParts.unshift(`rotate(${-rotation} ${cxG.toFixed(2)} ${cyG.toFixed(2)})`);
4503
5760
  }
4504
5761
  if (xform && flip.horizontal) {
4505
5762
  const cxG = (xform.outer.x + xform.outer.w / 2) / EMU_PER_PX;
4506
5763
  tParts.push(`translate(${(2 * cxG).toFixed(2)} 0) scale(-1 1)`);
5764
+ inverseParts.unshift(`translate(${(2 * cxG).toFixed(2)} 0) scale(-1 1)`);
4507
5765
  }
4508
5766
  if (xform && flip.vertical) {
4509
5767
  const cyG = (xform.outer.y + xform.outer.h / 2) / EMU_PER_PX;
4510
5768
  tParts.push(`translate(0 ${(2 * cyG).toFixed(2)}) scale(1 -1)`);
5769
+ inverseParts.unshift(`translate(0 ${(2 * cyG).toFixed(2)}) scale(1 -1)`);
4511
5770
  }
4512
5771
  if (xform) {
4513
5772
  const ox = xform.outer.x;
@@ -4525,10 +5784,13 @@ const renderShape = (shape, pres, theme, ctx) => {
4525
5784
  const tx = ((ox - ix * (ow / iw)) / EMU_PER_PX).toFixed(2);
4526
5785
  const ty = ((oy - iy * (oh / ih)) / EMU_PER_PX).toFixed(2);
4527
5786
  tParts.push(`translate(${tx} ${ty})`, `scale(${sx} ${sy})`);
5787
+ inverseParts.unshift(`scale(${1 / Number(sx)} ${1 / Number(sy)}) translate(${-Number(tx)} ${-Number(ty)})`);
4528
5788
  }
4529
5789
  const groupTransform = tParts.length > 0 ? ` transform="${tParts.join(" ")}"` : "";
4530
- const childCtx = groupScaleX === 1 && groupScaleY === 1 ? ctx : {
5790
+ const childCtx = tParts.length === 0 ? ctx : {
4531
5791
  ...ctx,
5792
+ inverseGroupTransform: `${inverseParts.join(" ")} ${ctx.inverseGroupTransform}`,
5793
+ groupReflected: ctx.groupReflected !== Boolean(xform && flip.horizontal !== flip.vertical),
4532
5794
  groupScale: {
4533
5795
  sx: ctx.groupScale.sx * groupScaleX,
4534
5796
  sy: ctx.groupScale.sy * groupScaleY
@@ -4537,9 +5799,21 @@ const renderShape = (shape, pres, theme, ctx) => {
4537
5799
  return `<g${groupTransform}>${children.map((c) => renderShape(c, pres, theme, childCtx)).join("")}</g>`;
4538
5800
  }
4539
5801
  const p = paint(shape, fill, stroke, theme, phType !== null, pres);
5802
+ if (fill.kind === "background") {
5803
+ ctx.background.id ||= mintId();
5804
+ const id = mintId();
5805
+ const inverse = [];
5806
+ if (flip.vertical) inverse.push(`translate(0 ${E(2 * cy)}) scale(1 -1)`);
5807
+ if (flip.horizontal) inverse.push(`translate(${E(2 * cx)} 0) scale(-1 1)`);
5808
+ if (rotation !== 0) inverse.push(`rotate(${-rotation} ${E(cx)} ${E(cy)})`);
5809
+ inverse.push(ctx.inverseGroupTransform);
5810
+ p.fill = `url(#${id})`;
5811
+ p.fillAttrs = "";
5812
+ p.defs += `<defs><pattern id="${id}" patternUnits="userSpaceOnUse" width="${ctx.background.width}" height="${ctx.background.height}" patternTransform="${inverse.join(" ")}"><use href="#${ctx.background.id}" xlink:href="#${ctx.background.id}"/></pattern></defs>`;
5813
+ }
4540
5814
  if (kind === "graphicFrame") {
4541
5815
  if (isChartShape(shape)) {
4542
- const chartSvg = renderChart(shape, x, y, w, h, transform, theme);
5816
+ const chartSvg = renderChart(shape, x, y, w, h, transform, theme, ctx.groupReflected);
4543
5817
  if (chartSvg) return chartSvg;
4544
5818
  }
4545
5819
  if (isTableShape(shape)) {
@@ -4594,8 +5868,6 @@ const renderShape = (shape, pres, theme, ctx) => {
4594
5868
  geomSvg = reflection.svg + geomSvg;
4595
5869
  fxDefs += reflection.defs;
4596
5870
  }
4597
- const url = getShapeHyperlink(shape);
4598
- const tooltip = getShapeHyperlinkTooltip(shape);
4599
5871
  const shapeName = (() => {
4600
5872
  try {
4601
5873
  return getShapeName(shape);
@@ -4622,22 +5894,39 @@ const renderShape = (shape, pres, theme, ctx) => {
4622
5894
  const ariaAttr = a11yLabel ? ` role="img" aria-label="${escapeXml(a11yLabel)}"` : "";
4623
5895
  const placedText = textOverlay ? `<g${textTransform}>${textOverlay}</g>` : "";
4624
5896
  const custGeomAttr = isCustGeom ? " data-pptx-fallback=\"custGeom\"" : "";
4625
- const inner = `${p.defs}${fxDefs}<g${nameAttr}${ariaAttr}${custGeomAttr}><g${transform}>${geomSvg}</g>${placedText}</g>`;
5897
+ return `${p.defs}${fxDefs}<g${nameAttr}${ariaAttr}${custGeomAttr}><g${transform}>${geomSvg}</g>${placedText}</g>`;
5898
+ };
5899
+ const renderShape = (shape, pres, theme, ctx) => {
5900
+ if (isShapeHidden(shape)) return "";
5901
+ const inner = renderShapeContent(shape, pres, theme, ctx);
5902
+ if (!inner) return inner;
5903
+ const href = clickActionHref(pres, getShapeClickAction(shape));
5904
+ const tooltip = href === void 0 ? null : getShapeHyperlinkTooltip(shape);
4626
5905
  const titleEl = tooltip ? `<title>${escapeXml(tooltip)}</title>` : "";
4627
- if (url) return `<a href="${escapeXml(url)}" target="_blank" rel="noopener noreferrer">${titleEl}${inner}</a>`;
4628
- const action = getShapeClickAction(shape);
4629
- if (action) {
4630
- let href = null;
4631
- if (action.kind === "slide") {
4632
- const idx = getSlideIndex(pres, action.slide);
4633
- if (idx >= 0) href = `#slide-${idx + 1}`;
4634
- } else if (action.kind === "url") href = action.url;
4635
- if (href !== null) {
4636
- const targetAttrs = href.startsWith("#") ? "" : " target=\"_blank\" rel=\"noopener noreferrer\"";
4637
- return `<a href="${escapeXml(href)}"${targetAttrs}>${titleEl}${inner}</a>`;
5906
+ const targetAttrs = href === void 0 || href.startsWith("#") ? "" : " target=\"_blank\" rel=\"noopener noreferrer\"";
5907
+ return wrapWithShapeId(shape, href === void 0 ? inner : `<a href="${escapeXml(href)}"${targetAttrs}>${titleEl}${inner}</a>`, ctx);
5908
+ };
5909
+ /**
5910
+ * Names the object the slide's animation timing would target, so a player can
5911
+ * find it without knowing how the shape was drawn. One wrapper per object,
5912
+ * whatever its kind — a group carries its own id and its children theirs, so a
5913
+ * build on a group child is reachable inside a group that is animated itself.
5914
+ *
5915
+ * The id is the shape's `<p:cNvPr id>`, which is what `<p:spTgt spid>` names.
5916
+ * A deck may repeat one (nothing in the schema forbids it), and the attribute
5917
+ * repeats with it rather than inventing a unique number the timing could not
5918
+ * refer to.
5919
+ */
5920
+ const wrapWithShapeId = (shape, svg, ctx) => {
5921
+ if (!ctx.ownShapes) return svg;
5922
+ const id = (() => {
5923
+ try {
5924
+ return getShapeId(shape);
5925
+ } catch {
5926
+ return null;
4638
5927
  }
4639
- }
4640
- return inner;
5928
+ })();
5929
+ return id === null ? svg : `<g data-pptx-shape-id="${id}">${svg}</g>`;
4641
5930
  };
4642
5931
  const buildReflection = (pres, shape, geomRaw, box) => {
4643
5932
  let effects;
@@ -4672,6 +5961,7 @@ const buildEffectsFilter = (pres, shape) => {
4672
5961
  const id = mintId();
4673
5962
  const primitives = [];
4674
5963
  const layers = [];
5964
+ const innerShadows = [];
4675
5965
  for (const e of effects) if (e.kind === "outerShdw") {
4676
5966
  const rad = e.angleDeg * Math.PI / 180;
4677
5967
  const dx = e.distEmu * Math.cos(rad) / EMU_PER_PX;
@@ -4691,7 +5981,7 @@ const buildEffectsFilter = (pres, shape) => {
4691
5981
  const opacity = e.opacity ?? 1;
4692
5982
  const i = primitives.length;
4693
5983
  primitives.push(`<feGaussianBlur in="SourceAlpha" stdDeviation="${blurPx.toFixed(2)}" result="innerBlur${i}"/>`, `<feOffset in="innerBlur${i}" dx="${dx.toFixed(2)}" dy="${dy.toFixed(2)}" result="innerOff${i}"/>`, `<feComposite in="innerOff${i}" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="innerMask${i}"/>`, `<feFlood flood-color="${color}" flood-opacity="${opacity.toFixed(3)}" result="innerCol${i}"/>`, `<feComposite in="innerCol${i}" in2="innerMask${i}" operator="in" result="innerOut${i}"/>`);
4694
- layers.push(`innerOut${i}`);
5984
+ innerShadows.push(`innerOut${i}`);
4695
5985
  } else if (e.kind === "glow") {
4696
5986
  const radiusPx = e.radiusEmu / EMU_PER_PX;
4697
5987
  const dilatePx = radiusPx * .5;
@@ -4714,11 +6004,12 @@ const buildEffectsFilter = (pres, shape) => {
4714
6004
  layers.length = 0;
4715
6005
  layers.push(`blurOut${i}`);
4716
6006
  }
4717
- if (layers.length === 0) return null;
6007
+ if (layers.length === 0 && innerShadows.length === 0) return null;
4718
6008
  const replacedSource = effects.some((e) => e.kind === "softEdge" || e.kind === "blur");
4719
6009
  const mergeChildren = layers.map((l) => `<feMergeNode in="${l}"/>`).join("");
4720
6010
  const sourceMerge = replacedSource ? "" : "<feMergeNode in=\"SourceGraphic\"/>";
4721
- primitives.push(`<feMerge>${mergeChildren}${sourceMerge}</feMerge>`);
6011
+ const innerMerge = innerShadows.map((layer) => `<feMergeNode in="${layer}"/>`).join("");
6012
+ primitives.push(`<feMerge>${mergeChildren}${sourceMerge}${innerMerge}</feMerge>`);
4722
6013
  return {
4723
6014
  id,
4724
6015
  defs: `<defs><filter id="${id}" x="-25%" y="-25%" width="150%" height="150%">${primitives.join("")}</filter></defs>`
@@ -4752,13 +6043,23 @@ const renderSlideSvg = (pres, slide, opts = {}) => {
4752
6043
  const theme = getPresentationTheme(pres);
4753
6044
  activeColorMap = getEffectiveColorMap(slide);
4754
6045
  activeDeckTextColor = resolveDeckBodyTextColor(slide) ?? "#000000";
6046
+ const position = getSlides(pres).indexOf(slide);
6047
+ activeSlideNumber = String(getPresentationFirstSlideNumber(pres) + (position < 0 ? 0 : position));
4755
6048
  const ctx = {
4756
6049
  groupScale: {
4757
6050
  sx: 1,
4758
6051
  sy: 1
4759
6052
  },
6053
+ groupReflected: false,
4760
6054
  mode: opts.textLayout ?? "foreignObject",
4761
- measure: opts.measureText ?? defaultMeasurer
6055
+ measure: opts.measureText ?? defaultMeasurer,
6056
+ ownShapes: true,
6057
+ background: {
6058
+ id: "",
6059
+ width: W / EMU_PER_PX,
6060
+ height: H / EMU_PER_PX
6061
+ },
6062
+ inverseGroupTransform: ""
4762
6063
  };
4763
6064
  let bg = getSlideBackground(slide);
4764
6065
  if (bg.kind === "inherit") {
@@ -4773,6 +6074,8 @@ const renderSlideSvg = (pres, slide, opts = {}) => {
4773
6074
  }
4774
6075
  }
4775
6076
  let bgColor = "#FFFFFF";
6077
+ const bgOpacity = bg.kind === "solid" ? bg.opacity : void 0;
6078
+ const bgOpacityAttr = bgOpacity === void 0 ? "" : ` fill-opacity="${bgOpacity}"`;
4776
6079
  let bgGradient = "";
4777
6080
  let bgGradientDefs = "";
4778
6081
  if (bg.kind === "solid") bgColor = resolveColor(bg.color, theme, "#FFFFFF");
@@ -4818,25 +6121,50 @@ const renderSlideSvg = (pres, slide, opts = {}) => {
4818
6121
  if (bytes) {
4819
6122
  const fmt = detectImageFormatLocal(bytes);
4820
6123
  const dataUrl = `data:${fmt ? imageMime[fmt] ?? "image/png" : "image/png"};base64,${u8ToBase64(bytes)}`;
4821
- bgImage = `<image x="0" y="0" width="${E(W)}" height="${E(H)}" href="${dataUrl}" xlink:href="${dataUrl}" preserveAspectRatio="xMidYMid slice"/>`;
6124
+ const layout = getSlideBackgroundImageFillLayout(slide);
6125
+ const crop = getSlideBackgroundImageCrop(slide);
6126
+ const cropLeft = crop?.left ?? 0, cropTop = crop?.top ?? 0;
6127
+ const cropRight = crop?.right ?? 0, cropBottom = crop?.bottom ?? 0;
6128
+ const opacity = getSlideBackgroundImageOpacity(slide) ?? 1;
6129
+ const intrinsic = layout?.mode === "tile" ? getSlideBackgroundImageIntrinsicSize(slide) : null;
6130
+ if (layout?.mode === "tile" && intrinsic) {
6131
+ const pattern = imageTilePattern(dataUrl, layout, intrinsic, 0, 0, W, H, cropLeft, cropTop, cropRight, cropBottom);
6132
+ bgImage = `${pattern.defs}<rect width="${E(W)}" height="${E(H)}" fill="${pattern.fill}" opacity="${opacity}"/>`;
6133
+ } else {
6134
+ const stretch = layout?.mode === "stretch" ? layout : null;
6135
+ const left = stretch?.left ?? 0;
6136
+ const top = stretch?.top ?? 0;
6137
+ const width = Math.max(0, W * (1 - left - (stretch?.right ?? 0)));
6138
+ const height = Math.max(0, H * (1 - top - (stretch?.bottom ?? 0)));
6139
+ const sourceWidth = 1 - cropLeft - cropRight;
6140
+ const sourceHeight = 1 - cropTop - cropBottom;
6141
+ const imageWidth = sourceWidth > 0 ? width / sourceWidth : 0;
6142
+ const imageHeight = sourceHeight > 0 ? height / sourceHeight : 0;
6143
+ const clipId = mintId();
6144
+ const sourceClipId = mintId();
6145
+ bgImage = `<defs><clipPath id="${clipId}"><rect width="${E(W)}" height="${E(H)}"/></clipPath><clipPath id="${sourceClipId}"><rect x="${E(W * left)}" y="${E(H * top)}" width="${E(width)}" height="${E(height)}"/></clipPath></defs><g clip-path="url(#${clipId})"><g clip-path="url(#${sourceClipId})"><image opacity="${opacity}" x="${E(W * left - imageWidth * cropLeft)}" y="${E(H * top - imageHeight * cropTop)}" width="${E(imageWidth)}" height="${E(imageHeight)}" href="${dataUrl}" xlink:href="${dataUrl}" preserveAspectRatio="none"/></g></g>`;
6146
+ }
4822
6147
  }
4823
6148
  }
4824
6149
  let layoutBgShapes = "";
4825
6150
  const layoutForBg = getSlideLayout(slide);
4826
- if (layoutForBg) try {
4827
- const masterShapes = topLevelShapes(getSlideMasterShapes(pres, layoutForBg), { dropPlaceholders: true });
6151
+ if (layoutForBg && !isSlideBackgroundGraphicsHidden(slide)) try {
6152
+ const masterShapes = topLevelShapes(isSlideLayoutBackgroundGraphicsHidden(layoutForBg) ? [] : getSlideMasterShapes(pres, layoutForBg), { dropPlaceholders: true });
4828
6153
  const layoutShapes = topLevelShapes(getSlideLayoutShapes(pres, layoutForBg), { dropPlaceholders: true });
4829
- layoutBgShapes = [...masterShapes, ...layoutShapes].map((s) => renderShape(s, pres, theme, ctx)).join("");
6154
+ const bgCtx = {
6155
+ ...ctx,
6156
+ ownShapes: false
6157
+ };
6158
+ layoutBgShapes = [...masterShapes, ...layoutShapes].map((s) => renderShape(s, pres, theme, bgCtx)).join("");
4830
6159
  } catch {
4831
6160
  layoutBgShapes = "";
4832
6161
  }
4833
6162
  const shapesSvg = topLevelShapes(getSlideShapes(slide), { dropPlaceholders: false }).map((s) => renderShape(s, pres, theme, ctx)).join("");
6163
+ const backgroundSvg = `${bgOpacity === void 0 ? "" : `<rect width="${E(W)}" height="${E(H)}" fill="#FFFFFF"/>`}<rect width="${E(W)}" height="${E(H)}" fill="${bgColor}"${bgOpacityAttr}/>${bgGradient}${bgImage}`;
4834
6164
  return [
4835
6165
  `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 ${E(W)} ${E(H)}" preserveAspectRatio="xMidYMid meet">`,
4836
6166
  bgGradientDefs,
4837
- `<rect width="${E(W)}" height="${E(H)}" fill="${bgColor}"/>`,
4838
- bgGradient,
4839
- bgImage,
6167
+ ctx.background.id ? `<defs><g id="${ctx.background.id}">${backgroundSvg}</g></defs><use href="#${ctx.background.id}" xlink:href="#${ctx.background.id}"/>` : backgroundSvg,
4840
6168
  layoutBgShapes,
4841
6169
  shapesSvg,
4842
6170
  "</svg>"
@@ -4872,10 +6200,10 @@ const auditShape = (pres, theme, shape, slideIndex, measure, tolerancePx, report
4872
6200
  }, theme, getShapePlaceholderType(shape), measure, "#000000");
4873
6201
  if (model === null) return;
4874
6202
  const vert = verticalLayoutOf(model.effectiveBody.vert ?? getShapeTextDirection(shape));
4875
- const cols = getShapeTextColumns(shape);
6203
+ const cols = model.effectiveBody.columns;
4876
6204
  const columns = vert === "none" && cols && cols.count >= 2 ? {
4877
6205
  count: cols.count,
4878
- gapPx: cols.gapEmu !== void 0 ? cols.gapEmu / EMU_PER_PX : 12
6206
+ gapPx: cols.gapEmu !== void 0 ? cols.gapEmu / EMU_PER_PX : 0
4879
6207
  } : null;
4880
6208
  const rect = model.svgTextRect(vert);
4881
6209
  if (rect.w <= 0 || rect.h <= 0) return;
@@ -4892,6 +6220,7 @@ const auditShape = (pres, theme, shape, slideIndex, measure, tolerancePx, report
4892
6220
  themeFace: model.themeFace,
4893
6221
  defaultColor: "#000000",
4894
6222
  anchor: model.anchor,
6223
+ anchorCentered: model.effectiveBody.anchorCentered ?? false,
4895
6224
  wrap: model.effectiveBody.wrap !== "none",
4896
6225
  innerX: rect.x,
4897
6226
  innerY: rect.y,
@@ -4968,7 +6297,7 @@ const detectApproximate = (paragraphs, measure) => {
4968
6297
  const seen = /* @__PURE__ */ new Set();
4969
6298
  for (const para of paragraphs) for (const piece of para.pieces) {
4970
6299
  if (piece.isBreak || piece.text === "") continue;
4971
- const key = `${piece.family}|${piece.sizePx}|${piece.bold}|${piece.italic}|${piece.text}`;
6300
+ const key = `${piece.family}|${piece.sizePx}|${piece.bold}|${piece.italic}|${piece.kerning ?? true}|${piece.text}`;
4972
6301
  if (seen.has(key)) continue;
4973
6302
  seen.add(key);
4974
6303
  const spec = {
@@ -4976,7 +6305,8 @@ const detectApproximate = (paragraphs, measure) => {
4976
6305
  sizePx: piece.sizePx,
4977
6306
  bold: piece.bold,
4978
6307
  italic: piece.italic,
4979
- letterSpacingPx: piece.letterSpacingPx
6308
+ letterSpacingPx: piece.letterSpacingPx,
6309
+ kerning: piece.kerning ?? true
4980
6310
  };
4981
6311
  const r = measure(piece.text, spec);
4982
6312
  if (r.approximate === true || r.ascentPx === void 0) return true;
@@ -5002,6 +6332,47 @@ const auditTextLayout = (pres, options = {}) => {
5002
6332
  return issues;
5003
6333
  };
5004
6334
  //#endregion
5005
- export { MONO as a, TIMES as c, substituteFamily as d, AVG_GLYPH_W_RATIO as i, defaultMeasurer as l, renderSlideSvg as n, SANS as o, ARIAL as r, SERIF as s, auditTextLayout as t, isCjk as u };
6335
+ //#region src/autofit.ts
6336
+ /**
6337
+ * How much the preview shrinks `shape`'s text to fit its box: a multiplier on
6338
+ * every authored size — font size, line spacing in points, paragraph spacing
6339
+ * and indents.
6340
+ *
6341
+ * `1` for a shape with no `<a:normAutofit>`: `<a:noAutofit>` overflows and
6342
+ * `<a:spAutoFit>` grows the box instead, so neither shrinks the text. A shape
6343
+ * with a baked `fontScale` reports that, unchanged — it is what PowerPoint
6344
+ * itself applied.
6345
+ */
6346
+ function shapeAutoFitScale(pres, shape, options = {}) {
6347
+ const bounds = options.bounds ?? getShapeBoundsResolved(pres, shape);
6348
+ if (!bounds) return 1;
6349
+ return resolveTextBodyModel(pres, shape, {
6350
+ x: bounds.x,
6351
+ y: bounds.y,
6352
+ w: bounds.w,
6353
+ h: bounds.h
6354
+ }, getPresentationTheme(pres), getShapePlaceholderType(shape), options.measureText ?? defaultMeasurer, "#000000")?.autoFitScale ?? 1;
6355
+ }
6356
+ /** Translation in preview pixels applied to the entire text block by centered
6357
+ * text anchoring. Paragraph widths and alignment remain unchanged. Editing
6358
+ * surfaces must apply the same offset to keep the caret over the preview. */
6359
+ function shapeTextAnchorOffset(pres, shape, options = {}) {
6360
+ const bounds = options.bounds ?? getShapeBoundsResolved(pres, shape);
6361
+ if (!bounds) return {
6362
+ x: 0,
6363
+ y: 0
6364
+ };
6365
+ return resolveTextBodyModel(pres, shape, {
6366
+ x: bounds.x,
6367
+ y: bounds.y,
6368
+ w: bounds.w,
6369
+ h: bounds.h
6370
+ }, getPresentationTheme(pres), getShapePlaceholderType(shape), options.measureText ?? defaultMeasurer, "#000000")?.anchorOffset ?? {
6371
+ x: 0,
6372
+ y: 0
6373
+ };
6374
+ }
6375
+ //#endregion
6376
+ export { textColumnsStyle as _, renderTextEffectsSvg as a, shapeCustomTextRect as b, MONO as c, TIMES as d, defaultMeasurer as f, textUnderlineStyle as g, paragraphNumberLabels as h, renderSlideSvg as i, SANS as l, substituteFamily as m, shapeTextAnchorOffset as n, ARIAL as o, isCjk as p, auditTextLayout as r, AVG_GLYPH_W_RATIO as s, shapeAutoFitScale as t, SERIF as u, verticalTextStyle as v, resolveTextBodyRect as y };
5006
6377
 
5007
- //# sourceMappingURL=src-DC6HxO-i.js.map
6378
+ //# sourceMappingURL=src-DYfIZIh8.js.map