@shbernal/pptxgenjs 5.2.0 → 5.4.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,32 +1,25 @@
1
- import { a as emuToInches, c as inchesToEmu, i as STANDARD_LAYOUTS } from "./units-DmzbVUNp.js";
2
- import { A as DEF_TEXT_GLOW, B as ONEPT, D as DEF_SHAPE_SHADOW, G as SCHEME_COLOR_NAMES, H as PIECHART_COLORS, I as LAYOUT_IDX_SERIES_BASE, K as SHAPE_TYPE, L as LETTERS, M as EMU, N as IMG_BROKEN, R as LINEH_MODIFIER, T as DEF_PRES_LAYOUT_NAME, U as PLACEHOLDER_TYPES, V as OutputType, W as REGEX_HEX_COLOR, X as ShapeType, Y as SchemeColor, _ as DEF_CELL_MARGIN_IN, a as AXIS_ID_SERIES_PRIMARY, b as DEF_CHART_GRIDLINE, c as AlignH, f as CHART_TYPE, g as DEF_CELL_BORDER, i as AXIS_ID_CATEGORY_SECONDARY, j as DEF_TEXT_SHADOW, k as DEF_SLIDE_MARGIN_IN, l as AlignV, m as ChartType, o as AXIS_ID_VALUE_PRIMARY, q as SLDNUMFLDID, r as AXIS_ID_CATEGORY_PRIMARY, s as AXIS_ID_VALUE_SECONDARY, u as BARCHART_COLORS, w as DEF_PRES_LAYOUT, x as DEF_FONT_COLOR, y as DEF_CHART_BORDER } from "./core-interfaces-vUc0ElZs.js";
1
+ import { a as coordToEmu, i as STANDARD_LAYOUTS, l as inchesToEmu, o as emuToInches } from "./units-BMrBTU0-.js";
2
+ import { A as DEF_SLIDE_MARGIN_IN, E as DEF_PRES_LAYOUT_NAME, G as REGEX_HEX_COLOR, H as OutputType, J as SLDNUMFLDID, K as SCHEME_COLOR_NAMES, L as LAYOUT_IDX_SERIES_BASE, M as DEF_TEXT_SHADOW, N as EMU, O as DEF_SHAPE_SHADOW, P as IMG_BROKEN, R as LETTERS, S as DEF_FONT_COLOR, T as DEF_PRES_LAYOUT, U as PIECHART_COLORS, V as ONEPT, W as PLACEHOLDER_TYPES, X as SchemeColor, Z as ShapeType, _ as DEF_CELL_BORDER, a as AXIS_ID_SERIES_PRIMARY, b as DEF_CHART_BORDER, c as AlignH, f as CHART_TYPE, h as ChartType, i as AXIS_ID_CATEGORY_SECONDARY, j as DEF_TEXT_GLOW, l as AlignV, o as AXIS_ID_VALUE_PRIMARY, p as CONNECTOR_PRESETS, q as SHAPE_TYPE, r as AXIS_ID_CATEGORY_PRIMARY, s as AXIS_ID_VALUE_SECONDARY, tt as VALID_SHAPE_PRESETS, u as BARCHART_COLORS, v as DEF_CELL_MARGIN_IN, x as DEF_CHART_GRIDLINE, z as LINEH_MODIFIER } from "./core-interfaces-C091uvh_.js";
3
3
  import JSZip from "jszip";
4
4
  //#region src/gen-utils.ts
5
5
  /**
6
6
  * PptxGenJS: Utility Methods
7
7
  */
8
8
  /**
9
- * Translates any type of `x`/`y`/`w`/`h` prop to EMU
10
- * - guaranteed to return a result regardless of undefined, null, etc. (0)
11
- * - {number} - 12800 (EMU)
12
- * - {number} - 0.5 (inches)
13
- * - {string} - "75%"
14
- * @param {number|string} size - numeric ("5.5") or percentage ("90%")
15
- * @param {'X' | 'Y'} xyDir - direction
16
- * @param {PresLayout} layout - presentation layout
17
- * @returns {number} calculated size
9
+ * Resolve a user `Coord` (x/y/w/h) to EMU — the single user-coordinate → EMU boundary.
10
+ * - bare `number` **inches** (no magnitude guessing); `"<n>%"` → percent of the slide axis;
11
+ * `"<n>in"`/`"<n>pt"`/`"<n>emu"` explicit units (see {@link Coord} / {@link coordToEmu})
12
+ * - `null`/`undefined` 0 (callers may omit a coordinate)
13
+ * - throws on a non-finite number rather than silently collapsing the object to zero size
14
+ * @param {Coord|null|undefined} size - user coordinate
15
+ * @param {'X' | 'Y'} xyDir - axis (selects slide width vs height for percentages)
16
+ * @param {PresLayout} layout - presentation layout (EMU dimensions)
17
+ * @returns {Emu} resolved EMU value
18
18
  */
19
19
  function getSmartParseNumber(size, xyDir, layout) {
20
- if (typeof size === "string" && !isNaN(Number(size))) size = Number(size);
20
+ if (size === null || size === void 0) return 0;
21
21
  if (typeof size === "number" && !isFinite(size)) throw new Error(`Invalid ${xyDir || "coordinate"} value: expected a finite number but received ${String(size)}. This usually means a layout dimension was read from a missing property (e.g. \`layout.width\` returning \`undefined\`). Use \`slide.width\`/\`slide.height\` or \`STANDARD_LAYOUTS.<NAME>.width\`/\`.height\` (inches).`);
22
- if (typeof size === "number" && size < 100) return inch2Emu(size);
23
- if (typeof size === "number" && size >= 100) return size;
24
- if (typeof size === "string" && size.includes("%")) {
25
- if (xyDir && xyDir === "X") return Math.round(parseFloat(size) / 100 * layout.width);
26
- if (xyDir && xyDir === "Y") return Math.round(parseFloat(size) / 100 * layout.height);
27
- return Math.round(parseFloat(size) / 100 * layout.width);
28
- }
29
- return 0;
22
+ return coordToEmu(size, xyDir === "Y" ? layout.height : layout.width);
30
23
  }
31
24
  /**
32
25
  * Basic UUID Generator Adapted
@@ -99,14 +92,16 @@ function getDuplicateObjectNames(names) {
99
92
  return Array.from(dupes);
100
93
  }
101
94
  /**
102
- * Convert inches into EMU
103
- * @param {number|string} inches - as string or number
104
- * @returns {number} EMU value
95
+ * Convert inches into EMU.
96
+ * - accepts a number (inches) or a numeric/`"<n>in"` string
97
+ * - no magnitude guessing: values are always treated as inches (use {@link coordToEmu} for
98
+ * user coordinates that may carry other units)
99
+ * @param {number|string} inches - inches as number or string
100
+ * @returns {Emu} EMU value
105
101
  */
106
102
  function inch2Emu(inches) {
107
- if (typeof inches === "number" && inches > 100) return inches;
108
103
  if (typeof inches === "string") inches = Number(inches.replace(/in*/gi, ""));
109
- return Math.round(EMU * inches);
104
+ return inchesToEmu(inches);
110
105
  }
111
106
  /**
112
107
  * Convert `pt` into points (using `ONEPT`)
@@ -118,6 +113,33 @@ function valToPts(pt) {
118
113
  return isNaN(points) ? 0 : Math.round(points * ONEPT);
119
114
  }
120
115
  /**
116
+ * Convert a transparency percentage (0-100) into a schema-valid `<a:alpha>` value
117
+ * (ST_PositiveFixedPercentage, 0-100000). Out-of-range transparency yields an
118
+ * alpha that PowerPoint rejects as needing repair, so clamp into range and warn.
119
+ */
120
+ function transparencyToAlpha(transparency) {
121
+ const pct = Math.min(100, Math.max(0, transparency));
122
+ if (pct !== transparency) console.warn(`Warning: transparency ${transparency} is outside the valid range 0-100; using ${pct}.`);
123
+ return Math.round((100 - pct) * 1e3);
124
+ }
125
+ /** Convert an opacity (0-1) into a schema-valid `<a:alpha>` value (0-100000); clamps + warns on out-of-range input. */
126
+ function opacityToAlpha(opacity) {
127
+ const o = Math.min(1, Math.max(0, opacity));
128
+ if (o !== opacity) console.warn(`Warning: opacity ${opacity} is outside the valid range 0-1; using ${o}.`);
129
+ return Math.round(o * 1e5);
130
+ }
131
+ /**
132
+ * Convert a line width (points) to EMU clamped into ST_LineWidth (0..20116800 EMU,
133
+ * i.e. 0-1584pt). Out-of-range widths make PowerPoint report the package as needing
134
+ * repair, so clamp into range and warn.
135
+ */
136
+ function lineWidthToEmu(widthPts) {
137
+ const raw = valToPts(widthPts);
138
+ const clamped = Math.min(20116800, Math.max(0, raw));
139
+ if (clamped !== raw) console.warn(`Warning: line width ${widthPts} is outside the valid range 0-1584pt; using ${clamped / ONEPT}.`);
140
+ return clamped;
141
+ }
142
+ /**
121
143
  * Convert degrees (0..360) to PowerPoint `rot` value
122
144
  * @param {number} d degrees
123
145
  * @returns {number} calculated `rot` value
@@ -165,8 +187,10 @@ function createColorElement(colorStr, innerElements) {
165
187
  }
166
188
  let colorVal = (colorStr || "").replace("#", "");
167
189
  if (/^[0-9a-fA-F]{8}$/.test(colorVal)) {
168
- const alphaHex = colorVal.slice(6, 8);
169
- innerElements = `<a:alpha val="${Math.round(parseInt(alphaHex, 16) / 255 * 1e5)}"/>${innerElements || ""}`;
190
+ if (!innerElements?.includes("<a:alpha")) {
191
+ const alphaHex = colorVal.slice(6, 8);
192
+ innerElements = `<a:alpha val="${Math.round(parseInt(alphaHex, 16) / 255 * 1e5)}"/>${innerElements || ""}`;
193
+ }
170
194
  colorVal = colorVal.slice(0, 6);
171
195
  }
172
196
  if (!REGEX_HEX_COLOR.test(colorVal) && colorVal !== "bg1" && colorVal !== "bg2" && colorVal !== "tx1" && colorVal !== "tx2" && colorVal !== "accent1" && colorVal !== "accent2" && colorVal !== "accent3" && colorVal !== "accent4" && colorVal !== "accent5" && colorVal !== "accent6") {
@@ -192,12 +216,38 @@ function createGlowElement(options, defaults) {
192
216
  };
193
217
  const size = Math.round(opts.size * ONEPT);
194
218
  const color = opts.color || "000000";
195
- const opacity = Math.round((opts.opacity ?? 0) * 1e5);
219
+ const opacity = opacityToAlpha(opts.opacity ?? 0);
196
220
  strXml += `<a:glow rad="${size}">`;
197
221
  strXml += createColorElement(color, `<a:alpha val="${opacity}"/>`);
198
222
  strXml += "</a:glow>";
199
223
  return strXml;
200
224
  }
225
+ /**
226
+ * Creates an `a:outerShdw`/`a:innerShdw` element for a text run or shape.
227
+ * Returns the shadow element only (no wrapping `a:effectLst`) so callers can
228
+ * combine it with other effects (e.g. glow) inside a single `a:effectLst`.
229
+ * @param {ShadowProps} options shadow properties
230
+ * @param {ShadowProps} defaults defaults for unspecified properties in `options`
231
+ * @see http://officeopenxml.com/drwSp-effects.php
232
+ * @returns {string} XML string, or '' when type is 'none'
233
+ */
234
+ function createShadowElement$1(options, defaults) {
235
+ const opts = {
236
+ ...defaults,
237
+ ...options
238
+ };
239
+ if (opts.type === "none") return "";
240
+ const type = opts.type || "outer";
241
+ const blur = valToPts(opts.blur ?? 0);
242
+ const offset = valToPts(opts.offset ?? 0);
243
+ const angle = Math.round((opts.angle ?? 0) * 6e4);
244
+ const opacity = Math.round((opts.opacity ?? .75) * 1e5);
245
+ const color = opts.color || "000000";
246
+ let strXml = `<a:${type}Shdw ${type === "outer" ? "sx=\"100000\" sy=\"100000\" kx=\"0\" ky=\"0\" algn=\"bl\" rotWithShape=\"0\" " : ""}blurRad="${blur}" dist="${offset}" dir="${angle}">`;
247
+ strXml += createColorElement(color, `<a:alpha val="${opacity}"/>`);
248
+ strXml += `</a:${type}Shdw>`;
249
+ return strXml;
250
+ }
201
251
  function boolToXml(value) {
202
252
  return value ? "1" : "0";
203
253
  }
@@ -208,8 +258,8 @@ function normalizeGradientAngle(angle) {
208
258
  }
209
259
  function gradientStopColorAdjustments(stop) {
210
260
  let internalElements = "";
211
- if (stop.alpha) internalElements += `<a:alpha val="${Math.round((100 - stop.alpha) * 1e3)}"/>`;
212
- if (stop.transparency) internalElements += `<a:alpha val="${Math.round((100 - stop.transparency) * 1e3)}"/>`;
261
+ if (stop.alpha) internalElements += `<a:alpha val="${transparencyToAlpha(stop.alpha)}"/>`;
262
+ if (stop.transparency) internalElements += `<a:alpha val="${transparencyToAlpha(stop.transparency)}"/>`;
213
263
  return internalElements;
214
264
  }
