@office-kit/pptx-preview 0.14.0 → 0.16.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,4 @@
1
- import { emu, getEffectiveColorMap, getGroupChildren, getGroupTransform, getParagraphAlignment, getParagraphBullet, getParagraphBulletImageBytes, getParagraphBulletStyle, getParagraphIndent, getParagraphLevel, getParagraphPropertiesEffective, getParagraphSpacing, getPresentationFirstSlideNumber, getPresentationFonts, getPresentationTheme, getPresetGeometry, 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, getShapeText3D, 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, getTableCellText3D, getTableCellTextDirection, getTableCells, getTableColumnWidths, getTableDimensions, getTableRowHeights, isChartShape, isParagraphBulletPicture, isShapeHidden, isShapeImageGrayscale, isShapePlaceholder, isShapeTextBox, isSlideBackgroundGraphicsHidden, isSlideLayoutBackgroundGraphicsHidden, isTableShape, resolveDeckBodyTextColor } from "@office-kit/pptx";
1
+ import { emu, getEffectiveColorMap, getGroupChildren, getGroupTransform, getParagraphAlignment, getParagraphBullet, getParagraphBulletImageBytes, getParagraphBulletStyle, getParagraphElementLanguages, getParagraphIndent, getParagraphLevel, getParagraphPropertiesEffective, getParagraphSpacing, getPresentationFirstSlideNumber, getPresentationFonts, getPresentationTheme, getPresetGeometry, getShape3D, 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, getShapeStrokeGradient, getShapeStrokeJoin, getShapeStrokeOpacity, getShapeStrokeSketch, getShapeText3D, 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, getTableBackgroundEffective, getTableCellAnchor, getTableCellAppearanceEffective, getTableCellMargins, getTableCellParagraphs, getTableCellRunFormatEffective, getTableCellSpan, getTableCellText3D, getTableCellTextDirection, getTableCells, getTableColumnWidths, getTableDimensions, getTableRowHeights, isChartShape, isParagraphBulletPicture, isShapeHidden, isShapeImageGrayscale, isShapePlaceholder, isShapeTextBox, isSlideBackgroundGraphicsHidden, isSlideLayoutBackgroundGraphicsHidden, isTableShape, resolveDeckBodyTextColor } from "@office-kit/pptx";
2
2
  //#region src/text-body-rect.ts
3
3
  /**
4
4
  * Where a shape lays out its text, as fractions of its own extent: the
@@ -29,7 +29,7 @@ function shapeTextRect(shape) {
29
29
  * {@link shapeTextRect}, or `null` for the whole box) placed in `bounds`, less
30
30
  * the body insets. All coordinates and margins must use the same unit
31
31
  * (normally EMU). When margins collapse a region, the region is kept without
32
- * them, as PowerPoint does.
32
+ * them, as the reference desktop app does.
33
33
  */
34
34
  function resolveTextBodyRect(bounds, margins, region) {
35
35
  const x = bounds.x + (region?.l ?? 0) * bounds.w;
@@ -53,17 +53,17 @@ function resolveTextBodyRect(bounds, margins, region) {
53
53
  }
54
54
  //#endregion
55
55
  //#region src/text-body-style.ts
56
- /** Mac PowerPoint uses zero spacing when `spcCol` is absent. */
56
+ /** The reference desktop app on Mac uses zero spacing when `spcCol` is absent. */
57
57
  const DEFAULT_COLUMN_GAP_PX = 0;
58
- const EMU_PER_PX$2 = 9525;
58
+ const EMU_PER_PX$3 = 9525;
59
59
  const NONE = {
60
60
  declarations: "",
61
61
  transform: ""
62
62
  };
63
63
  /**
64
64
  * `<a:bodyPr vert="…"/>` as CSS. `writing-mode` is the right primitive for
65
- * the East-Asian and Mongolian cases, and `text-orientation` for the WordArt
66
- * ones, which stack upright glyphs. `vert270` reads bottom-to-top, which no
65
+ * the East-Asian and Mongolian cases, and `text-orientation` for the
66
+ * `wordArtVert` ones, which stack upright glyphs. `vert270` reads bottom-to-top, which no
67
67
  * writing-mode expresses on its own — hence the half turn. Its columns still
68
68
  * follow the OOXML vertical-rl progression before that half turn; using
69
69
  * vertical-lr here reverses the column order relative to the SVG renderer.
@@ -101,7 +101,7 @@ function verticalTextStyle(vert) {
101
101
  */
102
102
  function textColumnsStyle(cols) {
103
103
  if (!cols || cols.count < 2) return "";
104
- const gapPx = cols.gapEmu !== void 0 ? (cols.gapEmu / EMU_PER_PX$2).toFixed(2) : DEFAULT_COLUMN_GAP_PX;
104
+ const gapPx = cols.gapEmu !== void 0 ? (cols.gapEmu / EMU_PER_PX$3).toFixed(2) : DEFAULT_COLUMN_GAP_PX;
105
105
  return `column-count:${cols.count};column-gap:${gapPx}px`;
106
106
  }
107
107
  //#endregion
@@ -217,6 +217,30 @@ const paragraphNumberLabels = (paraData) => {
217
217
  return numberLabels;
218
218
  };
219
219
  //#endregion
220
+ //#region src/decimal-separator.ts
221
+ const separators = /* @__PURE__ */ new Map();
222
+ /**
223
+ * The character a decimal tab stop aligns on for a run's language
224
+ * (`<a:rPr lang>`): `,` for `de-DE`, `.` for `en-US`. Runs without a
225
+ * language, or with one Intl does not know, align on `.`. The reference desktop app on Mac
226
+ * uses the run language too, not the UI or system locale (native capture
227
+ * 2026-10-07: Set Proofing Language to German moved alignment to `,`).
228
+ */
229
+ function decimalSeparatorOf(lang) {
230
+ if (!lang) return ".";
231
+ let separator = separators.get(lang);
232
+ if (separator === void 0) {
233
+ separator = ".";
234
+ try {
235
+ separator = new Intl.NumberFormat(lang, { numberingSystem: "latn" }).formatToParts(1.5).find((part) => part.type === "decimal")?.value ?? ".";
236
+ } catch (error) {
237
+ if (!(error instanceof RangeError)) throw error;
238
+ }
239
+ separators.set(lang, separator);
240
+ }
241
+ return separator;
242
+ }
243
+ //#endregion
220
244
  //#region src/emf.ts
221
245
  const WORD_BYTES = 4;
222
246
  const SHORT_BYTES = 2;
@@ -493,7 +517,7 @@ const FALLBACK_DESCENT = .22;
493
517
  const FALLBACK_LINEGAP = .08;
494
518
  const BASELINE_LEADING_DROP = .036;
495
519
  const GRID_NUDGE_X = -.75;
496
- const EMU_PER_PX$1 = 9525;
520
+ const EMU_PER_PX$2 = 9525;
497
521
  let reflectionNamespace = 0;
498
522
  const EAST_ASIAN_CHAR = /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩]/;
499
523
  const CLOSING_PUNCT = /^[、。,.・)」』】〉》”’!?:;ー〜…,.!?:;)\]}]+$/;
@@ -540,6 +564,8 @@ const escapeXml$1 = (s) => s.replace(/[&<>"]/g, (c) => ({
540
564
  ">": "&gt;",
541
565
  "\"": "&quot;"
542
566
  })[c] ?? c);