215
265
  function normalizeGradientStops(stops) {
@@ -259,6 +309,17 @@ function genXmlPatternFill(pattern) {
259
309
  * @param {Color | ShapeFillProps | ShapeLineProps} props fill props
260
310
  * @returns XML string
261
311
  */
312
+ /**
313
+ * Map a friendly `LineCap` value to the OOXML `cap` attribute value (`flat`/`sq`/`rnd`).
314
+ * @param {LineCap} [lineCap] - line cap style (defaults to `flat`)
315
+ * @returns {string} value for the `cap` attribute on `<a:ln>`
316
+ */
317
+ function createLineCap(lineCap) {
318
+ if (!lineCap || lineCap === "flat") return "flat";
319
+ else if (lineCap === "square") return "sq";
320
+ else if (lineCap === "round") return "rnd";
321
+ else throw new Error(`Invalid line cap: ${String(lineCap)}`);
322
+ }
262
323
  function genXmlColorSelection(props) {
263
324
  let fillType = "solid";
264
325
  let colorVal = "";
@@ -269,8 +330,8 @@ function genXmlColorSelection(props) {
269
330
  else {
270
331
  if (props.type) fillType = props.type;
271
332
  if (props.color) colorVal = props.color;
272
- if (props.alpha) internalElements += `<a:alpha val="${Math.round((100 - props.alpha) * 1e3)}"/>`;
273
- if (props.transparency) internalElements += `<a:alpha val="${Math.round((100 - props.transparency) * 1e3)}"/>`;
333
+ if (props.alpha) internalElements += `<a:alpha val="${transparencyToAlpha(props.alpha)}"/>`;
334
+ if (props.transparency) internalElements += `<a:alpha val="${transparencyToAlpha(props.transparency)}"/>`;
274
335
  }
275
336
  switch (fillType) {
276
337
  case "solid":
@@ -368,15 +429,16 @@ function decodeBase64ToBytes(b64) {
368
429
  }
369
430
  }
370
431
  /**
371
- * Read the intrinsic pixel dimensions of a raster image from its header bytes.
432
+ * Read the intrinsic dimensions of an image from its header bytes.
372
433
  * - synchronous: parses only file-format headers, never decodes pixels
373
- * - supports PNG, JPEG, GIF, BMP, and WebP (VP8 / VP8L / VP8X)
374
- * - vector (SVG) and unrecognized formats return `null` (no intrinsic pixel size)
434
+ * - raster: PNG, JPEG, GIF, BMP, and WebP (VP8 / VP8L / VP8X) — natural pixels
435
+ * - vector: SVG intrinsic size from the root `<svg>` width/height or viewBox
436
+ * - unrecognized formats return `null` (no measurable intrinsic size)
375
437
  *
376
438
  * Used by image `sizing: 'cover' | 'contain'` to compute an aspect-correct
377
439
  * `<a:srcRect>` crop from the *natural* image ratio rather than the displayed box.
378
440
  * @param {string} dataB64 - base64 image payload or `data:` URI
379
- * @returns {{ w: number, h: number } | null} natural pixel size, or `null` when unmeasurable
441
+ * @returns {{ w: number, h: number } | null} natural size, or `null` when unmeasurable
380
442
  */
381
443
  function getImageSizeFromBase64(dataB64) {
382
444
  const b = decodeBase64ToBytes(dataB64);
@@ -462,8 +524,46 @@ function getImageSizeFromBase64(dataB64) {
462
524
  }
463
525
  return null;
464
526
  }
527
+ const text = utf8Decode(b);
528
+ if (/<svg[\s>]/i.test(text)) return getSvgSizeFromMarkup(text);
465
529
  return null;
466
530
  }
531
+ /**
532
+ * Read the intrinsic size of an SVG document from its root `<svg>` element.
533
+ * Follows the SVG sizing model: an explicit absolute `width`/`height` pair wins;
534
+ * otherwise the `viewBox` width/height defines the size (and thus aspect ratio).
535
+ * Percentage or missing `width`/`height` fall through to `viewBox`.
536
+ * @param {string} svg - SVG markup
537
+ * @returns {{ w: number, h: number } | null} intrinsic size, or `null` when undeterminable
538
+ */
539
+ function getSvgSizeFromMarkup(svg) {
540
+ const openTag = /<svg\b[^>]*>/i.exec(svg)?.[0];
541
+ if (!openTag) return null;
542
+ const attr = (name) => new RegExp(`\\b${name}\\s*=\\s*["']([^"']*)["']`, "i").exec(openTag)?.[1] ?? null;
543
+ const absLength = (val) => {
544
+ if (val == null || /%\s*$/.test(val)) return NaN;
545
+ const m = /^\s*\+?(\d*\.?\d+)/.exec(val);
546
+ return m ? parseFloat(m[1]) : NaN;
547
+ };
548
+ let w = absLength(attr("width"));
549
+ let h = absLength(attr("height"));
550
+ if (!(w > 0 && h > 0)) {
551
+ const vb = attr("viewBox");
552
+ const p = vb ? vb.trim().split(/[\s,]+/).map(Number) : [];
553
+ if (p.length === 4 && p[2] > 0 && p[3] > 0) {
554
+ w = p[2];
555
+ h = p[3];
556
+ }
557
+ }
558
+ return w > 0 && h > 0 ? {
559
+ w,
560
+ h
561
+ } : null;
562
+ }
563
+ /** Decode UTF-8 bytes to a string, isomorphic across Node and browsers. */
564
+ function utf8Decode(bytes) {
565
+ return new TextDecoder().decode(bytes);
566
+ }
467
567
  //#endregion
468
568
  //#region src/gen-tables.ts
469
569
  /**
@@ -614,6 +714,7 @@ function getSlidesForTableRows(tableRows = [], tableProps = {}, presLayout, mast
614
714
  let emuSlideTabH = EMU * 1;
615
715
  let emuTabCurrH = 0;
616
716
  let numCols = 0;
717
+ let warnedNoTabH = false;
617
718
  const tableRowSlides = [];
618
719
  const tablePropX = getSmartParseNumber(tableProps.x, "X", presLayout);
619
720
  const tablePropY = getSmartParseNumber(tableProps.y, "Y", presLayout);
@@ -633,6 +734,15 @@ function getSlidesForTableRows(tableRows = [], tableProps = {}, presLayout, mast
633
734
  if (emuSlideTabH < tablePropH) emuSlideTabH = tablePropH;
634
735
  }
635
736
  }
737
+ if (emuSlideTabH <= 0) {
738
+ const emuStartY = tableRowSlides.length === 0 ? tablePropY || inch2Emu(arrInchMargins[0]) : inch2Emu(tableProps.autoPageSlideStartY || tableProps.newSlideStartY || arrInchMargins[0]);
739
+ const fallbackH = presLayout.height - emuStartY - inch2Emu(arrInchMargins[2]);
740
+ if (!warnedNoTabH) {
741
+ console.warn("addTable/autoPage: the table height (`h`) leaves no room to paginate; ignoring it and using the slide height. Increase `h` or decrease `y`.");
742
+ warnedNoTabH = true;
743
+ }
744
+ emuSlideTabH = fallbackH > 0 ? fallbackH : presLayout.height;
745
+ }
636
746
  }
637
747
  if (tableProps.verbose) {
638
748
  console.log("[[VERBOSE MODE]]");
@@ -805,7 +915,7 @@ function getSlidesForTableRows(tableRows = [], tableProps = {}, presLayout, mast
805
915
  console.log("|-----------------------------------------------------------------------|\n\n");
806
916
  }
807
917
  if (currTableRow.length > 0 && currTableRow.map((cell) => Array.isArray(cell.text) ? cell.text.length : 0).reduce((p, n) => p + n) > 0) newTableRowSlide.rows.push(currTableRow);
808
- tableRowSlides.push(newTableRowSlide);
918
+ if (newTableRowSlide.rows.length > 0) tableRowSlides.push(newTableRowSlide);
809
919
  newTableRowSlide = { rows: [] };
810
920
  currTableRow = [];
811
921
  row.forEach((cell) => currTableRow.push({
@@ -854,7 +964,7 @@ function getSlidesForTableRows(tableRows = [], tableProps = {}, presLayout, mast
854
964
  for (let c = 0; c < numCols; c++) if (colSpanDepths[c] > 0) colSpanDepths[c]--;
855
965
  if (tableProps.verbose) console.log(`- SLIDE [${tableRowSlides.length}]: ROW [${iRow}]: ...COMPLETE ...... emuTabCurrH = ${(emuTabCurrH / EMU).toFixed(2)} ( emuSlideTabH = ${(emuSlideTabH / EMU).toFixed(2)} )`);
856
966
  });
857
- tableRowSlides.push(newTableRowSlide);
967
+ if (newTableRowSlide.rows.length > 0 || tableRowSlides.length === 0) tableRowSlides.push(newTableRowSlide);
858
968
  if (tableProps.verbose) {
859
969
  console.log("\n|================================================|");
860
970
  console.log(`| FINAL: tableRowSlides.length = ${tableRowSlides.length}`);
@@ -864,6 +974,47 @@ function getSlidesForTableRows(tableRows = [], tableProps = {}, presLayout, mast
864
974
  return tableRowSlides;
865
975
  }
866
976
  /**
977
+ * Convert a computed CSS border (width string + color string) from `getComputedStyle` into a
978
+ * pptx `BorderProps`.
979
+ *
980
+ * Preserves *fractional* widths: a hairline CSS border such as `0.5px` must not be rounded to
981
+ * `0pt` and silently vanish — the table serializer (`valToPts`) emits fractional points just
982
+ * fine, so there is no reason to integer-round here (upstream gitbrent/PptxGenJS#1235). A
983
+ * computed width of `0` (or a non-finite value) yields `{ type: 'none' }` so we never emit a
984
+ * zero-width line.
985
+ * @param {string} widthStr - computed `border-<side>-width`, e.g. `"0.5px"`
986
+ * @param {string} colorStr - computed `border-<side>-color`, e.g. `"rgb(102, 102, 102)"`
987
+ * @returns {BorderProps} border props for the cell side
988
+ */
989
+ function htmlBorderToProps(widthStr, colorStr) {
990
+ const pt = Number(String(widthStr).replace("px", ""));
991
+ if (!isFinite(pt) || pt <= 0) return { type: "none" };
992
+ const arrRGB = String(colorStr).replace(/\s+/gi, "").replace("rgba(", "").replace("rgb(", "").replace(")", "").split(",");
993
+ return {
994
+ pt,
995
+ color: rgbToHex(Number(arrRGB[0]), Number(arrRGB[1]), Number(arrRGB[2]))
996
+ };
997
+ }
998
+ /**
999
+ * Resolve a single HTML-table column width for `tableToSlides`.
1000
+ *
1001
+ * Precedence: an explicit `data-pptx-width` wins outright; otherwise the proportional width
1002
+ * derived from the live table is used, raised to `data-pptx-min-width` when that floor is larger.
1003
+ *
1004
+ * Hidden tables report `offsetWidth` 0 for every cell, which makes `calcWidth` non-finite (a 0/0
1005
+ * proportional calc). Fall back to `0` there so an explicit `data-pptx-width` / `data-pptx-min-width`
1006
+ * override still drives the column instead of emitting a `NaN` width (upstream gitbrent/PptxGenJS#1157).
1007
+ * @param {number} calcWidth - proportional width derived from `offsetWidth` (may be `NaN` for hidden tables)
1008
+ * @param {number} setWidth - `data-pptx-width` override (`0`/`NaN` when absent or invalid)
1009
+ * @param {number} minWidth - `data-pptx-min-width` floor (`0`/`NaN` when absent or invalid)
1010
+ * @returns {number} resolved column width
1011
+ */
1012
+ function resolveHtmlColWidth(calcWidth, setWidth, minWidth) {
1013
+ const safeCalc = isFinite(calcWidth) ? calcWidth : 0;
1014
+ if (isFinite(setWidth) && setWidth > 0) return setWidth;
1015
+ return isFinite(minWidth) && minWidth > safeCalc ? minWidth : safeCalc;
1016
+ }
1017
+ /**
867
1018
  * Reproduces an HTML table as a PowerPoint table - including column widths, style, etc. - creates 1 or more slides as needed
868
1019
  * @param {TableToSlidesHost} pptx - pptxgenjs instance
869
1020
  * @param {string} tabEleId - HTMLElementID of the table
@@ -927,12 +1078,10 @@ function genTableToSlides(pptx, tabEleId, options = {}, masterSlide) {
927
1078
  });
928
1079
  arrTabColW.forEach((colW, idxW) => {
929
1080
  const intCalcWidth = Number((Number(emuSlideTabW) * (colW / intTabW * 100) / 100 / EMU).toFixed(2));
930
- let intMinWidth = 0;
931
- const colSelectorMin = document.querySelector(`#${tabEleId} thead tr:first-child th:nth-child(${idxW + 1})`);
932
- if (colSelectorMin) intMinWidth = Number(colSelectorMin.getAttribute("data-pptx-min-width"));
933
- const colSelectorSet = document.querySelector(`#${tabEleId} thead tr:first-child th:nth-child(${idxW + 1})`);
934
- if (colSelectorSet) intMinWidth = Number(colSelectorSet.getAttribute("data-pptx-width"));
935
- arrColW.push(intMinWidth > intCalcWidth ? intMinWidth : intCalcWidth);
1081
+ const headCell = document.querySelector(`#${tabEleId} thead tr:first-child th:nth-child(${idxW + 1})`);
1082
+ const intSetWidth = headCell ? Number(headCell.getAttribute("data-pptx-width")) : 0;
1083
+ const intMinWidth = headCell ? Number(headCell.getAttribute("data-pptx-min-width")) : 0;
1084
+ arrColW.push(resolveHtmlColWidth(intCalcWidth, intSetWidth, intMinWidth));
936
1085
  });
937
1086
  if (opts.verbose) console.log(`| arrColW ......................................... = [${arrColW.join(", ")}]`);
938
1087
  [
@@ -1011,12 +1160,8 @@ function genTableToSlides(pptx, tabEleId, options = {}, masterSlide) {
1011
1160
  "bottom",
1012
1161
  "left"
1013
1162
  ].forEach((val, idxb) => {
1014
- const intBorderW = Math.round(Number(window.getComputedStyle(cell).getPropertyValue("border-" + val + "-width").replace("px", "")));
1015
- const arrRGB = window.getComputedStyle(cell).getPropertyValue("border-" + val + "-color").replace(/\s+/gi, "").replace("rgba(", "").replace("rgb(", "").replace(")", "").split(",");
1016
- cellBorder[idxb] = {
1017
- pt: intBorderW,
1018
- color: rgbToHex(Number(arrRGB[0]), Number(arrRGB[1]), Number(arrRGB[2]))
1019
- };
1163
+ const style = window.getComputedStyle(cell);
1164
+ cellBorder[idxb] = htmlBorderToProps(style.getPropertyValue("border-" + val + "-width"), style.getPropertyValue("border-" + val + "-color"));
1020
1165
  });
1021
1166
  cellOpts.border = cellBorder;
1022
1167
  }
@@ -1086,6 +1231,8 @@ function genTableToSlides(pptx, tabEleId, options = {}, masterSlide) {
1086
1231
  */
1087
1232
  /** counter for included charts (used for index in their filenames) */
1088
1233
  let _chartCounter = 0;
1234
+ /** DPI PowerPoint assumes when sizing an inserted raster image (natural pixels / 96 == inches) */
1235
+ const IMAGE_NATURAL_DPI = 96;
1089
1236
  function normalizeBorderTuple(border) {
1090
1237
  return Array.isArray(border) ? border : [
1091
1238
  border,
@@ -1108,7 +1255,7 @@ function createSlideMaster(props, target) {
1108
1255
  else if ("line" in object) addShapeDefinition(tgt, "line", object.line);
1109
1256
  else if ("rect" in object) addShapeDefinition(tgt, "rect", object.rect);
1110
1257
  else if ("roundRect" in object) addShapeDefinition(tgt, "roundRect", object.roundRect);
1111
- else if ("text" in object) addTextDefinition(tgt, [{ text: object.text.text }], object.text.options || {}, false);
1258
+ else if ("text" in object) addTextDefinition(tgt, Array.isArray(object.text.text) ? object.text.text : [{ text: object.text.text }], object.text.options || {}, false);
1112
1259
  else if ("placeholder" in object) {
1113
1260
  const placeholder = object.placeholder;
1114
1261
  const { name, type, ...rawPlaceholderOptions } = placeholder.options;
@@ -1122,6 +1269,26 @@ function createSlideMaster(props, target) {
1122
1269
  if (props.slideNumber && typeof props.slideNumber === "object") target._slideNumberProps = props.slideNumber;
1123
1270
  }
1124
1271
  /**
1272
+ * Round and clamp an integer chart percentage/angle option into a schema-valid range.
1273
+ *
1274
+ * Several chart attributes are bounded integer types whose out-of-range values make
1275
+ * PowerPoint report the package as needing repair: `<c:overlap>` (ST_Overlap, -100..100),
1276
+ * `<c:gapWidth>`/`<c:gapDepth>` (ST_GapAmount, 0..500), `<c:holeSize>` (ST_HoleSize, 10..90)
1277
+ * and `<c:firstSliceAng>` (ST_FirstSliceAng, 0..360). Missing/non-numeric input returns
1278
+ * `undefined` so the caller can apply its own default; an out-of-range value is clamped
1279
+ * and a warning is emitted (per the library's warn-rather-than-degrade policy).
1280
+ * @param value - caller-supplied option value
1281
+ * @param min - inclusive lower bound
1282
+ * @param max - inclusive upper bound
1283
+ * @param name - option name, for the warning message
1284
+ */
1285
+ function clampChartPct(value, min, max, name) {
1286
+ if (typeof value !== "number" || isNaN(value)) return void 0;
1287
+ const clamped = Math.min(max, Math.max(min, Math.round(value)));
1288
+ if (clamped !== value) console.warn(`Warning: ${name} ${value} is outside the valid range ${min}-${max}; using ${clamped}.`);
1289
+ return clamped;
1290
+ }
1291
+ /**
1125
1292
  * Generate the chart based on input data.
1126
1293
  * OOXML Chart Spec: ISO/IEC 29500-1:2016(E)
1127
1294
  *
@@ -1293,7 +1460,13 @@ function addChartDefinition(target, type, data, opt) {
1293
1460
  "marker",
1294
1461
  "filled"
1295
1462
  ].includes(options.radarStyle || "")) options.radarStyle = "standard";
1296
- options.lineDataSymbolSize = options.lineDataSymbolSize && !isNaN(options.lineDataSymbolSize) ? options.lineDataSymbolSize : 6;
1463
+ {
1464
+ const rawSymbolSize = options.lineDataSymbolSize;
1465
+ const hasSymbolSize = rawSymbolSize != null && !isNaN(rawSymbolSize);
1466
+ const symbolSize = Math.min(72, Math.max(2, Math.round(hasSymbolSize ? rawSymbolSize : 6)));
1467
+ if (hasSymbolSize && symbolSize !== rawSymbolSize) console.warn(`Warning: lineDataSymbolSize ${rawSymbolSize} is outside the valid marker size range (integer 2-72); using ${symbolSize}.`);
1468
+ options.lineDataSymbolSize = symbolSize;
1469
+ }
1297
1470
  options.lineDataSymbolLineSize = options.lineDataSymbolLineSize && !isNaN(options.lineDataSymbolLineSize) ? valToPts(options.lineDataSymbolLineSize) : valToPts(.75);
1298
1471
  const chartLayout = options.layout;
1299
1472
  if (chartLayout) [
@@ -1343,8 +1516,11 @@ function addChartDefinition(target, type, data, opt) {
1343
1516
  options.v3DRotY = typeof options.v3DRotY === "number" && !isNaN(options.v3DRotY) && options.v3DRotY >= 0 && options.v3DRotY <= 360 ? options.v3DRotY : 30;
1344
1517
  options.v3DRAngAx = options.v3DRAngAx || !options.v3DRAngAx ? options.v3DRAngAx : true;
1345
1518
  options.v3DPerspective = typeof options.v3DPerspective === "number" && !isNaN(options.v3DPerspective) && options.v3DPerspective >= 0 && options.v3DPerspective <= 240 ? options.v3DPerspective : 30;
1346
- options.barGapWidthPct = typeof options.barGapWidthPct === "number" && !isNaN(options.barGapWidthPct) && options.barGapWidthPct >= 0 && options.barGapWidthPct <= 1e3 ? options.barGapWidthPct : 150;
1347
- options.barGapDepthPct = typeof options.barGapDepthPct === "number" && !isNaN(options.barGapDepthPct) && options.barGapDepthPct >= 0 && options.barGapDepthPct <= 1e3 ? options.barGapDepthPct : 150;
1519
+ options.barGapWidthPct = clampChartPct(options.barGapWidthPct, 0, 500, "barGapWidthPct") ?? 150;
1520
+ options.barGapDepthPct = clampChartPct(options.barGapDepthPct, 0, 500, "barGapDepthPct") ?? 150;
1521
+ options.barOverlapPct = clampChartPct(options.barOverlapPct, -100, 100, "barOverlapPct");
1522
+ options.holeSize = clampChartPct(options.holeSize, 10, 90, "holeSize");
1523
+ options.firstSliceAng = clampChartPct(options.firstSliceAng, 0, 360, "firstSliceAng");
1348
1524
  options.chartColors = Array.isArray(options.chartColors) ? options.chartColors : options._type === "pie" || options._type === "doughnut" ? PIECHART_COLORS : BARCHART_COLORS;
1349
1525
  options.chartColorsOpacity = options.chartColorsOpacity && !isNaN(options.chartColorsOpacity) ? options.chartColorsOpacity : void 0;
1350
1526
  options.border = options.border && typeof options.border === "object" ? options.border : void 0;
@@ -1433,16 +1609,29 @@ function addImageDefinition(target, opt) {
1433
1609
  else if (strImageData?.toLowerCase().includes("image/svg+xml")) strImgExtn = "svg";
1434
1610
  newObject._type = "image";
1435
1611
  newObject.image = strImagePath || "preencoded.png";
1612
+ let defWidth = intWidth;
1613
+ let defHeight = intHeight;
1614
+ if ((!intWidth || !intHeight) && strImageData && strImgExtn !== "svg") {
1615
+ const natural = getImageSizeFromBase64(strImageData);
1616
+ if (natural) {
1617
+ if (!intWidth && !intHeight) {
1618
+ defWidth = natural.w / IMAGE_NATURAL_DPI;
1619
+ defHeight = natural.h / IMAGE_NATURAL_DPI;
1620
+ } else if (typeof intWidth === "number" && intWidth && !intHeight) defHeight = intWidth * (natural.h / natural.w);
1621
+ else if (typeof intHeight === "number" && intHeight && !intWidth) defWidth = intHeight * (natural.w / natural.h);
1622
+ }
1623
+ }
1436
1624
  const objectOptions = {
1437
1625
  x: intPosX || 0,
1438
1626
  y: intPosY || 0,
1439
- w: intWidth || 1,
1440
- h: intHeight || 1,
1627
+ w: defWidth || 1,
1628
+ h: defHeight || 1,
1441
1629
  altText: opt.altText || "",
1442
1630
  rounding: typeof opt.rounding === "boolean" ? opt.rounding : false,
1443
1631
  shape: opt.shape,
1444
1632
  points: opt.points,
1445
1633
  rectRadius: opt.rectRadius,
1634
+ shapeAdjust: opt.shapeAdjust,
1446
1635
  sizing,
1447
1636
  placeholder: opt.placeholder,
1448
1637
  rotate: opt.rotate || 0,
@@ -1451,6 +1640,7 @@ function addImageDefinition(target, opt) {
1451
1640
  transparency: opt.transparency || 0,
1452
1641
  duotone: opt.duotone,
1453
1642
  objectName,
1643
+ objectLock: opt.objectLock,
1454
1644
  shadow: correctShadowOptions(opt.shadow)
1455
1645
  };
1456
1646
  newObject.options = objectOptions;
@@ -1480,7 +1670,10 @@ function addImageDefinition(target, opt) {
1480
1670
  });
1481
1671
  newObject.imageRid = imageRelId + 1;
1482
1672
  } else {
1483
- const dupeItem = target._relsMedia.find((item) => item.path && item.path === strImagePath && item.type === "image/" + strImgExtn && !item.isDuplicate);
1673
+ const dupeItem = target._relsMedia.find((item) => {
1674
+ if (item.isDuplicate || !item.Target || item.type !== "image/" + strImgExtn) return false;
1675
+ return strImagePath ? item.path === strImagePath : !!strImageData && item.data === strImageData;
1676
+ });
1484
1677
  target._relsMedia.push({
1485
1678
  path: strImagePath || "preencoded." + strImgExtn,
1486
1679
  type: "image/" + strImgExtn,
@@ -1538,6 +1731,7 @@ function addMediaDefinition(target, opt) {
1538
1731
  slideData.options.h = intSizeY;
1539
1732
  slideData.options.objectName = objectName;
1540
1733
  if (opt.altText) slideData.options.altText = opt.altText;
1734
+ if (opt.objectLock) slideData.options.objectLock = opt.objectLock;
1541
1735
  /**
1542
1736
  * NOTE:
1543
1737
  * - rId starts at 2 (hence the intRels+1 below) as slideLayout.xml is rId=1!
@@ -1567,7 +1761,10 @@ function addMediaDefinition(target, opt) {
1567
1761
  Target: `../media/image-${target._slideNum}-${target._relsMedia.length + 1}.png`
1568
1762
  });
1569
1763
  } else {
1570
- const dupeItem = target._relsMedia.find((item) => item.path && item.path === strPath && item.type === strType + "/" + strExtn && !item.isDuplicate);
1764
+ const dupeItem = target._relsMedia.find((item) => {
1765
+ if (item.isDuplicate || !item.Target || item.type !== strType + "/" + strExtn) return false;
1766
+ return strPath ? item.path === strPath : !!strData && item.data === strData;
1767
+ });
1571
1768
  const relId1 = getNewRelId(target);
1572
1769
  target._relsMedia.push({
1573
1770
  path: strPath || "preencoded" + strExtn,
@@ -1632,12 +1829,14 @@ function addShapeDefinition(target, shapeName, opts) {
1632
1829
  const options = typeof opts === "object" ? opts : {};
1633
1830
  options.line = options.line || { type: "none" };
1634
1831
  options.shadow = correctShadowOptions(options.shadow);
1832
+ const resolvedShapeName = typeof shapeName === "string" && SHAPE_NAME_ALIASES[shapeName] ? SHAPE_NAME_ALIASES[shapeName] : shapeName;
1635
1833
  const newObject = {
1636
1834
  _type: "text",
1637
- shape: (typeof shapeName === "string" && SHAPE_NAME_ALIASES[shapeName] ? SHAPE_NAME_ALIASES[shapeName] : shapeName) || "rect",
1835
+ shape: resolvedShapeName || "rect",
1638
1836
  options
1639
1837
  };
1640
1838
  if (!shapeName) throw new Error("Missing/Invalid shape parameter! Example: `addShape(pptxgen.shapes.LINE, {x:1, y:1, w:1, h:1});`");
1839
+ if (!VALID_SHAPE_PRESETS.has(resolvedShapeName)) throw new Error(`Invalid shape "${String(shapeName)}"! Use a value from \`pptxgen.shapes.*\` (e.g. \`pptxgen.shapes.RECTANGLE\`). PowerPoint can't render unknown preset geometries and will drop the shape during repair.`);
1641
1840
  const newLineOpts = {
1642
1841
  type: options.line.type || "solid",
1643
1842
  color: options.line.color || "333333",
@@ -1666,6 +1865,51 @@ function addShapeDefinition(target, shapeName, opts) {
1666
1865
  target._slideObjects.push(newObject);
1667
1866
  }
1668
1867
  /**
1868
+ * Adds a connector object to a slide definition.
1869
+ * A connector is a line between two points emitted as a PowerPoint connector (`<p:cxnSp>`).
1870
+ * Endpoints are converted to a bounding box (`x/y/w/h`) plus `flipH`/`flipV` so the box can be
1871
+ * oriented from any corner; the connector preset geometry is derived from `type`.
1872
+ * @param {PresSlideInternal} target - slide the connector is added to
1873
+ * @param {ConnectorProps} opts - connector options (endpoints + line styling)
1874
+ */
1875
+ function addConnectorDefinition(target, opts) {
1876
+ if (!opts || [
1877
+ opts.x1,
1878
+ opts.y1,
1879
+ opts.x2,
1880
+ opts.y2
1881
+ ].some((v) => typeof v === "undefined")) throw new Error("addConnector requires { x1, y1, x2, y2 }. Example: `slide.addConnector({ x1:1, y1:1, x2:4, y2:3 })`");
1882
+ const preset = CONNECTOR_PRESETS[opts.type || "straight"];
1883
+ if (!preset) throw new Error(`Invalid connector type "${String(opts.type)}". Use 'straight', 'elbow', or 'curved'.`);
1884
+ const x1 = getSmartParseNumber(opts.x1, "X", target._presLayout) / EMU;
1885
+ const y1 = getSmartParseNumber(opts.y1, "Y", target._presLayout) / EMU;
1886
+ const x2 = getSmartParseNumber(opts.x2, "X", target._presLayout) / EMU;
1887
+ const y2 = getSmartParseNumber(opts.y2, "Y", target._presLayout) / EMU;
1888
+ const newObject = {
1889
+ _type: "connector",
1890
+ shape: preset,
1891
+ options: {
1892
+ x: Math.min(x1, x2),
1893
+ y: Math.min(y1, y2),
1894
+ w: Math.abs(x2 - x1),
1895
+ h: Math.abs(y2 - y1),
1896
+ flipH: x2 < x1,
1897
+ flipV: y2 < y1,
1898
+ line: {
1899
+ type: "solid",
1900
+ color: opts.color || "333333",
1901
+ width: typeof opts.width === "number" ? opts.width : 1,
1902
+ dashType: opts.dashType || "solid",
1903
+ beginArrowType: opts.beginArrowType,
1904
+ endArrowType: opts.endArrowType
1905
+ },
1906
+ altText: opts.altText,
1907
+ objectName: opts.objectName ? encodeXmlEntities(validateObjectName(opts.objectName, "connector")) : `Connector ${target._slideObjects.filter((obj) => obj._type === "connector").length}`
1908
+ }
1909
+ };
1910
+ target._slideObjects.push(newObject);
1911
+ }
1912
+ /**
1669
1913
  * Adds a table object to a slide definition.
1670
1914
  * @param {PresSlideInternal} target - slide object that the table should be added to
1671
1915
  * @param {TableRow[]} tableRows - table data
@@ -1734,9 +1978,8 @@ function addTableDefinition(target, tableRows, options, slideLayout, presLayout,
1734
1978
  }
1735
1979
  arrRows.push(newRow);
1736
1980
  });
1737
- opt.x = getSmartParseNumber(opt.x || (opt.x === 0 ? 0 : EMU / 2), "X", presLayout);
1738
- opt.y = getSmartParseNumber(opt.y || (opt.y === 0 ? 0 : EMU / 2), "Y", presLayout);
1739
- if (opt.h) opt.h = getSmartParseNumber(opt.h, "Y", presLayout);
1981
+ if (opt.x === void 0 || opt.x === null) opt.x = .5;
1982
+ if (opt.y === void 0 || opt.y === null) opt.y = .5;
1740
1983
  opt.fontSize = opt.fontSize || 12;
1741
1984
  opt.margin = opt.margin === 0 || opt.margin ? opt.margin : DEF_CELL_MARGIN_IN;
1742
1985
  if (typeof opt.margin === "number") opt.margin = [
@@ -1768,6 +2011,7 @@ function addTableDefinition(target, tableRows, options, slideLayout, presLayout,
1768
2011
  });
1769
2012
  }
1770
2013
  opt.autoPage = typeof opt.autoPage === "boolean" ? opt.autoPage : false;
2014
+ opt.autoPagePlaceholder = typeof opt.autoPagePlaceholder === "boolean" ? opt.autoPagePlaceholder : false;
1771
2015
  opt.autoPageRepeatHeader = typeof opt.autoPageRepeatHeader === "boolean" ? opt.autoPageRepeatHeader : false;
1772
2016
  opt.autoPageHeaderRows = typeof opt.autoPageHeaderRows !== "undefined" && !isNaN(Number(opt.autoPageHeaderRows)) ? Number(opt.autoPageHeaderRows) : 1;
1773
2017
  opt.autoPageLineWeight = typeof opt.autoPageLineWeight !== "undefined" && !isNaN(Number(opt.autoPageLineWeight)) ? Number(opt.autoPageLineWeight) : 0;
@@ -1805,12 +2049,7 @@ function addTableDefinition(target, tableRows, options, slideLayout, presLayout,
1805
2049
  console.warn("addTable: mismatch: (colW.length != data.length) Therefore, defaulting to evenly distributed col widths.");
1806
2050
  opt.colW = void 0;
1807
2051
  }
1808
- } else if (opt.w) opt.w = getSmartParseNumber(opt.w, "X", presLayout);
1809
- else opt.w = Math.floor((presLayout._sizeW || presLayout.width) / EMU - arrTableMargin[1] - arrTableMargin[3]);
1810
- if (opt.x && opt.x < 20) opt.x = inch2Emu(opt.x);
1811
- if (opt.y && opt.y < 20) opt.y = inch2Emu(opt.y);
1812
- if (opt.w && typeof opt.w === "number" && opt.w < 20) opt.w = inch2Emu(opt.w);
1813
- if (opt.h && typeof opt.h === "number" && opt.h < 20) opt.h = inch2Emu(opt.h);
2052
+ } else if (opt.w) {} else opt.w = Math.floor((presLayout._sizeW || presLayout.width) / EMU - arrTableMargin[1] - arrTableMargin[3]);
1814
2053
  arrRows.forEach((row) => {
1815
2054
  row.forEach((cell, idy) => {
1816
2055
  if (typeof cell === "number" || typeof cell === "string") row[idy] = {
@@ -1836,12 +2075,14 @@ function addTableDefinition(target, tableRows, options, slideLayout, presLayout,
1836
2075
  });
1837
2076
  } else {
1838
2077
  if (opt.autoPageRepeatHeader) opt._arrObjTabHeadRows = arrRows.filter((_row, idx) => idx < (opt.autoPageHeaderRows || 1));
2078
+ const sourcePlaceholders = opt.autoPagePlaceholder && Array.isArray(target._slideObjects) ? target._slideObjects.filter((obj) => obj._type !== "table" && obj.options?.placeholder) : [];
1839
2079
  getSlidesForTableRows(arrRows, opt, presLayout, slideLayout).forEach((slide, idx) => {
1840
2080
  if (!getSlide(target._slideNum + idx)) slides.push(addSlide({ masterName: slideLayout?._name || void 0 }));
1841
- if (idx > 0) opt.y = inch2Emu(opt.autoPageSlideStartY || opt.newSlideStartY || arrTableMargin[0]);
2081
+ if (idx > 0) opt.y = opt.autoPageSlideStartY || opt.newSlideStartY || arrTableMargin[0];
1842
2082
  {
1843
2083
  const newSlide = getSlide(target._slideNum + idx);
1844
2084
  opt.autoPage = false;
2085
+ if (idx > 0 && sourcePlaceholders.length > 0) sourcePlaceholders.forEach((ph) => newSlide._slideObjects.push(structuredClone(ph)));
1845
2086
  createHyperlinkRels(newSlide, slide.rows);
1846
2087
  newSlide.addTable(slide.rows, { ...opt });
1847
2088
  if (idx > 0) newAutoPagedSlides.push(newSlide);
@@ -1910,6 +2151,10 @@ function addTextDefinition(target, text, opts, isPlaceholder) {
1910
2151
  itemOpts._bodyProp.anchor = !itemOpts.placeholder ? "ctr" : void 0;
1911
2152
  itemOpts._bodyProp.vert = itemOpts.vert;
1912
2153
  itemOpts._bodyProp.wrap = typeof itemOpts.wrap === "boolean" ? itemOpts.wrap : true;
2154
+ if (itemOpts.columns !== void 0) if (typeof itemOpts.columns !== "number" || isNaN(itemOpts.columns) || itemOpts.columns < 1 || itemOpts.columns > 16) console.warn("Warning: text `columns` must be a number 1-16 (ignoring value)");
2155
+ else itemOpts._bodyProp.numCol = Math.round(itemOpts.columns);
2156
+ if (itemOpts.columnSpacing !== void 0) if (typeof itemOpts.columnSpacing !== "number" || isNaN(itemOpts.columnSpacing) || itemOpts.columnSpacing < 0) console.warn("Warning: text `columnSpacing` must be a number >= 0 (ignoring value)");
2157
+ else itemOpts._bodyProp.spcCol = valToPts(itemOpts.columnSpacing);
1913
2158
  if (itemOpts.inset && !isNaN(Number(itemOpts.inset)) || itemOpts.inset === 0) {
1914
2159
  itemOpts._bodyProp.lIns = inch2Emu(itemOpts.inset);
1915
2160
  itemOpts._bodyProp.rIns = inch2Emu(itemOpts.inset);
@@ -2190,6 +2435,16 @@ var Slide = class {
2190
2435
  return this;
2191
2436
  }
2192
2437
  /**
2438
+ * Add a connector (a line drawn between two points, emitted as a PowerPoint `<p:cxnSp>`).
2439
+ * @param {ConnectorProps} options - connector endpoints (`x1,y1,x2,y2`) and line styling
2440
+ * @return {Slide} this Slide
2441
+ * @example slide.addConnector({ type: 'elbow', x1: 1, y1: 1, x2: 5, y2: 3, endArrowType: 'triangle' })
2442
+ */
2443
+ addConnector(options) {
2444
+ addConnectorDefinition(this, options);
2445
+ return this;
2446
+ }
2447
+ /**
2193
2448
  * Add table to Slide
2194
2449
  * @param {TableRow[]} tableRows - table rows
2195
2450
  * @param {TableProps} options - table options
@@ -2419,6 +2674,22 @@ async function createExcelWorksheet(chartObject, zip) {
2419
2674
  });
2420
2675
  }
2421
2676
  /**
2677
+ * Emit the `<a:latin>/<a:ea>/<a:cs>` font trio for a chart text run.
2678
+ *
2679
+ * In DrawingML run properties a typeface applies only to the script class of
2680
+ * its element: `<a:latin>` covers Latin/ASCII, `<a:ea>` covers East Asian, and
2681
+ * `<a:cs>` covers complex scripts. Emitting `<a:latin>` alone leaves East Asian
2682
+ * (e.g. Chinese) and complex-script glyphs falling back to the theme font, so a
2683
+ * user-specified font never takes effect for that text — most visibly on
2684
+ * PowerPoint for Mac. Stamping the same typeface onto all three classes is what
2685
+ * choosing a font in PowerPoint's UI does (upstream gitbrent/PptxGenJS#1420).
2686
+ * @param {string} typeface - font face name
2687
+ * @return {string} `<a:latin/><a:ea/><a:cs/>` XML
2688
+ */
2689
+ function createChartTextFonts(typeface) {
2690
+ return `<a:latin typeface="${typeface}"/><a:ea typeface="${typeface}"/><a:cs typeface="${typeface}"/>`;
2691
+ }
2692
+ /**
2422
2693
  * Main entry point method for create charts
2423
2694
  * @see: http://www.datypic.com/sc/ooxml/s-dml-chart.xsd.html
2424
2695
  * @param {ISlideRelChart} rel - chart object
@@ -2428,6 +2699,10 @@ function makeXmlCharts(rel) {
2428
2699
  let strXml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";
2429
2700
  let usesSecondaryValAxis = false;
2430
2701
  let usesSecondaryCatAxis = false;
2702
+ let primaryCatAxisValType = null;
2703
+ let secondaryCatAxisValType = null;
2704
+ let primaryCatAxisHasCategoryChart = false;
2705
+ let secondaryCatAxisHasCategoryChart = false;
2431
2706
  strXml += "<c:chartSpace xmlns:c=\"http://schemas.openxmlformats.org/drawingml/2006/chart\" xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">";
2432
2707
  strXml += "<c:date1904 val=\"0\"/>";
2433
2708
  strXml += `<c:roundedCorners val="${rel.opts.chartArea.roundedCorners ? "1" : "0"}"/>`;
@@ -2440,6 +2715,8 @@ function makeXmlCharts(rel) {
2440
2715
  fontSize: rel.opts.titleFontSize || 18,
2441
2716
  titleAlign: rel.opts.titleAlign,
2442
2717
  titleBold: rel.opts.titleBold,
2718
+ titleItalic: rel.opts.titleItalic,
2719
+ titleUnderline: rel.opts.titleUnderline,
2443
2720
  titlePos: rel.opts.titlePos,
2444
2721
  titleRotate: rel.opts.titleRotate
2445
2722
  }, rel.opts.x, rel.opts.y);
@@ -2472,18 +2749,37 @@ function makeXmlCharts(rel) {
2472
2749
  const catAxisId = options.secondaryCatAxis ? AXIS_ID_CATEGORY_SECONDARY : AXIS_ID_CATEGORY_PRIMARY;
2473
2750
  usesSecondaryValAxis = usesSecondaryValAxis || options.secondaryValAxis;
2474
2751
  usesSecondaryCatAxis = usesSecondaryCatAxis || options.secondaryCatAxis;
2752
+ const usesValueXAxis = type.type === "scatter" || type.type === "bubble" || type.type === "bubble3D";
2753
+ if (options.secondaryCatAxis) if (usesValueXAxis) secondaryCatAxisValType = type.type;
2754
+ else secondaryCatAxisHasCategoryChart = true;
2755
+ else if (usesValueXAxis) primaryCatAxisValType = type.type;
2756
+ else primaryCatAxisHasCategoryChart = true;
2475
2757
  strXml += makeChartType(type.type, type.data, options, valAxisId, catAxisId);
2476
2758
  });
2477
2759
  else strXml += makeChartType(rel.opts._type, rel.data, rel.opts, AXIS_ID_VALUE_PRIMARY, AXIS_ID_CATEGORY_PRIMARY);
2478
2760
  if (rel.opts._type !== "pie" && rel.opts._type !== "doughnut") {
2479
2761
  if (rel.opts.valAxes && rel.opts.valAxes.length > 1 && !usesSecondaryValAxis) throw new Error("Secondary axis must be used by one of the multiple charts");
2762
+ const comboCatAxisType = (isSecondary) => {
2763
+ const valType = isSecondary ? secondaryCatAxisValType : primaryCatAxisValType;
2764
+ const hasCategoryChart = isSecondary ? secondaryCatAxisHasCategoryChart : primaryCatAxisHasCategoryChart;
2765
+ if (!valType) return {};
2766
+ if (hasCategoryChart) {
2767
+ console.warn(`A category-based chart and a scatter/bubble chart cannot share the same ${isSecondary ? "secondary" : "primary"} category axis; emitting a category axis. Put the scatter/bubble series on a separate axis.`);
2768
+ return {};
2769
+ }
2770
+ return { _type: valType };
2771
+ };
2480
2772
  if (rel.opts.catAxes) {
2481
2773
  if (!rel.opts.valAxes || rel.opts.valAxes.length !== rel.opts.catAxes.length) throw new Error("There must be the same number of value and category axes.");
2482
2774
  strXml += makeCatAxis({
2483
2775
  ...rel.opts,
2484
- ...rel.opts.catAxes[0]
2776
+ ...rel.opts.catAxes[0],
2777
+ ...comboCatAxisType(false)
2485
2778
  }, AXIS_ID_CATEGORY_PRIMARY, AXIS_ID_VALUE_PRIMARY);
2486
- } else strXml += makeCatAxis(rel.opts, AXIS_ID_CATEGORY_PRIMARY, AXIS_ID_VALUE_PRIMARY);
2779
+ } else strXml += makeCatAxis({
2780
+ ...rel.opts,
2781
+ ...comboCatAxisType(false)
2782
+ }, AXIS_ID_CATEGORY_PRIMARY, AXIS_ID_VALUE_PRIMARY);
2487
2783
  if (rel.opts.valAxes) {
2488
2784
  strXml += makeValAxis({
2489
2785
  ...rel.opts,
@@ -2500,9 +2796,13 @@ function makeXmlCharts(rel) {
2500
2796
  }
2501
2797
  if (rel.opts?.catAxes && rel.opts?.catAxes[1]) strXml += makeCatAxis({
2502
2798
  ...rel.opts,
2503
- ...rel.opts.catAxes[1]
2799
+ ...rel.opts.catAxes[1],
2800
+ ...comboCatAxisType(true)
2801
+ }, AXIS_ID_CATEGORY_SECONDARY, AXIS_ID_VALUE_SECONDARY);
2802
+ else if (usesSecondaryCatAxis && (!rel.opts.catAxes || !rel.opts.catAxes[1])) strXml += makeCatAxis({
2803
+ ...rel.opts,
2804
+ ...comboCatAxisType(true)
2504
2805
  }, AXIS_ID_CATEGORY_SECONDARY, AXIS_ID_VALUE_SECONDARY);
2505
- else if (usesSecondaryCatAxis && (!rel.opts.catAxes || !rel.opts.catAxes[1])) strXml += makeCatAxis(rel.opts, AXIS_ID_CATEGORY_SECONDARY, AXIS_ID_VALUE_SECONDARY);
2506
2806
  }
2507
2807
  if (rel.opts.showDataTable) {
2508
2808
  strXml += "<c:dTable>";
@@ -2557,8 +2857,7 @@ function makeXmlCharts(rel) {
2557
2857
  strXml += " <a:pPr>";
2558
2858
  strXml += rel.opts.legendFontSize ? `<a:defRPr sz="${Math.round(Number(rel.opts.legendFontSize) * 100)}">` : "<a:defRPr>";
2559
2859
  if (rel.opts.legendColor) strXml += genXmlColorSelection(rel.opts.legendColor);
2560
- if (rel.opts.legendFontFace) strXml += "<a:latin typeface=\"" + rel.opts.legendFontFace + "\"/>";
2561
- if (rel.opts.legendFontFace) strXml += "<a:cs typeface=\"" + rel.opts.legendFontFace + "\"/>";
2860
+ if (rel.opts.legendFontFace) strXml += createChartTextFonts(rel.opts.legendFontFace);
2562
2861
  strXml += " </a:defRPr>";
2563
2862
  strXml += " </a:pPr>";
2564
2863
  strXml += " <a:endParaRPr lang=\"en-US\"/>";
@@ -2596,6 +2895,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2596
2895
  let idxColLtr = 1;
2597
2896
  let optsChartData;
2598
2897
  let strXml = "";
2898
+ const valFmtCode = encodeXmlEntities(opts.valLabelFormatCode || opts.dataTableFormatCode || opts.dataLabelFormatCode || "General");
2599
2899
  switch (chartType) {
2600
2900
  case "area":
2601
2901
  case "bar":
@@ -2639,7 +2939,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2639
2939
  } else if (opts.dataBorder) strXml += `<a:ln w="${valToPts(opts.dataBorder.pt)}" cap="${createLineCap(opts.lineCap)}"><a:solidFill>${createColorElement(opts.dataBorder.color)}</a:solidFill><a:prstDash val="solid"/><a:round/></a:ln>`;
2640
2940
  strXml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW);
2641
2941
  strXml += " </c:spPr>";
2642
- if (chartType !== "line" && chartType !== "radar") strXml += " <c:invertIfNegative val=\"0\"/>";
2942
+ if (chartType === "bar" || chartType === "bar3D") strXml += " <c:invertIfNegative val=\"0\"/>";
2643
2943
  if (chartType === "line" || chartType === "radar") {
2644
2944
  strXml += "<c:marker>";
2645
2945
  strXml += " <c:symbol val=\"" + opts.lineDataSymbol + "\"/>";
@@ -2654,6 +2954,10 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2654
2954
  strXml += " </c:spPr>";
2655
2955
  strXml += "</c:marker>";
2656
2956
  }
2957
+ {
2958
+ const barVaryColors = (chartType === "bar" || chartType === "bar3D") && data.length === 1 && (opts.chartColors && opts.chartColors !== BARCHART_COLORS && opts.chartColors.length > 1 || opts.invertedColors?.length) ? opts.chartColors || BARCHART_COLORS : null;
2959
+ strXml += makeSeriesDataPointsXml(chartType, obj, opts, barVaryColors);
2960
+ }
2657
2961
  if (chartType !== "radar") {
2658
2962
  const lblColor = seriesOverride?.dataLabelColor ?? opts.dataLabelColor ?? "000000";
2659
2963
  const lblBold = seriesOverride?.dataLabelFontBold ?? opts.dataLabelFontBold ?? false;
@@ -2661,12 +2965,15 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2661
2965
  const lblSize = seriesOverride?.dataLabelFontSize ?? opts.dataLabelFontSize ?? 12;
2662
2966
  const lblFace = seriesOverride?.dataLabelFontFace ?? opts.dataLabelFontFace ?? "Arial";
2663
2967
  strXml += "<c:dLbls>";
2968
+ if (obj.customLabels?.length) obj.customLabels.forEach((lbl, idx) => {
2969
+ if (lbl) strXml += makeCustomDLblXml(idx, lbl, opts);
2970
+ });
2664
2971
  strXml += `<c:numFmt formatCode="${encodeXmlEntities(opts.dataLabelFormatCode) || "General"}" sourceLinked="0"/>`;
2665
2972
  if (opts.dataLabelBkgrdColors) strXml += `<c:spPr><a:solidFill>${createColorElement(seriesColor)}</a:solidFill></c:spPr>`;
2666
2973
  strXml += "<c:txPr><a:bodyPr/><a:lstStyle/><a:p><a:pPr>";
2667
2974
  strXml += `<a:defRPr b="${lblBold ? 1 : 0}" i="${lblItalic ? 1 : 0}" strike="noStrike" sz="${Math.round(lblSize * 100)}" u="none">`;
2668
2975
  strXml += `<a:solidFill>${createColorElement(lblColor)}</a:solidFill>`;
2669
- strXml += `<a:latin typeface="${lblFace}"/>`;
2976
+ strXml += createChartTextFonts(lblFace);
2670
2977
  strXml += "</a:defRPr></a:pPr></a:p></c:txPr>";
2671
2978
  if (opts.dataLabelPosition) strXml += `<c:dLblPos val="${opts.dataLabelPosition}"/>`;
2672
2979
  strXml += "<c:showLegendKey val=\"0\"/>";
@@ -2675,29 +2982,6 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2675
2982
  strXml += `<c:showLeaderLines val="${opts.showLeaderLines ? "1" : "0"}"/>`;
2676
2983
  strXml += "</c:dLbls>";
2677
2984
  }
2678
- if ((chartType === "bar" || chartType === "bar3D") && data.length === 1 && (opts.chartColors && opts.chartColors !== BARCHART_COLORS && opts.chartColors.length > 1 || opts.invertedColors?.length)) obj.values.forEach((value, index) => {
2679
- const arrColors = value < 0 ? opts.invertedColors || opts.chartColors || BARCHART_COLORS : opts.chartColors || [];
2680
- strXml += " <c:dPt>";
2681
- strXml += ` <c:idx val="${index}"/>`;
2682
- strXml += " <c:invertIfNegative val=\"0\"/>";
2683
- strXml += " <c:bubble3D val=\"0\"/>";
2684
- strXml += " <c:spPr>";
2685
- if (opts.lineSize === 0) strXml += "<a:ln><a:noFill/></a:ln>";
2686
- else if (chartType === "bar") {
2687
- strXml += "<a:solidFill>";
2688
- strXml += " <a:srgbClr val=\"" + arrColors[index % arrColors.length] + "\"/>";
2689
- strXml += "</a:solidFill>";
2690
- } else {
2691
- strXml += "<a:ln>";
2692
- strXml += " <a:solidFill>";
2693
- strXml += " <a:srgbClr val=\"" + arrColors[index % arrColors.length] + "\"/>";
2694
- strXml += " </a:solidFill>";
2695
- strXml += "</a:ln>";
2696
- }
2697
- strXml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW);
2698
- strXml += " </c:spPr>";
2699
- strXml += " </c:dPt>";
2700
- });
2701
2985
  strXml += "<c:cat>";
2702
2986
  if (opts.catLabelFormatCode) {
2703
2987
  strXml += " <c:numRef>";
@@ -2734,10 +3018,10 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2734
3018
  strXml += " <c:numRef>";
2735
3019
  strXml += `<c:f>Sheet1!$${getExcelColName(obj._dataIndex + obj.labels.length + 1)}$2:$${getExcelColName(obj._dataIndex + obj.labels.length + 1)}$${obj.labels[0].length + 1}</c:f>`;
2736
3020
  strXml += " <c:numCache>";
2737
- strXml += " <c:formatCode>" + (opts.valLabelFormatCode || opts.dataTableFormatCode || "General") + "</c:formatCode>";
3021
+ strXml += " <c:formatCode>" + valFmtCode + "</c:formatCode>";
2738
3022
  strXml += ` <c:ptCount val="${obj.labels[0].length}"/>`;
2739
3023
  obj.values.forEach((value, idx) => {
2740
- if (value != null) strXml += `<c:pt idx="${idx}"><c:v>${value}</c:v></c:pt>`;
3024
+ strXml += numCachePt(idx, value);
2741
3025
  });
2742
3026
  strXml += " </c:numCache>";
2743
3027
  strXml += " </c:numRef>";
@@ -2753,7 +3037,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2753
3037
  strXml += " <a:p><a:pPr>";
2754
3038
  strXml += ` <a:defRPr b="${opts.dataLabelFontBold ? 1 : 0}" i="${opts.dataLabelFontItalic ? 1 : 0}" strike="noStrike" sz="${Math.round((opts.dataLabelFontSize || 12) * 100)}" u="none">`;
2755
3039
  strXml += " <a:solidFill>" + createColorElement(opts.dataLabelColor || "000000") + "</a:solidFill>";
2756
- strXml += " <a:latin typeface=\"" + (opts.dataLabelFontFace || "Arial") + "\"/>";
3040
+ strXml += " " + createChartTextFonts(opts.dataLabelFontFace || "Arial");
2757
3041
  strXml += " </a:defRPr>";
2758
3042
  strXml += " </a:pPr></a:p>";
2759
3043
  strXml += " </c:txPr>";
@@ -2769,6 +3053,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2769
3053
  if (chartType === "bar") {
2770
3054
  strXml += ` <c:gapWidth val="${opts.barGapWidthPct}"/>`;
2771
3055
  strXml += ` <c:overlap val="${opts.barOverlapPct != null ? opts.barOverlapPct : (opts.barGrouping || "").includes("tacked") ? 100 : 0}"/>`;
3056
+ strXml += createSerLinesElement(opts.barSeriesLine);
2772
3057
  } else if (chartType === "bar3D") {
2773
3058
  strXml += ` <c:gapWidth val="${opts.barGapWidthPct}"/>`;
2774
3059
  strXml += ` <c:gapDepth val="${opts.barGapDepthPct}"/>`;
@@ -2820,6 +3105,10 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2820
3105
  strXml += "<a:effectLst/>";
2821
3106
  strXml += "</c:spPr>";
2822
3107
  strXml += "</c:marker>";
3108
+ {
3109
+ const scatterVaryColors = data.length === 1 && opts.chartColors !== BARCHART_COLORS ? opts.chartColors || BARCHART_COLORS : null;
3110
+ strXml += makeSeriesDataPointsXml(chartType, obj, opts, scatterVaryColors);
3111
+ }
2823
3112
  if (opts.showLabel) {
2824
3113
  const chartUuid = getUuid("-xxxx-xxxx-xxxx-xxxxxxxxxxxx");
2825
3114
  if (obj.labels[0] && (opts.dataLabelFormatScatter === "custom" || opts.dataLabelFormatScatter === "customXY")) {
@@ -2838,13 +3127,13 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2838
3127
  strXml += " <a:pPr>";
2839
3128
  strXml += ` <a:defRPr sz="${Math.round((opts.dataLabelFontSize || 12) * 100)}" b="${opts.dataLabelFontBold ? "1" : "0"}" i="${opts.dataLabelFontItalic ? "1" : "0"}" u="none" strike="noStrike">`;
2840
3129
  strXml += " <a:solidFill>" + createColorElement(opts.dataLabelColor || "000000") + "</a:solidFill>";
2841
- strXml += ` <a:latin typeface="${opts.dataLabelFontFace || "Arial"}"/>`;
3130
+ strXml += " " + createChartTextFonts(opts.dataLabelFontFace || "Arial");
2842
3131
  strXml += " </a:defRPr>";
2843
3132
  strXml += " </a:pPr>";
2844
3133
  strXml += " <a:r>";
2845
3134
  strXml += ` <a:rPr lang="${opts.lang || "en-US"}" sz="${Math.round((opts.dataLabelFontSize || 12) * 100)}" b="${opts.dataLabelFontBold ? "1" : "0"}" i="${opts.dataLabelFontItalic ? "1" : "0"}" u="none" strike="noStrike" dirty="0">`;
2846
3135
  strXml += " <a:solidFill>" + createColorElement(opts.dataLabelColor || "000000") + "</a:solidFill>";
2847
- strXml += ` <a:latin typeface="${opts.dataLabelFontFace || "Arial"}"/>`;
3136
+ strXml += " " + createChartTextFonts(opts.dataLabelFontFace || "Arial");
2848
3137
  strXml += " </a:rPr>";
2849
3138
  strXml += " <a:t>" + encodeXmlEntities(label) + "</a:t>";
2850
3139
  strXml += " </a:r>";
@@ -2924,7 +3213,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2924
3213
  strXml += " <a:pPr>";
2925
3214
  strXml += ` <a:defRPr sz="${Math.round((opts.dataLabelFontSize || 12) * 100)}" b="${opts.dataLabelFontBold ? "1" : "0"}" i="${opts.dataLabelFontItalic ? "1" : "0"}" u="none" strike="noStrike">`;
2926
3215
  strXml += " <a:solidFill>" + createColorElement(opts.dataLabelColor || "000000") + "</a:solidFill>";
2927
- strXml += ` <a:latin typeface="${opts.dataLabelFontFace || "Arial"}"/>`;
3216
+ strXml += " " + createChartTextFonts(opts.dataLabelFontFace || "Arial");
2928
3217
  strXml += " </a:defRPr>";
2929
3218
  strXml += " </a:pPr>";
2930
3219
  strXml += ` <a:endParaRPr lang="${opts.lang || "en-US"}"/>`;
@@ -2945,31 +3234,14 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2945
3234
  strXml += "</c:dLbls>";
2946
3235
  }
2947
3236
  }
2948
- if (data.length === 1 && opts.chartColors !== BARCHART_COLORS) obj.values.forEach((value, index) => {
2949
- const arrColors = value < 0 ? opts.invertedColors || opts.chartColors || BARCHART_COLORS : opts.chartColors || [];
2950
- strXml += " <c:dPt>";
2951
- strXml += ` <c:idx val="${index}"/>`;
2952
- strXml += " <c:invertIfNegative val=\"0\"/>";
2953
- strXml += " <c:bubble3D val=\"0\"/>";
2954
- strXml += " <c:spPr>";
2955
- if (opts.lineSize === 0) strXml += "<a:ln><a:noFill/></a:ln>";
2956
- else {
2957
- strXml += "<a:solidFill>";
2958
- strXml += " <a:srgbClr val=\"" + arrColors[index % arrColors.length] + "\"/>";
2959
- strXml += "</a:solidFill>";
2960
- }
2961
- strXml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW);
2962
- strXml += " </c:spPr>";
2963
- strXml += " </c:dPt>";
2964
- });
2965
3237
  strXml += "<c:xVal>";
2966
3238
  strXml += " <c:numRef>";
2967
3239
  strXml += ` <c:f>Sheet1!$A$2:$A$${data[0].values.length + 1}</c:f>`;
2968
3240
  strXml += " <c:numCache>";
2969
- strXml += " <c:formatCode>General</c:formatCode>";
3241
+ strXml += " <c:formatCode>" + valFmtCode + "</c:formatCode>";
2970
3242
  strXml += ` <c:ptCount val="${data[0].values.length}"/>`;
2971
3243
  data[0].values.forEach((value, idx) => {
2972
- if (value != null) strXml += `<c:pt idx="${idx}"><c:v>${value}</c:v></c:pt>`;
3244
+ strXml += numCachePt(idx, value);
2973
3245
  });
2974
3246
  strXml += " </c:numCache>";
2975
3247
  strXml += " </c:numRef>";
@@ -2978,10 +3250,10 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2978
3250
  strXml += " <c:numRef>";
2979
3251
  strXml += ` <c:f>Sheet1!$${getExcelColName(idx + 2)}$2:$${getExcelColName(idx + 2)}$${data[0].values.length + 1}</c:f>`;
2980
3252
  strXml += " <c:numCache>";
2981
- strXml += " <c:formatCode>General</c:formatCode>";
3253
+ strXml += " <c:formatCode>" + valFmtCode + "</c:formatCode>";
2982
3254
  strXml += ` <c:ptCount val="${data[0].values.length}"/>`;
2983
3255
  data[0].values.forEach((_value, idx) => {
2984
- if (obj.values[idx] != null) strXml += `<c:pt idx="${idx}"><c:v>${obj.values[idx]}</c:v></c:pt>`;
3256
+ strXml += numCachePt(idx, obj.values[idx]);
2985
3257
  });
2986
3258
  strXml += " </c:numCache>";
2987
3259
  strXml += " </c:numRef>";
@@ -2997,7 +3269,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2997
3269
  strXml += " <a:p><a:pPr>";
2998
3270
  strXml += ` <a:defRPr b="${opts.dataLabelFontBold ? "1" : "0"}" i="${opts.dataLabelFontItalic ? "1" : "0"}" strike="noStrike" sz="${Math.round((opts.dataLabelFontSize || 12) * 100)}" u="none">`;
2999
3271
  strXml += " <a:solidFill>" + createColorElement(opts.dataLabelColor || "000000") + "</a:solidFill>";
3000
- strXml += " <a:latin typeface=\"" + (opts.dataLabelFontFace || "Arial") + "\"/>";
3272
+ strXml += " " + createChartTextFonts(opts.dataLabelFontFace || "Arial");
3001
3273
  strXml += " </a:defRPr>";
3002
3274
  strXml += " </a:pPr></a:p>";
3003
3275
  strXml += " </c:txPr>";
@@ -3047,10 +3319,10 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
3047
3319
  strXml += " <c:numRef>";
3048
3320
  strXml += ` <c:f>Sheet1!$A$2:$A$${data[0].values.length + 1}</c:f>`;
3049
3321
  strXml += " <c:numCache>";
3050
- strXml += " <c:formatCode>General</c:formatCode>";
3322
+ strXml += " <c:formatCode>" + valFmtCode + "</c:formatCode>";
3051
3323
  strXml += ` <c:ptCount val="${data[0].values.length}"/>`;
3052
3324
  data[0].values.forEach((value, idx) => {
3053
- strXml += `<c:pt idx="${idx}"><c:v>${value || value === 0 ? value : ""}</c:v></c:pt>`;
3325
+ strXml += numCachePt(idx, value);
3054
3326
  });
3055
3327
  strXml += " </c:numCache>";
3056
3328
  strXml += " </c:numRef>";
@@ -3060,10 +3332,10 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
3060
3332
  strXml += `<c:f>Sheet1!$${getExcelColName(idxColLtr + 1)}$2:$${getExcelColName(idxColLtr + 1)}$${data[0].values.length + 1}</c:f>`;
3061
3333
  idxColLtr++;
3062
3334
  strXml += " <c:numCache>";
3063
- strXml += " <c:formatCode>General</c:formatCode>";
3335
+ strXml += " <c:formatCode>" + valFmtCode + "</c:formatCode>";
3064
3336
  strXml += ` <c:ptCount val="${data[0].values.length}"/>`;
3065
3337
  data[0].values.forEach((_value, idx) => {
3066
- strXml += `<c:pt idx="${idx}"><c:v>${obj.values[idx] || obj.values[idx] === 0 ? obj.values[idx] : ""}</c:v></c:pt>`;
3338
+ strXml += numCachePt(idx, obj.values[idx]);
3067
3339
  });
3068
3340
  strXml += " </c:numCache>";
3069
3341
  strXml += " </c:numRef>";
@@ -3076,7 +3348,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
3076
3348
  strXml += " <c:formatCode>General</c:formatCode>";
3077
3349
  strXml += ` <c:ptCount val="${obj.sizes.length}"/>`;
3078
3350
  obj.sizes.forEach((value, idx) => {
3079
- strXml += `<c:pt idx="${idx}"><c:v>${value ?? ""}</c:v></c:pt>`;
3351
+ strXml += numCachePt(idx, value);
3080
3352
  });
3081
3353
  strXml += " </c:numCache>";
3082
3354
  strXml += " </c:numRef>";
@@ -3089,12 +3361,12 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
3089
3361
  strXml += "<c:txPr><a:bodyPr/><a:lstStyle/><a:p><a:pPr>";
3090
3362
  strXml += `<a:defRPr b="${opts.dataLabelFontBold ? 1 : 0}" i="${opts.dataLabelFontItalic ? 1 : 0}" strike="noStrike" sz="${Math.round(Math.round(opts.dataLabelFontSize || 12) * 100)}" u="none">`;
3091
3363
  strXml += `<a:solidFill>${createColorElement(opts.dataLabelColor || "000000")}</a:solidFill>`;
3092
- strXml += `<a:latin typeface="${opts.dataLabelFontFace || "Arial"}"/>`;
3364
+ strXml += createChartTextFonts(opts.dataLabelFontFace || "Arial");
3093
3365
  strXml += "</a:defRPr></a:pPr></a:p></c:txPr>";
3094
3366
  if (opts.dataLabelPosition) strXml += `<c:dLblPos val="${opts.dataLabelPosition}"/>`;
3095
3367
  strXml += "<c:showLegendKey val=\"0\"/>";
3096
3368
  strXml += `<c:showVal val="${opts.showValue ? "1" : "0"}"/>`;
3097
- strXml += `<c:showCatName val="0"/><c:showSerName val="${opts.showSerName ? "1" : "0"}"/><c:showPercent val="0"/><c:showBubbleSize val="0"/>`;
3369
+ strXml += `<c:showCatName val="0"/><c:showSerName val="${opts.showSerName ? "1" : "0"}"/><c:showPercent val="0"/><c:showBubbleSize val="${opts.showBubbleSize ? "1" : "0"}"/>`;
3098
3370
  strXml += "<c:extLst>";
3099
3371
  strXml += " <c:ext uri=\"{CE6537A1-D6FC-4f65-9D91-7224C49458BB}\" xmlns:c15=\"http://schemas.microsoft.com/office/drawing/2012/chart\">";
3100
3372
  strXml += " <c15:showLeaderLines val=\"" + (opts.showLeaderLines ? "1" : "0") + "\"/>";
@@ -3128,33 +3400,37 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
3128
3400
  else strXml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW);
3129
3401
  strXml += " </c:spPr>";
3130
3402
  optsChartData.labels[0].forEach((_label, idx) => {
3403
+ const ptStyle = optsChartData.pointStyles?.[idx];
3131
3404
  strXml += "<c:dPt>";
3132
3405
  strXml += ` <c:idx val="${idx}"/>`;
3133
3406
  strXml += " <c:bubble3D val=\"0\"/>";
3134
3407
  strXml += " <c:spPr>";
3135
- strXml += `<a:solidFill>${createColorElement(opts.chartColors[idx + 1 > opts.chartColors.length ? Math.floor(Math.random() * opts.chartColors.length) : idx])}</a:solidFill>`;
3136
- if (opts.dataBorder) strXml += `<a:ln w="${valToPts(opts.dataBorder.pt)}" cap="flat"><a:solidFill>${createColorElement(opts.dataBorder.color)}</a:solidFill><a:prstDash val="solid"/><a:round/></a:ln>`;
3408
+ strXml += `<a:solidFill>${createColorElement(ptStyle?.fill || opts.chartColors[idx + 1 > opts.chartColors.length ? Math.floor(Math.random() * opts.chartColors.length) : idx])}</a:solidFill>`;
3409
+ if (ptStyle?.border) strXml += createChartBorderLine(ptStyle.border);
3410
+ else if (opts.dataBorder) strXml += `<a:ln w="${valToPts(opts.dataBorder.pt)}" cap="flat"><a:solidFill>${createColorElement(opts.dataBorder.color)}</a:solidFill><a:prstDash val="solid"/><a:round/></a:ln>`;
3137
3411
  strXml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW);
3138
3412
  strXml += " </c:spPr>";
3139
3413
  strXml += "</c:dPt>";
3140
3414
  });
3141
3415
  strXml += "<c:dLbls>";
3142
3416
  optsChartData.labels[0].forEach((_label, idx) => {
3417
+ const customLbl = optsChartData.customLabels?.[idx];
3143
3418
  strXml += "<c:dLbl>";
3144
3419
  strXml += ` <c:idx val="${idx}"/>`;
3420
+ if (customLbl) strXml += `<c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:r><a:rPr lang="${opts.lang || "en-US"}" dirty="0"/><a:t>${encodeXmlEntities(customLbl)}</a:t></a:r></a:p></c:rich></c:tx>`;
3145
3421
  strXml += ` <c:numFmt formatCode="${encodeXmlEntities(opts.dataLabelFormatCode) || "General"}" sourceLinked="0"/>`;
3146
3422
  strXml += " <c:spPr/><c:txPr>";
3147
3423
  strXml += " <a:bodyPr/><a:lstStyle/>";
3148
3424
  strXml += " <a:p><a:pPr>";
3149
3425
  strXml += ` <a:defRPr sz="${Math.round((opts.dataLabelFontSize || 12) * 100)}" b="${opts.dataLabelFontBold ? 1 : 0}" i="${opts.dataLabelFontItalic ? 1 : 0}" u="none" strike="noStrike">`;
3150
3426
  strXml += " <a:solidFill>" + createColorElement(opts.dataLabelColor || "000000") + "</a:solidFill>";
3151
- strXml += ` <a:latin typeface="${opts.dataLabelFontFace || "Arial"}"/>`;
3427
+ strXml += " " + createChartTextFonts(opts.dataLabelFontFace || "Arial");
3152
3428
  strXml += " </a:defRPr>";
3153
3429
  strXml += " </a:pPr></a:p>";
3154
3430
  strXml += " </c:txPr>";
3155
3431
  if (chartType === "pie" && opts.dataLabelPosition) strXml += `<c:dLblPos val="${opts.dataLabelPosition}"/>`;
3156
3432
  strXml += " <c:showLegendKey val=\"0\"/>";
3157
- strXml += " <c:showVal val=\"" + (opts.showValue ? "1" : "0") + "\"/>";
3433
+ strXml += " <c:showVal val=\"" + (customLbl ? "0" : opts.showValue ? "1" : "0") + "\"/>";
3158
3434
  strXml += " <c:showCatName val=\"" + (opts.showLabel ? "1" : "0") + "\"/>";
3159
3435
  strXml += " <c:showSerName val=\"" + (opts.showSerName ? "1" : "0") + "\"/>";
3160
3436
  strXml += " <c:showPercent val=\"" + (opts.showPercent ? "1" : "0") + "\"/>";
@@ -3169,7 +3445,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
3169
3445
  strXml += " <a:pPr>";
3170
3446
  strXml += ` <a:defRPr sz="${Math.round((opts.dataLabelFontSize || 12) * 100)}" b="${opts.dataLabelFontBold ? "1" : "0"}" i="${opts.dataLabelFontItalic ? "1" : "0"}" u="none" strike="noStrike">`;
3171
3447
  strXml += " <a:solidFill>" + createColorElement(opts.dataLabelColor || "000000") + "</a:solidFill>";
3172
- strXml += ` <a:latin typeface="${opts.dataLabelFontFace || "Arial"}"/>`;
3448
+ strXml += " " + createChartTextFonts(opts.dataLabelFontFace || "Arial");
3173
3449
  strXml += " </a:defRPr>";
3174
3450
  strXml += " </a:pPr>";
3175
3451
  strXml += " </a:p>";
@@ -3182,6 +3458,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
3182
3458
  strXml += " <c:showPercent val=\"1\"/>";
3183
3459
  strXml += " <c:showBubbleSize val=\"0\"/>";
3184
3460
  strXml += ` <c:showLeaderLines val="${opts.showLeaderLines ? "1" : "0"}"/>`;
3461
+ strXml += createLeaderLinesElement(opts);
3185
3462
  strXml += "</c:dLbls>";
3186
3463
  strXml += "<c:cat>";
3187
3464
  strXml += " <c:strRef>";
@@ -3198,6 +3475,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
3198
3475
  strXml += " <c:numRef>";
3199
3476
  strXml += ` <c:f>Sheet1!$B$2:$B$${optsChartData.labels[0].length + 1}</c:f>`;
3200
3477
  strXml += " <c:numCache>";
3478
+ strXml += " <c:formatCode>" + valFmtCode + "</c:formatCode>";
3201
3479
  strXml += ` <c:ptCount val="${optsChartData.labels[0].length}"/>`;
3202
3480
  optsChartData.values.forEach((value, idx) => {
3203
3481
  strXml += `<c:pt idx="${idx}"><c:v>${value || value === 0 ? value : ""}</c:v></c:pt>`;
@@ -3273,7 +3551,7 @@ function makeCatAxis(opts, axisId, valAxisId) {
3273
3551
  strXml += " <a:pPr>";
3274
3552
  strXml += ` <a:defRPr sz="${Math.round((opts.catAxisLabelFontSize || 12) * 100)}" b="${opts.catAxisLabelFontBold ? 1 : 0}" i="${opts.catAxisLabelFontItalic ? 1 : 0}" u="none" strike="noStrike">`;
3275
3553
  strXml += " <a:solidFill>" + createColorElement(opts.catAxisLabelColor || "000000") + "</a:solidFill>";
3276
- strXml += " <a:latin typeface=\"" + (opts.catAxisLabelFontFace || "Arial") + "\"/>";
3554
+ strXml += " " + createChartTextFonts(opts.catAxisLabelFontFace || "Arial");
3277
3555
  strXml += " </a:defRPr>";
3278
3556
  strXml += " </a:pPr>";
3279
3557
  strXml += " <a:endParaRPr lang=\"" + (opts.lang || "en-US") + "\"/>";
@@ -3366,7 +3644,7 @@ function makeValAxis(opts, valAxisId) {
3366
3644
  strXml += " <a:pPr>";
3367
3645
  strXml += ` <a:defRPr sz="${Math.round((opts.valAxisLabelFontSize || 12) * 100)}" b="${opts.valAxisLabelFontBold ? 1 : 0}" i="${opts.valAxisLabelFontItalic ? 1 : 0}" u="none" strike="noStrike">`;
3368
3646
  strXml += " <a:solidFill>" + createColorElement(opts.valAxisLabelColor || "000000") + "</a:solidFill>";
3369
- strXml += " <a:latin typeface=\"" + (opts.valAxisLabelFontFace || "Arial") + "\"/>";
3647
+ strXml += " " + createChartTextFonts(opts.valAxisLabelFontFace || "Arial");
3370
3648
  strXml += " </a:defRPr>";
3371
3649
  strXml += " </a:pPr>";
3372
3650
  strXml += " <a:endParaRPr lang=\"" + (opts.lang || "en-US") + "\"/>";
@@ -3422,7 +3700,7 @@ function makeSerAxis(opts, axisId, valAxisId) {
3422
3700
  strXml += " <a:pPr>";
3423
3701
  strXml += ` <a:defRPr sz="${Math.round((opts.serAxisLabelFontSize || 12) * 100)}" b="${opts.serAxisLabelFontBold ? "1" : "0"}" i="${opts.serAxisLabelFontItalic ? "1" : "0"}" u="none" strike="noStrike">`;
3424
3702
  strXml += ` <a:solidFill>${createColorElement(opts.serAxisLabelColor || "000000")}</a:solidFill>`;
3425
- strXml += ` <a:latin typeface="${opts.serAxisLabelFontFace || "Arial"}"/>`;
3703
+ strXml += " " + createChartTextFonts(opts.serAxisLabelFontFace || "Arial");
3426
3704
  strXml += " </a:defRPr>";
3427
3705
  strXml += " </a:pPr>";
3428
3706
  strXml += " <a:endParaRPr lang=\"" + (opts.lang || "en-US") + "\"/>";
@@ -3462,17 +3740,31 @@ function genXmlTitle(opts, chartX, chartY) {
3462
3740
  const rotate = opts.titleRotate ? `<a:bodyPr rot="${convertRotationDegrees(opts.titleRotate)}"/>` : "<a:bodyPr/>";
3463
3741
  const sizeAttr = opts.fontSize ? `sz="${Math.round(opts.fontSize * 100)}"` : "";
3464
3742
  const titleBold = opts.titleBold ? 1 : 0;
3743
+ const titleItalic = opts.titleItalic ? 1 : 0;
3744
+ const titleUnderline = opts.titleUnderline ? "sng" : "none";
3465
3745
  let layout = "<c:layout/>";
3466
- if (opts.titlePos && typeof opts.titlePos.x === "number" && typeof opts.titlePos.y === "number") {
3467
- const totalX = opts.titlePos.x + chartX;
3468
- const totalY = opts.titlePos.y + chartY;
3469
- let valX = totalX === 0 ? 0 : totalX * (totalX / 5) / 10;
3470
- if (valX >= 1) valX = valX / 10;
3471
- if (valX >= .1) valX = valX / 10;
3472
- let valY = totalY === 0 ? 0 : totalY * (totalY / 5) / 10;
3473
- if (valY >= 1) valY = valY / 10;
3474
- if (valY >= .1) valY = valY / 10;
3475
- layout = `<c:layout><c:manualLayout><c:xMode val="edge"/><c:yMode val="edge"/><c:x val="${valX}"/><c:y val="${valY}"/></c:manualLayout></c:layout>`;
3746
+ const hasX = opts.titlePos && typeof opts.titlePos.x === "number";
3747
+ const hasY = opts.titlePos && typeof opts.titlePos.y === "number";
3748
+ if (hasX || hasY) {
3749
+ let modes = "";
3750
+ let vals = "";
3751
+ if (hasX) {
3752
+ const totalX = opts.titlePos.x + chartX;
3753
+ let valX = totalX === 0 ? 0 : totalX * (totalX / 5) / 10;
3754
+ if (valX >= 1) valX = valX / 10;
3755
+ if (valX >= .1) valX = valX / 10;
3756
+ modes += "<c:xMode val=\"edge\"/>";
3757
+ vals += `<c:x val="${valX}"/>`;
3758
+ }
3759
+ if (hasY) {
3760
+ const totalY = opts.titlePos.y + chartY;
3761
+ let valY = totalY === 0 ? 0 : totalY * (totalY / 5) / 10;
3762
+ if (valY >= 1) valY = valY / 10;
3763
+ if (valY >= .1) valY = valY / 10;
3764
+ modes += "<c:yMode val=\"edge\"/>";
3765
+ vals += `<c:y val="${valY}"/>`;
3766
+ }
3767
+ layout = `<c:layout><c:manualLayout>${modes}${vals}</c:manualLayout></c:layout>`;
3476
3768
  }
3477
3769
  return `<c:title>
3478
3770
  <c:tx>
@@ -3481,15 +3773,15 @@ function genXmlTitle(opts, chartX, chartY) {
3481
3773
  <a:lstStyle/>
3482
3774
  <a:p>
3483
3775
  ${align}
3484
- <a:defRPr ${sizeAttr} b="${titleBold}" i="0" u="none" strike="noStrike">
3776
+ <a:defRPr ${sizeAttr} b="${titleBold}" i="${titleItalic}" u="${titleUnderline}" strike="noStrike">
3485
3777
  <a:solidFill>${createColorElement(opts.color || "000000")}</a:solidFill>
3486
- <a:latin typeface="${opts.fontFace || "Arial"}"/>
3778
+ ${createChartTextFonts(opts.fontFace || "Arial")}
3487
3779
  </a:defRPr>
3488
3780
  </a:pPr>
3489
3781
  <a:r>
3490
- <a:rPr ${sizeAttr} b="${titleBold}" i="0" u="none" strike="noStrike">
3782
+ <a:rPr ${sizeAttr} b="${titleBold}" i="${titleItalic}" u="${titleUnderline}" strike="noStrike">
3491
3783
  <a:solidFill>${createColorElement(opts.color || "000000")}</a:solidFill>
3492
- <a:latin typeface="${opts.fontFace || "Arial"}"/>
3784
+ ${createChartTextFonts(opts.fontFace || "Arial")}
3493
3785
  </a:rPr>
3494
3786
  <a:t>${encodeXmlEntities(opts.title) || ""}</a:t>
3495
3787
  </a:r>
@@ -3563,11 +3855,123 @@ function createGridLineElement(glOpts) {
3563
3855
  strXml += "</c:majorGridlines>";
3564
3856
  return strXml;
3565
3857
  }
3566
- function createLineCap(lineCap) {
3567
- if (!lineCap || lineCap === "flat") return "flat";
3568
- else if (lineCap === "square") return "sq";
3569
- else if (lineCap === "round") return "rnd";
3570
- else throw new Error(`Invalid chart line cap: ${lineCap}`);
3858
+ /**
3859
+ * Build a `<c:pt>` numeric-cache data point, or '' to leave a gap.
3860
+ *
3861
+ * `<c:v>` inside a `<c:numCache>` is an `xsd:double`; emitting `NaN`, `Infinity`
3862
+ * or an empty string yields an invalid value that makes PowerPoint report the
3863
+ * package as needing repair. Null/undefined are intentional gaps and are skipped
3864
+ * silently (a sparse, idx-keyed cache is valid); other non-finite numbers are
3865
+ * skipped with a warning, per the library's "warn rather than emit a degenerate
3866
+ * result" policy.
3867
+ * @param idx - zero-based data-point index (emitted as `idx`)
3868
+ * @param value - numeric value (or null/undefined gap)
3869
+ */
3870
+ function numCachePt(idx, value) {
3871
+ if (value == null) return "";
3872
+ if (!Number.isFinite(value)) {
3873
+ console.warn(`Warning: chart value "${value}" at index ${idx} is not a finite number; data point omitted.`);
3874
+ return "";
3875
+ }
3876
+ return `<c:pt idx="${idx}"><c:v>${value}</c:v></c:pt>`;
3877
+ }
3878
+ /**
3879
+ * Build a `<c:serLines>` ("Series Lines") element for a bar chart.
3880
+ * @param opt - `true` for PowerPoint automatic styling, an {@link OptsChartGridLine}
3881
+ * to customize the line, or falsy / `{ style: 'none' }` to omit the element.
3882
+ */
3883
+ function createSerLinesElement(opt) {
3884
+ if (!opt) return "";
3885
+ if (opt === true) return "<c:serLines/>";
3886
+ if (opt.style === "none") return "";
3887
+ let strXml = "<c:serLines><c:spPr>";
3888
+ strXml += `<a:ln w="${valToPts(opt.size || DEF_CHART_GRIDLINE.size)}" cap="${createLineCap(opt.cap || DEF_CHART_GRIDLINE.cap)}">`;
3889
+ strXml += `<a:solidFill><a:srgbClr val="${opt.color || DEF_CHART_GRIDLINE.color}"/></a:solidFill>`;
3890
+ strXml += `<a:prstDash val="${opt.style || DEF_CHART_GRIDLINE.style}"/><a:round/>`;
3891
+ strXml += "</a:ln></c:spPr></c:serLines>";
3892
+ return strXml;
3893
+ }
3894
+ /**
3895
+ * Build the `<c:leaderLines>` element for pie/doughnut data labels.
3896
+ *
3897
+ * Schema position: inside `<c:dLbls>`, immediately after `<c:showLeaderLines>`
3898
+ * (CT_DLbls / Group_DLbls order: showLeaderLines → leaderLines).
3899
+ *
3900
+ * Returns `''` unless the caller both enabled leader lines (`showLeaderLines`)
3901
+ * and configured their appearance (`leaderLineColor` / `leaderLineSize`). When
3902
+ * appearance is unset we leave the element off so PowerPoint applies its
3903
+ * automatic leader-line color, matching prior behavior.
3904
+ *
3905
+ * @param opts - chart options (reads `showLeaderLines`, `leaderLineColor`, `leaderLineSize`)
3906
+ */
3907
+ function createLeaderLinesElement(opts) {
3908
+ if (!opts.showLeaderLines) return "";
3909
+ if (!opts.leaderLineColor && opts.leaderLineSize == null) return "";
3910
+ return `<c:leaderLines><c:spPr><a:ln w="${valToPts(opts.leaderLineSize ?? .75)}" cap="flat"><a:solidFill>${createColorElement(opts.leaderLineColor || "808080")}</a:solidFill><a:prstDash val="solid"/><a:round/></a:ln><a:effectLst/></c:spPr></c:leaderLines>`;
3911
+ }
3912
+ function makeCustomDLblXml(idx, text, opts) {
3913
+ const sz = Math.round((opts.dataLabelFontSize || 12) * 100);
3914
+ const bold = opts.dataLabelFontBold ? "1" : "0";
3915
+ const italic = opts.dataLabelFontItalic ? "1" : "0";
3916
+ const color = createColorElement(opts.dataLabelColor || "000000");
3917
+ const face = opts.dataLabelFontFace || "Arial";
3918
+ const lang = opts.lang || "en-US";
3919
+ return `<c:dLbl><c:idx val="${idx}"/><c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:pPr><a:defRPr sz="${sz}" b="${bold}" i="${italic}" u="none" strike="noStrike"><a:solidFill>${color}</a:solidFill>${createChartTextFonts(face)}</a:defRPr></a:pPr><a:r><a:rPr lang="${lang}" sz="${sz}" b="${bold}" i="${italic}" u="none" strike="noStrike" dirty="0"><a:solidFill>${color}</a:solidFill>${createChartTextFonts(face)}</a:rPr><a:t>${encodeXmlEntities(text)}</a:t></a:r></a:p></c:rich></c:tx><c:showLegendKey val="0"/><c:showVal val="0"/><c:showCatName val="0"/><c:showSerName val="0"/><c:showPercent val="0"/><c:showBubbleSize val="0"/></c:dLbl>`;
3920
+ }
3921
+ /**
3922
+ * Build an `<a:ln>` border element from a per-data-point `BorderProps`.
3923
+ * @param border - point border style (`type`, `color`, `pt`)
3924
+ */
3925
+ function createChartBorderLine(border) {
3926
+ if (border.type === "none") return "<a:ln><a:noFill/></a:ln>";
3927
+ const dash = border.type === "dash" ? "dash" : "solid";
3928
+ return `<a:ln w="${valToPts(border.pt ?? 1)}" cap="flat"><a:solidFill>${createColorElement(border.color || "666666")}</a:solidFill><a:prstDash val="${dash}"/><a:round/></a:ln>`;
3929
+ }
3930
+ /**
3931
+ * Build `<c:dPt>` entries for a series in the bar/line/area/scatter loops.
3932
+ *
3933
+ * Merges two sources into a single `c:dPt` per index so we never emit a
3934
+ * duplicate `<c:idx>` (which corrupts the chart):
3935
+ * - legacy single-series color-vary fills (bar/scatter), supplied via `varyColors`
3936
+ * - per-point `pointStyles` border/fill overrides
3937
+ *
3938
+ * Must be emitted in schema position *before* `c:dLbls` (CT_*Ser order).
3939
+ * RADAR is skipped: extra per-point markup historically corrupts the chart.
3940
+ *
3941
+ * @param chartType - series chart type
3942
+ * @param obj - series data (reads `values`, `pointStyles`)
3943
+ * @param opts - chart options (fill/shadow/lineSize context)
3944
+ * @param varyColors - color array when single-series color-vary applies, else `null`
3945
+ */
3946
+ function makeSeriesDataPointsXml(chartType, obj, opts, varyColors) {
3947
+ if (chartType === "radar") return "";
3948
+ const pointStyles = obj.pointStyles;
3949
+ if (!varyColors && !pointStyles?.length) return "";
3950
+ const isBar = chartType === "bar" || chartType === "bar3D";
3951
+ const isScatter = chartType === "scatter";
3952
+ let xml = "";
3953
+ obj.values.forEach((value, index) => {
3954
+ const ptStyle = pointStyles?.[index];
3955
+ const arrColors = varyColors ? value < 0 ? opts.invertedColors || opts.chartColors || BARCHART_COLORS : varyColors : null;
3956
+ const fillColor = ptStyle?.fill || (arrColors ? arrColors[index % arrColors.length] : null);
3957
+ const border = ptStyle?.border;
3958
+ if (!fillColor && !border) return;
3959
+ xml += "<c:dPt>";
3960
+ xml += `<c:idx val="${index}"/>`;
3961
+ if (isBar) xml += "<c:invertIfNegative val=\"0\"/>";
3962
+ xml += "<c:bubble3D val=\"0\"/>";
3963
+ xml += "<c:spPr>";
3964
+ if ((isBar || isScatter) && opts.lineSize === 0 && !border && !ptStyle?.fill) xml += "<a:ln><a:noFill/></a:ln>";
3965
+ else {
3966
+ if (fillColor) if (chartType === "bar3D") xml += `<a:ln><a:solidFill>${createColorElement(fillColor)}</a:solidFill></a:ln>`;
3967
+ else xml += `<a:solidFill>${createColorElement(fillColor)}</a:solidFill>`;
3968
+ if (border) xml += createChartBorderLine(border);
3969
+ }
3970
+ xml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW);
3971
+ xml += "</c:spPr>";
3972
+ xml += "</c:dPt>";
3973
+ });
3974
+ return xml;
3571
3975
  }
3572
3976
  //#endregion
3573
3977
  //#region src/gen-media.ts
@@ -3580,9 +3984,11 @@ function hasEncodingPath(rel) {
3580
3984
  /**
3581
3985
  * Encode Image/Audio/Video into base64
3582
3986
  * @param {PresSlideInternal | SlideLayoutInternal} layout - slide layout
3987
+ * @param {RuntimeAdapter} runtime - runtime adapter (Node/browser media loader)
3988
+ * @param {'throw' | 'placeholder'} onMediaError - failure policy: reject the export (default) or substitute a placeholder and warn
3583
3989
  * @return {Promise} promise
3584
3990
  */
3585
- function encodeSlideMediaRels(layout, runtime) {
3991
+ function encodeSlideMediaRels(layout, runtime, onMediaError = "throw") {
3586
3992
  const imageProms = [];
3587
3993
  const candidateRels = layout._relsMedia.filter((rel) => rel.type !== "online" && !rel.data && hasEncodingPath(rel));
3588
3994
  const unqPaths = [];
@@ -3600,9 +4006,13 @@ function encodeSlideMediaRels(layout, runtime) {
3600
4006
  if (rel.isSvgPng) await runtime.createSvgPngPreview(rel);
3601
4007
  return "done";
3602
4008
  } catch (ex) {
3603
- rel.data = IMG_BROKEN;
3604
- candidateRels.filter((dupe) => dupe.isDuplicate && dupe.path === rel.path).forEach((dupe) => dupe.data = rel.data);
3605
- throw ex;
4009
+ if (onMediaError === "placeholder") {
4010
+ console.warn(`[WARNING] Failed to load media "${rel.path}"; embedding a broken-image placeholder. (${String(ex)})`);
4011
+ rel.data = IMG_BROKEN;
4012
+ candidateRels.filter((dupe) => dupe.isDuplicate && dupe.path === rel.path).forEach((dupe) => dupe.data = rel.data);
4013
+ return "done";
4014
+ }
4015
+ throw new Error(`Failed to load media "${rel.path}" during export.`, { cause: ex });
3606
4016
  }
3607
4017
  })());
3608
4018
  });
@@ -3616,6 +4026,37 @@ function encodeSlideMediaRels(layout, runtime) {
3616
4026
  /**
3617
4027
  * PptxGenJS: XML Generation
3618
4028
  */
4029
+ const _warnedTextRangeMsgs = /* @__PURE__ */ new Set();
4030
+ function warnTextRangeOnce(msg) {
4031
+ if (_warnedTextRangeMsgs.has(msg)) return;
4032
+ _warnedTextRangeMsgs.add(msg);
4033
+ console.warn(msg);
4034
+ }
4035
+ /**
4036
+ * Clamp a font size (points) into ST_TextFontSize (1-4000pt) and return it in
4037
+ * hundredths of a point for the `sz` attribute. Out-of-range sizes make
4038
+ * PowerPoint report the package as needing repair (e.g. `sz` > 400000 or < 100).
4039
+ */
4040
+ function clampFontSizeSz(fontSizePts) {
4041
+ const raw = Math.round(fontSizePts * 100);
4042
+ const clamped = Math.min(4e5, Math.max(100, raw));
4043
+ if (clamped !== raw) warnTextRangeOnce(`Warning: fontSize ${fontSizePts} is outside the valid range 1-4000pt; using ${clamped / 100}.`);
4044
+ return clamped;
4045
+ }
4046
+ /** Clamp character spacing (points) into ST_TextPoint (-4000..4000pt); returns hundredths for the `spc` attribute. */
4047
+ function clampCharSpacingSpc(charSpacingPts) {
4048
+ const raw = Math.round(charSpacingPts * 100);
4049
+ const clamped = Math.min(4e5, Math.max(-4e5, raw));
4050
+ if (clamped !== raw) warnTextRangeOnce(`Warning: charSpacing ${charSpacingPts} is outside the valid range -4000..4000pt; using ${clamped / 100}.`);
4051
+ return clamped;
4052
+ }
4053
+ /** Clamp line spacing (points) into ST_TextSpacingPoint (0..1584pt); returns hundredths for `<a:spcPts val>`. */
4054
+ function clampLineSpacingPts(lineSpacingPts) {
4055
+ const raw = Math.round(lineSpacingPts * 100);
4056
+ const clamped = Math.min(158400, Math.max(0, raw));
4057
+ if (clamped !== raw) warnTextRangeOnce(`Warning: lineSpacing ${lineSpacingPts} is outside the valid range 0-1584pt; using ${clamped / 100}.`);
4058
+ return clamped;
4059
+ }
3619
4060
  const ImageSizingXml = {
3620
4061
  cover: function(imgSize, boxDim) {
3621
4062
  const imgRatio = imgSize.h / imgSize.w;
@@ -3662,23 +4103,90 @@ const ImageSizingXml = {
3662
4103
  * @return {string} `<a:prstGeom>` XML
3663
4104
  */
3664
4105
  const RECT_RADIUS_ADJ1_SHAPES = new Set(["round2SameRect", "round2DiagRect"]);
4106
+ const SHAPE_LOCK_ATTRS = [
4107
+ "noGrp",
4108
+ "noSelect",
4109
+ "noRot",
4110
+ "noChangeAspect",
4111
+ "noMove",
4112
+ "noResize",
4113
+ "noEditPoints",
4114
+ "noAdjustHandles",
4115
+ "noChangeArrowheads",
4116
+ "noChangeShapeType",
4117
+ "noTextEdit"
4118
+ ];
4119
+ const PICTURE_LOCK_ATTRS = [
4120
+ "noGrp",
4121
+ "noSelect",
4122
+ "noRot",
4123
+ "noChangeAspect",
4124
+ "noMove",
4125
+ "noResize",
4126
+ "noEditPoints",
4127
+ "noAdjustHandles",
4128
+ "noChangeArrowheads",
4129
+ "noChangeShapeType",
4130
+ "noCrop"
4131
+ ];
4132
+ const GRAPHIC_FRAME_LOCK_ATTRS = [
4133
+ "noGrp",
4134
+ "noDrilldown",
4135
+ "noSelect",
4136
+ "noChangeAspect",
4137
+ "noMove",
4138
+ "noResize"
4139
+ ];
4140
+ /**
4141
+ * Serialize an object-lock element (`a:spLocks` / `a:picLocks` / `a:graphicFrameLocks`).
4142
+ * Only flags set to `true` AND valid for this element type are emitted; a flag set on an
4143
+ * unsupported element type is dropped with a warning (silent coercion is a footgun).
4144
+ * @param tag - locking element tag, e.g. `'a:spLocks'`
4145
+ * @param allowed - attribute names this element type supports, in desired emit order
4146
+ * @param locks - merged lock flags (callers fold any hard-coded default in first)
4147
+ * @param objectName - for the warning message
4148
+ * @returns the locking element string, or `''` when no applicable flag is set
4149
+ */
4150
+ function genXmlObjectLock(tag, allowed, locks, objectName) {
4151
+ if (!locks) return "";
4152
+ const lockMap = locks;
4153
+ for (const key of Object.keys(lockMap)) if (lockMap[key] && !allowed.includes(key)) console.warn(`Warning: objectLock.${key} is not supported on <${tag}> (object "${objectName ?? ""}") and was ignored.`);
4154
+ const attrs = allowed.filter((name) => lockMap[name] === true).map((name) => `${name}="1"`);
4155
+ return attrs.length > 0 ? `<${tag} ${attrs.join(" ")}/>` : "";
4156
+ }
3665
4157
  function genXmlPresetGeom(shapeName, options, cx, cy) {
3666
- let strXml = `<a:prstGeom prst="${shapeName}"><a:avLst>`;
4158
+ if (!VALID_SHAPE_PRESETS.has(shapeName)) throw new Error(`Invalid shape "${String(shapeName)}"! Use a value from \`pptxgen.shapes.*\` (e.g. \`pptxgen.shapes.RECTANGLE\`). PowerPoint can't render unknown preset geometries and will drop the shape during repair.`);
4159
+ let avLst = "";
4160
+ const emittedAdjNames = /* @__PURE__ */ new Set();
4161
+ const emitGuide = (name, fmlaVal) => {
4162
+ avLst += `<a:gd name="${name}" fmla="val ${fmlaVal}"/>`;
4163
+ emittedAdjNames.add(name);
4164
+ };
3667
4165
  if (options.rectRadius) {
3668
4166
  const adjVal = Math.round(options.rectRadius * EMU * 1e5 / Math.min(cx, cy));
3669
4167
  if (RECT_RADIUS_ADJ1_SHAPES.has(shapeName)) {
3670
- strXml += `<a:gd name="adj1" fmla="val ${adjVal}"/>`;
3671
- strXml += "<a:gd name=\"adj2\" fmla=\"val 0\"/>";
3672
- } else strXml += `<a:gd name="adj" fmla="val ${adjVal}"/>`;
4168
+ emitGuide("adj1", adjVal);
4169
+ emitGuide("adj2", 0);
4170
+ } else emitGuide("adj", adjVal);
3673
4171
  } else if (options.angleRange) {
3674
4172
  for (let i = 0; i < 2; i++) {
3675
4173
  const angle = options.angleRange[i];
3676
- strXml += `<a:gd name="adj${i + 1}" fmla="val ${convertRotationDegrees(angle)}" />`;
4174
+ emitGuide(`adj${i + 1}`, convertRotationDegrees(angle));
3677
4175
  }
3678
- if (options.arcThicknessRatio) strXml += `<a:gd name="adj3" fmla="val ${Math.round(options.arcThicknessRatio * 5e4)}" />`;
4176
+ if (options.arcThicknessRatio) emitGuide("adj3", Math.round(options.arcThicknessRatio * 5e4));
3679
4177
  }
3680
- strXml += "</a:avLst></a:prstGeom>";
3681
- return strXml;
4178
+ if (options.shapeAdjust) (Array.isArray(options.shapeAdjust) ? options.shapeAdjust : [options.shapeAdjust]).forEach((adj) => {
4179
+ if (!adj || typeof adj.name !== "string" || adj.name.length === 0 || typeof adj.value !== "number" || !isFinite(adj.value)) {
4180
+ console.warn(`Warning: shapeAdjust entry ${JSON.stringify(adj)} is invalid (needs { name:string, value:number }) and was ignored.`);
4181
+ return;
4182
+ }
4183
+ if (emittedAdjNames.has(adj.name)) {
4184
+ console.warn(`Warning: shapeAdjust "${adj.name}" was ignored because rectRadius/angleRange already set that handle.`);
4185
+ return;
4186
+ }
4187
+ emitGuide(adj.name, Math.round(adj.value * 1e5));
4188
+ });
4189
+ return `<a:prstGeom prst="${shapeName}"><a:avLst>${avLst}</a:avLst></a:prstGeom>`;
3682
4190
  }
3683
4191
  /**
3684
4192
  * Emit an `<a:custGeom>` for a freeform path built from `points`.
@@ -3731,6 +4239,45 @@ function genXmlCustGeom(points, cx, cy, layout) {
3731
4239
  }
3732
4240
  const PLACEHOLDER_TYPE_MAP = PLACEHOLDER_TYPES;
3733
4241
  /**
4242
+ * Emit the `<a:lnL>/<a:lnR>/<a:lnT>/<a:lnB>` border children of an `<a:tcPr>` for a table cell.
4243
+ * Shared by normal cells and the dummy span (`_hmerge`/`_vmerge`) cells so a merged region's
4244
+ * outer edges render with the same border as its origin cell (Issue #680).
4245
+ * @param {BorderProps[]} cellBorder - 4-tuple of border props in [top, right, bottom, left] order
4246
+ * @return {string} concatenated border element XML, in the LRTB document order PowerPoint expects
4247
+ */
4248
+ function genTableCellBorderXml(cellBorder) {
4249
+ let strXml = "";
4250
+ [
4251
+ {
4252
+ idx: 3,
4253
+ name: "lnL"
4254
+ },
4255
+ {
4256
+ idx: 1,
4257
+ name: "lnR"
4258
+ },
4259
+ {
4260
+ idx: 0,
4261
+ name: "lnT"
4262
+ },
4263
+ {
4264
+ idx: 2,
4265
+ name: "lnB"
4266
+ }
4267
+ ].forEach((obj) => {
4268
+ const border = cellBorder[obj.idx];
4269
+ if (!border) return;
4270
+ const cap = createLineCap(border.cap);
4271
+ if (border.type !== "none") {
4272
+ strXml += `<a:${obj.name} w="${valToPts(border.pt)}" cap="${cap}" cmpd="sng" algn="ctr">`;
4273
+ strXml += `<a:solidFill>${createColorElement(border.color)}</a:solidFill>`;
4274
+ strXml += `<a:prstDash val="${border.type === "dash" ? "sysDash" : "solid"}"/><a:round/><a:headEnd type="none" w="med" len="med"/><a:tailEnd type="none" w="med" len="med"/>`;
4275
+ strXml += `</a:${obj.name}>`;
4276
+ } else strXml += `<a:${obj.name} w="0" cap="${cap}" cmpd="sng" algn="ctr"><a:noFill/></a:${obj.name}>`;
4277
+ });
4278
+ return strXml;
4279
+ }
4280
+ /**
3734
4281
  * Transforms a slide or slideLayout to resulting XML string - Creates `ppt/slide*.xml`
3735
4282
  * @param {PresSlideInternal|SlideLayoutInternal} slideObject - slide object created within createSlideObject
3736
4283
  * @return {string} XML string with <p:cSld> as the root
@@ -3789,7 +4336,10 @@ function slideObjectToXml(slide) {
3789
4336
  intColCnt += cellOpts?.colspan ? Number(cellOpts.colspan) : 1;
3790
4337
  });
3791
4338
  strXml = `<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="${intTableNum * slide._slideNum + 1}" name="${slideItemObj.options.objectName}" descr="${encodeXmlEntities(slideItemObj.options.altText || "")}"/>`;
3792
- strXml += "<p:cNvGraphicFramePr><a:graphicFrameLocks noGrp=\"1\"/></p:cNvGraphicFramePr> <p:nvPr><p:extLst><p:ext uri=\"{D42A27DB-BD31-4B8C-83A1-F6EECF244321}\"><p14:modId xmlns:p14=\"http://schemas.microsoft.com/office/powerpoint/2010/main\" val=\"1579011935\"/></p:ext></p:extLst></p:nvPr></p:nvGraphicFramePr>";
4339
+ strXml += `<p:cNvGraphicFramePr>${genXmlObjectLock("a:graphicFrameLocks", GRAPHIC_FRAME_LOCK_ATTRS, {
4340
+ noGrp: true,
4341
+ ...slideItemObj.options.objectLock
4342
+ }, slideItemObj.options.objectName)}</p:cNvGraphicFramePr> <p:nvPr><p:extLst><p:ext uri="{D42A27DB-BD31-4B8C-83A1-F6EECF244321}"><p14:modId xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" val="1579011935"/></p:ext></p:extLst></p:nvPr></p:nvGraphicFramePr>`;
3793
4343
  strXml += `<p:xfrm><a:off x="${x || (x === 0 ? 0 : EMU)}" y="${y || (y === 0 ? 0 : EMU)}"/><a:ext cx="${cx || (cx === 0 ? 0 : EMU)}" cy="${cy || EMU}"/></p:xfrm>`;
3794
4344
  {
3795
4345
  const tblPrAttrs = (objTabOpts.hasHeader ? " firstRow=\"1\"" : "") + (objTabOpts.hasFooter ? " lastRow=\"1\"" : "") + (objTabOpts.hasBandedRows ? " bandRow=\"1\"" : "") + (objTabOpts.hasBandedColumns ? " bandCol=\"1\"" : "") + (objTabOpts.hasFirstColumn ? " firstCol=\"1\"" : "") + (objTabOpts.hasLastColumn ? " lastCol=\"1\"" : "");
@@ -3821,7 +4371,8 @@ function slideObjectToXml(slide) {
3821
4371
  return {
3822
4372
  _type: "tablecell",
3823
4373
  options: { rowspan },
3824
- _hmerge: true
4374
+ _hmerge: true,
4375
+ _spanOrigin: cell
3825
4376
  };
3826
4377
  });
3827
4378
  cells.splice(cIdx + 1, 0, ...vMergeCells);
@@ -3837,12 +4388,14 @@ function slideObjectToXml(slide) {
3837
4388
  const colspan = cell.options?.colspan;
3838
4389
  const _hmerge = cell._hmerge;
3839
4390
  if (rowspan && rowspan > 1) {
4391
+ const _spanOrigin = cell._spanOrigin || cell;
3840
4392
  const hMergeCell = {
3841
4393
  _type: "tablecell",
3842
4394
  options: { colspan },
3843
4395
  _rowContinue: rowspan - 1,
3844
4396
  _vmerge: true,
3845
- _hmerge
4397
+ _hmerge,
4398
+ _spanOrigin
3846
4399
  };
3847
4400
  nextRow.splice(cIdx, 0, hMergeCell);
3848
4401
  }
@@ -3852,7 +4405,7 @@ function slideObjectToXml(slide) {
3852
4405
  let intRowH = 0;
3853
4406
  if (Array.isArray(objTabOpts.rowH) && objTabOpts.rowH[rIdx]) intRowH = inch2Emu(Number(objTabOpts.rowH[rIdx]));
3854
4407
  else if (objTabOpts.rowH && !isNaN(Number(objTabOpts.rowH))) intRowH = inch2Emu(Number(objTabOpts.rowH));
3855
- else if (slideItemObj.options.cy || slideItemObj.options.h) intRowH = Math.round((slideItemObj.options.h ? inch2Emu(slideItemObj.options.h) : typeof slideItemObj.options.cy === "number" ? slideItemObj.options.cy : 1) / arrTabRows.length);
4408
+ else if (slideItemObj.options.cy || slideItemObj.options.h) intRowH = Math.round((slideItemObj.options.h ? cy : typeof slideItemObj.options.cy === "number" ? slideItemObj.options.cy : 1) / arrTabRows.length);
3856
4409
  strXml += `<a:tr h="${intRowH}">`;
3857
4410
  cells.forEach((cellObj) => {
3858
4411
  const cell = cellObj;
@@ -3865,7 +4418,17 @@ function slideObjectToXml(slide) {
3865
4418
  let cellSpanAttrStr = Object.entries(cellSpanAttrs).filter(([, v]) => !!v).map(([k, v]) => `${String(k)}="${String(v)}"`).join(" ");
3866
4419
  if (cellSpanAttrStr) cellSpanAttrStr = " " + cellSpanAttrStr;
3867
4420
  if (cell._hmerge || cell._vmerge) {
3868
- strXml += `<a:tc${cellSpanAttrStr}><a:tcPr/></a:tc>`;
4421
+ const origin = cell._spanOrigin;
4422
+ let spanPrXml = "";
4423
+ if (origin) {
4424
+ const originOpts = origin.options || {};
4425
+ const originBorder = Array.isArray(originOpts.border) ? originOpts.border : null;
4426
+ if (originBorder) spanPrXml += genTableCellBorderXml(originBorder);
4427
+ let spanFill = origin._optImp?.fill?.color ? origin._optImp.fill.color : origin._optImp?.fill && typeof origin._optImp.fill === "string" ? origin._optImp.fill : "";
4428
+ spanFill = spanFill || originOpts.fill ? originOpts.fill : "";
4429
+ if (spanFill) spanPrXml += genXmlColorSelection(spanFill);
4430
+ }
4431
+ strXml += `<a:tc${cellSpanAttrStr}><a:tcPr>${spanPrXml}</a:tcPr></a:tc>`;
3869
4432
  return;
3870
4433
  }
3871
4434
  const cellOpts = cell.options || {};
@@ -3909,32 +4472,7 @@ function slideObjectToXml(slide) {
3909
4472
  else cellMarginXml = ` marL="${inch2Emu(cellMargin[3])}" marR="${inch2Emu(cellMargin[1])}" marT="${inch2Emu(cellMargin[0])}" marB="${inch2Emu(cellMargin[2])}"`;
3910
4473
  strXml += `<a:tc${cellSpanAttrStr}>${genXmlTextBody(cell)}<a:tcPr${cellMarginXml}${cellValign}${cellTextDir}>`;
3911
4474
  const cellBorder = Array.isArray(cellOpts.border) ? cellOpts.border : null;
3912
- if (cellBorder) [
3913
- {
3914
- idx: 3,
3915
- name: "lnL"
3916
- },
3917
- {
3918
- idx: 1,
3919
- name: "lnR"
3920
- },
3921
- {
3922
- idx: 0,
3923
- name: "lnT"
3924
- },
3925
- {
3926
- idx: 2,
3927
- name: "lnB"
3928
- }
3929
- ].forEach((obj) => {
3930
- const border = cellBorder[obj.idx];
3931
- if (border.type !== "none") {
3932
- strXml += `<a:${obj.name} w="${valToPts(border.pt)}" cap="flat" cmpd="sng" algn="ctr">`;
3933
- strXml += `<a:solidFill>${createColorElement(border.color)}</a:solidFill>`;
3934
- strXml += `<a:prstDash val="${border.type === "dash" ? "sysDash" : "solid"}"/><a:round/><a:headEnd type="none" w="med" len="med"/><a:tailEnd type="none" w="med" len="med"/>`;
3935
- strXml += `</a:${obj.name}>`;
3936
- } else strXml += `<a:${obj.name} w="0" cap="flat" cmpd="sng" algn="ctr"><a:noFill/></a:${obj.name}>`;
3937
- });
4475
+ if (cellBorder) strXml += genTableCellBorderXml(cellBorder);
3938
4476
  strXml += cellFill;
3939
4477
  strXml += " </a:tcPr>";
3940
4478
  strXml += " </a:tc>";
@@ -3953,10 +4491,10 @@ function slideObjectToXml(slide) {
3953
4491
  if (!slideItemObj.options.line && cy === 0) cy = EMU * .3;
3954
4492
  if (!slideItemObj.options._bodyProp) slideItemObj.options._bodyProp = {};
3955
4493
  if (slideItemObj.options.margin && Array.isArray(slideItemObj.options.margin)) {
3956
- slideItemObj.options._bodyProp.lIns = valToPts(slideItemObj.options.margin[0] || 0);
4494
+ slideItemObj.options._bodyProp.tIns = valToPts(slideItemObj.options.margin[0] || 0);
3957
4495
  slideItemObj.options._bodyProp.rIns = valToPts(slideItemObj.options.margin[1] || 0);
3958
4496
  slideItemObj.options._bodyProp.bIns = valToPts(slideItemObj.options.margin[2] || 0);
3959
- slideItemObj.options._bodyProp.tIns = valToPts(slideItemObj.options.margin[3] || 0);
4497
+ slideItemObj.options._bodyProp.lIns = valToPts(slideItemObj.options.margin[3] || 0);
3960
4498
  } else if (typeof slideItemObj.options.margin === "number") {
3961
4499
  slideItemObj.options._bodyProp.lIns = valToPts(slideItemObj.options.margin);
3962
4500
  slideItemObj.options._bodyProp.rIns = valToPts(slideItemObj.options.margin);
@@ -3968,7 +4506,11 @@ function slideObjectToXml(slide) {
3968
4506
  if (slideItemObj.options.hyperlink?.url) strSlideXml += `<a:hlinkClick r:id="rId${slideItemObj.options.hyperlink._rId}" tooltip="${slideItemObj.options.hyperlink.tooltip ? encodeXmlEntities(slideItemObj.options.hyperlink.tooltip) : ""}"/>`;
3969
4507
  if (slideItemObj.options.hyperlink?.slide) strSlideXml += `<a:hlinkClick r:id="rId${slideItemObj.options.hyperlink._rId}" tooltip="${slideItemObj.options.hyperlink.tooltip ? encodeXmlEntities(slideItemObj.options.hyperlink.tooltip) : ""}" action="ppaction://hlinksldjump"/>`;
3970
4508
  strSlideXml += "</p:cNvPr>";
3971
- strSlideXml += "<p:cNvSpPr" + (slideItemObj.options?.isTextBox ? " txBox=\"1\"/>" : "/>");
4509
+ {
4510
+ const spLockXml = genXmlObjectLock("a:spLocks", SHAPE_LOCK_ATTRS, slideItemObj.options.objectLock, slideItemObj.options.objectName);
4511
+ strSlideXml += "<p:cNvSpPr" + (slideItemObj.options?.isTextBox ? " txBox=\"1\"" : "");
4512
+ strSlideXml += spLockXml ? `>${spLockXml}</p:cNvSpPr>` : "/>";
4513
+ }
3972
4514
  strSlideXml += `<p:nvPr>${slideItemObj._type === "placeholder" ? genXmlPlaceholder(slideItemObj) : genXmlPlaceholder(placeholderObj)}</p:nvPr>`;
3973
4515
  strSlideXml += "</p:nvSpPr><p:spPr>";
3974
4516
  strSlideXml += `<a:xfrm${locationAttr}>`;
@@ -3978,7 +4520,8 @@ function slideObjectToXml(slide) {
3978
4520
  else strSlideXml += genXmlPresetGeom(slideItemObj.shape, slideItemObj.options, cx, cy);
3979
4521
  strSlideXml += slideItemObj.options.fill ? genXmlColorSelection(slideItemObj.options.fill) : "<a:noFill/>";
3980
4522
  if (slideItemObj.options.line) {
3981
- strSlideXml += slideItemObj.options.line.width ? `<a:ln w="${valToPts(slideItemObj.options.line.width)}">` : "<a:ln>";
4523
+ const lnAttrs = (slideItemObj.options.line.width ? ` w="${lineWidthToEmu(slideItemObj.options.line.width)}"` : "") + (slideItemObj.options.line.cap ? ` cap="${createLineCap(slideItemObj.options.line.cap)}"` : "");
4524
+ strSlideXml += `<a:ln${lnAttrs}>`;
3982
4525
  if (slideItemObj.options.line.color) strSlideXml += genXmlColorSelection(slideItemObj.options.line);
3983
4526
  if (slideItemObj.options.line.dashType) strSlideXml += `<a:prstDash val="${slideItemObj.options.line.dashType}"/>`;
3984
4527
  if (slideItemObj.options.line.beginArrowType) strSlideXml += `<a:headEnd type="${slideItemObj.options.line.beginArrowType}"/>`;
@@ -4004,6 +4547,24 @@ function slideObjectToXml(slide) {
4004
4547
  strSlideXml += genXmlTextBody(slideItemObj);
4005
4548
  strSlideXml += "</p:sp>";
4006
4549
  break;
4550
+ case "connector":
4551
+ strSlideXml += "<p:cxnSp><p:nvCxnSpPr>";
4552
+ strSlideXml += `<p:cNvPr id="${idx + 2}" name="${slideItemObj.options.objectName}" descr="${encodeXmlEntities(slideItemObj.options.altText || "")}"/>`;
4553
+ strSlideXml += "<p:cNvCxnSpPr/><p:nvPr/></p:nvCxnSpPr><p:spPr>";
4554
+ strSlideXml += `<a:xfrm${locationAttr}><a:off x="${x}" y="${y}"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm>`;
4555
+ strSlideXml += `<a:prstGeom prst="${slideItemObj.shape}"><a:avLst/></a:prstGeom>`;
4556
+ {
4557
+ const ln = slideItemObj.options.line || {};
4558
+ const lnAttrs = (ln.width ? ` w="${lineWidthToEmu(ln.width)}"` : "") + (ln.cap ? ` cap="${createLineCap(ln.cap)}"` : "");
4559
+ strSlideXml += `<a:ln${lnAttrs}>`;
4560
+ if (ln.color) strSlideXml += genXmlColorSelection(ln);
4561
+ if (ln.dashType) strSlideXml += `<a:prstDash val="${ln.dashType}"/>`;
4562
+ if (ln.beginArrowType) strSlideXml += `<a:headEnd type="${ln.beginArrowType}"/>`;
4563
+ if (ln.endArrowType) strSlideXml += `<a:tailEnd type="${ln.endArrowType}"/>`;
4564
+ strSlideXml += "</a:ln>";
4565
+ }
4566
+ strSlideXml += "</p:spPr></p:cxnSp>";
4567
+ break;
4007
4568
  case "image":
4008
4569
  strSlideXml += "<p:pic>";
4009
4570
  strSlideXml += " <p:nvPicPr>";
@@ -4011,7 +4572,10 @@ function slideObjectToXml(slide) {
4011
4572
  if (slideItemObj.hyperlink?.url) strSlideXml += `<a:hlinkClick r:id="rId${slideItemObj.hyperlink._rId}" tooltip="${slideItemObj.hyperlink.tooltip ? encodeXmlEntities(slideItemObj.hyperlink.tooltip) : ""}"/>`;
4012
4573
  if (slideItemObj.hyperlink?.slide) strSlideXml += `<a:hlinkClick r:id="rId${slideItemObj.hyperlink._rId}" tooltip="${slideItemObj.hyperlink.tooltip ? encodeXmlEntities(slideItemObj.hyperlink.tooltip) : ""}" action="ppaction://hlinksldjump"/>`;
4013
4574
  strSlideXml += " </p:cNvPr>";
4014
- strSlideXml += " <p:cNvPicPr><a:picLocks noChangeAspect=\"1\"/></p:cNvPicPr>";
4575
+ strSlideXml += ` <p:cNvPicPr>${genXmlObjectLock("a:picLocks", PICTURE_LOCK_ATTRS, {
4576
+ noChangeAspect: true,
4577
+ ...slideItemObj.options.objectLock
4578
+ }, slideItemObj.options.objectName)}</p:cNvPicPr>`;
4015
4579
  strSlideXml += " <p:nvPr>" + genXmlPlaceholder(placeholderObj) + "</p:nvPr>";
4016
4580
  strSlideXml += " </p:nvPicPr>";
4017
4581
  strSlideXml += "<p:blipFill>";
@@ -4044,7 +4608,7 @@ function slideObjectToXml(slide) {
4044
4608
  const relData = (slide._relsMedia || []).find((rel) => rel.rId === slideItemObj.imageRid)?.data;
4045
4609
  const natural = typeof relData === "string" ? getImageSizeFromBase64(relData) : null;
4046
4610
  if (natural) cropSize = natural;
4047
- else console.warn(`Warning: sizing '${sizing.type}' could not measure natural dimensions for image "${slideItemObj.options.objectName}"; falling back to displayed aspect ratio (crop may be inexact). Provide a raster image (PNG/JPEG/GIF/BMP/WebP) to enable an aspect-correct crop.`);
4611
+ else console.warn(`Warning: sizing '${sizing.type}' could not measure natural dimensions for image "${slideItemObj.options.objectName}"; falling back to displayed aspect ratio (crop may be inexact). Provide a raster image (PNG/JPEG/GIF/BMP/WebP) or an SVG with width/height or a viewBox to enable an aspect-correct crop.`);
4048
4612
  }
4049
4613
  strSlideXml += ImageSizingXml[sizing.type](cropSize, {
4050
4614
  w: boxW,
@@ -4063,6 +4627,16 @@ function slideObjectToXml(slide) {
4063
4627
  strSlideXml += " </a:xfrm>";
4064
4628
  if (slideItemObj.options.points) strSlideXml += " " + genXmlCustGeom(slideItemObj.options.points, imgWidth, imgHeight, slide._presLayout);
4065
4629
  else strSlideXml += " " + genXmlPresetGeom(slideItemObj.options.shape ?? (rounding ? "ellipse" : "rect"), slideItemObj.options, imgWidth, imgHeight);
4630
+ if (slideItemObj.options.line) {
4631
+ const imgLine = slideItemObj.options.line;
4632
+ const lnAttrs = (imgLine.width ? ` w="${lineWidthToEmu(imgLine.width)}"` : "") + (imgLine.cap ? ` cap="${createLineCap(imgLine.cap)}"` : "");
4633
+ strSlideXml += `<a:ln${lnAttrs}>`;
4634
+ if (imgLine.color) strSlideXml += genXmlColorSelection(imgLine);
4635
+ if (imgLine.dashType) strSlideXml += `<a:prstDash val="${imgLine.dashType}"/>`;
4636
+ if (imgLine.beginArrowType) strSlideXml += `<a:headEnd type="${imgLine.beginArrowType}"/>`;
4637
+ if (imgLine.endArrowType) strSlideXml += `<a:tailEnd type="${imgLine.endArrowType}"/>`;
4638
+ strSlideXml += "</a:ln>";
4639
+ }
4066
4640
  if (slideItemObj.options.shadow && slideItemObj.options.shadow.type !== "none") {
4067
4641
  const sh = slideItemObj.options.shadow;
4068
4642
  const shadowType = sh.type || "outer";
@@ -4086,7 +4660,7 @@ function slideObjectToXml(slide) {
4086
4660
  strSlideXml += "<p:pic>";
4087
4661
  strSlideXml += " <p:nvPicPr>";
4088
4662
  strSlideXml += `<p:cNvPr id="${slideItemObj.mediaRid + 2}" name="${slideItemObj.options.objectName}" descr="${encodeXmlEntities(slideItemObj.options.altText || "")}"/>`;
4089
- strSlideXml += " <p:cNvPicPr/>";
4663
+ strSlideXml += ` <p:cNvPicPr>${genXmlObjectLock("a:picLocks", PICTURE_LOCK_ATTRS, slideItemObj.options.objectLock, slideItemObj.options.objectName)}</p:cNvPicPr>`;
4090
4664
  strSlideXml += " <p:nvPr>";
4091
4665
  strSlideXml += ` <a:videoFile r:link="rId${slideItemObj.mediaRid}"/>`;
4092
4666
  strSlideXml += " </p:nvPr>";
@@ -4101,7 +4675,10 @@ function slideObjectToXml(slide) {
4101
4675
  strSlideXml += "<p:pic>";
4102
4676
  strSlideXml += " <p:nvPicPr>";
4103
4677
  strSlideXml += `<p:cNvPr id="${slideItemObj.mediaRid + 2}" name="${slideItemObj.options.objectName}" descr="${encodeXmlEntities(slideItemObj.options.altText || "")}"><a:hlinkClick r:id="" action="ppaction://media"/></p:cNvPr>`;
4104
- strSlideXml += " <p:cNvPicPr><a:picLocks noChangeAspect=\"1\"/></p:cNvPicPr>";
4678
+ strSlideXml += ` <p:cNvPicPr>${genXmlObjectLock("a:picLocks", PICTURE_LOCK_ATTRS, {
4679
+ noChangeAspect: true,
4680
+ ...slideItemObj.options.objectLock
4681
+ }, slideItemObj.options.objectName)}</p:cNvPicPr>`;
4105
4682
  strSlideXml += " <p:nvPr>";
4106
4683
  strSlideXml += ` <a:videoFile r:link="rId${slideItemObj.mediaRid}"/>`;
4107
4684
  strSlideXml += " <p:extLst>";
@@ -4165,7 +4742,7 @@ function slideObjectToXml(slide) {
4165
4742
  strSlideXml += "/>";
4166
4743
  strSlideXml += " <a:lstStyle><a:lvl1pPr>";
4167
4744
  if (slide._slideNumberProps.fontFace || slide._slideNumberProps.fontSize || slide._slideNumberProps.color) {
4168
- strSlideXml += `<a:defRPr sz="${Math.round((slide._slideNumberProps.fontSize || 12) * 100)}">`;
4745
+ strSlideXml += `<a:defRPr sz="${clampFontSizeSz(slide._slideNumberProps.fontSize || 12)}">`;
4169
4746
  if (slide._slideNumberProps.color) strSlideXml += genXmlColorSelection(slide._slideNumberProps.color);
4170
4747
  if (slide._slideNumberProps.fontFace) strSlideXml += `<a:latin typeface="${slide._slideNumberProps.fontFace}"/><a:ea typeface="${slide._slideNumberProps.fontFace}"/><a:cs typeface="${slide._slideNumberProps.fontFace}"/>`;
4171
4748
  strSlideXml += "</a:defRPr>";
@@ -4254,7 +4831,7 @@ function genXmlParagraphProperties(textObj, isDefault) {
4254
4831
  paragraphPropXml += "";
4255
4832
  break;
4256
4833
  }
4257
- if (textObj.options.lineSpacing) strXmlLnSpc = `<a:lnSpc><a:spcPts val="${Math.round(textObj.options.lineSpacing * 100)}"/></a:lnSpc>`;
4834
+ if (textObj.options.lineSpacing) strXmlLnSpc = `<a:lnSpc><a:spcPts val="${clampLineSpacingPts(textObj.options.lineSpacing)}"/></a:lnSpc>`;
4258
4835
  else if (textObj.options.lineSpacingMultiple) strXmlLnSpc = `<a:lnSpc><a:spcPct val="${Math.round(textObj.options.lineSpacingMultiple * 1e5)}"/></a:lnSpc>`;
4259
4836
  if (textObj.options.indentLevel && !isNaN(Number(textObj.options.indentLevel)) && textObj.options.indentLevel > 0) paragraphPropXml += ` lvl="${textObj.options.indentLevel}"`;
4260
4837
  if (textObj.options.paraSpaceBefore && !isNaN(Number(textObj.options.paraSpaceBefore)) && textObj.options.paraSpaceBefore > 0) strXmlParaSpc += `<a:spcBef><a:spcPts val="${Math.round(textObj.options.paraSpaceBefore * 100)}"/></a:spcBef>`;
@@ -4262,9 +4839,17 @@ function genXmlParagraphProperties(textObj, isDefault) {
4262
4839
  if (typeof textObj.options.bullet === "object") {
4263
4840
  if (textObj?.options?.bullet?.indent) bulletMarL = valToPts(textObj.options.bullet.indent);
4264
4841
  if (textObj.options.bullet.color) strXmlBulletColor = `<a:buClr>${createColorElement(textObj.options.bullet.color)}</a:buClr>`;
4842
+ let bulletSizePct = 1e5;
4843
+ if (textObj.options.bullet.size !== void 0) {
4844
+ const bulletSize = Number(textObj.options.bullet.size);
4845
+ if (isNaN(bulletSize) || bulletSize < 25 || bulletSize > 400) console.warn("Warning: `bullet.size` must be a percentage between 25 and 400!");
4846
+ else bulletSizePct = Math.round(bulletSize * 1e3);
4847
+ }
4848
+ const strXmlBulletSize = `<a:buSzPct val="${bulletSizePct}"/>`;
4849
+ const strXmlBulletFont = textObj.options.bullet.fontFace ? `<a:buFont typeface="${encodeXmlEntities(textObj.options.bullet.fontFace)}"/>` : "";
4265
4850
  if (textObj.options.bullet.type && textObj.options.bullet.type.toString().toLowerCase() === "number") {
4266
4851
  paragraphPropXml += ` marL="${textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL}" indent="-${bulletMarL}"`;
4267
- strXmlBullet = `<a:buSzPct val="100000"/><a:buFont typeface="+mj-lt"/><a:buAutoNum type="${textObj.options.bullet.style || "arabicPeriod"}" startAt="${textObj.options.bullet.numberStartAt || textObj.options.bullet.startAt || "1"}"/>`;
4852
+ strXmlBullet = `${strXmlBulletSize}${strXmlBulletFont || "<a:buFont typeface=\"+mj-lt\"/>"}<a:buAutoNum type="${textObj.options.bullet.style || "arabicPeriod"}" startAt="${textObj.options.bullet.numberStartAt || textObj.options.bullet.startAt || "1"}"/>`;
4268
4853
  } else if (textObj.options.bullet.characterCode) {
4269
4854
  let bulletCode = `&#x${textObj.options.bullet.characterCode};`;
4270
4855
  if (!/^[0-9A-Fa-f]{4}$/.test(textObj.options.bullet.characterCode)) {
@@ -4272,7 +4857,7 @@ function genXmlParagraphProperties(textObj, isDefault) {
4272
4857
  bulletCode = "&#x2022;";
4273
4858
  }
4274
4859
  paragraphPropXml += ` marL="${textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL}" indent="-${bulletMarL}"`;
4275
- strXmlBullet = "<a:buSzPct val=\"100000\"/><a:buChar char=\"" + bulletCode + "\"/>";
4860
+ strXmlBullet = strXmlBulletSize + strXmlBulletFont + "<a:buChar char=\"" + bulletCode + "\"/>";
4276
4861
  } else if (textObj.options.bullet.code) {
4277
4862
  let bulletCode = `&#x${textObj.options.bullet.code};`;
4278
4863
  if (!/^[0-9A-Fa-f]{4}$/.test(textObj.options.bullet.code)) {
@@ -4280,10 +4865,10 @@ function genXmlParagraphProperties(textObj, isDefault) {
4280
4865
  bulletCode = "&#x2022;";
4281
4866
  }
4282
4867
  paragraphPropXml += ` marL="${textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL}" indent="-${bulletMarL}"`;
4283
- strXmlBullet = "<a:buSzPct val=\"100000\"/><a:buChar char=\"" + bulletCode + "\"/>";
4868
+ strXmlBullet = strXmlBulletSize + strXmlBulletFont + "<a:buChar char=\"" + bulletCode + "\"/>";
4284
4869
  } else {
4285
4870
  paragraphPropXml += ` marL="${textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL}" indent="-${bulletMarL}"`;
4286
- strXmlBullet = `<a:buSzPct val="100000"/><a:buChar char="&#x2022;"/>`;
4871
+ strXmlBullet = `${strXmlBulletSize}${strXmlBulletFont}<a:buChar char="&#x2022;"/>`;
4287
4872
  }
4288
4873
  } else if (textObj.options.bullet) {
4289
4874
  paragraphPropXml += ` marL="${textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL}" indent="-${bulletMarL}"`;
@@ -4308,7 +4893,7 @@ function genXmlTextRunProperties(opts, isDefault) {
4308
4893
  let runProps = "";
4309
4894
  const runPropsTag = isDefault ? "a:defRPr" : "a:rPr";
4310
4895
  runProps += "<" + runPropsTag + " lang=\"" + (opts.lang ? opts.lang : "en-US") + "\"" + (opts.lang ? " altLang=\"en-US\"" : "");
4311
- runProps += opts.fontSize ? ` sz="${Math.round(opts.fontSize * 100)}"` : "";
4896
+ runProps += opts.fontSize ? ` sz="${clampFontSizeSz(opts.fontSize)}"` : "";
4312
4897
  runProps += opts?.bold ? ` b="${opts.bold ? "1" : "0"}"` : "";
4313
4898
  runProps += opts?.italic ? ` i="${opts.italic ? "1" : "0"}"` : "";
4314
4899
  runProps += opts?.strike ? ` strike="${typeof opts.strike === "string" ? opts.strike : "sngStrike"}"` : "";
@@ -4319,17 +4904,23 @@ function genXmlTextRunProperties(opts, isDefault) {
4319
4904
  if (opts.baseline) runProps += ` baseline="${Math.round(opts.baseline * 50)}"`;
4320
4905
  else if (opts.subscript) runProps += " baseline=\"-40000\"";
4321
4906
  else if (opts.superscript) runProps += " baseline=\"30000\"";
4322
- runProps += opts.charSpacing ? ` spc="${Math.round(opts.charSpacing * 100)}" kern="0"` : "";
4907
+ runProps += opts.charSpacing ? ` spc="${clampCharSpacingSpc(opts.charSpacing)}" kern="0"` : "";
4323
4908
  runProps += " dirty=\"0\">";
4324
- if (opts.color || opts.fontFace || opts.outline || typeof opts.underline === "object" && opts.underline.color) {
4325
- if (opts.outline && typeof opts.outline === "object") runProps += `<a:ln w="${valToPts(opts.outline.size || .75)}">${genXmlColorSelection(opts.outline.color || "FFFFFF")}</a:ln>`;
4909
+ const hasShadow = !!opts.shadow && opts.shadow.type !== "none";
4910
+ if (opts.color || opts.fontFace || opts.outline || opts.glow || hasShadow || typeof opts.underline === "object" && opts.underline.color) {
4911
+ if (opts.outline && typeof opts.outline === "object") runProps += `<a:ln w="${lineWidthToEmu(opts.outline.size || .75)}">${genXmlColorSelection(opts.outline.color || "FFFFFF")}</a:ln>`;
4326
4912
  if (opts.color) runProps += genXmlColorSelection({
4327
4913
  color: opts.color,
4328
4914
  transparency: opts.transparency
4329
4915
  });
4916
+ if (opts.glow || hasShadow) {
4917
+ runProps += "<a:effectLst>";
4918
+ if (opts.glow) runProps += createGlowElement(opts.glow, DEF_TEXT_GLOW);
4919
+ if (hasShadow) runProps += createShadowElement$1(opts.shadow, DEF_TEXT_SHADOW);
4920
+ runProps += "</a:effectLst>";
4921
+ }
4330
4922
  if (opts.highlight) runProps += `<a:highlight>${createColorElement(opts.highlight)}</a:highlight>`;
4331
4923
  if (typeof opts.underline === "object" && opts.underline.color) runProps += `<a:uFill>${genXmlColorSelection(opts.underline.color)}</a:uFill>`;
4332
- if (opts.glow) runProps += `<a:effectLst>${createGlowElement(opts.glow, DEF_TEXT_GLOW)}</a:effectLst>`;
4333
4924
  if (opts.fontFace) runProps += `<a:latin typeface="${opts.fontFace}" pitchFamily="34" charset="0"/><a:ea typeface="${opts.fontFace}" pitchFamily="34" charset="-122"/><a:cs typeface="${opts.fontFace}" pitchFamily="34" charset="-120"/>`;
4334
4925
  }
4335
4926
  if (opts.hyperlink) {
@@ -4359,6 +4950,28 @@ function genXmlTextRun(textObj) {
4359
4950
  return `<a:r>${genXmlTextRunProperties(textObj.options, false)}<a:t>${encodeXmlEntities(String(textObj.text))}</a:t></a:r>`;
4360
4951
  }
4361
4952
  /**
4953
+ * Builds `<a:normAutofit>` with explicit fontScale/lnSpcReduction for "shrink text on overflow"
4954
+ * @param {TextFitShrinkProps} fit - shrink fit options
4955
+ * @return {string} XML string (`<a:normAutofit .../>`)
4956
+ * @see ECMA-376 CT_TextNormAutofit (attributes in 1000ths of a percent)
4957
+ */
4958
+ function genXmlNormAutofit(fit) {
4959
+ let attrs = "";
4960
+ const pct = (val, name) => {
4961
+ if (val === void 0 || val === null) return null;
4962
+ if (typeof val !== "number" || isNaN(val) || val < 0 || val > 100) {
4963
+ console.warn(`Warning: fit.${name} must be a number between 0 and 100 (percent); received ${String(val)} - attribute ignored.`);
4964
+ return null;
4965
+ }
4966
+ return Math.round(val * 1e3);
4967
+ };
4968
+ const fontScale = pct(fit.fontScale, "fontScale");
4969
+ if (fontScale !== null) attrs += ` fontScale="${fontScale}"`;
4970
+ const lnSpcReduction = pct(fit.lnSpcReduction, "lnSpcReduction");
4971
+ if (lnSpcReduction !== null) attrs += ` lnSpcReduction="${lnSpcReduction}"`;
4972
+ return `<a:normAutofit${attrs}/>`;
4973
+ }
4974
+ /**
4362
4975
  * Builds `<a:bodyPr></a:bodyPr>` tag for "genXmlTextBody()"
4363
4976
  * @param {ISlideObject | TableCell} slideObject - various options
4364
4977
  * @return {string} XML string
@@ -4371,6 +4984,8 @@ function genXmlBodyProperties(slideObject) {
4371
4984
  if (slideObject.options._bodyProp.tIns || slideObject.options._bodyProp.tIns === 0) bodyProperties += ` tIns="${slideObject.options._bodyProp.tIns}"`;
4372
4985
  if (slideObject.options._bodyProp.rIns || slideObject.options._bodyProp.rIns === 0) bodyProperties += ` rIns="${slideObject.options._bodyProp.rIns}"`;
4373
4986
  if (slideObject.options._bodyProp.bIns || slideObject.options._bodyProp.bIns === 0) bodyProperties += ` bIns="${slideObject.options._bodyProp.bIns}"`;
4987
+ if (slideObject.options._bodyProp.numCol) bodyProperties += ` numCol="${slideObject.options._bodyProp.numCol}"`;
4988
+ if (slideObject.options._bodyProp.spcCol) bodyProperties += ` spcCol="${slideObject.options._bodyProp.spcCol}"`;
4374
4989
  bodyProperties += " rtlCol=\"0\"";
4375
4990
  if (slideObject.options._bodyProp.anchor) bodyProperties += " anchor=\"" + slideObject.options._bodyProp.anchor + "\"";
4376
4991
  if (slideObject.options._bodyProp.vert) bodyProperties += " vert=\"" + slideObject.options._bodyProp.vert + "\"";
@@ -4381,9 +4996,11 @@ function genXmlBodyProperties(slideObject) {
4381
4996
  * @see: http://www.datypic.com/sc/ooxml/g-a_EG_TextAutofit.html
4382
4997
  */
4383
4998
  if (slideObject.options.fit) {
4384
- if (slideObject.options.fit === "none") bodyProperties += "";
4385
- else if (slideObject.options.fit === "shrink") bodyProperties += "<a:normAutofit/>";
4386
- else if (slideObject.options.fit === "resize") bodyProperties += "<a:spAutoFit/>";
4999
+ const fit = slideObject.options.fit;
5000
+ if (fit === "none") bodyProperties += "";
5001
+ else if (fit === "shrink") bodyProperties += "<a:normAutofit/>";
5002
+ else if (fit === "resize") bodyProperties += "<a:spAutoFit/>";
5003
+ else if (typeof fit === "object" && fit.type === "shrink") bodyProperties += genXmlNormAutofit(fit);
4387
5004
  }
4388
5005
  if (slideObject.options.shrinkText) bodyProperties += "<a:normAutofit/>";
4389
5006
  bodyProperties += slideObject.options._bodyProp.autoFit ? "<a:spAutoFit/>" : "";
@@ -4519,13 +5136,13 @@ function genXmlTextBody(slideObj) {
4519
5136
  }
4520
5137
  });
4521
5138
  if (slideObj._type === "tablecell" && (opts.fontSize || opts.fontFace)) if (opts.fontFace) {
4522
- strSlideXml += `<a:endParaRPr lang="${opts.lang || "en-US"}"` + (opts.fontSize ? ` sz="${Math.round(opts.fontSize * 100)}"` : "") + " dirty=\"0\">";
5139
+ strSlideXml += `<a:endParaRPr lang="${opts.lang || "en-US"}"` + (opts.fontSize ? ` sz="${clampFontSizeSz(opts.fontSize)}"` : "") + " dirty=\"0\">";
4523
5140
  strSlideXml += `<a:latin typeface="${opts.fontFace}" charset="0"/>`;
4524
5141
  strSlideXml += `<a:ea typeface="${opts.fontFace}" charset="0"/>`;
4525
5142
  strSlideXml += `<a:cs typeface="${opts.fontFace}" charset="0"/>`;
4526
5143
  strSlideXml += "</a:endParaRPr>";
4527
- } else strSlideXml += `<a:endParaRPr lang="${opts.lang || "en-US"}"` + (opts.fontSize ? ` sz="${Math.round(opts.fontSize * 100)}"` : "") + " dirty=\"0\"/>";
4528
- else if (reqsClosingFontSize) strSlideXml += `<a:endParaRPr lang="${opts.lang || "en-US"}"` + (opts.fontSize ? ` sz="${Math.round(opts.fontSize * 100)}"` : "") + " dirty=\"0\"/>";
5144
+ } else strSlideXml += `<a:endParaRPr lang="${opts.lang || "en-US"}"` + (opts.fontSize ? ` sz="${clampFontSizeSz(opts.fontSize)}"` : "") + " dirty=\"0\"/>";
5145
+ else if (reqsClosingFontSize) strSlideXml += `<a:endParaRPr lang="${opts.lang || "en-US"}"` + (opts.fontSize ? ` sz="${clampFontSizeSz(opts.fontSize)}"` : "") + " dirty=\"0\"/>";
4529
5146
  else strSlideXml += `<a:endParaRPr lang="${opts.lang || "en-US"}" dirty="0"/>`;
4530
5147
  strSlideXml += "</a:p>";
4531
5148
  });
@@ -4866,11 +5483,51 @@ function getLayoutIdxForSlide(slides, slideLayouts, slideNumber) {
4866
5483
  return 1;
4867
5484
  }
4868
5485
  /**
5486
+ * Theme `<a:clrScheme>` slots in OOXML document order, with their default Office color child.
5487
+ * `dk1`/`lt1` default to `sysClr` (windowText/window); the rest are `srgbClr`. A user override
5488
+ * for any slot is emitted as `<a:srgbClr>` (see `buildThemeClrScheme`).
5489
+ */
5490
+ const THEME_CLR_SCHEME_DEFAULTS = [
5491
+ ["dk1", "<a:sysClr val=\"windowText\" lastClr=\"000000\"/>"],
5492
+ ["lt1", "<a:sysClr val=\"window\" lastClr=\"FFFFFF\"/>"],
5493
+ ["dk2", "<a:srgbClr val=\"44546A\"/>"],
5494
+ ["lt2", "<a:srgbClr val=\"E7E6E6\"/>"],
5495
+ ["accent1", "<a:srgbClr val=\"4472C4\"/>"],
5496
+ ["accent2", "<a:srgbClr val=\"ED7D31\"/>"],
5497
+ ["accent3", "<a:srgbClr val=\"A5A5A5\"/>"],
5498
+ ["accent4", "<a:srgbClr val=\"FFC000\"/>"],
5499
+ ["accent5", "<a:srgbClr val=\"5B9BD5\"/>"],
5500
+ ["accent6", "<a:srgbClr val=\"70AD47\"/>"],
5501
+ ["hlink", "<a:srgbClr val=\"0563C1\"/>"],
5502
+ ["folHlink", "<a:srgbClr val=\"954F72\"/>"]
5503
+ ];
5504
+ /**
5505
+ * Build the theme `<a:clrScheme>` block, applying any caller-supplied color overrides over the
5506
+ * default Office scheme. Invalid (non 6-digit-hex) overrides warn and keep the default rather
5507
+ * than emitting a degenerate color.
5508
+ * @param {ThemeColorScheme} [scheme] - per-slot hex overrides
5509
+ * @return {string} the `<a:clrScheme>...</a:clrScheme>` XML
5510
+ */
5511
+ function buildThemeClrScheme(scheme) {
5512
+ return `<a:clrScheme name="Office">${THEME_CLR_SCHEME_DEFAULTS.map(([slot, defaultChild]) => {
5513
+ const override = scheme?.[slot];
5514
+ let child = defaultChild;
5515
+ if (typeof override === "string" && override.length > 0) {
5516
+ const hex = override.replace("#", "");
5517
+ if (REGEX_HEX_COLOR.test(hex)) child = `<a:srgbClr val="${hex.toUpperCase()}"/>`;
5518
+ else console.warn(`makeXmlTheme: colorScheme.${slot} "${override}" is not a 6-digit hex color; keeping the Office default.`);
5519
+ }
5520
+ return `<a:${slot}>${child}</a:${slot}>`;
5521
+ }).join("")}</a:clrScheme>`;
5522
+ }
5523
+ /**
4869
5524
  * Creates `ppt/theme/theme1.xml`
4870
5525
  * @return {string} XML
4871
5526
  */
4872
5527
  function makeXmlTheme(pres) {
4873
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme"><a:themeElements><a:clrScheme name="Office"><a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1><a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="44546A"/></a:dk2><a:lt2><a:srgbClr val="E7E6E6"/></a:lt2><a:accent1><a:srgbClr val="4472C4"/></a:accent1><a:accent2><a:srgbClr val="ED7D31"/></a:accent2><a:accent3><a:srgbClr val="A5A5A5"/></a:accent3><a:accent4><a:srgbClr val="FFC000"/></a:accent4><a:accent5><a:srgbClr val="5B9BD5"/></a:accent5><a:accent6><a:srgbClr val="70AD47"/></a:accent6><a:hlink><a:srgbClr val="0563C1"/></a:hlink><a:folHlink><a:srgbClr val="954F72"/></a:folHlink></a:clrScheme><a:fontScheme name="Office"><a:majorFont>${pres.theme?.headFontFace ? `<a:latin typeface="${pres.theme?.headFontFace}"/>` : "<a:latin typeface=\"Calibri Light\" panose=\"020F0302020204030204\"/>"}<a:ea typeface=""/><a:cs typeface=""/><a:font script="Jpan" typeface="游ゴシック Light"/><a:font script="Hang" typeface="맑은 고딕"/><a:font script="Hans" typeface="等线 Light"/><a:font script="Hant" typeface="新細明體"/><a:font script="Arab" typeface="Times New Roman"/><a:font script="Hebr" typeface="Times New Roman"/><a:font script="Thai" typeface="Angsana New"/><a:font script="Ethi" typeface="Nyala"/><a:font script="Beng" typeface="Vrinda"/><a:font script="Gujr" typeface="Shruti"/><a:font script="Khmr" typeface="MoolBoran"/><a:font script="Knda" typeface="Tunga"/><a:font script="Guru" typeface="Raavi"/><a:font script="Cans" typeface="Euphemia"/><a:font script="Cher" typeface="Plantagenet Cherokee"/><a:font script="Yiii" typeface="Microsoft Yi Baiti"/><a:font script="Tibt" typeface="Microsoft Himalaya"/><a:font script="Thaa" typeface="MV Boli"/><a:font script="Deva" typeface="Mangal"/><a:font script="Telu" typeface="Gautami"/><a:font script="Taml" typeface="Latha"/><a:font script="Syrc" typeface="Estrangelo Edessa"/><a:font script="Orya" typeface="Kalinga"/><a:font script="Mlym" typeface="Kartika"/><a:font script="Laoo" typeface="DokChampa"/><a:font script="Sinh" typeface="Iskoola Pota"/><a:font script="Mong" typeface="Mongolian Baiti"/><a:font script="Viet" typeface="Times New Roman"/><a:font script="Uigh" typeface="Microsoft Uighur"/><a:font script="Geor" typeface="Sylfaen"/><a:font script="Armn" typeface="Arial"/><a:font script="Bugi" typeface="Leelawadee UI"/><a:font script="Bopo" typeface="Microsoft JhengHei"/><a:font script="Java" typeface="Javanese Text"/><a:font script="Lisu" typeface="Segoe UI"/><a:font script="Mymr" typeface="Myanmar Text"/><a:font script="Nkoo" typeface="Ebrima"/><a:font script="Olck" typeface="Nirmala UI"/><a:font script="Osma" typeface="Ebrima"/><a:font script="Phag" typeface="Phagspa"/><a:font script="Syrn" typeface="Estrangelo Edessa"/><a:font script="Syrj" typeface="Estrangelo Edessa"/><a:font script="Syre" typeface="Estrangelo Edessa"/><a:font script="Sora" typeface="Nirmala UI"/><a:font script="Tale" typeface="Microsoft Tai Le"/><a:font script="Talu" typeface="Microsoft New Tai Lue"/><a:font script="Tfng" typeface="Ebrima"/></a:majorFont><a:minorFont>${pres.theme?.bodyFontFace ? `<a:latin typeface="${pres.theme?.bodyFontFace}"/>` : "<a:latin typeface=\"Calibri\" panose=\"020F0502020204030204\"/>"}<a:ea typeface=""/><a:cs typeface=""/><a:font script="Jpan" typeface="游ゴシック"/><a:font script="Hang" typeface="맑은 고딕"/><a:font script="Hans" typeface="等线"/><a:font script="Hant" typeface="新細明體"/><a:font script="Arab" typeface="Arial"/><a:font script="Hebr" typeface="Arial"/><a:font script="Thai" typeface="Cordia New"/><a:font script="Ethi" typeface="Nyala"/><a:font script="Beng" typeface="Vrinda"/><a:font script="Gujr" typeface="Shruti"/><a:font script="Khmr" typeface="DaunPenh"/><a:font script="Knda" typeface="Tunga"/><a:font script="Guru" typeface="Raavi"/><a:font script="Cans" typeface="Euphemia"/><a:font script="Cher" typeface="Plantagenet Cherokee"/><a:font script="Yiii" typeface="Microsoft Yi Baiti"/><a:font script="Tibt" typeface="Microsoft Himalaya"/><a:font script="Thaa" typeface="MV Boli"/><a:font script="Deva" typeface="Mangal"/><a:font script="Telu" typeface="Gautami"/><a:font script="Taml" typeface="Latha"/><a:font script="Syrc" typeface="Estrangelo Edessa"/><a:font script="Orya" typeface="Kalinga"/><a:font script="Mlym" typeface="Kartika"/><a:font script="Laoo" typeface="DokChampa"/><a:font script="Sinh" typeface="Iskoola Pota"/><a:font script="Mong" typeface="Mongolian Baiti"/><a:font script="Viet" typeface="Arial"/><a:font script="Uigh" typeface="Microsoft Uighur"/><a:font script="Geor" typeface="Sylfaen"/><a:font script="Armn" typeface="Arial"/><a:font script="Bugi" typeface="Leelawadee UI"/><a:font script="Bopo" typeface="Microsoft JhengHei"/><a:font script="Java" typeface="Javanese Text"/><a:font script="Lisu" typeface="Segoe UI"/><a:font script="Mymr" typeface="Myanmar Text"/><a:font script="Nkoo" typeface="Ebrima"/><a:font script="Olck" typeface="Nirmala UI"/><a:font script="Osma" typeface="Ebrima"/><a:font script="Phag" typeface="Phagspa"/><a:font script="Syrn" typeface="Estrangelo Edessa"/><a:font script="Syrj" typeface="Estrangelo Edessa"/><a:font script="Syre" typeface="Estrangelo Edessa"/><a:font script="Sora" typeface="Nirmala UI"/><a:font script="Tale" typeface="Microsoft Tai Le"/><a:font script="Talu" typeface="Microsoft New Tai Lue"/><a:font script="Tfng" typeface="Ebrima"/></a:minorFont></a:fontScheme><a:fmtScheme name="Office"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:lumMod val="110000"/><a:satMod val="105000"/><a:tint val="67000"/></a:schemeClr></a:gs><a:gs pos="50000"><a:schemeClr val="phClr"><a:lumMod val="105000"/><a:satMod val="103000"/><a:tint val="73000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:lumMod val="105000"/><a:satMod val="109000"/><a:tint val="81000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:satMod val="103000"/><a:lumMod val="102000"/><a:tint val="94000"/></a:schemeClr></a:gs><a:gs pos="50000"><a:schemeClr val="phClr"><a:satMod val="110000"/><a:lumMod val="100000"/><a:shade val="100000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:lumMod val="99000"/><a:satMod val="120000"/><a:shade val="78000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill></a:fillStyleLst><a:lnStyleLst><a:ln w="6350" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln><a:ln w="12700" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln><a:ln w="19050" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst><a:outerShdw blurRad="57150" dist="19050" dir="5400000" algn="ctr" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="63000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"><a:tint val="95000"/><a:satMod val="170000"/></a:schemeClr></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="93000"/><a:satMod val="150000"/><a:shade val="98000"/><a:lumMod val="102000"/></a:schemeClr></a:gs><a:gs pos="50000"><a:schemeClr val="phClr"><a:tint val="98000"/><a:satMod val="130000"/><a:shade val="90000"/><a:lumMod val="103000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="63000"/><a:satMod val="120000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements><a:objectDefaults/><a:extraClrSchemeLst/><a:extLst><a:ext uri="{05A4C25C-085E-4340-85A3-A5531E510DB2}"><thm15:themeFamily xmlns:thm15="http://schemas.microsoft.com/office/thememl/2012/main" name="Office Theme" id="{62F939B6-93AF-4DB8-9C6B-D6C7DFDC589F}" vid="{4A3C46E8-61CC-4603-A589-7422A47A8E4A}"/></a:ext></a:extLst></a:theme>`;
5528
+ const majorFont = pres.theme?.headFontFace ? `<a:latin typeface="${pres.theme?.headFontFace}"/>` : "<a:latin typeface=\"Calibri Light\" panose=\"020F0302020204030204\"/>";
5529
+ const minorFont = pres.theme?.bodyFontFace ? `<a:latin typeface="${pres.theme?.bodyFontFace}"/>` : "<a:latin typeface=\"Calibri\" panose=\"020F0502020204030204\"/>";
5530
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme"><a:themeElements>${buildThemeClrScheme(pres.theme?.colorScheme)}<a:fontScheme name="Office"><a:majorFont>${majorFont}<a:ea typeface=""/><a:cs typeface=""/><a:font script="Jpan" typeface="游ゴシック Light"/><a:font script="Hang" typeface="맑은 고딕"/><a:font script="Hans" typeface="等线 Light"/><a:font script="Hant" typeface="新細明體"/><a:font script="Arab" typeface="Times New Roman"/><a:font script="Hebr" typeface="Times New Roman"/><a:font script="Thai" typeface="Angsana New"/><a:font script="Ethi" typeface="Nyala"/><a:font script="Beng" typeface="Vrinda"/><a:font script="Gujr" typeface="Shruti"/><a:font script="Khmr" typeface="MoolBoran"/><a:font script="Knda" typeface="Tunga"/><a:font script="Guru" typeface="Raavi"/><a:font script="Cans" typeface="Euphemia"/><a:font script="Cher" typeface="Plantagenet Cherokee"/><a:font script="Yiii" typeface="Microsoft Yi Baiti"/><a:font script="Tibt" typeface="Microsoft Himalaya"/><a:font script="Thaa" typeface="MV Boli"/><a:font script="Deva" typeface="Mangal"/><a:font script="Telu" typeface="Gautami"/><a:font script="Taml" typeface="Latha"/><a:font script="Syrc" typeface="Estrangelo Edessa"/><a:font script="Orya" typeface="Kalinga"/><a:font script="Mlym" typeface="Kartika"/><a:font script="Laoo" typeface="DokChampa"/><a:font script="Sinh" typeface="Iskoola Pota"/><a:font script="Mong" typeface="Mongolian Baiti"/><a:font script="Viet" typeface="Times New Roman"/><a:font script="Uigh" typeface="Microsoft Uighur"/><a:font script="Geor" typeface="Sylfaen"/><a:font script="Armn" typeface="Arial"/><a:font script="Bugi" typeface="Leelawadee UI"/><a:font script="Bopo" typeface="Microsoft JhengHei"/><a:font script="Java" typeface="Javanese Text"/><a:font script="Lisu" typeface="Segoe UI"/><a:font script="Mymr" typeface="Myanmar Text"/><a:font script="Nkoo" typeface="Ebrima"/><a:font script="Olck" typeface="Nirmala UI"/><a:font script="Osma" typeface="Ebrima"/><a:font script="Phag" typeface="Phagspa"/><a:font script="Syrn" typeface="Estrangelo Edessa"/><a:font script="Syrj" typeface="Estrangelo Edessa"/><a:font script="Syre" typeface="Estrangelo Edessa"/><a:font script="Sora" typeface="Nirmala UI"/><a:font script="Tale" typeface="Microsoft Tai Le"/><a:font script="Talu" typeface="Microsoft New Tai Lue"/><a:font script="Tfng" typeface="Ebrima"/></a:majorFont><a:minorFont>${minorFont}<a:ea typeface=""/><a:cs typeface=""/><a:font script="Jpan" typeface="游ゴシック"/><a:font script="Hang" typeface="맑은 고딕"/><a:font script="Hans" typeface="等线"/><a:font script="Hant" typeface="新細明體"/><a:font script="Arab" typeface="Arial"/><a:font script="Hebr" typeface="Arial"/><a:font script="Thai" typeface="Cordia New"/><a:font script="Ethi" typeface="Nyala"/><a:font script="Beng" typeface="Vrinda"/><a:font script="Gujr" typeface="Shruti"/><a:font script="Khmr" typeface="DaunPenh"/><a:font script="Knda" typeface="Tunga"/><a:font script="Guru" typeface="Raavi"/><a:font script="Cans" typeface="Euphemia"/><a:font script="Cher" typeface="Plantagenet Cherokee"/><a:font script="Yiii" typeface="Microsoft Yi Baiti"/><a:font script="Tibt" typeface="Microsoft Himalaya"/><a:font script="Thaa" typeface="MV Boli"/><a:font script="Deva" typeface="Mangal"/><a:font script="Telu" typeface="Gautami"/><a:font script="Taml" typeface="Latha"/><a:font script="Syrc" typeface="Estrangelo Edessa"/><a:font script="Orya" typeface="Kalinga"/><a:font script="Mlym" typeface="Kartika"/><a:font script="Laoo" typeface="DokChampa"/><a:font script="Sinh" typeface="Iskoola Pota"/><a:font script="Mong" typeface="Mongolian Baiti"/><a:font script="Viet" typeface="Arial"/><a:font script="Uigh" typeface="Microsoft Uighur"/><a:font script="Geor" typeface="Sylfaen"/><a:font script="Armn" typeface="Arial"/><a:font script="Bugi" typeface="Leelawadee UI"/><a:font script="Bopo" typeface="Microsoft JhengHei"/><a:font script="Java" typeface="Javanese Text"/><a:font script="Lisu" typeface="Segoe UI"/><a:font script="Mymr" typeface="Myanmar Text"/><a:font script="Nkoo" typeface="Ebrima"/><a:font script="Olck" typeface="Nirmala UI"/><a:font script="Osma" typeface="Ebrima"/><a:font script="Phag" typeface="Phagspa"/><a:font script="Syrn" typeface="Estrangelo Edessa"/><a:font script="Syrj" typeface="Estrangelo Edessa"/><a:font script="Syre" typeface="Estrangelo Edessa"/><a:font script="Sora" typeface="Nirmala UI"/><a:font script="Tale" typeface="Microsoft Tai Le"/><a:font script="Talu" typeface="Microsoft New Tai Lue"/><a:font script="Tfng" typeface="Ebrima"/></a:minorFont></a:fontScheme><a:fmtScheme name="Office"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:lumMod val="110000"/><a:satMod val="105000"/><a:tint val="67000"/></a:schemeClr></a:gs><a:gs pos="50000"><a:schemeClr val="phClr"><a:lumMod val="105000"/><a:satMod val="103000"/><a:tint val="73000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:lumMod val="105000"/><a:satMod val="109000"/><a:tint val="81000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:satMod val="103000"/><a:lumMod val="102000"/><a:tint val="94000"/></a:schemeClr></a:gs><a:gs pos="50000"><a:schemeClr val="phClr"><a:satMod val="110000"/><a:lumMod val="100000"/><a:shade val="100000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:lumMod val="99000"/><a:satMod val="120000"/><a:shade val="78000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill></a:fillStyleLst><a:lnStyleLst><a:ln w="6350" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln><a:ln w="12700" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln><a:ln w="19050" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst><a:outerShdw blurRad="57150" dist="19050" dir="5400000" algn="ctr" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="63000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"><a:tint val="95000"/><a:satMod val="170000"/></a:schemeClr></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="93000"/><a:satMod val="150000"/><a:shade val="98000"/><a:lumMod val="102000"/></a:schemeClr></a:gs><a:gs pos="50000"><a:schemeClr val="phClr"><a:tint val="98000"/><a:satMod val="130000"/><a:shade val="90000"/><a:lumMod val="103000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="63000"/><a:satMod val="120000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements><a:objectDefaults/><a:extraClrSchemeLst/><a:extLst><a:ext uri="{05A4C25C-085E-4340-85A3-A5531E510DB2}"><thm15:themeFamily xmlns:thm15="http://schemas.microsoft.com/office/thememl/2012/main" name="Office Theme" id="{62F939B6-93AF-4DB8-9C6B-D6C7DFDC589F}" vid="{4A3C46E8-61CC-4603-A589-7422A47A8E4A}"/></a:ext></a:extLst></a:theme>`;
4874
5531
  }
4875
5532
  /**
4876
5533
  * Create presentation file (`ppt/presentation.xml`)
@@ -5001,7 +5658,7 @@ function genXmlTableStyleBorders(border) {
5001
5658
  xml += `<a:${side}>`;
5002
5659
  if (b.type === "none") xml += "<a:ln><a:noFill/></a:ln>";
5003
5660
  else {
5004
- xml += `<a:ln w="${valToPts(b.pt ?? 1)}" cap="flat" cmpd="sng" algn="ctr">`;
5661
+ xml += `<a:ln w="${lineWidthToEmu(b.pt ?? 1)}" cap="flat" cmpd="sng" algn="ctr">`;
5005
5662
  xml += `<a:solidFill>${createColorElement(b.color ?? "666666")}</a:solidFill>`;
5006
5663
  xml += `<a:prstDash val="${b.type === "dash" ? "sysDash" : "solid"}"/>`;
5007
5664
  xml += "</a:ln>";
@@ -5079,7 +5736,7 @@ function makeXmlViewProps() {
5079
5736
  * @see https://docs.microsoft.com/en-us/office/open-xml/structure-of-a-presentationml-document
5080
5737
  * @see https://docs.microsoft.com/en-us/previous-versions/office/developer/office-2010/hh273476(v=office.14)
5081
5738
  */
5082
- const VERSION = "5.2.0";
5739
+ const VERSION = "5.4.0";
5083
5740
  function standardLayoutToPresLayout(layout) {
5084
5741
  return {
5085
5742
  name: layout.name,
@@ -5331,6 +5988,7 @@ var PptxGenJS = class {
5331
5988
  this._tableStyles = [];
5332
5989
  this._masterSlide = {
5333
5990
  addChart: null,
5991
+ addConnector: null,
5334
5992
  addImage: null,
5335
5993
  addMedia: null,
5336
5994
  addNotes: null,
@@ -5403,14 +6061,27 @@ var PptxGenJS = class {
5403
6061
  const arrChartPromises = [];
5404
6062
  let arrMediaPromises = [];
5405
6063
  const zip = new JSZip();
6064
+ const onMediaError = props.onMediaError ?? "throw";
5406
6065
  this._slides.forEach((slide) => {
5407
- arrMediaPromises = arrMediaPromises.concat(encodeSlideMediaRels(slide, this._runtime));
6066
+ arrMediaPromises = arrMediaPromises.concat(encodeSlideMediaRels(slide, this._runtime, onMediaError));
5408
6067
  });
5409
6068
  this._slideLayouts.forEach((layout) => {
5410
- arrMediaPromises = arrMediaPromises.concat(encodeSlideMediaRels(layout, this._runtime));
6069
+ arrMediaPromises = arrMediaPromises.concat(encodeSlideMediaRels(layout, this._runtime, onMediaError));
5411
6070
  });
5412
- arrMediaPromises = arrMediaPromises.concat(encodeSlideMediaRels(this._masterSlide, this._runtime));
6071
+ arrMediaPromises = arrMediaPromises.concat(encodeSlideMediaRels(this._masterSlide, this._runtime, onMediaError));
5413
6072
  return await Promise.all(arrMediaPromises).then(async () => {
6073
+ const canonicalMediaTargets = /* @__PURE__ */ new Map();
6074
+ for (const target of [
6075
+ ...this._slides,
6076
+ ...this._slideLayouts,
6077
+ this._masterSlide
6078
+ ]) for (const rel of target._relsMedia || []) {
6079
+ if (rel.type === "online" || rel.type === "hyperlink" || typeof rel.data !== "string" || !rel.data) continue;
6080
+ const key = (rel.extn || "") + "\0" + rel.data;
6081
+ const canonical = canonicalMediaTargets.get(key);
6082
+ if (canonical) rel.Target = canonical;
6083
+ else canonicalMediaTargets.set(key, rel.Target);
6084
+ }
5414
6085
  this._slides.forEach((slide) => {
5415
6086
  if (slide._slideLayout) addPlaceholdersToSlideLayouts(slide);
5416
6087
  });
@@ -5463,14 +6134,18 @@ var PptxGenJS = class {
5463
6134
  });
5464
6135
  this.createChartMediaRels(this._masterSlide, zip, arrChartPromises);
5465
6136
  return await Promise.all(arrChartPromises).then(async () => {
6137
+ const compression = props.compression === false ? "STORE" : "DEFLATE";
5466
6138
  if (props.outputType === "STREAM") return await zip.generateAsync({
5467
6139
  type: "nodebuffer",
5468
- compression: props.compression ? "DEFLATE" : "STORE"
6140
+ compression
6141
+ });
6142
+ else if (props.outputType) return await zip.generateAsync({
6143
+ type: props.outputType,
6144
+ compression
5469
6145
  });
5470
- else if (props.outputType) return await zip.generateAsync({ type: props.outputType });
5471
6146
  else return await zip.generateAsync({
5472
6147
  type: "blob",
5473
- compression: props.compression ? "DEFLATE" : "STORE"
6148
+ compression
5474
6149
  });
5475
6150
  });
5476
6151
  });
@@ -5493,10 +6168,12 @@ var PptxGenJS = class {
5493
6168
  */
5494
6169
  async write(props) {
5495
6170
  const propsOutpType = typeof props === "object" && props?.outputType ? props.outputType : props ? props : null;
5496
- const propsCompress = typeof props === "object" && props?.compression ? props.compression : false;
6171
+ const propsCompress = typeof props === "object" ? props?.compression : void 0;
6172
+ const propsMediaError = typeof props === "object" ? props?.onMediaError : void 0;
5497
6173
  return await this.exportPresentation({
5498
6174
  compression: propsCompress,
5499
- outputType: propsOutpType
6175
+ outputType: propsOutpType,
6176
+ onMediaError: propsMediaError
5500
6177
  });
5501
6178
  }
5502
6179
  /**
@@ -5510,11 +6187,12 @@ var PptxGenJS = class {
5510
6187
  console.warn("[WARNING] writeFile(string) is deprecated - pass { fileName } instead.");
5511
6188
  props = { fileName: props };
5512
6189
  }
5513
- const { fileName: rawName = "Presentation.pptx", compression = false } = props;
6190
+ const { fileName: rawName = "Presentation.pptx", compression, onMediaError } = props;
5514
6191
  const fileName = rawName.toLowerCase().endsWith(".pptx") ? rawName : `${rawName}.pptx`;
5515
6192
  const data = await this.exportPresentation({
5516
6193
  compression,
5517
- outputType: this._runtime.writeFileOutputType
6194
+ outputType: this._runtime.writeFileOutputType,
6195
+ onMediaError
5518
6196
  });
5519
6197
  return await this._runtime.writeFile(fileName, data);
5520
6198
  }
@@ -5676,4 +6354,4 @@ var PptxGenJS = class {
5676
6354
  //#endregion
5677
6355
  export { PptxGenJS as t };
5678
6356
 
5679
- //# sourceMappingURL=pptxgen--5RWzhb4.js.map
6357
+ //# sourceMappingURL=pptxgen-S8dEuBnC.js.map