567
+ const CSS_IDENT = /^-?[A-Za-z_\u0080-\u{10FFFF}][\w\u0080-\u{10FFFF}-]*$/u;
568
+ const cssFamilyName = (family) => family.split(" ").every((word) => CSS_IDENT.test(word)) ? family : `"${family.replace(/["\\]/g, "\\$&")}"`;
543
569
  const fmt = (n) => {
544
570
  const r = Math.round(n * 100) / 100;
545
571
  return Object.is(r, -0) ? "0" : String(r);
@@ -678,7 +704,7 @@ const layoutCore = (input, measure) => {
678
704
  fieldWidth = decimalWidth = 0;
679
705
  } else {
680
706
  fieldWidth += token.width;
681
- const decimal = token.text.indexOf(".");
707
+ const decimal = token.text.indexOf(token.piece.decimalSeparator ?? ".");
682
708
  decimalWidth = decimal < 0 ? decimalWidth + token.width : mWidth(token.text.slice(0, decimal), {
683
709
  ...specOf(token.piece),
684
710
  sizePx: renderedSizePxOf(token.piece)
@@ -836,7 +862,7 @@ const lineContentExtent = (line, dx) => {
836
862
  };
837
863
  };
838
864
  const resolveFillPaints = (placements) => {
839
- if (!placements.some(({ line }) => line.tokens.some((t) => t.piece.fillPaint))) return {
865
+ if (!placements.some(({ line }) => line.tokens.some((t) => t.piece.fillPaint || t.piece.outlinePaint))) return {
840
866
  defs: "",
841
867
  placements
842
868
  };
@@ -864,12 +890,7 @@ const resolveFillPaints = (placements) => {
864
890
  };
865
891
  const painted = /* @__PURE__ */ new Map();
866
892
  let defs = "";
867
- const pieces = /* @__PURE__ */ new Map();
868
- const repaint = (piece) => {
869
- const paint = piece.fillPaint;
870
- if (paint === void 0) return piece;
871
- let next = pieces.get(piece);
872
- if (next) return next;
893
+ const resolve = (paint) => {
873
894
  let fill = painted.get(paint);
874
895
  if (fill === void 0) {
875
896
  const built = paint(bounds);
@@ -877,9 +898,16 @@ const resolveFillPaints = (placements) => {
877
898
  fill = built.fill;
878
899
  painted.set(paint, fill);
879
900
  }
901
+ return fill;
902
+ };
903
+ const pieces = /* @__PURE__ */ new Map();
904
+ const repaint = (piece) => {
905
+ let next = pieces.get(piece);
906
+ if (next) return next;
880
907
  next = {
881
908
  ...piece,
882
- fillHex: fill
909
+ ...piece.fillPaint ? { fillHex: resolve(piece.fillPaint) } : {},
910
+ ...piece.outlinePaint ? { outlineHex: resolve(piece.outlinePaint) } : {}
883
911
  };
884
912
  pieces.set(piece, next);
885
913
  return next;
@@ -888,7 +916,7 @@ const resolveFillPaints = (placements) => {
888
916
  ...placement,
889
917
  line: {
890
918
  ...placement.line,
891
- tokens: placement.line.tokens.map((token) => token.piece.fillPaint ? {
919
+ tokens: placement.line.tokens.map((token) => token.piece.fillPaint || token.piece.outlinePaint ? {
892
920
  ...token,
893
921
  piece: repaint(token.piece)
894
922
  } : token)
@@ -989,7 +1017,7 @@ const emitPlacements = (placements, effectsOnly = false, effectKind = "all") =>
989
1017
  if (line.bullet && !effectsOnly) {
990
1018
  const b = line.bullet.b;
991
1019
  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"/>`);
992
- 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>`);
1020
+ else parts.push(`<text x="${fmt(line.bullet.x + dx + GRID_NUDGE_X)}" y="${fmt(baselineY)}" font-family="${escapeXml$1(b.cssFamily ?? b.family)}" font-size="${fmt(b.sizePx)}" fill="${b.fillHex}" xml:space="preserve">${escapeXml$1(b.text)}</text>`);
993
1021
  }
994
1022
  parts.push(emitLine(line, baselineY, dx, line.descent, (kind) => nextEffectId(kind), effectsOnly, effectKind));
995
1023
  }
@@ -1086,7 +1114,7 @@ const emitTextReflections = (groups, textAnchor, x0, baselineY, descent, nextEff
1086
1114
  continue;
1087
1115
  }
1088
1116
  const scaleY = reflection.scaleY ?? 1;
1089
- const distPx = (reflection.offsetEmu ?? 0) / EMU_PER_PX$1;
1117
+ const distPx = (reflection.offsetEmu ?? 0) / EMU_PER_PX$2;
1090
1118
  const contactY = baselineY + descent;
1091
1119
  const angleRad = (reflection.angleDeg ?? 0) * Math.PI / 180;
1092
1120
  const offsetX = distPx * Math.cos(angleRad);
@@ -1099,7 +1127,7 @@ const emitTextReflections = (groups, textAnchor, x0, baselineY, descent, nextEff
1099
1127
  const gradientId = `${id}-gradient`;
1100
1128
  const maskId = `${id}-mask`;
1101
1129
  const filterId = `${id}-blur`;
1102
- const blurPx = Math.max(0, (reflection.blurEmu ?? 0) / EMU_PER_PX$1 / 2);
1130
+ const blurPx = Math.max(0, (reflection.blurEmu ?? 0) / EMU_PER_PX$2 / 2);
1103
1131
  const fadeDirection = reflection.fadeDirection;
1104
1132
  const defs = `<defs><linearGradient id="${gradientId}" ${fadeDirection === void 0 || fadeDirection === 90 ? "x1=\"0\" y1=\"1\" x2=\"0\" y2=\"0\"" : (() => {
1105
1133
  const radians = fadeDirection * Math.PI / 180;
@@ -1135,8 +1163,8 @@ const emitTextOuterEffects = (groups, textAnchor, x0, baselineY, nextId) => {
1135
1163
  if (shadow) {
1136
1164
  const id = nextId("outer-shadow");
1137
1165
  const angle = (shadow.angleDeg ?? 45) * Math.PI / 180;
1138
- const distance = (shadow.offsetEmu ?? 38100) / EMU_PER_PX$1;
1139
- const blur = Math.max(0, (shadow.blurEmu ?? 50800) / EMU_PER_PX$1 / 2);
1166
+ const distance = (shadow.offsetEmu ?? 38100) / EMU_PER_PX$2;
1167
+ const blur = Math.max(0, (shadow.blurEmu ?? 50800) / EMU_PER_PX$2 / 2);
1140
1168
  const glyphSize = renderedSizePxOf(group.piece);
1141
1169
  const pad = Math.max(1, distance + blur * 3);
1142
1170
  const color = shadow.color ?? "#000000";
@@ -1146,7 +1174,7 @@ const emitTextOuterEffects = (groups, textAnchor, x0, baselineY, nextId) => {
1146
1174
  }
1147
1175
  if (glow) {
1148
1176
  const id = nextId("glow");
1149
- const radius = Math.max(0, (glow.radiusEmu ?? 63500) / EMU_PER_PX$1);
1177
+ const radius = Math.max(0, (glow.radiusEmu ?? 63500) / EMU_PER_PX$2);
1150
1178
  const glyphSize = renderedSizePxOf(group.piece);
1151
1179
  const pad = Math.max(1, radius * 2);
1152
1180
  const color = glow.color ?? "#FFFF00";
@@ -1170,9 +1198,9 @@ const emitTextInnerShadows = (groups, textAnchor, x0, baselineY, nextId) => {
1170
1198
  }
1171
1199
  const id = nextId("inner-shadow");
1172
1200
  const angle = (shadow.angleDeg ?? 45) * Math.PI / 180;
1173
- const dx = (shadow.offsetEmu ?? 38100) / EMU_PER_PX$1 * Math.cos(angle);
1174
- const dy = (shadow.offsetEmu ?? 38100) / EMU_PER_PX$1 * Math.sin(angle);
1175
- const blurPx = Math.max(0, (shadow.blurEmu ?? 50800) / EMU_PER_PX$1 / 2);
1201
+ const dx = (shadow.offsetEmu ?? 38100) / EMU_PER_PX$2 * Math.cos(angle);
1202
+ const dy = (shadow.offsetEmu ?? 38100) / EMU_PER_PX$2 * Math.sin(angle);
1203
+ const blurPx = Math.max(0, (shadow.blurEmu ?? 50800) / EMU_PER_PX$2 / 2);
1176
1204
  const color = shadow.color ?? "#000000";
1177
1205
  const opacity = Math.max(0, Math.min(1, shadow.opacity ?? 1));
1178
1206
  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>`;
@@ -1186,8 +1214,8 @@ const emitTextBevel = (groups, textAnchor, x0, baselineY, nextId) => {
1186
1214
  const bevel = groups.find((g) => g.piece.bevel && !g.isTab && g.width > 0)?.piece.bevel;
1187
1215
  if (!bevel) return "";
1188
1216
  const id = nextId("bevel");
1189
- const widthPx = bevel.widthEmu / EMU_PER_PX$1;
1190
- const surfaceScale = bevel.heightEmu / EMU_PER_PX$1 * bevel.relief;
1217
+ const widthPx = bevel.widthEmu / EMU_PER_PX$2;
1218
+ const surfaceScale = bevel.heightEmu / EMU_PER_PX$2 * bevel.relief;
1191
1219
  const table = [...bevel.profile.slice(1).map(() => 0), ...bevel.profile];
1192
1220
  const flat = Math.sin(bevel.elevationDeg * Math.PI / 180);
1193
1221
  const lightGain = bevel.contrast / (1 - flat);
@@ -1350,7 +1378,7 @@ const tspan = (g) => {
1350
1378
  const p = g.piece;
1351
1379
  const sizePx = renderedSizePxOf(p);
1352
1380
  const attrs = [
1353
- `font-family="${escapeXml$1(p.family)}"`,
1381
+ `font-family="${escapeXml$1(p.cssFamily ?? p.family)}"`,
1354
1382
  `font-size="${fmt(sizePx)}"`,
1355
1383
  `fill="${p.fillHex}"`
1356
1384
  ];
@@ -1461,7 +1489,7 @@ function browserTextMeasurer() {
1461
1489
  }
1462
1490
  //#endregion
1463
1491
  //#region src/text-bevel.ts
1464
- const DEFAULT_BEVEL_EMU = 76200;
1492
+ const DEFAULT_BEVEL_EMU$1 = 76200;
1465
1493
  const DEFAULT_PRESET = "circle";
1466
1494
  const DEFAULT_RIG = {
1467
1495
  type: "threePt",
@@ -1582,8 +1610,8 @@ const RELIEF = 2.5;
1582
1610
  const textBevelOf = (text3d) => {
1583
1611
  const bevel = text3d?.bevelTop;
1584
1612
  if (!bevel) return null;
1585
- const widthEmu = bevel.widthEmu ?? DEFAULT_BEVEL_EMU;
1586
- const heightEmu = bevel.heightEmu ?? DEFAULT_BEVEL_EMU;
1613
+ const widthEmu = bevel.widthEmu ?? DEFAULT_BEVEL_EMU$1;
1614
+ const heightEmu = bevel.heightEmu ?? DEFAULT_BEVEL_EMU$1;
1587
1615
  if (widthEmu <= 0 || heightEmu <= 0) return null;
1588
1616
  const rig = text3d.scene?.lightRig ?? DEFAULT_RIG;
1589
1617
  const look = RIG_LOOK(rig.type);
@@ -1603,6 +1631,63 @@ const textBevelOf = (text3d) => {
1603
1631
  };
1604
1632
  };
1605
1633
  //#endregion
1634
+ //#region src/shape-3d.ts
1635
+ const EMU_PER_PX$1 = 9525;
1636
+ const DEFAULT_BEVEL_EMU = 76200;
1637
+ const LIGHT_AZIMUTH_DEG = 225;
1638
+ const LIGHT_ELEVATION_DEG = 45;
1639
+ const radians = (degrees) => degrees * Math.PI / 180;
1640
+ /**
1641
+ * An SVG `matrix(...)` for the camera's explicit rotation about the point
1642
+ * (`cx`, `cy`) in px, or `null` when the camera keeps the shape face-on.
1643
+ * The camera turns, so the shape appears turned the opposite way: a camera
1644
+ * revolution of +6° (Picture Style "Rotated, White") tilts the picture 6°
1645
+ * counter-clockwise.
1646
+ */
1647
+ const cameraTransform = (shape3d, cx, cy) => {
1648
+ const rotation = shape3d?.scene?.cameraRotation;
1649
+ if (!rotation) return null;
1650
+ const { latitudeDeg, longitudeDeg, revolutionDeg } = rotation;
1651
+ if (latitudeDeg === 0 && longitudeDeg === 0 && revolutionDeg === 0) return null;
1652
+ const lon = radians(-longitudeDeg);
1653
+ const lat = radians(-latitudeDeg);
1654
+ const rev = radians(-revolutionDeg);
1655
+ const ex = [Math.cos(lon), Math.sin(lon) * Math.sin(lat)];
1656
+ const ey = [0, Math.cos(lat)];
1657
+ const cos = Math.cos(rev);
1658
+ const sin = Math.sin(rev);
1659
+ const turn = (v) => [cos * v[0] - sin * v[1], sin * v[0] + cos * v[1]];
1660
+ const [a, b] = turn(ex);
1661
+ const [c, d] = turn(ey);
1662
+ return `matrix(${[
1663
+ a,
1664
+ b,
1665
+ c,
1666
+ d,
1667
+ cx - a * cx - c * cy,
1668
+ cy - b * cx - d * cy
1669
+ ].map((n) => n.toFixed(6)).join(" ")})`;
1670
+ };
1671
+ /**
1672
+ * The `<filter>` body that shades a top bevel onto whatever the shape painted
1673
+ * (picture, fill and border alike): a lit height map built from the blurred
1674
+ * outline, multiplied in so flat areas keep their color and slopes facing
1675
+ * away from the light darken, plus a specular highlight on the lit slopes.
1676
+ * `null` when the shape has no top bevel.
1677
+ */
1678
+ const bevelFilterPrimitives = (shape3d) => {
1679
+ const bevel = shape3d?.bevelTop;
1680
+ if (!bevel) return null;
1681
+ const widthPx = (bevel.widthEmu ?? DEFAULT_BEVEL_EMU) / EMU_PER_PX$1;
1682
+ const heightPx = (bevel.heightEmu ?? DEFAULT_BEVEL_EMU) / EMU_PER_PX$1;
1683
+ if (widthPx <= 0 || heightPx <= 0) return null;
1684
+ const blur = widthPx / 2;
1685
+ const surfaceScale = Math.min(10, Math.max(1, heightPx / widthPx * 6));
1686
+ const diffuse = 1 / Math.sin(radians(LIGHT_ELEVATION_DEG));
1687
+ const light = `<feDistantLight azimuth="${LIGHT_AZIMUTH_DEG}" elevation="${LIGHT_ELEVATION_DEG}"/>`;
1688
+ return `<feGaussianBlur in="SourceAlpha" stdDeviation="${blur.toFixed(2)}" result="bevelHeight"/><feDiffuseLighting in="bevelHeight" surfaceScale="${surfaceScale.toFixed(2)}" diffuseConstant="${diffuse.toFixed(4)}" lighting-color="#fff" result="bevelShade">${light}</feDiffuseLighting><feComposite in="SourceGraphic" in2="bevelShade" operator="arithmetic" k1="1" result="bevelLit"/><feSpecularLighting in="bevelHeight" surfaceScale="${surfaceScale.toFixed(2)}" specularConstant="0.6" specularExponent="20" lighting-color="#fff" result="bevelSpec">${light}</feSpecularLighting><feComposite in="bevelSpec" in2="SourceAlpha" operator="in" result="bevelGloss"/><feComposite in="bevelLit" in2="bevelGloss" operator="arithmetic" k2="1" k3="1"/>`;
1689
+ };
1690
+ //#endregion
1606
1691
  //#region src/render-slide.ts
1607
1692
  const clickActionHref = (pres, action) => {
1608
1693
  if (!action) return void 0;
@@ -1631,6 +1716,10 @@ const DEFAULT_BODY_PT = 18;
1631
1716
  const DEFAULT_TITLE_PT = 44;
1632
1717
  const DEFAULT_FONT = "Calibri, 'Helvetica Neue', Arial, sans-serif";
1633
1718
  const browserFontFamily = (family) => family ? `${JSON.stringify(family)}, ${DEFAULT_FONT}` : DEFAULT_FONT;
1719
+ const BROWSER_FAMILIES = {
1720
+ resolveFamily: browserFontFamily,
1721
+ familyIsCssList: true
1722
+ };
1634
1723
  const DEFAULT_BULLET_FONT = "Arial";
1635
1724
  const DEFAULT_INSET_X = 91440;
1636
1725
  const DEFAULT_INSET_Y = 45720;
@@ -1749,20 +1838,64 @@ const renderPicture = (shape, pres, x, y, w, h, transform, textOverlay, bytes, f
1749
1838
  }
1750
1839
  const opacityAttr = opacity !== 1 ? ` opacity="${opacity.toFixed(3)}"` : "";
1751
1840
  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>` : "";
1841
+ const under = outline.fill !== "none" ? `${outline.defs}<g${transform} fill="${outline.fill}"${outline.fillAttrs}>${pictureClipGeometry(shape, preset, x, y, w, h)}</g>` : "";
1752
1842
  if (layout?.mode === "tile") {
1753
1843
  const intrinsic = getShapeImageIntrinsicSize(shape);
1754
1844
  if (intrinsic) {
1755
1845
  const pattern = imageTilePattern(dataUrl, layout, intrinsic, x, y, w, h, cropL, cropT, cropR, cropB);
1756
1846
  const geometry = pictureClipGeometry(shape, preset, x, y, w, h);
1757
- return `${clipDef}${pattern.defs}<g${transform} fill="${pattern.fill}"${filterAttr}${opacityAttr}>${geometry}</g>${border}<g${transform}>${textOverlay}</g>`;
1847
+ return `${clipDef}${pattern.defs}${pictureEffects(pres, shape, {
1848
+ x,
1849
+ y,
1850
+ w,
1851
+ h
1852
+ }, `${under}<g${transform} fill="${pattern.fill}"${filterAttr}${opacityAttr}>${geometry}</g>${border}`)}<g${transform}>${textOverlay}</g>`;
1758
1853
  }
1759
1854
  }
1760
- 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>`;
1855
+ return `${clipDef}${pictureEffects(pres, shape, {
1856
+ x,
1857
+ y,
1858
+ w,
1859
+ h
1860
+ }, `${under}<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>`;
1761
1861
  }
1762
1862
  const linkUrl = getShapeImageLinkUrl(shape);
1763
1863
  const label = !bytes ? linkUrl ? `picture (link: ${linkUrl.length > 48 ? linkUrl.slice(0, 45) + "…" : linkUrl})` : "picture (no bytes)" : `picture (${format ?? "unknown"}${bytes ? `, ${bytes.byteLength} B` : ""})`;
1764
1864
  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>`;
1765
1865
  };
1866
+ /**
1867
+ * Wraps a picture's markup in its 3-D approximation (bevel lighting, then the
1868
+ * camera rotation) and its `<a:effectLst>` (shadows, glow, soft edge and the
1869
+ * reflection), the same effects a shape gets.
1870
+ */
1871
+ const pictureEffects = (pres, shape, box, markup) => {
1872
+ const shape3d = getShape3D(shape);
1873
+ let defs = "";
1874
+ let content = markup;
1875
+ let mirrored = markup;
1876
+ const bevel = bevelFilterPrimitives(shape3d);
1877
+ if (bevel) {
1878
+ const id = mintId();
1879
+ defs += `<defs><filter id="${id}" x="-10%" y="-10%" width="120%" height="120%" color-interpolation-filters="sRGB">${bevel}</filter></defs>`;
1880
+ content = `<g filter="url(#${id})">${content}</g>`;
1881
+ }
1882
+ const camera = cameraTransform(shape3d, (box.x + box.w / 2) / EMU_PER_PX, (box.y + box.h / 2) / EMU_PER_PX);
1883
+ if (camera) {
1884
+ content = `<g transform="${camera}">${content}</g>`;
1885
+ mirrored = `<g transform="${camera}">${mirrored}</g>`;
1886
+ }
1887
+ const reflection = buildReflection(pres, shape, mirrored, box);
1888
+ const fx = buildEffectsFilter(pres, shape);
1889
+ if (fx) {
1890
+ defs += fx.defs;
1891
+ content = `<g filter="url(#${fx.id})">${content}</g>`;
1892
+ }
1893
+ if (reflection) {
1894
+ defs += reflection.defs;
1895
+ content = reflection.svg + content;
1896
+ }
1897
+ return defs + content;
1898
+ };
1766
1899
  const imageTilePattern = (dataUrl, layout, intrinsic, x, y, w, h, cropL = 0, cropT = 0, cropR = 0, cropB = 0) => {
1767
1900
  const sourceW = intrinsic.width * (layout.scaleX ?? 1);
1768
1901
  const sourceH = intrinsic.height * (layout.scaleY ?? 1);
@@ -2064,7 +2197,22 @@ const patternDef = (pat) => {
2064
2197
  fillAttr: `url(#${id})`
2065
2198
  };
2066
2199
  };
2067
- const textFillPaint = (fill, theme) => (box) => {
2200
+ const textFillPaint = (fill, theme, solid) => (box) => {
2201
+ if (fill.kind === "none") return {
2202
+ defs: "",
2203
+ fill: "none"
2204
+ };
2205
+ if (fill.kind === "image") {
2206
+ if (!fill.bytes) return {
2207
+ defs: "",
2208
+ fill: solid
2209
+ };
2210
+ const id = mintId();
2211
+ return {
2212
+ defs: `<defs><pattern id="${id}" patternUnits="userSpaceOnUse" x="${box.x.toFixed(2)}" y="${box.y.toFixed(2)}" width="${box.w.toFixed(2)}" height="${box.h.toFixed(2)}"><image href="${bytesToDataUrl(fill.bytes)}" width="${box.w.toFixed(2)}" height="${box.h.toFixed(2)}" preserveAspectRatio="none"/></pattern></defs>`,
2213
+ fill: `url(#${id})`
2214
+ };
2215
+ }
2068
2216
  if (fill.kind === "pattern") {
2069
2217
  const built = patternDef({
2070
2218
  preset: fill.preset,
@@ -2122,6 +2270,32 @@ const buildArrowMarker = (type, width, length, color, orient) => {
2122
2270
  def: `<defs><marker id="${id}" viewBox="0 0 ${w} ${h}" refX="${w}" refY="${h / 2}" markerWidth="${w}" markerHeight="${h}" orient="${orient}">${body}</marker></defs>`
2123
2271
  };
2124
2272
  };
2273
+ const unrotatedGradientTransform = (grad, shape, pres) => {
2274
+ const bounds = shape && (pres ? getShapeBoundsResolved(pres, shape) : getShapeBounds(shape));
2275
+ if (grad.rotateWithShape !== false || !shape || !bounds || bounds.w <= 0 || bounds.h <= 0) return "";
2276
+ const angle = getShapeRotation(shape) * Math.PI / 180;
2277
+ const cos = Math.cos(angle);
2278
+ const sin = Math.sin(angle);
2279
+ const w = bounds.w;
2280
+ const h = bounds.h;
2281
+ const rotatedW = Math.abs(cos) * w + Math.abs(sin) * h;
2282
+ const rotatedH = Math.abs(sin) * w + Math.abs(cos) * h;
2283
+ const flip = getShapeFlip(shape);
2284
+ const sx = flip?.horizontal ? -1 : 1;
2285
+ const sy = flip?.vertical ? -1 : 1;
2286
+ const a = sx * cos * rotatedW / w;
2287
+ const b = -sy * sin * rotatedW / h;
2288
+ const c = sx * sin * rotatedH / w;
2289
+ const d = sy * cos * rotatedH / h;
2290
+ return ` gradientTransform="matrix(${[
2291
+ a,
2292
+ b,
2293
+ c,
2294
+ d,
2295
+ (1 - a - c) / 2,
2296
+ (1 - b - d) / 2
2297
+ ].map((value) => value.toFixed(6)).join(" ")})"`;
2298
+ };
2125
2299
  const paint = (shape, fill, stroke, theme, isPlaceholder, pres) => {
2126
2300
  let fillColor;
2127
2301
  let fillAttrs = "";
@@ -2136,33 +2310,7 @@ const paint = (shape, fill, stroke, theme, isPlaceholder, pres) => {
2136
2310
  else if (fill.kind === "gradient") {
2137
2311
  const grad = shape ? pres ? getShapeGradientFillEffective(pres, shape) : getShapeGradientFill(shape) : null;
2138
2312
  if (grad) {
2139
- let transform = "";
2140
- const bounds = shape && (pres ? getShapeBoundsResolved(pres, shape) : getShapeBounds(shape));
2141
- if (grad.rotateWithShape === false && shape && bounds && bounds.w > 0 && bounds.h > 0) {
2142
- const angle = getShapeRotation(shape) * Math.PI / 180;
2143
- const cos = Math.cos(angle);
2144
- const sin = Math.sin(angle);
2145
- const w = bounds.w;
2146
- const h = bounds.h;
2147
- const rotatedW = Math.abs(cos) * w + Math.abs(sin) * h;
2148
- const rotatedH = Math.abs(sin) * w + Math.abs(cos) * h;
2149
- const flip = getShapeFlip(shape);
2150
- const sx = flip?.horizontal ? -1 : 1;
2151
- const sy = flip?.vertical ? -1 : 1;
2152
- const a = sx * cos * rotatedW / w;
2153
- const b = -sy * sin * rotatedW / h;
2154
- const c = sx * sin * rotatedH / w;
2155
- const d = sy * cos * rotatedH / h;
2156
- transform = ` gradientTransform="matrix(${[
2157
- a,
2158
- b,
2159
- c,
2160
- d,
2161
- (1 - a - c) / 2,
2162
- (1 - b - d) / 2
2163
- ].map((value) => value.toFixed(6)).join(" ")})"`;
2164
- }
2165
- const built = gradientDef(grad, theme, transform);
2313
+ const built = gradientDef(grad, theme, unrotatedGradientTransform(grad, shape, pres));
2166
2314
  defs = built.defs;
2167
2315
  fillColor = built.fillAttr;
2168
2316
  } else fillColor = (shape && pres ? getShapeFillColorResolved(pres, shape) : null) ?? "none";
@@ -2179,10 +2327,34 @@ const paint = (shape, fill, stroke, theme, isPlaceholder, pres) => {
2179
2327
  let strokeWidth = 0;
2180
2328
  const strokeAttrParts = [];
2181
2329
  let markerAttrs = "";
2330
+ let headColor = strokeColor;
2331
+ let tailColor = strokeColor;
2182
2332
  if (stroke.kind === "solid") {
2183
2333
  let resolved = null;
2184
2334
  if (shape && pres) resolved = getShapeStrokeColorResolved(pres, shape);
2185
2335
  strokeColor = resolved ?? resolveColor(stroke.color, theme, "#9CA3AF");
2336
+ headColor = strokeColor;
2337
+ tailColor = strokeColor;
2338
+ } else if (stroke.kind === "gradient") {
2339
+ const grad = shape ? getShapeStrokeGradient(shape, pres) : null;
2340
+ const bounds = shape && (pres ? getShapeBoundsResolved(pres, shape) : getShapeBounds(shape));
2341
+ if (grad && bounds) {
2342
+ const stopColor = (stop) => stop ? stop.resolvedColor ?? resolveColor(stop.color, theme, "#9CA3AF") : "#9CA3AF";
2343
+ const ordered = [...grad.stops].sort((a, b) => a.offset - b.offset);
2344
+ headColor = stopColor(ordered[0]);
2345
+ tailColor = stopColor(ordered[ordered.length - 1]);
2346
+ const widthEmu = stroke.widthEmu ?? 9525;
2347
+ const built = bounds.w > 0 && bounds.h > 0 ? gradientDef(grad, theme, unrotatedGradientTransform(grad, shape, pres)) : gradientDef(grad, theme, "", {
2348
+ x: (bounds.w > 0 ? bounds.x : bounds.x - widthEmu / 2) / EMU_PER_PX,
2349
+ y: (bounds.h > 0 ? bounds.y : bounds.y - widthEmu / 2) / EMU_PER_PX,
2350
+ w: Math.max(bounds.w, widthEmu) / EMU_PER_PX,
2351
+ h: Math.max(bounds.h, widthEmu) / EMU_PER_PX
2352
+ });
2353
+ defs += built.defs;
2354
+ strokeColor = built.fillAttr;
2355
+ }
2356
+ }
2357
+ if (strokeColor !== "none" && (stroke.kind === "solid" || stroke.kind === "gradient")) {
2186
2358
  strokeWidth = stroke.widthEmu ?? 9525;
2187
2359
  if (shape) {
2188
2360
  const opacity = getShapeStrokeOpacity(shape, pres);
@@ -2208,12 +2380,12 @@ const paint = (shape, fill, stroke, theme, isPlaceholder, pres) => {
2208
2380
  const head = getShapeStrokeArrow(shape, "head", pres);
2209
2381
  const tail = getShapeStrokeArrow(shape, "tail", pres);
2210
2382
  if (head && head.type !== "none") {
2211
- const m = buildArrowMarker(head.type, head.width, head.length, strokeColor, "auto-start-reverse");
2383
+ const m = buildArrowMarker(head.type, head.width, head.length, headColor, "auto-start-reverse");
2212
2384
  defs += m.def;
2213
2385
  markerAttrs += ` marker-start="url(#${m.id})"`;
2214
2386
  }
2215
2387
  if (tail && tail.type !== "none") {
2216
- const m = buildArrowMarker(tail.type, tail.width, tail.length, strokeColor, "auto");
2388
+ const m = buildArrowMarker(tail.type, tail.width, tail.length, tailColor, "auto");
2217
2389
  defs += m.def;
2218
2390
  markerAttrs += ` marker-end="url(#${m.id})"`;
2219
2391
  }
@@ -2260,7 +2432,7 @@ const cssColorWithOpacity = (hex, opacity) => {
2260
2432
  };
2261
2433
  const renderRun = (text, format, theme, effectivePt, defaultColor, _wasDefault = false, suppressLineHeight = false, lineHeightOverride = void 0) => {
2262
2434
  if (text === "") return "";
2263
- if (format?.textFill) {
2435
+ if (format?.textFill || format?.outline?.fill) {
2264
2436
  const { textFill: _fill, outline: _outline, shadow: _shadow, glow: _glow, underline: _underline, strike: _strike, ...layout } = format;
2265
2437
  return renderRun(text, {
2266
2438
  ...layout,
@@ -2320,6 +2492,10 @@ const renderRun = (text, format, theme, effectivePt, defaultColor, _wasDefault =
2320
2492
  const LINE_HEIGHT = 1.05;
2321
2493
  const AUTOFIT_FLOOR = .25;
2322
2494
  const AUTOFIT_STEP = .05;
2495
+ const decimalSeparatorField = (lang) => {
2496
+ const separator = decimalSeparatorOf(lang);
2497
+ return separator === "." ? {} : { decimalSeparator: separator };
2498
+ };
2323
2499
  const underlineStyleOf = (fmt) => {
2324
2500
  const u = fmt?.underline;
2325
2501
  if (u === void 0 || u === false || u === "none") return "none";
@@ -2349,7 +2525,7 @@ const strikeStyleOf = (fmt) => {
2349
2525
  if (s === "dblStrike") return "double";
2350
2526
  return s !== void 0 && s !== false && s !== "noStrike";
2351
2527
  };
2352
- const hasPaintedTextFill = (run) => !!run.fmt?.textFill && !run.href;
2528
+ const hasPaintedTextFill = (run) => !!(run.fmt?.textFill || run.fmt?.outline?.fill) && !run.href;
2353
2529
  const alignOf = (a) => a === "center" || a === "right" || a === "justify" ? a : "left";
2354
2530
  const verticalLayoutOf = (vert) => {
2355
2531
  switch (vert) {
@@ -2387,6 +2563,13 @@ const smallCapsParts = (text, base) => {
2387
2563
  flush();
2388
2564
  return parts;
2389
2565
  };
2566
+ const faceOf = (a, font) => {
2567
+ const family = (a.resolveFamily ?? substituteFamily)(font);
2568
+ return a.familyIsCssList ? { family } : {
2569
+ family,
2570
+ cssFamily: cssFamilyName(family)
2571
+ };
2572
+ };
2390
2573
  const buildSvgTextInput = (a) => {
2391
2574
  const scale = a.autoFitScale;
2392
2575
  const paragraphs = a.paraData.map((para, pi) => {
@@ -2395,7 +2578,7 @@ const buildSvgTextInput = (a) => {
2395
2578
  if (run.text === "\n") {
2396
2579
  pieces.push({
2397
2580
  ...breakPiece(),
2398
- family: (a.resolveFamily ?? substituteFamily)(run.fmt?.font ?? a.themeFace),
2581
+ ...faceOf(a, run.fmt?.font ?? a.themeFace),
2399
2582
  sizePx: run.sizePt * scale * PX_PER_PT,
2400
2583
  bold: run.fmt?.bold ?? false,
2401
2584
  italic: run.fmt?.italic ?? false
@@ -2411,7 +2594,7 @@ const buildSvgTextInput = (a) => {
2411
2594
  underline: fmt?.underline ?? true
2412
2595
  };
2413
2596
  }
2414
- const family = (a.resolveFamily ?? substituteFamily)(fmt?.font ?? a.themeFace);
2597
+ const face = faceOf(a, fmt?.font ?? a.themeFace);
2415
2598
  const sizePx = run.sizePt * scale * PX_PER_PT;
2416
2599
  const fillHex = fmt?.color !== void 0 && fmt.color !== null ? resolveColor(fmt.color, a.theme, "#000000") : a.defaultColor;
2417
2600
  const baseline = fmt?.baseline ?? 0;
@@ -2420,14 +2603,17 @@ const buildSvgTextInput = (a) => {
2420
2603
  const cap = fmt?.cap;
2421
2604
  const caps = cap === "all" || cap === "small";
2422
2605
  const base = {
2423
- family,
2606
+ ...face,
2424
2607
  sizePx,
2425
2608
  bold: fmt?.bold ?? false,
2426
2609
  italic: fmt?.italic ?? false,
2427
2610
  letterSpacingPx,
2428
2611
  kerning,
2429
2612
  fillHex,
2430
- ...fmt?.textFill && !run.href ? { fillPaint: textFillPaint(fmt.textFill, a.theme) } : {},
2613
+ ...hasPaintedTextFill(run) ? { fillPaint: fmt?.textFill ? textFillPaint(fmt.textFill, a.theme, fillHex) : () => ({
2614
+ defs: "",
2615
+ fill: fillHex
2616
+ }) } : {},
2431
2617
  ...fmt?.underlineColor !== void 0 && fmt.underlineColor !== null ? { underlineHex: resolveColor(fmt.underlineColor, a.theme, fillHex) } : {},
2432
2618
  underline: underlineStyleOf(fmt),
2433
2619
  strike: strikeStyleOf(fmt),
@@ -2436,6 +2622,21 @@ const buildSvgTextInput = (a) => {
2436
2622
  outlineHex: resolveColor(fmt.outline.color, a.theme, "#000000"),
2437
2623
  outlineWidthPx: (fmt.outline.widthEmu ?? 9525) / EMU_PER_PX
2438
2624
  } : {},
2625
+ ...fmt?.outline?.fill && !run.href ? (() => {
2626
+ const fill = fmt.outline.fill;
2627
+ const first = fill.stops[0];
2628
+ return {
2629
+ outlineHex: first ? first.resolvedColor ?? resolveColor(first.color, a.theme, "#000000") : "#000000",
2630
+ outlinePaint: (box) => {
2631
+ const built = gradientDef(fill, a.theme, "", box);
2632
+ return {
2633
+ defs: built.defs,
2634
+ fill: built.fillAttr
2635
+ };
2636
+ },
2637
+ outlineWidthPx: (fmt.outline.widthEmu ?? 9525) / EMU_PER_PX
2638
+ };
2639
+ })() : {},
2439
2640
  ...fmt?.shadow ? { shadow: {
2440
2641
  ...fmt.shadow,
2441
2642
  ...fmt.shadow.color ? { color: resolveColor(fmt.shadow.color, a.theme, "#000000") } : {}
@@ -2452,7 +2653,8 @@ const buildSvgTextInput = (a) => {
2452
2653
  } } : {},
2453
2654
  ...a.bevel ? { bevel: a.bevel } : {},
2454
2655
  href: run.href ?? null,
2455
- ...run.hrefTip !== void 0 ? { hrefTip: run.hrefTip } : {}
2656
+ ...run.hrefTip !== void 0 ? { hrefTip: run.hrefTip } : {},
2657
+ ...run.decimalSeparator !== void 0 ? { decimalSeparator: run.decimalSeparator } : {}
2456
2658
  };
2457
2659
  const segs = run.text.split("\n");
2458
2660
  segs.forEach((seg, i) => {
@@ -2547,10 +2749,9 @@ const buildBullet = (a, para, pi) => {
2547
2749
  const baseSizePx = (para.runs.find((r) => r.text !== "\n" && r.text !== "")?.sizePt ?? a.defaultPt) * PX_PER_PT * a.autoFitScale;
2548
2750
  const sizePx = para.bulletDetail.sizePct !== null ? baseSizePx * para.bulletDetail.sizePct : para.bulletDetail.sizePts !== null ? para.bulletDetail.sizePts * PX_PER_PT * a.autoFitScale : baseSizePx;
2549
2751
  const fillHex = bulletFillOf(para, a.theme, a.defaultColor);
2550
- 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;
2551
2752
  return {
2552
2753
  text: char,
2553
- family: (a.resolveFamily ?? substituteFamily)(bulletFont),
2754
+ ...faceOf(a, para.bulletDetail.fontFollowText ? para.runs.find((r) => r.text !== "\n" && r.text !== "")?.fmt?.font ?? DEFAULT_BULLET_FONT : para.bulletDetail.font ?? DEFAULT_BULLET_FONT),
2554
2755
  sizePx,
2555
2756
  fillHex,
2556
2757
  ...para.bulletImageHref ? { imageHref: para.bulletImageHref } : {}
@@ -2561,7 +2762,7 @@ const bulletFillOf = (para, theme, defaultColor) => {
2561
2762
  const runColor = para.runs.find((r) => r.text !== "\n" && r.text !== "")?.fmt?.color;
2562
2763
  return runColor === void 0 || runColor === null ? defaultColor : resolveColor(runColor, theme, defaultColor);
2563
2764
  };
2564
- const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defaultColor, useSvgTextRect = false) => {
2765
+ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defaultColor, useSvgTextRect = false, resolveFamily = substituteFamily) => {
2565
2766
  let paragraphCount;
2566
2767
  try {
2567
2768
  paragraphCount = getShapeParagraphCount(shape);
@@ -2684,10 +2885,11 @@ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defau
2684
2885
  } catch {
2685
2886
  elements = [];
2686
2887
  }
2888
+ const languages = elements.length ? getParagraphElementLanguages(shape, p) : [];
2687
2889
  let rIdx = 0;
2688
2890
  let fieldIndex = 0;
2689
2891
  let breakIndex = 0;
2690
- for (const el of elements) {
2892
+ for (const [elementIndex, el] of elements.entries()) {
2691
2893
  if (el.kind === "br") {
2692
2894
  const fmt = getShapeRunFormatEffective(pres, shape, p, { breakIndex: breakIndex++ });
2693
2895
  runs.push({
@@ -2722,6 +2924,7 @@ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defau
2722
2924
  text: txt,
2723
2925
  fmt,
2724
2926
  sizePt,
2927
+ ...decimalSeparatorField(languages[elementIndex]),
2725
2928
  ...href !== void 0 ? { href } : {},
2726
2929
  ...hrefTip !== void 0 ? { hrefTip } : {}
2727
2930
  });
@@ -2778,7 +2981,8 @@ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defau
2778
2981
  innerH: fitRect.h,
2779
2982
  measure,
2780
2983
  vert: fitVert,
2781
- columns: fitColumns
2984
+ columns: fitColumns,
2985
+ resolveFamily
2782
2986
  };
2783
2987
  let s = 1;
2784
2988
  while (s > AUTOFIT_FLOOR) {
@@ -2818,6 +3022,7 @@ const resolveTextBodyModel = (pres, shape, bounds, theme, phType, measure, defau
2818
3022
  measure,
2819
3023
  vert,
2820
3024
  autoFitScale,
3025
+ resolveFamily,
2821
3026
  columns: vert === "none" && cols && cols.count >= 2 ? {
2822
3027
  count: cols.count,
2823
3028
  gapPx: cols.gapEmu !== void 0 ? cols.gapEmu / EMU_PER_PX : 0
@@ -2861,9 +3066,15 @@ const renderHtmlParagraphs = (paraData, numberLabels, theme, autoFitScale, defau
2861
3066
  let runFmt = run.fmt;
2862
3067
  if (run.href) {
2863
3068
  const hlinkColor = theme ? normalizeHex(theme.hyperlink) : "#0563C1";
2864
- const { textFill: _linkFill, ...linkFmt } = runFmt ?? {};
3069
+ const { textFill: _linkFill, outline: linkOutline, ...linkFmt } = runFmt ?? {};
3070
+ const { fill: outlineFill, ...solidOutline } = linkOutline ?? {};
3071
+ const outlineColor = outlineFill?.stops[0]?.color ?? solidOutline.color;
2865
3072
  runFmt = {
2866
3073
  ...linkFmt,
3074
+ ...linkOutline ? { outline: {
3075
+ ...solidOutline,
3076
+ ...outlineColor !== void 0 ? { color: outlineColor } : {}
3077
+ } } : {},
2867
3078
  color: hlinkColor,
2868
3079
  underline: runFmt?.underline ?? true
2869
3080
  };
@@ -2894,8 +3105,8 @@ const renderHtmlParagraphs = (paraData, numberLabels, theme, autoFitScale, defau
2894
3105
  "padding:0",
2895
3106
  `text-align:${ALIGNMENT_TO_CSS[para.align] ?? "left"}`,
2896
3107
  lineHeightCss,
2897
- leftPx > 0 ? `padding-left:${leftPx.toFixed(2)}px` : "",
2898
- rightPx > 0 ? `padding-right:${rightPx.toFixed(2)}px` : "",
3108
+ leftPx > 0 ? `padding-inline-start:${leftPx.toFixed(2)}px` : "",
3109
+ rightPx > 0 ? `padding-inline-end:${rightPx.toFixed(2)}px` : "",
2899
3110
  firstLinePx !== 0 ? `text-indent:${firstLinePx.toFixed(2)}px` : ""
2900
3111
  ].filter(Boolean);
2901
3112
  let prefix = "";
@@ -2929,7 +3140,7 @@ const renderHtmlParagraphs = (paraData, numberLabels, theme, autoFitScale, defau
2929
3140
  return paragraphs;
2930
3141
  };
2931
3142
  const renderTextBody = (pres, shape, bounds, theme, phType, ctx, output = "body") => {
2932
- const model = resolveTextBodyModel(pres, shape, bounds, theme, phType, ctx.measure, activeDeckTextColor, ctx.mode === "svg" && output === "body");
3143
+ const model = resolveTextBodyModel(pres, shape, bounds, theme, phType, ctx.measure, activeDeckTextColor, ctx.mode === "svg" && output === "body", ctx.resolveFamily);
2933
3144
  if (model === null) return "";
2934
3145
  const bevel = textBevelOf(getShapeText3D(shape));
2935
3146
  const { paraData, numberLabels, authoredAutofit, autoFitScale, defaultPt, themeFace, effectiveDefaultFont, effectiveBody, anchor, innerX, innerY, innerW, innerH, svgTextRect } = model;
@@ -2965,7 +3176,7 @@ const renderTextBody = (pres, shape, bounds, theme, phType, ctx, output = "body"
2965
3176
  innerW: vInnerW,
2966
3177
  innerH: vInnerH,
2967
3178
  measure: ctx.mode === "svg" ? ctx.measure : browserTextMeasurer() ?? ctx.measure,
2968
- ...ctx.mode === "foreignObject" ? { resolveFamily: browserFontFamily } : {},
3179
+ ...ctx.mode === "foreignObject" ? BROWSER_FAMILIES : { resolveFamily: ctx.resolveFamily },
2969
3180
  vert: svgVert,
2970
3181
  columns: svgColumns,
2971
3182
  bevel
@@ -2975,7 +3186,7 @@ const renderTextBody = (pres, shape, bounds, theme, phType, ctx, output = "body"
2975
3186
  ...svgArgsBase,
2976
3187
  autoFitScale: authoredAutofit ? autoFitScale : 1,
2977
3188
  effectsOnly: true,
2978
- resolveFamily: browserFontFamily
3189
+ ...BROWSER_FAMILIES
2979
3190
  };
2980
3191
  const effects = [
2981
3192
  paraData.some((para) => para.runs.some((run) => run.fmt?.reflection != null)) ? buildAndLayoutSvgText({
@@ -3056,7 +3267,7 @@ const renderTextBody = (pres, shape, bounds, theme, phType, ctx, output = "body"
3056
3267
  vert: svgVert,
3057
3268
  columns: null,
3058
3269
  effectsOnly: true,
3059
- resolveFamily: browserFontFamily,
3270
+ ...BROWSER_FAMILIES,
3060
3271
  bevel
3061
3272
  };
3062
3273
  const hasCharacterReflection = paraData.some((para) => para.runs.some((run) => run.fmt?.reflection !== void 0 && run.fmt.reflection !== null));
@@ -3113,6 +3324,7 @@ const renderTextEffectsSvg = (pres, slide, shape, bounds, opts = {}) => {
3113
3324
  groupReflected: false,
3114
3325
  mode: "foreignObject",
3115
3326
  measure: opts.measureText ?? browserTextMeasurer() ?? defaultMeasurer,
3327
+ resolveFamily: substituteFamily,
3116
3328
  ownShapes: true,
3117
3329
  background: {
3118
3330
  id: "",
@@ -3149,6 +3361,7 @@ const renderTableCellEffectsSvgInternal = (pres, slide, shape, cellPosition, bou
3149
3361
  groupReflected: false,
3150
3362
  mode: "foreignObject",
3151
3363
  measure: opts.measureText ?? browserTextMeasurer() ?? defaultMeasurer,
3364
+ resolveFamily: substituteFamily,
3152
3365
  ownShapes: true,
3153
3366
  background: {
3154
3367
  id: "",
@@ -4554,10 +4767,11 @@ const cellParaData = (paragraphs, cell, pres) => {
4554
4767
  const bulletIsPicture = isParagraphBulletPicture(cell, index);
4555
4768
  const bulletImageBytes = bulletIsPicture ? getParagraphBulletImageBytes(cell, index) : null;
4556
4769
  const runs = [];
4770
+ const languages = getParagraphElementLanguages(cell, index);
4557
4771
  let rIdx = 0;
4558
4772
  let fieldIndex = 0;
4559
4773
  let breakIndex = 0;
4560
- for (const el of para.elements) {
4774
+ for (const [elementIndex, el] of para.elements.entries()) {
4561
4775
  if (el.kind === "br") {
4562
4776
  const fmt = getTableCellRunFormatEffective(pres, cell, index, { breakIndex: breakIndex++ });
4563
4777
  runs.push({
@@ -4574,6 +4788,7 @@ const cellParaData = (paragraphs, cell, pres) => {
4574
4788
  text: el.text,
4575
4789
  fmt,
4576
4790
  sizePt: fmt?.size ?? DEFAULT_BODY_PT,
4791
+ ...decimalSeparatorField(languages[elementIndex]),
4577
4792
  ...href ? {
4578
4793
  href,
4579
4794
  ...el.tooltip !== void 0 ? { hrefTip: el.tooltip } : {}
@@ -4642,7 +4857,7 @@ const renderTableCellText = (cell, paragraphs, cx, cy, cw, ch, color, pres, shap
4642
4857
  innerW: innerW * EMU_PER_PX,
4643
4858
  innerH: innerH * EMU_PER_PX,
4644
4859
  measure: ctx.mode === "svg" ? ctx.measure : browserTextMeasurer() ?? ctx.measure,
4645
- ...ctx.mode === "foreignObject" ? { resolveFamily: browserFontFamily } : {},
4860
+ ...ctx.mode === "foreignObject" ? BROWSER_FAMILIES : { resolveFamily: ctx.resolveFamily },
4646
4861
  vert: verticalLayoutOf(getTableCellTextDirection(cell)),
4647
4862
  columns: null,
4648
4863
  ...effectsOnly ? { effectsOnly: true } : {},
@@ -4679,6 +4894,33 @@ const renderTableCellText = (cell, paragraphs, cx, cy, cw, ch, color, pres, shap
4679
4894
  const body = renderHtmlParagraphs(paraData, numberLabels, theme, 1, DEFAULT_BODY_PT, color).join("");
4680
4895
  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()}${bevelOverlay()}`;
4681
4896
  };
4897
+ const TWO_STOP_TABLE_GRADIENT_EXPONENT = 1.85;
4898
+ const TWO_STOP_TABLE_GRADIENT_SAMPLES = 16;
4899
+ const twoStopTableGradient = (grad, theme) => {
4900
+ if (grad.path !== void 0 || grad.stops.length !== 2) return grad;
4901
+ const [start, end] = [...grad.stops].sort((a, b) => a.offset - b.offset);
4902
+ if (!start || !end || end.offset <= start.offset) return grad;
4903
+ const span = end.offset - start.offset;
4904
+ const colorOf = (stop) => stop.resolvedColor ?? resolveColor(stop.color, theme, "#E5E7EB");
4905
+ const startColor = colorOf(start);
4906
+ const endColor = colorOf(end);
4907
+ const stops = Array.from({ length: 17 }, (_, i) => {
4908
+ const along = i / TWO_STOP_TABLE_GRADIENT_SAMPLES;
4909
+ const startWeight = (1 - along) ** TWO_STOP_TABLE_GRADIENT_EXPONENT;
4910
+ const color = mixHex(startColor, endColor, startWeight);
4911
+ const opacity = start.opacity === void 0 && end.opacity === void 0 ? void 0 : (start.opacity ?? 1) * startWeight + (end.opacity ?? 1) * (1 - startWeight);
4912
+ return {
4913
+ offset: start.offset + along * span,
4914
+ color,
4915
+ resolvedColor: color,
4916
+ ...opacity === void 0 ? {} : { opacity }
4917
+ };
4918
+ });
4919
+ return {
4920
+ ...grad,
4921
+ stops
4922
+ };
4923
+ };
4682
4924
  const renderTable = (shape, pres, x, y, w, h, transform, theme, ctx) => {
4683
4925
  let dims;
4684
4926
  let widths;
@@ -4711,6 +4953,16 @@ const renderTable = (shape, pres, x, y, w, h, transform, theme, ctx) => {
4711
4953
  const tableThemeFace = getPresentationFonts(pres)?.minorLatin ?? null;
4712
4954
  const out = [];
4713
4955
  out.push(`<g${transform}>`);
4956
+ const background = getTableBackgroundEffective(pres, shape);
4957
+ const tableW = (colXs[dims.cols] ?? xPx) - xPx;
4958
+ const tableH = (rowYs[dims.rows] ?? yPx) - yPx;
4959
+ if (background?.fill.kind === "solid") {
4960
+ const opacity = background.fillOpacity !== void 0 && background.fillOpacity < 1 ? ` fill-opacity="${background.fillOpacity.toFixed(3)}"` : "";
4961
+ out.push(`<rect x="${px(xPx)}" y="${px(yPx)}" width="${px(tableW)}" height="${px(tableH)}" fill="${resolveColor(background.fill.color, theme, "#FFFFFF")}"${opacity}/>`);
4962
+ } else if (background?.gradient) {
4963
+ const built = gradientDef(twoStopTableGradient(background.gradient, theme), theme);
4964
+ out.push(`${built.defs}<rect x="${px(xPx)}" y="${px(yPx)}" width="${px(tableW)}" height="${px(tableH)}" fill="${built.fillAttr}"/>`);
4965
+ }
4714
4966
  const borderEdges = [];
4715
4967
  const borderEdgeCandidates = /* @__PURE__ */ new Map();
4716
4968
  for (let r = 0; r < dims.rows; r++) for (let c = 0; c < dims.cols; c++) {
@@ -4733,7 +4985,8 @@ const renderTable = (shape, pres, x, y, w, h, transform, theme, ctx) => {
4733
4985
  else if (fill.kind !== "inherit") resolvedFill = "none";
4734
4986
  else resolvedFill = "none";
4735
4987
  const cellTextColor = textColor;
4736
- out.push(`<g data-pptx-cell="${r},${c}"><rect x="${px(cx)}" y="${px(cy)}" width="${px(cw)}" height="${px(ch)}" fill="${resolvedFill}"/>`);
4988
+ const fillOpacity = fill.kind === "solid" && appearance.fillOpacity !== void 0 && appearance.fillOpacity < 1 ? ` fill-opacity="${appearance.fillOpacity.toFixed(3)}"` : "";
4989
+ out.push(`<g data-pptx-cell="${r},${c}"><rect x="${px(cx)}" y="${px(cy)}" width="${px(cw)}" height="${px(ch)}" fill="${resolvedFill}"${fillOpacity}/>`);
4737
4990
  const borders = appearance.borders;
4738
4991
  const dashAttr = (dash, widthPx) => {
4739
4992
  if (!dash || dash === "solid") return "";
@@ -4893,6 +5146,148 @@ const PATH_SHADE = {
4893
5146
  lighten: "fill=\"#fff\" fill-opacity=\"0.4\"",
4894
5147
  lightenLess: "fill=\"#fff\" fill-opacity=\"0.2\""
4895
5148
  };
5149
+ const SKETCH_AMPLITUDE = {
5150
+ curved: .008,
5151
+ freehand: .014,
5152
+ scribble: .022
5153
+ };
5154
+ const SKETCH_STEP = .12;
5155
+ const seededRandom = (seed) => {
5156
+ let state = seed >>> 0;
5157
+ return () => {
5158
+ state = state + 1831565813 >>> 0;
5159
+ let t = state;
5160
+ t = Math.imul(t ^ t >>> 15, t | 1);
5161
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
5162
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
5163
+ };
5164
+ };
5165
+ /**
5166
+ * An approximation of a sketched outline (`ask:lineSketchStyleProps`): every
5167
+ * edge of `geom` is redrawn as a seeded hand-drawn wobble. Only used for a
5168
+ * preset geometry the writer left crisp; sketches the reference desktop app wrote already
5169
+ * carry their hand-drawn path as custom geometry.
5170
+ */
5171
+ const sketchGeometry = (geom, sketch, w, h, seed) => {
5172
+ const random = seededRandom(seed);
5173
+ const jitter = (amp) => (random() * 2 - 1) * amp;
5174
+ const paths = geom.paths.map((path) => {
5175
+ const cw = path.w ?? w;
5176
+ const ch = path.h ?? h;
5177
+ const size = Math.max(Math.min(cw, ch), Math.max(cw, ch) / 4);
5178
+ const amp = SKETCH_AMPLITUDE[sketch] * size;
5179
+ const step = SKETCH_STEP * Math.max(cw, ch);
5180
+ const nudge = (pt) => ({
5181
+ x: pt.x + jitter(amp),
5182
+ y: pt.y + jitter(amp)
5183
+ });
5184
+ const commands = [];
5185
+ let curX = 0;
5186
+ let curY = 0;
5187
+ let startX = 0;
5188
+ let startY = 0;
5189
+ const wobbleTo = (x, y) => {
5190
+ const dx = x - curX;
5191
+ const dy = y - curY;
5192
+ const len = Math.hypot(dx, dy);
5193
+ const count = step > 0 ? Math.max(1, Math.round(len / step)) : 1;
5194
+ const nx = len > 0 ? -dy / len : 0;
5195
+ const ny = len > 0 ? dx / len : 0;
5196
+ let px = curX;
5197
+ let py = curY;
5198
+ for (let i = 1; i <= count; i++) {
5199
+ const last = i === count;
5200
+ const ex = curX + dx * i / count + (last ? 0 : jitter(amp / 2));
5201
+ const ey = curY + dy * i / count + (last ? 0 : jitter(amp / 2));
5202
+ const bend = jitter(amp);
5203
+ const control = {
5204
+ x: (px + ex) / 2 + nx * bend,
5205
+ y: (py + ey) / 2 + ny * bend
5206
+ };
5207
+ commands.push({
5208
+ kind: "quadBezTo",
5209
+ pts: [control, {
5210
+ x: ex,
5211
+ y: ey
5212
+ }]
5213
+ });
5214
+ px = ex;
5215
+ py = ey;
5216
+ }
5217
+ curX = x;
5218
+ curY = y;
5219
+ };
5220
+ for (const cmd of path.commands) switch (cmd.kind) {
5221
+ case "moveTo":
5222
+ curX = cmd.pt.x;
5223
+ curY = cmd.pt.y;
5224
+ startX = curX;
5225
+ startY = curY;
5226
+ commands.push(cmd);
5227
+ break;
5228
+ case "lnTo":
5229
+ wobbleTo(cmd.pt.x, cmd.pt.y);
5230
+ break;
5231
+ case "quadBezTo":
5232
+ commands.push({
5233
+ kind: "quadBezTo",
5234
+ pts: [nudge(cmd.pts[0]), cmd.pts[1]]
5235
+ });
5236
+ curX = cmd.pts[1].x;
5237
+ curY = cmd.pts[1].y;
5238
+ break;
5239
+ case "cubicBezTo":
5240
+ commands.push({
5241
+ kind: "cubicBezTo",
5242
+ pts: [
5243
+ nudge(cmd.pts[0]),
5244
+ nudge(cmd.pts[1]),
5245
+ cmd.pts[2]
5246
+ ]
5247
+ });
5248
+ curX = cmd.pts[2].x;
5249
+ curY = cmd.pts[2].y;
5250
+ break;
5251
+ case "arcTo":
5252
+ for (const s of arcToCubicSegments(curX, curY, cmd.wR, cmd.hR, cmd.stAng, cmd.swAng)) {
5253
+ commands.push({
5254
+ kind: "cubicBezTo",
5255
+ pts: [
5256
+ nudge({
5257
+ x: s.c1x,
5258
+ y: s.c1y
5259
+ }),
5260
+ nudge({
5261
+ x: s.c2x,
5262
+ y: s.c2y
5263
+ }),
5264
+ {
5265
+ x: s.ex,
5266
+ y: s.ey
5267
+ }
5268
+ ]
5269
+ });
5270
+ curX = s.ex;
5271
+ curY = s.ey;
5272
+ }
5273
+ break;
5274
+ case "close":
5275
+ if (curX !== startX || curY !== startY) wobbleTo(startX, startY);
5276
+ commands.push(cmd);
5277
+ curX = startX;
5278
+ curY = startY;
5279
+ break;
5280
+ }
5281
+ return {
5282
+ ...path,
5283
+ commands
5284
+ };
5285
+ });
5286
+ return {
5287
+ ...geom,
5288
+ paths
5289
+ };
5290
+ };
4896
5291
  /**
4897
5292
  * Renders an evaluated {@link CustomGeometry} (a `<a:custGeom>` or a preset
4898
5293
  * from `getPresetGeometry`) as `<path>`s in the shape's slide box. Returns
@@ -5109,6 +5504,11 @@ const renderShapeContent = (shape, pres, theme, ctx) => {
5109
5504
  const customGeom = rawPreset === null ? getShapeCustomGeometry(shape) : null;
5110
5505
  let geomSvg = customGeom !== null ? customGeometryToSvg(customGeom, x, y, w, h, p.fill, fa, p.stroke, p.strokeWidth, sa, ma) : "";
5111
5506
  const isCustGeom = geomSvg === "" && rawPreset === null && getShapeXmlString(shape).includes("custGeom");
5507
+ const sketch = rawPreset !== null ? getShapeStrokeSketch(shape) : null;
5508
+ if (geomSvg !== "") {} else if (sketch !== null && p.stroke !== "none") {
5509
+ const geometry = getPresetGeometry(preset, bounds, getShapeAdjustValues(shape));
5510
+ if (geometry !== null) geomSvg = customGeometryToSvg(sketchGeometry(geometry, sketch, w, h, getShapeId(shape)), x, y, w, h, p.fill, fa, p.stroke, p.strokeWidth, sa, ma);
5511
+ }
5112
5512
  if (geomSvg !== "") {} else if (preset === "rect") geomSvg = `<rect x="${E(x)}" y="${E(y)}" width="${E(w)}" height="${E(h)}" fill="${p.fill}"${fa} stroke="${p.stroke}" stroke-width="${E(p.strokeWidth)}"${sa}${ma}/>`;
5113
5513
  else {
5114
5514
  const geometry = getPresetGeometry(preset, bounds, getShapeAdjustValues(shape));
@@ -5201,11 +5601,14 @@ const buildReflection = (pres, shape, geomRaw, box) => {
5201
5601
  const f = refl.scaleY ?? -1;
5202
5602
  const startA = refl.startOpacity ?? 1;
5203
5603
  const endA = refl.opacity ?? 0;
5604
+ const startPos = refl.startPosition ?? 0;
5605
+ const endPos = Math.max(startPos, refl.endPosition ?? 1);
5204
5606
  const contactPx = (box.y + box.h) / EMU_PER_PX;
5205
5607
  const transform = `translate(0 ${(refl.distEmu / EMU_PER_PX + contactPx * (1 - f)).toFixed(2)}) scale(1 ${f})`;
5206
5608
  const maskId = mintId();
5207
5609
  const gradId = mintId();
5208
- const defs = `<defs><linearGradient id="${gradId}" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#fff" stop-opacity="${startA.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(#${gradId})"/></mask></defs>`;
5610
+ const [y1, y2] = f < 0 ? [1, 0] : [0, 1];
5611
+ const defs = `<defs><linearGradient id="${gradId}" x1="0" y1="${y1}" x2="0" y2="${y2}"><stop offset="${startPos.toFixed(3)}" stop-color="#fff" stop-opacity="${startA.toFixed(3)}"/><stop offset="${endPos.toFixed(3)}" stop-color="#fff" stop-opacity="${endA.toFixed(3)}"/></linearGradient><mask id="${maskId}" maskContentUnits="objectBoundingBox"><rect width="1" height="1" fill="url(#${gradId})"/></mask></defs>`;
5209
5612
  return {
5210
5613
  svg: `<g transform="${transform}" mask="url(#${maskId})" data-pptx-reflection="1">${geomRaw}</g>`,
5211
5614
  defs
@@ -5253,9 +5656,9 @@ const buildEffectsFilter = (pres, shape) => {
5253
5656
  primitives.push(`<feMorphology in="SourceAlpha" operator="dilate" radius="${dilatePx.toFixed(2)}" result="glowExp${i}"/>`, `<feGaussianBlur in="glowExp${i}" stdDeviation="${featherPx.toFixed(2)}" result="glowBlur${i}"/>`, `<feFlood flood-color="${color}" flood-opacity="${opacity.toFixed(3)}" result="glowCol${i}"/>`, `<feComposite in="glowCol${i}" in2="glowBlur${i}" operator="in" result="glowOut${i}"/>`);
5254
5657
  layers.push(`glowOut${i}`);
5255
5658
  } else if (e.kind === "softEdge") {
5256
- const blurPx = e.radiusEmu / EMU_PER_PX / 2;
5659
+ const radiusPx = e.radiusEmu / EMU_PER_PX;
5257
5660
  const i = primitives.length;
5258
- primitives.push(`<feGaussianBlur in="SourceGraphic" stdDeviation="${blurPx.toFixed(2)}" result="softOut${i}"/>`);
5661
+ primitives.push(`<feMorphology in="SourceAlpha" operator="erode" radius="${(radiusPx / 2).toFixed(2)}" result="softCore${i}"/>`, `<feGaussianBlur in="softCore${i}" stdDeviation="${(radiusPx / 4).toFixed(2)}" result="softMask${i}"/>`, `<feComposite in="SourceGraphic" in2="softMask${i}" operator="in" result="softOut${i}"/>`);
5259
5662
  layers.length = 0;
5260
5663
  layers.push(`softOut${i}`);
5261
5664
  } else if (e.kind === "blur") {
@@ -5297,14 +5700,33 @@ const topLevelShapes = (shapes, opts) => {
5297
5700
  }
5298
5701
  return out;
5299
5702
  };
5300
- const renderSlideSvg = (pres, slide, opts = {}) => {
5703
+ const renderSlideSvg = (pres, slide, opts = {}) => renderSurface(pres, {
5704
+ slide,
5705
+ layout: getSlideLayout(slide),
5706
+ master: false
5707
+ }, opts);
5708
+ /**
5709
+ * Draws a slide layout — or, with `master`, its slide master — the way
5710
+ * the reference desktop app's Slide Master view shows it behind the placeholders: the
5711
+ * background and the decorative (non-placeholder) shapes of the master and,
5712
+ * for a layout, of the layout itself. A layout that hides background graphics
5713
+ * leaves the master's shapes out. Placeholders are not drawn; an editor draws
5714
+ * them with their prompt text over this.
5715
+ */
5716
+ const renderSlideLayoutSvg = (pres, layout, opts = {}) => renderSurface(pres, {
5717
+ slide: null,
5718
+ layout,
5719
+ master: opts.master ?? false
5720
+ }, opts);
5721
+ const renderSurface = (pres, surface, opts) => {
5722
+ const { slide, master } = surface;
5301
5723
  const size = getSlideSize(pres) ?? DEFAULT_SIZE;
5302
5724
  const W = size.width;
5303
5725
  const H = size.height;
5304
5726
  const theme = getPresentationTheme(pres);
5305
- activeColorMap = getEffectiveColorMap(slide);
5306
- activeDeckTextColor = resolveDeckBodyTextColor(slide) ?? "#000000";
5307
- const position = getSlides(pres).indexOf(slide);
5727
+ activeColorMap = getEffectiveColorMap(slide ?? surface.layout);
5728
+ activeDeckTextColor = resolveDeckBodyTextColor(slide ?? surface.layout) ?? "#000000";
5729
+ const position = slide ? getSlides(pres).indexOf(slide) : 0;
5308
5730
  activeSlideNumber = String(getPresentationFirstSlideNumber(pres) + (position < 0 ? 0 : position));
5309
5731
  const ctx = {
5310
5732
  groupScale: {
@@ -5314,6 +5736,7 @@ const renderSlideSvg = (pres, slide, opts = {}) => {
5314
5736
  groupReflected: false,
5315
5737
  mode: opts.textLayout ?? "foreignObject",
5316
5738
  measure: opts.measureText ?? defaultMeasurer,
5739
+ resolveFamily: opts.textLayout === "svg" ? opts.resolveFamily ?? substituteFamily : substituteFamily,
5317
5740
  ownShapes: true,
5318
5741
  background: {
5319
5742
  id: "",
@@ -5322,11 +5745,11 @@ const renderSlideSvg = (pres, slide, opts = {}) => {
5322
5745
  },
5323
5746
  inverseGroupTransform: ""
5324
5747
  };
5325
- let bg = getSlideBackground(slide);
5748
+ let bg = slide ? getSlideBackground(slide) : { kind: "inherit" };
5326
5749
  if (bg.kind === "inherit") {
5327
- const layout = getSlideLayout(slide);
5750
+ const layout = surface.layout;
5328
5751
  if (layout) {
5329
- const layoutBg = getSlideLayoutBackground(layout);
5752
+ const layoutBg = master ? { kind: "inherit" } : getSlideLayoutBackground(layout);
5330
5753
  if (layoutBg.kind !== "inherit") bg = layoutBg;
5331
5754
  else {
5332
5755
  const masterBg = getSlideMasterBackground(pres, layout);
@@ -5341,11 +5764,11 @@ const renderSlideSvg = (pres, slide, opts = {}) => {
5341
5764
  let bgGradientDefs = "";
5342
5765
  if (bg.kind === "solid") bgColor = resolveColor(bg.color, theme, "#FFFFFF");
5343
5766
  else if (bg.kind === "gradient") {
5344
- let grad = getSlideBackgroundGradientFill(slide);
5767
+ let grad = slide ? getSlideBackgroundGradientFill(slide) : null;
5345
5768
  if (!grad) {
5346
- const layout = getSlideLayout(slide);
5769
+ const layout = surface.layout;
5347
5770
  if (layout) {
5348
- grad = getSlideLayoutBackgroundGradientFill(layout);
5771
+ grad = master ? null : getSlideLayoutBackgroundGradientFill(layout);
5349
5772
  if (!grad) grad = getSlideMasterBackgroundGradientFill(pres, layout);
5350
5773
  }
5351
5774
  }
@@ -5355,11 +5778,11 @@ const renderSlideSvg = (pres, slide, opts = {}) => {
5355
5778
  bgGradient = `<rect width="${E(W)}" height="${E(H)}" fill="${built.fillAttr}"/>`;
5356
5779
  }
5357
5780
  } else if (bg.kind === "pattern") {
5358
- let pat = getSlideBackgroundPatternFill(pres, slide);
5781
+ let pat = slide ? getSlideBackgroundPatternFill(pres, slide) : null;
5359
5782
  if (!pat) {
5360
- const layout = getSlideLayout(slide);
5783
+ const layout = surface.layout;
5361
5784
  if (layout) {
5362
- pat = getSlideLayoutBackgroundPatternFill(pres, layout);
5785
+ pat = master ? null : getSlideLayoutBackgroundPatternFill(pres, layout);
5363
5786
  if (!pat) pat = getSlideMasterBackgroundPatternFill(pres, layout);
5364
5787
  }
5365
5788
  }
@@ -5371,23 +5794,23 @@ const renderSlideSvg = (pres, slide, opts = {}) => {
5371
5794
  } else if (theme && bg.kind === "inherit") bgColor = normalizeHex(theme.light1);
5372
5795
  let bgImage = "";
5373
5796
  if (bg.kind === "image") {
5374
- let bytes = getSlideBackgroundImageBytes(slide);
5797
+ let bytes = slide ? getSlideBackgroundImageBytes(slide) : null;
5375
5798
  if (!bytes && pres) {
5376
- const layout = getSlideLayout(slide);
5799
+ const layout = surface.layout;
5377
5800
  if (layout) {
5378
- bytes = getSlideLayoutBackgroundImageBytes(pres, layout);
5801
+ bytes = master ? null : getSlideLayoutBackgroundImageBytes(pres, layout);
5379
5802
  if (!bytes) bytes = getSlideMasterBackgroundImageBytes(pres, layout);
5380
5803
  }
5381
5804
  }
5382
5805
  if (bytes) {
5383
5806
  const fmt = detectImageFormatLocal(bytes);
5384
5807
  const dataUrl = `data:${fmt ? imageMime[fmt] ?? "image/png" : "image/png"};base64,${u8ToBase64(bytes)}`;
5385
- const layout = getSlideBackgroundImageFillLayout(slide);
5386
- const crop = getSlideBackgroundImageCrop(slide);
5808
+ const layout = slide ? getSlideBackgroundImageFillLayout(slide) : null;
5809
+ const crop = slide ? getSlideBackgroundImageCrop(slide) : null;
5387
5810
  const cropLeft = crop?.left ?? 0, cropTop = crop?.top ?? 0;
5388
5811
  const cropRight = crop?.right ?? 0, cropBottom = crop?.bottom ?? 0;
5389
- const opacity = getSlideBackgroundImageOpacity(slide) ?? 1;
5390
- const intrinsic = layout?.mode === "tile" ? getSlideBackgroundImageIntrinsicSize(slide) : null;
5812
+ const opacity = (slide && getSlideBackgroundImageOpacity(slide)) ?? 1;
5813
+ const intrinsic = layout?.mode === "tile" && slide ? getSlideBackgroundImageIntrinsicSize(slide) : null;
5391
5814
  if (layout?.mode === "tile" && intrinsic) {
5392
5815
  const pattern = imageTilePattern(dataUrl, layout, intrinsic, 0, 0, W, H, cropLeft, cropTop, cropRight, cropBottom);
5393
5816
  bgImage = `${pattern.defs}<rect width="${E(W)}" height="${E(H)}" fill="${pattern.fill}" opacity="${opacity}"/>`;
@@ -5408,10 +5831,11 @@ const renderSlideSvg = (pres, slide, opts = {}) => {
5408
5831
  }
5409
5832
  }
5410
5833
  let layoutBgShapes = "";
5411
- const layoutForBg = getSlideLayout(slide);
5412
- if (layoutForBg && !isSlideBackgroundGraphicsHidden(slide)) try {
5413
- const masterShapes = topLevelShapes(isSlideLayoutBackgroundGraphicsHidden(layoutForBg) ? [] : getSlideMasterShapes(pres, layoutForBg), { dropPlaceholders: true });
5414
- const layoutShapes = topLevelShapes(getSlideLayoutShapes(pres, layoutForBg), { dropPlaceholders: true });
5834
+ const layoutForBg = surface.layout;
5835
+ const paintBackground = opts.background ?? true;
5836
+ if (paintBackground && layoutForBg && !(slide && isSlideBackgroundGraphicsHidden(slide))) try {
5837
+ const masterShapes = topLevelShapes(!master && isSlideLayoutBackgroundGraphicsHidden(layoutForBg) ? [] : getSlideMasterShapes(pres, layoutForBg), { dropPlaceholders: true });
5838
+ const layoutShapes = topLevelShapes(master ? [] : getSlideLayoutShapes(pres, layoutForBg), { dropPlaceholders: true });
5415
5839
  const bgCtx = {
5416
5840
  ...ctx,
5417
5841
  ownShapes: false
@@ -5420,12 +5844,12 @@ const renderSlideSvg = (pres, slide, opts = {}) => {
5420
5844
  } catch {
5421
5845
  layoutBgShapes = "";
5422
5846
  }
5423
- const shapesSvg = topLevelShapes(getSlideShapes(slide), { dropPlaceholders: false }).map((s) => renderShape(s, pres, theme, ctx)).join("");
5847
+ const shapesSvg = topLevelShapes(slide ? getSlideShapes(slide) : [], { dropPlaceholders: false }).map((s) => renderShape(s, pres, theme, ctx)).join("");
5424
5848
  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}`;
5425
5849
  return [
5426
5850
  `<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">`,
5427
5851
  bgGradientDefs,
5428
- ctx.background.id ? `<defs><g id="${ctx.background.id}">${backgroundSvg}</g></defs><use href="#${ctx.background.id}" xlink:href="#${ctx.background.id}"/>` : backgroundSvg,
5852
+ ctx.background.id ? `<defs><g id="${ctx.background.id}">${backgroundSvg}</g></defs>${paintBackground ? `<use href="#${ctx.background.id}" xlink:href="#${ctx.background.id}"/>` : ""}` : paintBackground ? backgroundSvg : "",
5429
5853
  layoutBgShapes,
5430
5854
  shapesSvg,
5431
5855
  "</svg>"
@@ -5458,7 +5882,7 @@ const auditShape = (pres, theme, shape, slideIndex, measure, tolerancePx, report
5458
5882
  y: bounds.y,
5459
5883
  w: bounds.w,
5460
5884
  h: bounds.h
5461
- }, theme, getShapePlaceholderType(shape), measure, "#000000");
5885
+ }, theme, getShapePlaceholderType(shape), measure, "#000000", false, passthroughFamily);
5462
5886
  if (model === null) return;
5463
5887
  const vert = verticalLayoutOf(model.effectiveBody.vert ?? getShapeTextDirection(shape));
5464
5888
  const cols = model.effectiveBody.columns;
@@ -5601,7 +6025,7 @@ const auditTextLayout = (pres, options = {}) => {
5601
6025
  *
5602
6026
  * `1` for a shape with no `<a:normAutofit>`: `<a:noAutofit>` overflows and
5603
6027
  * `<a:spAutoFit>` grows the box instead, so neither shrinks the text. A shape
5604
- * with a baked `fontScale` reports that, unchanged — it is what PowerPoint
6028
+ * with a baked `fontScale` reports that, unchanged — it is what the reference desktop app
5605
6029
  * itself applied.
5606
6030
  */
5607
6031
  function shapeAutoFitScale(pres, shape, options = {}) {
@@ -5634,6 +6058,6 @@ function shapeTextAnchorOffset(pres, shape, options = {}) {
5634
6058
  };
5635
6059
  }
5636
6060
  //#endregion
5637
- export { textColumnsStyle as _, renderTextEffectsSvg as a, shapeTextRect 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 };
6061
+ export { shapeTextRect as S, paragraphNumberLabels as _, renderSlideSvg as a, verticalTextStyle as b, AVG_GLYPH_W_RATIO as c, SERIF as d, TIMES as f, decimalSeparatorOf as g, substituteFamily as h, renderSlideLayoutSvg as i, MONO as l, isCjk as m, shapeTextAnchorOffset as n, renderTextEffectsSvg as o, defaultMeasurer as p, auditTextLayout as r, ARIAL as s, shapeAutoFitScale as t, SANS as u, textUnderlineStyle as v, resolveTextBodyRect as x, textColumnsStyle as y };
5638
6062
 
5639
- //# sourceMappingURL=src-Bwmf324Z.js.map
6063
+ //# sourceMappingURL=src-u1-ERSHC.js.map