@shbernal/pptxgenjs 5.1.0 → 5.3.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 EMU, B as PLACEHOLDER_TYPES, C as DEF_PRES_LAYOUT_NAME, D as DEF_SLIDE_MARGIN_IN, F as LINEH_MODIFIER, H as SCHEME_COLOR_NAMES, K as SchemeColor, L as ONEPT, N as LAYOUT_IDX_SERIES_BASE, O as DEF_TEXT_GLOW, P as LETTERS, R as OutputType, S as DEF_PRES_LAYOUT, T as DEF_SHAPE_SHADOW, U as SHAPE_TYPE, V as REGEX_HEX_COLOR, W as SLDNUMFLDID, _ as DEF_CHART_BORDER, a as AXIS_ID_VALUE_SECONDARY, c as BARCHART_COLORS, f as ChartType, h as DEF_CELL_MARGIN_IN, i as AXIS_ID_VALUE_PRIMARY, j as IMG_BROKEN, k as DEF_TEXT_SHADOW, m as DEF_CELL_BORDER, n as AXIS_ID_CATEGORY_SECONDARY, o as AlignH, q as ShapeType, r as AXIS_ID_SERIES_PRIMARY, s as AlignV, t as AXIS_ID_CATEGORY_PRIMARY, u as CHART_TYPE, v as DEF_CHART_GRIDLINE, y as DEF_FONT_COLOR, z as PIECHART_COLORS } from "./core-enums-BCaI1VAf.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_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, et as VALID_SHAPE_PRESETS, 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-BFXQk67T.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) {
@@ -244,10 +294,32 @@ function genXmlGradientFill(gradient) {
244
294
  return strXml;
245
295
  }
246
296
  /**
297
+ * Create a native DrawingML pattern fill.
298
+ * @param {PatternFillProps} pattern pattern fill options
299
+ * @returns XML string
300
+ */
301
+ function genXmlPatternFill(pattern) {
302
+ if (!pattern) throw new Error("Pattern fill requires a pattern object.");
303
+ const fgColor = pattern.fgColor ?? "000000";
304
+ const bgColor = pattern.bgColor ?? "FFFFFF";
305
+ return `<a:pattFill prst="${pattern.preset}"><a:fgClr>${createColorElement(fgColor)}</a:fgClr><a:bgClr>${createColorElement(bgColor)}</a:bgClr></a:pattFill>`;
306
+ }
307
+ /**
247
308
  * Create color selection
248
309
  * @param {Color | ShapeFillProps | ShapeLineProps} props fill props
249
310
  * @returns XML string
250
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
+ }
251
323
  function genXmlColorSelection(props) {
252
324
  let fillType = "solid";
253
325
  let colorVal = "";
@@ -258,8 +330,8 @@ function genXmlColorSelection(props) {
258
330
  else {
259
331
  if (props.type) fillType = props.type;
260
332
  if (props.color) colorVal = props.color;
261
- if (props.alpha) internalElements += `<a:alpha val="${Math.round((100 - props.alpha) * 1e3)}"/>`;
262
- 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)}"/>`;
263
335
  }
264
336
  switch (fillType) {
265
337
  case "solid":
@@ -268,6 +340,9 @@ function genXmlColorSelection(props) {
268
340
  case "gradient":
269
341
  outText += genXmlGradientFill(typeof props === "string" ? void 0 : props.gradient);
270
342
  break;
343
+ case "pattern":
344
+ outText += genXmlPatternFill(typeof props === "string" ? void 0 : props.pattern);
345
+ break;
271
346
  default:
272
347
  outText += "";
273
348
  break;
@@ -333,6 +408,123 @@ function svgMarkupToDataUri(svg) {
333
408
  for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
334
409
  return `data:image/svg+xml;base64,${btoa(binary)}`;
335
410
  }
411
+ /**
412
+ * Decode a base64 image payload (raw base64 or a `data:` URI) to bytes.
413
+ * - tolerant of the `data:[mime];base64,` prefix and of whitespace in the payload
414
+ * @param {string} b64 - base64 string or data URI
415
+ * @returns {Uint8Array | null} decoded bytes, or `null` when the payload is empty/undecodable
416
+ */
417
+ function decodeBase64ToBytes(b64) {
418
+ if (!b64) return null;
419
+ const comma = b64.indexOf("base64,");
420
+ const payload = (comma >= 0 ? b64.slice(comma + 7) : b64).replace(/\s/g, "");
421
+ if (!payload) return null;
422
+ try {
423
+ const binary = atob(payload);
424
+ const bytes = new Uint8Array(binary.length);
425
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
426
+ return bytes;
427
+ } catch {
428
+ return null;
429
+ }
430
+ }
431
+ /**
432
+ * Read the intrinsic pixel dimensions of a raster image from its header bytes.
433
+ * - synchronous: parses only file-format headers, never decodes pixels
434
+ * - supports PNG, JPEG, GIF, BMP, and WebP (VP8 / VP8L / VP8X)
435
+ * - vector (SVG) and unrecognized formats return `null` (no intrinsic pixel size)
436
+ *
437
+ * Used by image `sizing: 'cover' | 'contain'` to compute an aspect-correct
438
+ * `<a:srcRect>` crop from the *natural* image ratio rather than the displayed box.
439
+ * @param {string} dataB64 - base64 image payload or `data:` URI
440
+ * @returns {{ w: number, h: number } | null} natural pixel size, or `null` when unmeasurable
441
+ */
442
+ function getImageSizeFromBase64(dataB64) {
443
+ const b = decodeBase64ToBytes(dataB64);
444
+ if (!b || b.length < 24) return null;
445
+ if (b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71) {
446
+ const w = b[16] << 24 | b[17] << 16 | b[18] << 8 | b[19];
447
+ const h = b[20] << 24 | b[21] << 16 | b[22] << 8 | b[23];
448
+ return w > 0 && h > 0 ? {
449
+ w,
450
+ h
451
+ } : null;
452
+ }
453
+ if (b[0] === 71 && b[1] === 73 && b[2] === 70) {
454
+ const w = b[6] | b[7] << 8;
455
+ const h = b[8] | b[9] << 8;
456
+ return w > 0 && h > 0 ? {
457
+ w,
458
+ h
459
+ } : null;
460
+ }
461
+ if (b[0] === 66 && b[1] === 77) {
462
+ const w = b[18] | b[19] << 8 | b[20] << 16 | b[21] << 24;
463
+ const h = b[22] | b[23] << 8 | b[24] << 16 | b[25] << 24;
464
+ const aw = Math.abs(w);
465
+ const ah = Math.abs(h);
466
+ return aw > 0 && ah > 0 ? {
467
+ w: aw,
468
+ h: ah
469
+ } : null;
470
+ }
471
+ if (b[0] === 82 && b[1] === 73 && b[2] === 70 && b[3] === 70 && b[8] === 87 && b[9] === 69 && b[10] === 66 && b[11] === 80) {
472
+ const fourCC = String.fromCharCode(b[12], b[13], b[14], b[15]);
473
+ if (fourCC === "VP8 " && b.length >= 30) {
474
+ const w = (b[26] | b[27] << 8) & 16383;
475
+ const h = (b[28] | b[29] << 8) & 16383;
476
+ return w > 0 && h > 0 ? {
477
+ w,
478
+ h
479
+ } : null;
480
+ }
481
+ if (fourCC === "VP8L" && b.length >= 25) {
482
+ const bits = b[21] | b[22] << 8 | b[23] << 16 | b[24] << 24;
483
+ const w = (bits & 16383) + 1;
484
+ const h = (bits >> 14 & 16383) + 1;
485
+ return w > 0 && h > 0 ? {
486
+ w,
487
+ h
488
+ } : null;
489
+ }
490
+ if (fourCC === "VP8X" && b.length >= 30) {
491
+ const w = (b[24] | b[25] << 8 | b[26] << 16) + 1;
492
+ const h = (b[27] | b[28] << 8 | b[29] << 16) + 1;
493
+ return w > 0 && h > 0 ? {
494
+ w,
495
+ h
496
+ } : null;
497
+ }
498
+ return null;
499
+ }
500
+ if (b[0] === 255 && b[1] === 216) {
501
+ let i = 2;
502
+ while (i + 9 < b.length) {
503
+ if (b[i] !== 255) {
504
+ i++;
505
+ continue;
506
+ }
507
+ const marker = b[i + 1];
508
+ if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
509
+ const h = b[i + 5] << 8 | b[i + 6];
510
+ const w = b[i + 7] << 8 | b[i + 8];
511
+ return w > 0 && h > 0 ? {
512
+ w,
513
+ h
514
+ } : null;
515
+ }
516
+ if (marker >= 208 && marker <= 217 || marker === 1) {
517
+ i += 2;
518
+ continue;
519
+ }
520
+ const segLen = b[i + 2] << 8 | b[i + 3];
521
+ if (segLen < 2) break;
522
+ i += 2 + segLen;
523
+ }
524
+ return null;
525
+ }
526
+ return null;
527
+ }
336
528
  //#endregion
337
529
  //#region src/gen-tables.ts
338
530
  /**
@@ -386,52 +578,62 @@ function parseTextToLines(cell, colWidth, verbose) {
386
578
  */
387
579
  let newLine = [];
388
580
  inputCells.forEach((cell) => {
389
- if (typeof cell.text === "string") {
390
- if (cell.text.split("\n").length > 1) cell.text.split("\n").forEach((textLine) => {
391
- newLine.push({
581
+ if (typeof cell.text !== "string") return;
582
+ if (cell.text.includes("\n")) {
583
+ const parts = cell.text.split("\n");
584
+ parts.forEach((part, partIdx) => {
585
+ if (partIdx === parts.length - 1) newLine.push({
392
586
  _type: "tablecell",
393
- text: textLine,
394
- options: {
395
- ...cell.options,
396
- breakLine: true
397
- }
587
+ text: part,
588
+ options: cell.options
398
589
  });
590
+ else {
591
+ newLine.push({
592
+ _type: "tablecell",
593
+ text: part,
594
+ options: {
595
+ ...cell.options,
596
+ breakLine: true
597
+ }
598
+ });
599
+ inputLines1.push(newLine);
600
+ newLine = [];
601
+ }
399
602
  });
400
- else newLine.push({
401
- _type: "tablecell",
402
- text: cell.text.trim(),
403
- options: cell.options
404
- });
405
- if (cell.options?.breakLine) {
406
- if (verbose) console.log(`inputCells: new line > ${JSON.stringify(newLine)}`);
407
- inputLines1.push(newLine);
408
- newLine = [];
409
- }
410
- }
411
- if (newLine.length > 0) {
603
+ } else newLine.push({
604
+ _type: "tablecell",
605
+ text: cell.text.trim(),
606
+ options: cell.options
607
+ });
608
+ if (cell.options?.breakLine) {
609
+ if (verbose) console.log(`inputCells: new line > ${JSON.stringify(newLine)}`);
412
610
  inputLines1.push(newLine);
413
611
  newLine = [];
414
612
  }
415
613
  });
614
+ if (newLine.length > 0) {
615
+ inputLines1.push(newLine);
616
+ newLine = [];
617
+ }
416
618
  if (verbose) {
417
619
  console.log(`[2/4] inputLines1 (${inputLines1.length})`);
418
620
  inputLines1.forEach((line, idx) => console.log(`[2/4] [${idx + 1}] line: ${JSON.stringify(line)}`));
419
621
  }
420
622
  inputLines1.forEach((line) => {
623
+ const lineTokens = [];
421
624
  line.forEach((cell) => {
422
- const lineCells = [];
423
625
  const lineWords = String(cell.text).split(" ");
424
626
  lineWords.forEach((word, idx) => {
425
627
  const cellProps = { ...cell.options };
426
628
  if (cellProps?.breakLine) cellProps.breakLine = idx + 1 === lineWords.length;
427
- lineCells.push({
629
+ lineTokens.push({
428
630
  _type: "tablecell",
429
631
  text: word + (idx + 1 < lineWords.length ? " " : ""),
430
632
  options: cellProps
431
633
  });
432
634
  });
433
- inputLines2.push(lineCells);
434
635
  });
636
+ inputLines2.push(lineTokens);
435
637
  });
436
638
  if (verbose) {
437
639
  console.log(`[3/4] inputLines2 (${inputLines2.length})`);
@@ -539,6 +741,7 @@ function getSlidesForTableRows(tableRows = [], tableProps = {}, presLayout, mast
539
741
  numCols += Number(cellOpts?.colspan ? cellOpts.colspan : 1);
540
742
  });
541
743
  if (tableProps.verbose) console.log(`| numCols ......................................... = ${numCols}`);
744
+ const colSpanDepths = new Array(numCols).fill(0);
542
745
  if (!tablePropW && tableProps.colW) {
543
746
  tableCalcW = Array.isArray(tableProps.colW) ? tableProps.colW.reduce((p, n) => p + n) * EMU : tableProps.colW * numCols || 0;
544
747
  if (tableProps.verbose) console.log(`| tableCalcW ...................................... = ${tableCalcW / EMU}`);
@@ -559,6 +762,7 @@ function getSlidesForTableRows(tableRows = [], tableProps = {}, presLayout, mast
559
762
  }
560
763
  let newTableRowSlide = { rows: [] };
561
764
  tableRows.forEach((row, iRow) => {
765
+ const hasActiveRowSpan = colSpanDepths.some((d) => d > 0);
562
766
  const rowCellLines = [];
563
767
  let maxCellMarTopEmu = 0;
564
768
  let maxCellMarBtmEmu = 0;
@@ -655,7 +859,7 @@ function getSlidesForTableRows(tableRows = [], tableProps = {}, presLayout, mast
655
859
  rowCellLines.forEach((cell) => {
656
860
  if (cell._lineHeight >= emuLineMaxH) emuLineMaxH = cell._lineHeight;
657
861
  });
658
- if (emuTabCurrH + emuLineMaxH > emuSlideTabH) {
862
+ if (emuTabCurrH + emuLineMaxH > emuSlideTabH && !hasActiveRowSpan) {
659
863
  if (tableProps.verbose) {
660
864
  console.log("\n|-----------------------------------------------------------------------|");
661
865
  console.log(`|-- NEW SLIDE CREATED (currTabH+currLineH > maxH) => ${(emuTabCurrH / EMU).toFixed(2)} + ${(srcCell._lineHeight / EMU).toFixed(2)} > ${emuSlideTabH / EMU}`);
@@ -699,6 +903,16 @@ function getSlidesForTableRows(tableRows = [], tableProps = {}, presLayout, mast
699
903
  if (rowCellLines.map((cell) => cell._lines.length).reduce((prev, next) => prev + next) === 0) isDone = true;
700
904
  }
701
905
  if (currTableRow.length > 0) newTableRowSlide.rows.push(currTableRow);
906
+ const occupiedBefore = [...colSpanDepths];
907
+ let colCursor = 0;
908
+ row.forEach((cell) => {
909
+ while (colCursor < numCols && occupiedBefore[colCursor] > 0) colCursor++;
910
+ const cellColspan = cell.options?.colspan ?? 1;
911
+ const cellRowspan = cell.options?.rowspan ?? 1;
912
+ if (cellRowspan > 1) for (let c = 0; c < cellColspan && colCursor + c < numCols; c++) colSpanDepths[colCursor + c] = cellRowspan;
913
+ colCursor += cellColspan;
914
+ });
915
+ for (let c = 0; c < numCols; c++) if (colSpanDepths[c] > 0) colSpanDepths[c]--;
702
916
  if (tableProps.verbose) console.log(`- SLIDE [${tableRowSlides.length}]: ROW [${iRow}]: ...COMPLETE ...... emuTabCurrH = ${(emuTabCurrH / EMU).toFixed(2)} ( emuSlideTabH = ${(emuSlideTabH / EMU).toFixed(2)} )`);
703
917
  });
704
918
  tableRowSlides.push(newTableRowSlide);
@@ -711,6 +925,28 @@ function getSlidesForTableRows(tableRows = [], tableProps = {}, presLayout, mast
711
925
  return tableRowSlides;
712
926
  }
713
927
  /**
928
+ * Convert a computed CSS border (width string + color string) from `getComputedStyle` into a
929
+ * pptx `BorderProps`.
930
+ *
931
+ * Preserves *fractional* widths: a hairline CSS border such as `0.5px` must not be rounded to
932
+ * `0pt` and silently vanish — the table serializer (`valToPts`) emits fractional points just
933
+ * fine, so there is no reason to integer-round here (upstream gitbrent/PptxGenJS#1235). A
934
+ * computed width of `0` (or a non-finite value) yields `{ type: 'none' }` so we never emit a
935
+ * zero-width line.
936
+ * @param {string} widthStr - computed `border-<side>-width`, e.g. `"0.5px"`
937
+ * @param {string} colorStr - computed `border-<side>-color`, e.g. `"rgb(102, 102, 102)"`
938
+ * @returns {BorderProps} border props for the cell side
939
+ */
940
+ function htmlBorderToProps(widthStr, colorStr) {
941
+ const pt = Number(String(widthStr).replace("px", ""));
942
+ if (!isFinite(pt) || pt <= 0) return { type: "none" };
943
+ const arrRGB = String(colorStr).replace(/\s+/gi, "").replace("rgba(", "").replace("rgb(", "").replace(")", "").split(",");
944
+ return {
945
+ pt,
946
+ color: rgbToHex(Number(arrRGB[0]), Number(arrRGB[1]), Number(arrRGB[2]))
947
+ };
948
+ }
949
+ /**
714
950
  * Reproduces an HTML table as a PowerPoint table - including column widths, style, etc. - creates 1 or more slides as needed
715
951
  * @param {TableToSlidesHost} pptx - pptxgenjs instance
716
952
  * @param {string} tabEleId - HTMLElementID of the table
@@ -858,12 +1094,8 @@ function genTableToSlides(pptx, tabEleId, options = {}, masterSlide) {
858
1094
  "bottom",
859
1095
  "left"
860
1096
  ].forEach((val, idxb) => {
861
- const intBorderW = Math.round(Number(window.getComputedStyle(cell).getPropertyValue("border-" + val + "-width").replace("px", "")));
862
- const arrRGB = window.getComputedStyle(cell).getPropertyValue("border-" + val + "-color").replace(/\s+/gi, "").replace("rgba(", "").replace("rgb(", "").replace(")", "").split(",");
863
- cellBorder[idxb] = {
864
- pt: intBorderW,
865
- color: rgbToHex(Number(arrRGB[0]), Number(arrRGB[1]), Number(arrRGB[2]))
866
- };
1097
+ const style = window.getComputedStyle(cell);
1098
+ cellBorder[idxb] = htmlBorderToProps(style.getPropertyValue("border-" + val + "-width"), style.getPropertyValue("border-" + val + "-color"));
867
1099
  });
868
1100
  cellOpts.border = cellBorder;
869
1101
  }
@@ -933,6 +1165,8 @@ function genTableToSlides(pptx, tabEleId, options = {}, masterSlide) {
933
1165
  */
934
1166
  /** counter for included charts (used for index in their filenames) */
935
1167
  let _chartCounter = 0;
1168
+ /** DPI PowerPoint assumes when sizing an inserted raster image (natural pixels / 96 == inches) */
1169
+ const IMAGE_NATURAL_DPI = 96;
936
1170
  function normalizeBorderTuple(border) {
937
1171
  return Array.isArray(border) ? border : [
938
1172
  border,
@@ -954,6 +1188,7 @@ function createSlideMaster(props, target) {
954
1188
  else if ("image" in object) addImageDefinition(tgt, object.image);
955
1189
  else if ("line" in object) addShapeDefinition(tgt, "line", object.line);
956
1190
  else if ("rect" in object) addShapeDefinition(tgt, "rect", object.rect);
1191
+ else if ("roundRect" in object) addShapeDefinition(tgt, "roundRect", object.roundRect);
957
1192
  else if ("text" in object) addTextDefinition(tgt, [{ text: object.text.text }], object.text.options || {}, false);
958
1193
  else if ("placeholder" in object) {
959
1194
  const placeholder = object.placeholder;
@@ -968,6 +1203,26 @@ function createSlideMaster(props, target) {
968
1203
  if (props.slideNumber && typeof props.slideNumber === "object") target._slideNumberProps = props.slideNumber;
969
1204
  }
970
1205
  /**
1206
+ * Round and clamp an integer chart percentage/angle option into a schema-valid range.
1207
+ *
1208
+ * Several chart attributes are bounded integer types whose out-of-range values make
1209
+ * PowerPoint report the package as needing repair: `<c:overlap>` (ST_Overlap, -100..100),
1210
+ * `<c:gapWidth>`/`<c:gapDepth>` (ST_GapAmount, 0..500), `<c:holeSize>` (ST_HoleSize, 10..90)
1211
+ * and `<c:firstSliceAng>` (ST_FirstSliceAng, 0..360). Missing/non-numeric input returns
1212
+ * `undefined` so the caller can apply its own default; an out-of-range value is clamped
1213
+ * and a warning is emitted (per the library's warn-rather-than-degrade policy).
1214
+ * @param value - caller-supplied option value
1215
+ * @param min - inclusive lower bound
1216
+ * @param max - inclusive upper bound
1217
+ * @param name - option name, for the warning message
1218
+ */
1219
+ function clampChartPct(value, min, max, name) {
1220
+ if (typeof value !== "number" || isNaN(value)) return void 0;
1221
+ const clamped = Math.min(max, Math.max(min, Math.round(value)));
1222
+ if (clamped !== value) console.warn(`Warning: ${name} ${value} is outside the valid range ${min}-${max}; using ${clamped}.`);
1223
+ return clamped;
1224
+ }
1225
+ /**
971
1226
  * Generate the chart based on input data.
972
1227
  * OOXML Chart Spec: ISO/IEC 29500-1:2016(E)
973
1228
  *
@@ -1139,7 +1394,13 @@ function addChartDefinition(target, type, data, opt) {
1139
1394
  "marker",
1140
1395
  "filled"
1141
1396
  ].includes(options.radarStyle || "")) options.radarStyle = "standard";
1142
- options.lineDataSymbolSize = options.lineDataSymbolSize && !isNaN(options.lineDataSymbolSize) ? options.lineDataSymbolSize : 6;
1397
+ {
1398
+ const rawSymbolSize = options.lineDataSymbolSize;
1399
+ const hasSymbolSize = rawSymbolSize != null && !isNaN(rawSymbolSize);
1400
+ const symbolSize = Math.min(72, Math.max(2, Math.round(hasSymbolSize ? rawSymbolSize : 6)));
1401
+ if (hasSymbolSize && symbolSize !== rawSymbolSize) console.warn(`Warning: lineDataSymbolSize ${rawSymbolSize} is outside the valid marker size range (integer 2-72); using ${symbolSize}.`);
1402
+ options.lineDataSymbolSize = symbolSize;
1403
+ }
1143
1404
  options.lineDataSymbolLineSize = options.lineDataSymbolLineSize && !isNaN(options.lineDataSymbolLineSize) ? valToPts(options.lineDataSymbolLineSize) : valToPts(.75);
1144
1405
  const chartLayout = options.layout;
1145
1406
  if (chartLayout) [
@@ -1189,8 +1450,11 @@ function addChartDefinition(target, type, data, opt) {
1189
1450
  options.v3DRotY = typeof options.v3DRotY === "number" && !isNaN(options.v3DRotY) && options.v3DRotY >= 0 && options.v3DRotY <= 360 ? options.v3DRotY : 30;
1190
1451
  options.v3DRAngAx = options.v3DRAngAx || !options.v3DRAngAx ? options.v3DRAngAx : true;
1191
1452
  options.v3DPerspective = typeof options.v3DPerspective === "number" && !isNaN(options.v3DPerspective) && options.v3DPerspective >= 0 && options.v3DPerspective <= 240 ? options.v3DPerspective : 30;
1192
- options.barGapWidthPct = typeof options.barGapWidthPct === "number" && !isNaN(options.barGapWidthPct) && options.barGapWidthPct >= 0 && options.barGapWidthPct <= 1e3 ? options.barGapWidthPct : 150;
1193
- options.barGapDepthPct = typeof options.barGapDepthPct === "number" && !isNaN(options.barGapDepthPct) && options.barGapDepthPct >= 0 && options.barGapDepthPct <= 1e3 ? options.barGapDepthPct : 150;
1453
+ options.barGapWidthPct = clampChartPct(options.barGapWidthPct, 0, 500, "barGapWidthPct") ?? 150;
1454
+ options.barGapDepthPct = clampChartPct(options.barGapDepthPct, 0, 500, "barGapDepthPct") ?? 150;
1455
+ options.barOverlapPct = clampChartPct(options.barOverlapPct, -100, 100, "barOverlapPct");
1456
+ options.holeSize = clampChartPct(options.holeSize, 10, 90, "holeSize");
1457
+ options.firstSliceAng = clampChartPct(options.firstSliceAng, 0, 360, "firstSliceAng");
1194
1458
  options.chartColors = Array.isArray(options.chartColors) ? options.chartColors : options._type === "pie" || options._type === "doughnut" ? PIECHART_COLORS : BARCHART_COLORS;
1195
1459
  options.chartColorsOpacity = options.chartColorsOpacity && !isNaN(options.chartColorsOpacity) ? options.chartColorsOpacity : void 0;
1196
1460
  options.border = options.border && typeof options.border === "object" ? options.border : void 0;
@@ -1279,23 +1543,38 @@ function addImageDefinition(target, opt) {
1279
1543
  else if (strImageData?.toLowerCase().includes("image/svg+xml")) strImgExtn = "svg";
1280
1544
  newObject._type = "image";
1281
1545
  newObject.image = strImagePath || "preencoded.png";
1546
+ let defWidth = intWidth;
1547
+ let defHeight = intHeight;
1548
+ if ((!intWidth || !intHeight) && strImageData && strImgExtn !== "svg") {
1549
+ const natural = getImageSizeFromBase64(strImageData);
1550
+ if (natural) {
1551
+ if (!intWidth && !intHeight) {
1552
+ defWidth = natural.w / IMAGE_NATURAL_DPI;
1553
+ defHeight = natural.h / IMAGE_NATURAL_DPI;
1554
+ } else if (typeof intWidth === "number" && intWidth && !intHeight) defHeight = intWidth * (natural.h / natural.w);
1555
+ else if (typeof intHeight === "number" && intHeight && !intWidth) defWidth = intHeight * (natural.w / natural.h);
1556
+ }
1557
+ }
1282
1558
  const objectOptions = {
1283
1559
  x: intPosX || 0,
1284
1560
  y: intPosY || 0,
1285
- w: intWidth || 1,
1286
- h: intHeight || 1,
1561
+ w: defWidth || 1,
1562
+ h: defHeight || 1,
1287
1563
  altText: opt.altText || "",
1288
1564
  rounding: typeof opt.rounding === "boolean" ? opt.rounding : false,
1289
1565
  shape: opt.shape,
1290
1566
  points: opt.points,
1291
1567
  rectRadius: opt.rectRadius,
1568
+ shapeAdjust: opt.shapeAdjust,
1292
1569
  sizing,
1293
1570
  placeholder: opt.placeholder,
1294
1571
  rotate: opt.rotate || 0,
1295
1572
  flipV: opt.flipV || false,
1296
1573
  flipH: opt.flipH || false,
1297
1574
  transparency: opt.transparency || 0,
1575
+ duotone: opt.duotone,
1298
1576
  objectName,
1577
+ objectLock: opt.objectLock,
1299
1578
  shadow: correctShadowOptions(opt.shadow)
1300
1579
  };
1301
1580
  newObject.options = objectOptions;
@@ -1325,7 +1604,10 @@ function addImageDefinition(target, opt) {
1325
1604
  });
1326
1605
  newObject.imageRid = imageRelId + 1;
1327
1606
  } else {
1328
- const dupeItem = target._relsMedia.find((item) => item.path && item.path === strImagePath && item.type === "image/" + strImgExtn && !item.isDuplicate);
1607
+ const dupeItem = target._relsMedia.find((item) => {
1608
+ if (item.isDuplicate || !item.Target || item.type !== "image/" + strImgExtn) return false;
1609
+ return strImagePath ? item.path === strImagePath : !!strImageData && item.data === strImageData;
1610
+ });
1329
1611
  target._relsMedia.push({
1330
1612
  path: strImagePath || "preencoded." + strImgExtn,
1331
1613
  type: "image/" + strImgExtn,
@@ -1344,7 +1626,7 @@ function addImageDefinition(target, opt) {
1344
1626
  type: "hyperlink",
1345
1627
  data: objHyperlink.slide ? "slide" : "dummy",
1346
1628
  rId: imageRelId,
1347
- Target: objHyperlink.url || String(objHyperlink.slide)
1629
+ Target: objHyperlink.url ? encodeXmlEntities(objHyperlink.url) : String(objHyperlink.slide)
1348
1630
  });
1349
1631
  objHyperlink._rId = imageRelId;
1350
1632
  newObject.hyperlink = objHyperlink;
@@ -1383,6 +1665,7 @@ function addMediaDefinition(target, opt) {
1383
1665
  slideData.options.h = intSizeY;
1384
1666
  slideData.options.objectName = objectName;
1385
1667
  if (opt.altText) slideData.options.altText = opt.altText;
1668
+ if (opt.objectLock) slideData.options.objectLock = opt.objectLock;
1386
1669
  /**
1387
1670
  * NOTE:
1388
1671
  * - rId starts at 2 (hence the intRels+1 below) as slideLayout.xml is rId=1!
@@ -1412,7 +1695,10 @@ function addMediaDefinition(target, opt) {
1412
1695
  Target: `../media/image-${target._slideNum}-${target._relsMedia.length + 1}.png`
1413
1696
  });
1414
1697
  } else {
1415
- const dupeItem = target._relsMedia.find((item) => item.path && item.path === strPath && item.type === strType + "/" + strExtn && !item.isDuplicate);
1698
+ const dupeItem = target._relsMedia.find((item) => {
1699
+ if (item.isDuplicate || !item.Target || item.type !== strType + "/" + strExtn) return false;
1700
+ return strPath ? item.path === strPath : !!strData && item.data === strData;
1701
+ });
1416
1702
  const relId1 = getNewRelId(target);
1417
1703
  target._relsMedia.push({
1418
1704
  path: strPath || "preencoded" + strExtn,
@@ -1477,12 +1763,14 @@ function addShapeDefinition(target, shapeName, opts) {
1477
1763
  const options = typeof opts === "object" ? opts : {};
1478
1764
  options.line = options.line || { type: "none" };
1479
1765
  options.shadow = correctShadowOptions(options.shadow);
1766
+ const resolvedShapeName = typeof shapeName === "string" && SHAPE_NAME_ALIASES[shapeName] ? SHAPE_NAME_ALIASES[shapeName] : shapeName;
1480
1767
  const newObject = {
1481
1768
  _type: "text",
1482
- shape: (typeof shapeName === "string" && SHAPE_NAME_ALIASES[shapeName] ? SHAPE_NAME_ALIASES[shapeName] : shapeName) || "rect",
1769
+ shape: resolvedShapeName || "rect",
1483
1770
  options
1484
1771
  };
1485
1772
  if (!shapeName) throw new Error("Missing/Invalid shape parameter! Example: `addShape(pptxgen.shapes.LINE, {x:1, y:1, w:1, h:1});`");
1773
+ 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.`);
1486
1774
  const newLineOpts = {
1487
1775
  type: options.line.type || "solid",
1488
1776
  color: options.line.color || "333333",
@@ -1579,9 +1867,8 @@ function addTableDefinition(target, tableRows, options, slideLayout, presLayout,
1579
1867
  }
1580
1868
  arrRows.push(newRow);
1581
1869
  });
1582
- opt.x = getSmartParseNumber(opt.x || (opt.x === 0 ? 0 : EMU / 2), "X", presLayout);
1583
- opt.y = getSmartParseNumber(opt.y || (opt.y === 0 ? 0 : EMU / 2), "Y", presLayout);
1584
- if (opt.h) opt.h = getSmartParseNumber(opt.h, "Y", presLayout);
1870
+ if (opt.x === void 0 || opt.x === null) opt.x = .5;
1871
+ if (opt.y === void 0 || opt.y === null) opt.y = .5;
1585
1872
  opt.fontSize = opt.fontSize || 12;
1586
1873
  opt.margin = opt.margin === 0 || opt.margin ? opt.margin : DEF_CELL_MARGIN_IN;
1587
1874
  if (typeof opt.margin === "number") opt.margin = [
@@ -1650,12 +1937,7 @@ function addTableDefinition(target, tableRows, options, slideLayout, presLayout,
1650
1937
  console.warn("addTable: mismatch: (colW.length != data.length) Therefore, defaulting to evenly distributed col widths.");
1651
1938
  opt.colW = void 0;
1652
1939
  }
1653
- } else if (opt.w) opt.w = getSmartParseNumber(opt.w, "X", presLayout);
1654
- else opt.w = Math.floor((presLayout._sizeW || presLayout.width) / EMU - arrTableMargin[1] - arrTableMargin[3]);
1655
- if (opt.x && opt.x < 20) opt.x = inch2Emu(opt.x);
1656
- if (opt.y && opt.y < 20) opt.y = inch2Emu(opt.y);
1657
- if (opt.w && typeof opt.w === "number" && opt.w < 20) opt.w = inch2Emu(opt.w);
1658
- if (opt.h && typeof opt.h === "number" && opt.h < 20) opt.h = inch2Emu(opt.h);
1940
+ } else if (opt.w) {} else opt.w = Math.floor((presLayout._sizeW || presLayout.width) / EMU - arrTableMargin[1] - arrTableMargin[3]);
1659
1941
  arrRows.forEach((row) => {
1660
1942
  row.forEach((cell, idy) => {
1661
1943
  if (typeof cell === "number" || typeof cell === "string") row[idy] = {
@@ -1683,7 +1965,7 @@ function addTableDefinition(target, tableRows, options, slideLayout, presLayout,
1683
1965
  if (opt.autoPageRepeatHeader) opt._arrObjTabHeadRows = arrRows.filter((_row, idx) => idx < (opt.autoPageHeaderRows || 1));
1684
1966
  getSlidesForTableRows(arrRows, opt, presLayout, slideLayout).forEach((slide, idx) => {
1685
1967
  if (!getSlide(target._slideNum + idx)) slides.push(addSlide({ masterName: slideLayout?._name || void 0 }));
1686
- if (idx > 0) opt.y = inch2Emu(opt.autoPageSlideStartY || opt.newSlideStartY || arrTableMargin[0]);
1968
+ if (idx > 0) opt.y = opt.autoPageSlideStartY || opt.newSlideStartY || arrTableMargin[0];
1687
1969
  {
1688
1970
  const newSlide = getSlide(target._slideNum + idx);
1689
1971
  opt.autoPage = false;
@@ -1755,6 +2037,10 @@ function addTextDefinition(target, text, opts, isPlaceholder) {
1755
2037
  itemOpts._bodyProp.anchor = !itemOpts.placeholder ? "ctr" : void 0;
1756
2038
  itemOpts._bodyProp.vert = itemOpts.vert;
1757
2039
  itemOpts._bodyProp.wrap = typeof itemOpts.wrap === "boolean" ? itemOpts.wrap : true;
2040
+ 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)");
2041
+ else itemOpts._bodyProp.numCol = Math.round(itemOpts.columns);
2042
+ 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)");
2043
+ else itemOpts._bodyProp.spcCol = valToPts(itemOpts.columnSpacing);
1758
2044
  if (itemOpts.inset && !isNaN(Number(itemOpts.inset)) || itemOpts.inset === 0) {
1759
2045
  itemOpts._bodyProp.lIns = inch2Emu(itemOpts.inset);
1760
2046
  itemOpts._bodyProp.rIns = inch2Emu(itemOpts.inset);
@@ -2102,7 +2388,7 @@ async function createExcelWorksheet(chartObject, zip) {
2102
2388
  if (chartObject.opts._type === "bubble" || chartObject.opts._type === "bubble3D") strSharedStrings += `<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="${intBubbleCols}" uniqueCount="${intBubbleCols}">`;
2103
2389
  else if (chartObject.opts._type === "scatter") strSharedStrings += `<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="${data.length}" uniqueCount="${data.length}">`;
2104
2390
  else if (IS_MULTI_CAT_AXES) {
2105
- let totCount = data.length;
2391
+ let totCount = data.length + 1;
2106
2392
  data[0].labels.forEach((arrLabel) => totCount += arrLabel.filter((label) => label && label !== "").length);
2107
2393
  strSharedStrings += `<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="${totCount}" uniqueCount="${totCount}">`;
2108
2394
  strSharedStrings += "<si><t/></si>";
@@ -2170,7 +2456,7 @@ async function createExcelWorksheet(chartObject, zip) {
2170
2456
  strSheetXml += "<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\" xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\" mc:Ignorable=\"x14ac\" xmlns:x14ac=\"http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac\">";
2171
2457
  if (chartObject.opts._type === "bubble" || chartObject.opts._type === "bubble3D") strSheetXml += `<dimension ref="A1:${getExcelColName(intBubbleCols)}${data[0].values.length + 1}"/>`;
2172
2458
  else if (chartObject.opts._type === "scatter") strSheetXml += `<dimension ref="A1:${getExcelColName(data.length)}${data[0].values.length + 1}"/>`;
2173
- else strSheetXml += `<dimension ref="A1:${getExcelColName(data.length + 1)}${data[0].values.length + 1}"/>`;
2459
+ else strSheetXml += `<dimension ref="A1:${getExcelColName(data.length + data[0].labels.length)}${data[0].values.length + 1}"/>`;
2174
2460
  strSheetXml += "<sheetViews><sheetView tabSelected=\"1\" workbookViewId=\"0\"><selection activeCell=\"B1\" sqref=\"B1\"/></sheetView></sheetViews>";
2175
2461
  strSheetXml += "<sheetFormatPr baseColWidth=\"10\" defaultRowHeight=\"16\"/>";
2176
2462
  if (chartObject.opts._type === "bubble" || chartObject.opts._type === "bubble3D") {
@@ -2222,82 +2508,28 @@ async function createExcelWorksheet(chartObject, zip) {
2222
2508
  strSheetXml += "</row>";
2223
2509
  });
2224
2510
  } else {
2225
- strSheetXml += `<row r="1" spans="1:${data.length + data[0].labels.length}">`;
2226
- for (let idx = 0; idx < data[0].labels.length; idx++) strSheetXml += `<c r="${getExcelColName(idx + 1)}1" t="s"><v>0</v></c>`;
2227
- for (let idx = data[0].labels.length - 1; idx < data.length + data[0].labels.length - 1; idx++) strSheetXml += `<c r="${getExcelColName(idx + data[0].labels.length)}1" t="s"><v>${idx}</v></c>`;
2228
- strSheetXml += "</row>";
2229
- /**
2230
- * @example INPUT
2231
- * const LABELS = [
2232
- * ["Gear", "Berg", "Motr", "Swch", "Plug", "Cord", "Pump", "Leak", "Seal"],
2233
- * ["Mech", "", "", "Elec", "", "", "Hydr", "", ""],
2234
- * ];
2235
- * const arrDataRegions = [
2236
- * { name: "West", labels: LABELS, values: [11, 8, 3, 0, 11, 3, 0, 0, 0] },
2237
- * { name: "Ctrl", labels: LABELS, values: [0, 11, 6, 19, 12, 5, 0, 0, 0] },
2238
- * { name: "East", labels: LABELS, values: [0, 3, 2, 0, 0, 0, 4, 3, 1] },
2239
- * ];
2240
- */
2241
- /**
2242
- * @example OUTPUT EXCEL SHEET
2243
- * |/|---A--|---B--|---C--|---D--|---E--|
2244
- * |1| | | West | Ctrl | East |
2245
- * |2| Mech | Gear | ## | ## | ## |
2246
- * |3| | Brng | ## | ## | ## |
2247
- * |4| | Motr | ## | ## | ## |
2248
- * |5| Elec | Swch | ## | ## | ## |
2249
- * |6| | Plug | ## | ## | ## |
2250
- * |7| | Cord | ## | ## | ## |
2251
- * |8| Hydr | Pump | ## | ## | ## |
2252
- * |9| | Leak | ## | ## | ## |
2253
- *|10| | Seal | ## | ## | ## |
2254
- */
2255
- /**
2256
- * @example OUTPUT EXCEL SHEET XML
2257
- * <row r="1" spans="1:5">
2258
- * <c r="A1" t="s"><v>0</v></c>
2259
- * <c r="B1" t="s"><v>0</v></c>
2260
- * <c r="C1" t="s"><v>1</v></c>
2261
- * <c r="D1" t="s"><v>2</v></c>
2262
- * <c r="E1" t="s"><v>3</v></c>
2263
- * </row>
2264
- * <row r="2" spans="1:5">
2265
- * <c r="A2" t="s"><v>4</v></c>
2266
- * <c r="B2" t="s"><v>7</v></c>
2267
- * <c r="C2" ><v>###</v></c>
2268
- * </row>
2269
- * <row r="3" spans="1:5">
2270
- * <c r="A3" />
2271
- * <c r="B3" t="s"><v>8</v></c>
2272
- * <c r="C3" ><v>###</v></c>
2273
- * </row>
2274
- */
2275
- /**
2276
- * @example SHARED-STRINGS
2277
- * 1=West, 2=Ctrl, 3=East, 4=Mech, 5=Elec, 6=Mydr, 7=Gear, 8=Brng, [...], 15=Seal
2278
- */
2279
- /**
2280
- * const LABELS = [
2281
- * ["Gear", "Berg", "Motr", "Swch", "Plug", "Cord", "Pump", "Leak", "Seal"],
2282
- * ["Mech", "", "", "Elec", "", "", "Hydr", "", ""],
2283
- * ["2010", "", "", "", "", "", "", "", ""],
2284
- * ];
2285
- */
2286
2511
  const TOT_SER = data.length;
2287
2512
  const TOT_CAT = data[0].labels[0].length;
2288
2513
  const TOT_LVL = data[0].labels.length;
2514
+ const revLabelGroups = data[0].labels.slice().reverse();
2515
+ const ssLabelMap = /* @__PURE__ */ new Map();
2516
+ let ssIdx = TOT_SER + 1;
2517
+ revLabelGroups.forEach((labelsGroup, revLevelIdx) => {
2518
+ labelsGroup.forEach((label, rowIdx) => {
2519
+ if (label && label !== "") ssLabelMap.set(`${revLevelIdx}:${rowIdx}`, ssIdx++);
2520
+ });
2521
+ });
2522
+ strSheetXml += `<row r="1" spans="1:${TOT_SER + TOT_LVL}">`;
2523
+ for (let col = 1; col <= TOT_LVL; col++) strSheetXml += `<c r="${getExcelColName(col)}1" t="s"><v>0</v></c>`;
2524
+ for (let ser = 0; ser < TOT_SER; ser++) strSheetXml += `<c r="${getExcelColName(TOT_LVL + ser + 1)}1" t="s"><v>${ser + 1}</v></c>`;
2525
+ strSheetXml += "</row>";
2289
2526
  for (let idx = 0; idx < TOT_CAT; idx++) {
2290
2527
  strSheetXml += `<row r="${idx + 2}" spans="1:${TOT_SER + TOT_LVL}">`;
2291
- let totLabels = TOT_SER;
2292
- const revLabelGroups = data[0].labels.slice().reverse();
2293
2528
  revLabelGroups.forEach((labelsGroup, idy) => {
2294
- if (labelsGroup[idx]) {
2295
- const totGrpLbls = idy === 0 ? 1 : revLabelGroups[idy - 1].filter((label) => label && label !== "").length;
2296
- totLabels += totGrpLbls;
2297
- strSheetXml += `<c r="${getExcelColName(idx + 1 + idy)}${idx + 2}" t="s"><v>${totLabels}</v></c>`;
2298
- }
2529
+ const colLabel = labelsGroup[idx];
2530
+ if (colLabel && colLabel !== "") strSheetXml += `<c r="${getExcelColName(idy + 1)}${idx + 2}" t="s"><v>${ssLabelMap.get(`${idy}:${idx}`)}</v></c>`;
2299
2531
  });
2300
- for (let idy = 0; idy < TOT_SER; idy++) strSheetXml += `<c r="${getExcelColName(TOT_LVL + idy + 1)}${idx + 2}"><v>${data[idy].values[idx] || 0}</v></c>`;
2532
+ for (let idy = 0; idy < TOT_SER; idy++) strSheetXml += `<c r="${getExcelColName(TOT_LVL + idy + 1)}${idx + 2}"><v>${data[idy].values[idx] ?? ""}</v></c>`;
2301
2533
  strSheetXml += "</row>";
2302
2534
  }
2303
2535
  }
@@ -2318,6 +2550,22 @@ async function createExcelWorksheet(chartObject, zip) {
2318
2550
  });
2319
2551
  }
2320
2552
  /**
2553
+ * Emit the `<a:latin>/<a:ea>/<a:cs>` font trio for a chart text run.
2554
+ *
2555
+ * In DrawingML run properties a typeface applies only to the script class of
2556
+ * its element: `<a:latin>` covers Latin/ASCII, `<a:ea>` covers East Asian, and
2557
+ * `<a:cs>` covers complex scripts. Emitting `<a:latin>` alone leaves East Asian
2558
+ * (e.g. Chinese) and complex-script glyphs falling back to the theme font, so a
2559
+ * user-specified font never takes effect for that text — most visibly on
2560
+ * PowerPoint for Mac. Stamping the same typeface onto all three classes is what
2561
+ * choosing a font in PowerPoint's UI does (upstream gitbrent/PptxGenJS#1420).
2562
+ * @param {string} typeface - font face name
2563
+ * @return {string} `<a:latin/><a:ea/><a:cs/>` XML
2564
+ */
2565
+ function createChartTextFonts(typeface) {
2566
+ return `<a:latin typeface="${typeface}"/><a:ea typeface="${typeface}"/><a:cs typeface="${typeface}"/>`;
2567
+ }
2568
+ /**
2321
2569
  * Main entry point method for create charts
2322
2570
  * @see: http://www.datypic.com/sc/ooxml/s-dml-chart.xsd.html
2323
2571
  * @param {ISlideRelChart} rel - chart object
@@ -2327,6 +2575,10 @@ function makeXmlCharts(rel) {
2327
2575
  let strXml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";
2328
2576
  let usesSecondaryValAxis = false;
2329
2577
  let usesSecondaryCatAxis = false;
2578
+ let primaryCatAxisValType = null;
2579
+ let secondaryCatAxisValType = null;
2580
+ let primaryCatAxisHasCategoryChart = false;
2581
+ let secondaryCatAxisHasCategoryChart = false;
2330
2582
  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\">";
2331
2583
  strXml += "<c:date1904 val=\"0\"/>";
2332
2584
  strXml += `<c:roundedCorners val="${rel.opts.chartArea.roundedCorners ? "1" : "0"}"/>`;
@@ -2339,6 +2591,8 @@ function makeXmlCharts(rel) {
2339
2591
  fontSize: rel.opts.titleFontSize || 18,
2340
2592
  titleAlign: rel.opts.titleAlign,
2341
2593
  titleBold: rel.opts.titleBold,
2594
+ titleItalic: rel.opts.titleItalic,
2595
+ titleUnderline: rel.opts.titleUnderline,
2342
2596
  titlePos: rel.opts.titlePos,
2343
2597
  titleRotate: rel.opts.titleRotate
2344
2598
  }, rel.opts.x, rel.opts.y);
@@ -2371,18 +2625,37 @@ function makeXmlCharts(rel) {
2371
2625
  const catAxisId = options.secondaryCatAxis ? AXIS_ID_CATEGORY_SECONDARY : AXIS_ID_CATEGORY_PRIMARY;
2372
2626
  usesSecondaryValAxis = usesSecondaryValAxis || options.secondaryValAxis;
2373
2627
  usesSecondaryCatAxis = usesSecondaryCatAxis || options.secondaryCatAxis;
2628
+ const usesValueXAxis = type.type === "scatter" || type.type === "bubble" || type.type === "bubble3D";
2629
+ if (options.secondaryCatAxis) if (usesValueXAxis) secondaryCatAxisValType = type.type;
2630
+ else secondaryCatAxisHasCategoryChart = true;
2631
+ else if (usesValueXAxis) primaryCatAxisValType = type.type;
2632
+ else primaryCatAxisHasCategoryChart = true;
2374
2633
  strXml += makeChartType(type.type, type.data, options, valAxisId, catAxisId);
2375
2634
  });
2376
2635
  else strXml += makeChartType(rel.opts._type, rel.data, rel.opts, AXIS_ID_VALUE_PRIMARY, AXIS_ID_CATEGORY_PRIMARY);
2377
2636
  if (rel.opts._type !== "pie" && rel.opts._type !== "doughnut") {
2378
2637
  if (rel.opts.valAxes && rel.opts.valAxes.length > 1 && !usesSecondaryValAxis) throw new Error("Secondary axis must be used by one of the multiple charts");
2638
+ const comboCatAxisType = (isSecondary) => {
2639
+ const valType = isSecondary ? secondaryCatAxisValType : primaryCatAxisValType;
2640
+ const hasCategoryChart = isSecondary ? secondaryCatAxisHasCategoryChart : primaryCatAxisHasCategoryChart;
2641
+ if (!valType) return {};
2642
+ if (hasCategoryChart) {
2643
+ 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.`);
2644
+ return {};
2645
+ }
2646
+ return { _type: valType };
2647
+ };
2379
2648
  if (rel.opts.catAxes) {
2380
2649
  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.");
2381
2650
  strXml += makeCatAxis({
2382
2651
  ...rel.opts,
2383
- ...rel.opts.catAxes[0]
2652
+ ...rel.opts.catAxes[0],
2653
+ ...comboCatAxisType(false)
2384
2654
  }, AXIS_ID_CATEGORY_PRIMARY, AXIS_ID_VALUE_PRIMARY);
2385
- } else strXml += makeCatAxis(rel.opts, AXIS_ID_CATEGORY_PRIMARY, AXIS_ID_VALUE_PRIMARY);
2655
+ } else strXml += makeCatAxis({
2656
+ ...rel.opts,
2657
+ ...comboCatAxisType(false)
2658
+ }, AXIS_ID_CATEGORY_PRIMARY, AXIS_ID_VALUE_PRIMARY);
2386
2659
  if (rel.opts.valAxes) {
2387
2660
  strXml += makeValAxis({
2388
2661
  ...rel.opts,
@@ -2399,9 +2672,13 @@ function makeXmlCharts(rel) {
2399
2672
  }
2400
2673
  if (rel.opts?.catAxes && rel.opts?.catAxes[1]) strXml += makeCatAxis({
2401
2674
  ...rel.opts,
2402
- ...rel.opts.catAxes[1]
2675
+ ...rel.opts.catAxes[1],
2676
+ ...comboCatAxisType(true)
2677
+ }, AXIS_ID_CATEGORY_SECONDARY, AXIS_ID_VALUE_SECONDARY);
2678
+ else if (usesSecondaryCatAxis && (!rel.opts.catAxes || !rel.opts.catAxes[1])) strXml += makeCatAxis({
2679
+ ...rel.opts,
2680
+ ...comboCatAxisType(true)
2403
2681
  }, AXIS_ID_CATEGORY_SECONDARY, AXIS_ID_VALUE_SECONDARY);
2404
- else if (usesSecondaryCatAxis && (!rel.opts.catAxes || !rel.opts.catAxes[1])) strXml += makeCatAxis(rel.opts, AXIS_ID_CATEGORY_SECONDARY, AXIS_ID_VALUE_SECONDARY);
2405
2682
  }
2406
2683
  if (rel.opts.showDataTable) {
2407
2684
  strXml += "<c:dTable>";
@@ -2440,6 +2717,13 @@ function makeXmlCharts(rel) {
2440
2717
  if (rel.opts.showLegend) {
2441
2718
  strXml += "<c:legend>";
2442
2719
  strXml += "<c:legendPos val=\"" + rel.opts.legendPos + "\"/>";
2720
+ if (Array.isArray(rel.opts._type)) {
2721
+ let seriesIdx = 0;
2722
+ rel.opts._type.forEach((type) => {
2723
+ if (type.options?.showLegend === false) for (let i = 0; i < type.data.length; i++) strXml += `<c:legendEntry><c:idx val="${seriesIdx + i}"/><c:delete val="1"/></c:legendEntry>`;
2724
+ seriesIdx += type.data.length;
2725
+ });
2726
+ }
2443
2727
  strXml += "<c:overlay val=\"0\"/>";
2444
2728
  if (rel.opts.legendFontFace || rel.opts.legendFontSize || rel.opts.legendColor) {
2445
2729
  strXml += "<c:txPr>";
@@ -2449,8 +2733,7 @@ function makeXmlCharts(rel) {
2449
2733
  strXml += " <a:pPr>";
2450
2734
  strXml += rel.opts.legendFontSize ? `<a:defRPr sz="${Math.round(Number(rel.opts.legendFontSize) * 100)}">` : "<a:defRPr>";
2451
2735
  if (rel.opts.legendColor) strXml += genXmlColorSelection(rel.opts.legendColor);
2452
- if (rel.opts.legendFontFace) strXml += "<a:latin typeface=\"" + rel.opts.legendFontFace + "\"/>";
2453
- if (rel.opts.legendFontFace) strXml += "<a:cs typeface=\"" + rel.opts.legendFontFace + "\"/>";
2736
+ if (rel.opts.legendFontFace) strXml += createChartTextFonts(rel.opts.legendFontFace);
2454
2737
  strXml += " </a:defRPr>";
2455
2738
  strXml += " </a:pPr>";
2456
2739
  strXml += " <a:endParaRPr lang=\"en-US\"/>";
@@ -2488,6 +2771,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2488
2771
  let idxColLtr = 1;
2489
2772
  let optsChartData;
2490
2773
  let strXml = "";
2774
+ const valFmtCode = encodeXmlEntities(opts.valLabelFormatCode || opts.dataTableFormatCode || opts.dataLabelFormatCode || "General");
2491
2775
  switch (chartType) {
2492
2776
  case "area":
2493
2777
  case "bar":
@@ -2515,20 +2799,23 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2515
2799
  strXml += " <c:strCache><c:ptCount val=\"1\"/><c:pt idx=\"0\"><c:v>" + encodeXmlEntities(obj.name) + "</c:v></c:pt></c:strCache>";
2516
2800
  strXml += " </c:strRef>";
2517
2801
  strXml += " </c:tx>";
2518
- const seriesColor = opts.chartColors ? opts.chartColors[colorIndex % opts.chartColors.length] : null;
2802
+ const seriesOverride = opts.seriesOptions?.[obj._dataIndex];
2803
+ const seriesColor = seriesOverride?.color ?? (opts.chartColors ? opts.chartColors[colorIndex % opts.chartColors.length] : null);
2519
2804
  strXml += " <c:spPr>";
2520
2805
  if (seriesColor === "transparent") strXml += "<a:noFill/>";
2521
2806
  else if (opts.chartColorsOpacity) strXml += "<a:solidFill>" + createColorElement(seriesColor, `<a:alpha val="${Math.round(opts.chartColorsOpacity * 1e3)}"/>`) + "</a:solidFill>";
2522
2807
  else strXml += "<a:solidFill>" + createColorElement(seriesColor) + "</a:solidFill>";
2523
- if (chartType === "line" || chartType === "radar") if (opts.lineSize === 0) strXml += "<a:ln><a:noFill/></a:ln>";
2524
- else {
2525
- strXml += `<a:ln w="${valToPts(opts.lineSize)}" cap="${createLineCap(opts.lineCap)}"><a:solidFill>${createColorElement(seriesColor)}</a:solidFill>`;
2526
- strXml += `<a:prstDash val="${opts.lineDashValues?.[colorIndex] ?? opts.lineDash ?? "solid"}"/><a:round/></a:ln>`;
2527
- }
2528
- 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>`;
2808
+ if (chartType === "line" || chartType === "radar") {
2809
+ const effectiveLineSize = seriesOverride?.lineSize ?? opts.lineSize;
2810
+ if (effectiveLineSize === 0) strXml += "<a:ln><a:noFill/></a:ln>";
2811
+ else {
2812
+ strXml += `<a:ln w="${valToPts(effectiveLineSize)}" cap="${createLineCap(opts.lineCap)}"><a:solidFill>${createColorElement(seriesColor)}</a:solidFill>`;
2813
+ strXml += `<a:prstDash val="${opts.lineDashValues?.[colorIndex] ?? opts.lineDash ?? "solid"}"/><a:round/></a:ln>`;
2814
+ }
2815
+ } 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>`;
2529
2816
  strXml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW);
2530
2817
  strXml += " </c:spPr>";
2531
- if (chartType !== "line" && chartType !== "radar") strXml += " <c:invertIfNegative val=\"0\"/>";
2818
+ if (chartType === "bar" || chartType === "bar3D") strXml += " <c:invertIfNegative val=\"0\"/>";
2532
2819
  if (chartType === "line" || chartType === "radar") {
2533
2820
  strXml += "<c:marker>";
2534
2821
  strXml += " <c:symbol val=\"" + opts.lineDataSymbol + "\"/>";
@@ -2543,14 +2830,26 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2543
2830
  strXml += " </c:spPr>";
2544
2831
  strXml += "</c:marker>";
2545
2832
  }
2833
+ {
2834
+ 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;
2835
+ strXml += makeSeriesDataPointsXml(chartType, obj, opts, barVaryColors);
2836
+ }
2546
2837
  if (chartType !== "radar") {
2838
+ const lblColor = seriesOverride?.dataLabelColor ?? opts.dataLabelColor ?? "000000";
2839
+ const lblBold = seriesOverride?.dataLabelFontBold ?? opts.dataLabelFontBold ?? false;
2840
+ const lblItalic = seriesOverride?.dataLabelFontItalic ?? opts.dataLabelFontItalic ?? false;
2841
+ const lblSize = seriesOverride?.dataLabelFontSize ?? opts.dataLabelFontSize ?? 12;
2842
+ const lblFace = seriesOverride?.dataLabelFontFace ?? opts.dataLabelFontFace ?? "Arial";
2547
2843
  strXml += "<c:dLbls>";
2844
+ if (obj.customLabels?.length) obj.customLabels.forEach((lbl, idx) => {
2845
+ if (lbl) strXml += makeCustomDLblXml(idx, lbl, opts);
2846
+ });
2548
2847
  strXml += `<c:numFmt formatCode="${encodeXmlEntities(opts.dataLabelFormatCode) || "General"}" sourceLinked="0"/>`;
2549
2848
  if (opts.dataLabelBkgrdColors) strXml += `<c:spPr><a:solidFill>${createColorElement(seriesColor)}</a:solidFill></c:spPr>`;
2550
2849
  strXml += "<c:txPr><a:bodyPr/><a:lstStyle/><a:p><a:pPr>";
2551
- strXml += `<a:defRPr b="${opts.dataLabelFontBold ? 1 : 0}" i="${opts.dataLabelFontItalic ? 1 : 0}" strike="noStrike" sz="${Math.round((opts.dataLabelFontSize || 12) * 100)}" u="none">`;
2552
- strXml += `<a:solidFill>${createColorElement(opts.dataLabelColor || "000000")}</a:solidFill>`;
2553
- strXml += `<a:latin typeface="${opts.dataLabelFontFace || "Arial"}"/>`;
2850
+ strXml += `<a:defRPr b="${lblBold ? 1 : 0}" i="${lblItalic ? 1 : 0}" strike="noStrike" sz="${Math.round(lblSize * 100)}" u="none">`;
2851
+ strXml += `<a:solidFill>${createColorElement(lblColor)}</a:solidFill>`;
2852
+ strXml += createChartTextFonts(lblFace);
2554
2853
  strXml += "</a:defRPr></a:pPr></a:p></c:txPr>";
2555
2854
  if (opts.dataLabelPosition) strXml += `<c:dLblPos val="${opts.dataLabelPosition}"/>`;
2556
2855
  strXml += "<c:showLegendKey val=\"0\"/>";
@@ -2559,29 +2858,6 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2559
2858
  strXml += `<c:showLeaderLines val="${opts.showLeaderLines ? "1" : "0"}"/>`;
2560
2859
  strXml += "</c:dLbls>";
2561
2860
  }
2562
- 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) => {
2563
- const arrColors = value < 0 ? opts.invertedColors || opts.chartColors || BARCHART_COLORS : opts.chartColors || [];
2564
- strXml += " <c:dPt>";
2565
- strXml += ` <c:idx val="${index}"/>`;
2566
- strXml += " <c:invertIfNegative val=\"0\"/>";
2567
- strXml += " <c:bubble3D val=\"0\"/>";
2568
- strXml += " <c:spPr>";
2569
- if (opts.lineSize === 0) strXml += "<a:ln><a:noFill/></a:ln>";
2570
- else if (chartType === "bar") {
2571
- strXml += "<a:solidFill>";
2572
- strXml += " <a:srgbClr val=\"" + arrColors[index % arrColors.length] + "\"/>";
2573
- strXml += "</a:solidFill>";
2574
- } else {
2575
- strXml += "<a:ln>";
2576
- strXml += " <a:solidFill>";
2577
- strXml += " <a:srgbClr val=\"" + arrColors[index % arrColors.length] + "\"/>";
2578
- strXml += " </a:solidFill>";
2579
- strXml += "</a:ln>";
2580
- }
2581
- strXml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW);
2582
- strXml += " </c:spPr>";
2583
- strXml += " </c:dPt>";
2584
- });
2585
2861
  strXml += "<c:cat>";
2586
2862
  if (opts.catLabelFormatCode) {
2587
2863
  strXml += " <c:numRef>";
@@ -2618,10 +2894,10 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2618
2894
  strXml += " <c:numRef>";
2619
2895
  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>`;
2620
2896
  strXml += " <c:numCache>";
2621
- strXml += " <c:formatCode>" + (opts.valLabelFormatCode || opts.dataTableFormatCode || "General") + "</c:formatCode>";
2897
+ strXml += " <c:formatCode>" + valFmtCode + "</c:formatCode>";
2622
2898
  strXml += ` <c:ptCount val="${obj.labels[0].length}"/>`;
2623
2899
  obj.values.forEach((value, idx) => {
2624
- if (value != null) strXml += `<c:pt idx="${idx}"><c:v>${value}</c:v></c:pt>`;
2900
+ strXml += numCachePt(idx, value);
2625
2901
  });
2626
2902
  strXml += " </c:numCache>";
2627
2903
  strXml += " </c:numRef>";
@@ -2637,7 +2913,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2637
2913
  strXml += " <a:p><a:pPr>";
2638
2914
  strXml += ` <a:defRPr b="${opts.dataLabelFontBold ? 1 : 0}" i="${opts.dataLabelFontItalic ? 1 : 0}" strike="noStrike" sz="${Math.round((opts.dataLabelFontSize || 12) * 100)}" u="none">`;
2639
2915
  strXml += " <a:solidFill>" + createColorElement(opts.dataLabelColor || "000000") + "</a:solidFill>";
2640
- strXml += " <a:latin typeface=\"" + (opts.dataLabelFontFace || "Arial") + "\"/>";
2916
+ strXml += " " + createChartTextFonts(opts.dataLabelFontFace || "Arial");
2641
2917
  strXml += " </a:defRPr>";
2642
2918
  strXml += " </a:pPr></a:p>";
2643
2919
  strXml += " </c:txPr>";
@@ -2653,6 +2929,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2653
2929
  if (chartType === "bar") {
2654
2930
  strXml += ` <c:gapWidth val="${opts.barGapWidthPct}"/>`;
2655
2931
  strXml += ` <c:overlap val="${opts.barOverlapPct != null ? opts.barOverlapPct : (opts.barGrouping || "").includes("tacked") ? 100 : 0}"/>`;
2932
+ strXml += createSerLinesElement(opts.barSeriesLine);
2656
2933
  } else if (chartType === "bar3D") {
2657
2934
  strXml += ` <c:gapWidth val="${opts.barGapWidthPct}"/>`;
2658
2935
  strXml += ` <c:gapDepth val="${opts.barGapDepthPct}"/>`;
@@ -2704,6 +2981,10 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2704
2981
  strXml += "<a:effectLst/>";
2705
2982
  strXml += "</c:spPr>";
2706
2983
  strXml += "</c:marker>";
2984
+ {
2985
+ const scatterVaryColors = data.length === 1 && opts.chartColors !== BARCHART_COLORS ? opts.chartColors || BARCHART_COLORS : null;
2986
+ strXml += makeSeriesDataPointsXml(chartType, obj, opts, scatterVaryColors);
2987
+ }
2707
2988
  if (opts.showLabel) {
2708
2989
  const chartUuid = getUuid("-xxxx-xxxx-xxxx-xxxxxxxxxxxx");
2709
2990
  if (obj.labels[0] && (opts.dataLabelFormatScatter === "custom" || opts.dataLabelFormatScatter === "customXY")) {
@@ -2722,13 +3003,13 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2722
3003
  strXml += " <a:pPr>";
2723
3004
  strXml += ` <a:defRPr sz="${Math.round((opts.dataLabelFontSize || 12) * 100)}" b="${opts.dataLabelFontBold ? "1" : "0"}" i="${opts.dataLabelFontItalic ? "1" : "0"}" u="none" strike="noStrike">`;
2724
3005
  strXml += " <a:solidFill>" + createColorElement(opts.dataLabelColor || "000000") + "</a:solidFill>";
2725
- strXml += ` <a:latin typeface="${opts.dataLabelFontFace || "Arial"}"/>`;
3006
+ strXml += " " + createChartTextFonts(opts.dataLabelFontFace || "Arial");
2726
3007
  strXml += " </a:defRPr>";
2727
3008
  strXml += " </a:pPr>";
2728
3009
  strXml += " <a:r>";
2729
3010
  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">`;
2730
3011
  strXml += " <a:solidFill>" + createColorElement(opts.dataLabelColor || "000000") + "</a:solidFill>";
2731
- strXml += ` <a:latin typeface="${opts.dataLabelFontFace || "Arial"}"/>`;
3012
+ strXml += " " + createChartTextFonts(opts.dataLabelFontFace || "Arial");
2732
3013
  strXml += " </a:rPr>";
2733
3014
  strXml += " <a:t>" + encodeXmlEntities(label) + "</a:t>";
2734
3015
  strXml += " </a:r>";
@@ -2808,7 +3089,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2808
3089
  strXml += " <a:pPr>";
2809
3090
  strXml += ` <a:defRPr sz="${Math.round((opts.dataLabelFontSize || 12) * 100)}" b="${opts.dataLabelFontBold ? "1" : "0"}" i="${opts.dataLabelFontItalic ? "1" : "0"}" u="none" strike="noStrike">`;
2810
3091
  strXml += " <a:solidFill>" + createColorElement(opts.dataLabelColor || "000000") + "</a:solidFill>";
2811
- strXml += ` <a:latin typeface="${opts.dataLabelFontFace || "Arial"}"/>`;
3092
+ strXml += " " + createChartTextFonts(opts.dataLabelFontFace || "Arial");
2812
3093
  strXml += " </a:defRPr>";
2813
3094
  strXml += " </a:pPr>";
2814
3095
  strXml += ` <a:endParaRPr lang="${opts.lang || "en-US"}"/>`;
@@ -2829,31 +3110,14 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2829
3110
  strXml += "</c:dLbls>";
2830
3111
  }
2831
3112
  }
2832
- if (data.length === 1 && opts.chartColors !== BARCHART_COLORS) obj.values.forEach((value, index) => {
2833
- const arrColors = value < 0 ? opts.invertedColors || opts.chartColors || BARCHART_COLORS : opts.chartColors || [];
2834
- strXml += " <c:dPt>";
2835
- strXml += ` <c:idx val="${index}"/>`;
2836
- strXml += " <c:invertIfNegative val=\"0\"/>";
2837
- strXml += " <c:bubble3D val=\"0\"/>";
2838
- strXml += " <c:spPr>";
2839
- if (opts.lineSize === 0) strXml += "<a:ln><a:noFill/></a:ln>";
2840
- else {
2841
- strXml += "<a:solidFill>";
2842
- strXml += " <a:srgbClr val=\"" + arrColors[index % arrColors.length] + "\"/>";
2843
- strXml += "</a:solidFill>";
2844
- }
2845
- strXml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW);
2846
- strXml += " </c:spPr>";
2847
- strXml += " </c:dPt>";
2848
- });
2849
3113
  strXml += "<c:xVal>";
2850
3114
  strXml += " <c:numRef>";
2851
3115
  strXml += ` <c:f>Sheet1!$A$2:$A$${data[0].values.length + 1}</c:f>`;
2852
3116
  strXml += " <c:numCache>";
2853
- strXml += " <c:formatCode>General</c:formatCode>";
3117
+ strXml += " <c:formatCode>" + valFmtCode + "</c:formatCode>";
2854
3118
  strXml += ` <c:ptCount val="${data[0].values.length}"/>`;
2855
3119
  data[0].values.forEach((value, idx) => {
2856
- if (value != null) strXml += `<c:pt idx="${idx}"><c:v>${value}</c:v></c:pt>`;
3120
+ strXml += numCachePt(idx, value);
2857
3121
  });
2858
3122
  strXml += " </c:numCache>";
2859
3123
  strXml += " </c:numRef>";
@@ -2862,10 +3126,10 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2862
3126
  strXml += " <c:numRef>";
2863
3127
  strXml += ` <c:f>Sheet1!$${getExcelColName(idx + 2)}$2:$${getExcelColName(idx + 2)}$${data[0].values.length + 1}</c:f>`;
2864
3128
  strXml += " <c:numCache>";
2865
- strXml += " <c:formatCode>General</c:formatCode>";
3129
+ strXml += " <c:formatCode>" + valFmtCode + "</c:formatCode>";
2866
3130
  strXml += ` <c:ptCount val="${data[0].values.length}"/>`;
2867
3131
  data[0].values.forEach((_value, idx) => {
2868
- if (obj.values[idx] != null) strXml += `<c:pt idx="${idx}"><c:v>${obj.values[idx]}</c:v></c:pt>`;
3132
+ strXml += numCachePt(idx, obj.values[idx]);
2869
3133
  });
2870
3134
  strXml += " </c:numCache>";
2871
3135
  strXml += " </c:numRef>";
@@ -2881,7 +3145,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2881
3145
  strXml += " <a:p><a:pPr>";
2882
3146
  strXml += ` <a:defRPr b="${opts.dataLabelFontBold ? "1" : "0"}" i="${opts.dataLabelFontItalic ? "1" : "0"}" strike="noStrike" sz="${Math.round((opts.dataLabelFontSize || 12) * 100)}" u="none">`;
2883
3147
  strXml += " <a:solidFill>" + createColorElement(opts.dataLabelColor || "000000") + "</a:solidFill>";
2884
- strXml += " <a:latin typeface=\"" + (opts.dataLabelFontFace || "Arial") + "\"/>";
3148
+ strXml += " " + createChartTextFonts(opts.dataLabelFontFace || "Arial");
2885
3149
  strXml += " </a:defRPr>";
2886
3150
  strXml += " </a:pPr></a:p>";
2887
3151
  strXml += " </c:txPr>";
@@ -2931,10 +3195,10 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2931
3195
  strXml += " <c:numRef>";
2932
3196
  strXml += ` <c:f>Sheet1!$A$2:$A$${data[0].values.length + 1}</c:f>`;
2933
3197
  strXml += " <c:numCache>";
2934
- strXml += " <c:formatCode>General</c:formatCode>";
3198
+ strXml += " <c:formatCode>" + valFmtCode + "</c:formatCode>";
2935
3199
  strXml += ` <c:ptCount val="${data[0].values.length}"/>`;
2936
3200
  data[0].values.forEach((value, idx) => {
2937
- strXml += `<c:pt idx="${idx}"><c:v>${value || value === 0 ? value : ""}</c:v></c:pt>`;
3201
+ strXml += numCachePt(idx, value);
2938
3202
  });
2939
3203
  strXml += " </c:numCache>";
2940
3204
  strXml += " </c:numRef>";
@@ -2944,10 +3208,10 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2944
3208
  strXml += `<c:f>Sheet1!$${getExcelColName(idxColLtr + 1)}$2:$${getExcelColName(idxColLtr + 1)}$${data[0].values.length + 1}</c:f>`;
2945
3209
  idxColLtr++;
2946
3210
  strXml += " <c:numCache>";
2947
- strXml += " <c:formatCode>General</c:formatCode>";
3211
+ strXml += " <c:formatCode>" + valFmtCode + "</c:formatCode>";
2948
3212
  strXml += ` <c:ptCount val="${data[0].values.length}"/>`;
2949
3213
  data[0].values.forEach((_value, idx) => {
2950
- strXml += `<c:pt idx="${idx}"><c:v>${obj.values[idx] || obj.values[idx] === 0 ? obj.values[idx] : ""}</c:v></c:pt>`;
3214
+ strXml += numCachePt(idx, obj.values[idx]);
2951
3215
  });
2952
3216
  strXml += " </c:numCache>";
2953
3217
  strXml += " </c:numRef>";
@@ -2960,7 +3224,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2960
3224
  strXml += " <c:formatCode>General</c:formatCode>";
2961
3225
  strXml += ` <c:ptCount val="${obj.sizes.length}"/>`;
2962
3226
  obj.sizes.forEach((value, idx) => {
2963
- strXml += `<c:pt idx="${idx}"><c:v>${value ?? ""}</c:v></c:pt>`;
3227
+ strXml += numCachePt(idx, value);
2964
3228
  });
2965
3229
  strXml += " </c:numCache>";
2966
3230
  strXml += " </c:numRef>";
@@ -2973,12 +3237,12 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
2973
3237
  strXml += "<c:txPr><a:bodyPr/><a:lstStyle/><a:p><a:pPr>";
2974
3238
  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">`;
2975
3239
  strXml += `<a:solidFill>${createColorElement(opts.dataLabelColor || "000000")}</a:solidFill>`;
2976
- strXml += `<a:latin typeface="${opts.dataLabelFontFace || "Arial"}"/>`;
3240
+ strXml += createChartTextFonts(opts.dataLabelFontFace || "Arial");
2977
3241
  strXml += "</a:defRPr></a:pPr></a:p></c:txPr>";
2978
3242
  if (opts.dataLabelPosition) strXml += `<c:dLblPos val="${opts.dataLabelPosition}"/>`;
2979
3243
  strXml += "<c:showLegendKey val=\"0\"/>";
2980
3244
  strXml += `<c:showVal val="${opts.showValue ? "1" : "0"}"/>`;
2981
- strXml += `<c:showCatName val="0"/><c:showSerName val="${opts.showSerName ? "1" : "0"}"/><c:showPercent val="0"/><c:showBubbleSize val="0"/>`;
3245
+ strXml += `<c:showCatName val="0"/><c:showSerName val="${opts.showSerName ? "1" : "0"}"/><c:showPercent val="0"/><c:showBubbleSize val="${opts.showBubbleSize ? "1" : "0"}"/>`;
2982
3246
  strXml += "<c:extLst>";
2983
3247
  strXml += " <c:ext uri=\"{CE6537A1-D6FC-4f65-9D91-7224C49458BB}\" xmlns:c15=\"http://schemas.microsoft.com/office/drawing/2012/chart\">";
2984
3248
  strXml += " <c15:showLeaderLines val=\"" + (opts.showLeaderLines ? "1" : "0") + "\"/>";
@@ -3012,33 +3276,37 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
3012
3276
  else strXml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW);
3013
3277
  strXml += " </c:spPr>";
3014
3278
  optsChartData.labels[0].forEach((_label, idx) => {
3279
+ const ptStyle = optsChartData.pointStyles?.[idx];
3015
3280
  strXml += "<c:dPt>";
3016
3281
  strXml += ` <c:idx val="${idx}"/>`;
3017
3282
  strXml += " <c:bubble3D val=\"0\"/>";
3018
3283
  strXml += " <c:spPr>";
3019
- strXml += `<a:solidFill>${createColorElement(opts.chartColors[idx + 1 > opts.chartColors.length ? Math.floor(Math.random() * opts.chartColors.length) : idx])}</a:solidFill>`;
3020
- 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>`;
3284
+ strXml += `<a:solidFill>${createColorElement(ptStyle?.fill || opts.chartColors[idx + 1 > opts.chartColors.length ? Math.floor(Math.random() * opts.chartColors.length) : idx])}</a:solidFill>`;
3285
+ if (ptStyle?.border) strXml += createChartBorderLine(ptStyle.border);
3286
+ 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>`;
3021
3287
  strXml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW);
3022
3288
  strXml += " </c:spPr>";
3023
3289
  strXml += "</c:dPt>";
3024
3290
  });
3025
3291
  strXml += "<c:dLbls>";
3026
3292
  optsChartData.labels[0].forEach((_label, idx) => {
3293
+ const customLbl = optsChartData.customLabels?.[idx];
3027
3294
  strXml += "<c:dLbl>";
3028
3295
  strXml += ` <c:idx val="${idx}"/>`;
3296
+ 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>`;
3029
3297
  strXml += ` <c:numFmt formatCode="${encodeXmlEntities(opts.dataLabelFormatCode) || "General"}" sourceLinked="0"/>`;
3030
3298
  strXml += " <c:spPr/><c:txPr>";
3031
3299
  strXml += " <a:bodyPr/><a:lstStyle/>";
3032
3300
  strXml += " <a:p><a:pPr>";
3033
3301
  strXml += ` <a:defRPr sz="${Math.round((opts.dataLabelFontSize || 12) * 100)}" b="${opts.dataLabelFontBold ? 1 : 0}" i="${opts.dataLabelFontItalic ? 1 : 0}" u="none" strike="noStrike">`;
3034
3302
  strXml += " <a:solidFill>" + createColorElement(opts.dataLabelColor || "000000") + "</a:solidFill>";
3035
- strXml += ` <a:latin typeface="${opts.dataLabelFontFace || "Arial"}"/>`;
3303
+ strXml += " " + createChartTextFonts(opts.dataLabelFontFace || "Arial");
3036
3304
  strXml += " </a:defRPr>";
3037
3305
  strXml += " </a:pPr></a:p>";
3038
3306
  strXml += " </c:txPr>";
3039
3307
  if (chartType === "pie" && opts.dataLabelPosition) strXml += `<c:dLblPos val="${opts.dataLabelPosition}"/>`;
3040
3308
  strXml += " <c:showLegendKey val=\"0\"/>";
3041
- strXml += " <c:showVal val=\"" + (opts.showValue ? "1" : "0") + "\"/>";
3309
+ strXml += " <c:showVal val=\"" + (customLbl ? "0" : opts.showValue ? "1" : "0") + "\"/>";
3042
3310
  strXml += " <c:showCatName val=\"" + (opts.showLabel ? "1" : "0") + "\"/>";
3043
3311
  strXml += " <c:showSerName val=\"" + (opts.showSerName ? "1" : "0") + "\"/>";
3044
3312
  strXml += " <c:showPercent val=\"" + (opts.showPercent ? "1" : "0") + "\"/>";
@@ -3053,7 +3321,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
3053
3321
  strXml += " <a:pPr>";
3054
3322
  strXml += ` <a:defRPr sz="${Math.round((opts.dataLabelFontSize || 12) * 100)}" b="${opts.dataLabelFontBold ? "1" : "0"}" i="${opts.dataLabelFontItalic ? "1" : "0"}" u="none" strike="noStrike">`;
3055
3323
  strXml += " <a:solidFill>" + createColorElement(opts.dataLabelColor || "000000") + "</a:solidFill>";
3056
- strXml += ` <a:latin typeface="${opts.dataLabelFontFace || "Arial"}"/>`;
3324
+ strXml += " " + createChartTextFonts(opts.dataLabelFontFace || "Arial");
3057
3325
  strXml += " </a:defRPr>";
3058
3326
  strXml += " </a:pPr>";
3059
3327
  strXml += " </a:p>";
@@ -3082,6 +3350,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId) {
3082
3350
  strXml += " <c:numRef>";
3083
3351
  strXml += ` <c:f>Sheet1!$B$2:$B$${optsChartData.labels[0].length + 1}</c:f>`;
3084
3352
  strXml += " <c:numCache>";
3353
+ strXml += " <c:formatCode>" + valFmtCode + "</c:formatCode>";
3085
3354
  strXml += ` <c:ptCount val="${optsChartData.labels[0].length}"/>`;
3086
3355
  optsChartData.values.forEach((value, idx) => {
3087
3356
  strXml += `<c:pt idx="${idx}"><c:v>${value || value === 0 ? value : ""}</c:v></c:pt>`;
@@ -3157,7 +3426,7 @@ function makeCatAxis(opts, axisId, valAxisId) {
3157
3426
  strXml += " <a:pPr>";
3158
3427
  strXml += ` <a:defRPr sz="${Math.round((opts.catAxisLabelFontSize || 12) * 100)}" b="${opts.catAxisLabelFontBold ? 1 : 0}" i="${opts.catAxisLabelFontItalic ? 1 : 0}" u="none" strike="noStrike">`;
3159
3428
  strXml += " <a:solidFill>" + createColorElement(opts.catAxisLabelColor || "000000") + "</a:solidFill>";
3160
- strXml += " <a:latin typeface=\"" + (opts.catAxisLabelFontFace || "Arial") + "\"/>";
3429
+ strXml += " " + createChartTextFonts(opts.catAxisLabelFontFace || "Arial");
3161
3430
  strXml += " </a:defRPr>";
3162
3431
  strXml += " </a:pPr>";
3163
3432
  strXml += " <a:endParaRPr lang=\"" + (opts.lang || "en-US") + "\"/>";
@@ -3250,7 +3519,7 @@ function makeValAxis(opts, valAxisId) {
3250
3519
  strXml += " <a:pPr>";
3251
3520
  strXml += ` <a:defRPr sz="${Math.round((opts.valAxisLabelFontSize || 12) * 100)}" b="${opts.valAxisLabelFontBold ? 1 : 0}" i="${opts.valAxisLabelFontItalic ? 1 : 0}" u="none" strike="noStrike">`;
3252
3521
  strXml += " <a:solidFill>" + createColorElement(opts.valAxisLabelColor || "000000") + "</a:solidFill>";
3253
- strXml += " <a:latin typeface=\"" + (opts.valAxisLabelFontFace || "Arial") + "\"/>";
3522
+ strXml += " " + createChartTextFonts(opts.valAxisLabelFontFace || "Arial");
3254
3523
  strXml += " </a:defRPr>";
3255
3524
  strXml += " </a:pPr>";
3256
3525
  strXml += " <a:endParaRPr lang=\"" + (opts.lang || "en-US") + "\"/>";
@@ -3306,7 +3575,7 @@ function makeSerAxis(opts, axisId, valAxisId) {
3306
3575
  strXml += " <a:pPr>";
3307
3576
  strXml += ` <a:defRPr sz="${Math.round((opts.serAxisLabelFontSize || 12) * 100)}" b="${opts.serAxisLabelFontBold ? "1" : "0"}" i="${opts.serAxisLabelFontItalic ? "1" : "0"}" u="none" strike="noStrike">`;
3308
3577
  strXml += ` <a:solidFill>${createColorElement(opts.serAxisLabelColor || "000000")}</a:solidFill>`;
3309
- strXml += ` <a:latin typeface="${opts.serAxisLabelFontFace || "Arial"}"/>`;
3578
+ strXml += " " + createChartTextFonts(opts.serAxisLabelFontFace || "Arial");
3310
3579
  strXml += " </a:defRPr>";
3311
3580
  strXml += " </a:pPr>";
3312
3581
  strXml += " <a:endParaRPr lang=\"" + (opts.lang || "en-US") + "\"/>";
@@ -3346,17 +3615,31 @@ function genXmlTitle(opts, chartX, chartY) {
3346
3615
  const rotate = opts.titleRotate ? `<a:bodyPr rot="${convertRotationDegrees(opts.titleRotate)}"/>` : "<a:bodyPr/>";
3347
3616
  const sizeAttr = opts.fontSize ? `sz="${Math.round(opts.fontSize * 100)}"` : "";
3348
3617
  const titleBold = opts.titleBold ? 1 : 0;
3618
+ const titleItalic = opts.titleItalic ? 1 : 0;
3619
+ const titleUnderline = opts.titleUnderline ? "sng" : "none";
3349
3620
  let layout = "<c:layout/>";
3350
- if (opts.titlePos && typeof opts.titlePos.x === "number" && typeof opts.titlePos.y === "number") {
3351
- const totalX = opts.titlePos.x + chartX;
3352
- const totalY = opts.titlePos.y + chartY;
3353
- let valX = totalX === 0 ? 0 : totalX * (totalX / 5) / 10;
3354
- if (valX >= 1) valX = valX / 10;
3355
- if (valX >= .1) valX = valX / 10;
3356
- let valY = totalY === 0 ? 0 : totalY * (totalY / 5) / 10;
3357
- if (valY >= 1) valY = valY / 10;
3358
- if (valY >= .1) valY = valY / 10;
3359
- 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>`;
3621
+ const hasX = opts.titlePos && typeof opts.titlePos.x === "number";
3622
+ const hasY = opts.titlePos && typeof opts.titlePos.y === "number";
3623
+ if (hasX || hasY) {
3624
+ let modes = "";
3625
+ let vals = "";
3626
+ if (hasX) {
3627
+ const totalX = opts.titlePos.x + chartX;
3628
+ let valX = totalX === 0 ? 0 : totalX * (totalX / 5) / 10;
3629
+ if (valX >= 1) valX = valX / 10;
3630
+ if (valX >= .1) valX = valX / 10;
3631
+ modes += "<c:xMode val=\"edge\"/>";
3632
+ vals += `<c:x val="${valX}"/>`;
3633
+ }
3634
+ if (hasY) {
3635
+ const totalY = opts.titlePos.y + chartY;
3636
+ let valY = totalY === 0 ? 0 : totalY * (totalY / 5) / 10;
3637
+ if (valY >= 1) valY = valY / 10;
3638
+ if (valY >= .1) valY = valY / 10;
3639
+ modes += "<c:yMode val=\"edge\"/>";
3640
+ vals += `<c:y val="${valY}"/>`;
3641
+ }
3642
+ layout = `<c:layout><c:manualLayout>${modes}${vals}</c:manualLayout></c:layout>`;
3360
3643
  }
3361
3644
  return `<c:title>
3362
3645
  <c:tx>
@@ -3365,15 +3648,15 @@ function genXmlTitle(opts, chartX, chartY) {
3365
3648
  <a:lstStyle/>
3366
3649
  <a:p>
3367
3650
  ${align}
3368
- <a:defRPr ${sizeAttr} b="${titleBold}" i="0" u="none" strike="noStrike">
3651
+ <a:defRPr ${sizeAttr} b="${titleBold}" i="${titleItalic}" u="${titleUnderline}" strike="noStrike">
3369
3652
  <a:solidFill>${createColorElement(opts.color || "000000")}</a:solidFill>
3370
- <a:latin typeface="${opts.fontFace || "Arial"}"/>
3653
+ ${createChartTextFonts(opts.fontFace || "Arial")}
3371
3654
  </a:defRPr>
3372
3655
  </a:pPr>
3373
3656
  <a:r>
3374
- <a:rPr ${sizeAttr} b="${titleBold}" i="0" u="none" strike="noStrike">
3657
+ <a:rPr ${sizeAttr} b="${titleBold}" i="${titleItalic}" u="${titleUnderline}" strike="noStrike">
3375
3658
  <a:solidFill>${createColorElement(opts.color || "000000")}</a:solidFill>
3376
- <a:latin typeface="${opts.fontFace || "Arial"}"/>
3659
+ ${createChartTextFonts(opts.fontFace || "Arial")}
3377
3660
  </a:rPr>
3378
3661
  <a:t>${encodeXmlEntities(opts.title) || ""}</a:t>
3379
3662
  </a:r>
@@ -3447,11 +3730,105 @@ function createGridLineElement(glOpts) {
3447
3730
  strXml += "</c:majorGridlines>";
3448
3731
  return strXml;
3449
3732
  }
3450
- function createLineCap(lineCap) {
3451
- if (!lineCap || lineCap === "flat") return "flat";
3452
- else if (lineCap === "square") return "sq";
3453
- else if (lineCap === "round") return "rnd";
3454
- else throw new Error(`Invalid chart line cap: ${lineCap}`);
3733
+ /**
3734
+ * Build a `<c:pt>` numeric-cache data point, or '' to leave a gap.
3735
+ *
3736
+ * `<c:v>` inside a `<c:numCache>` is an `xsd:double`; emitting `NaN`, `Infinity`
3737
+ * or an empty string yields an invalid value that makes PowerPoint report the
3738
+ * package as needing repair. Null/undefined are intentional gaps and are skipped
3739
+ * silently (a sparse, idx-keyed cache is valid); other non-finite numbers are
3740
+ * skipped with a warning, per the library's "warn rather than emit a degenerate
3741
+ * result" policy.
3742
+ * @param idx - zero-based data-point index (emitted as `idx`)
3743
+ * @param value - numeric value (or null/undefined gap)
3744
+ */
3745
+ function numCachePt(idx, value) {
3746
+ if (value == null) return "";
3747
+ if (!Number.isFinite(value)) {
3748
+ console.warn(`Warning: chart value "${value}" at index ${idx} is not a finite number; data point omitted.`);
3749
+ return "";
3750
+ }
3751
+ return `<c:pt idx="${idx}"><c:v>${value}</c:v></c:pt>`;
3752
+ }
3753
+ /**
3754
+ * Build a `<c:serLines>` ("Series Lines") element for a bar chart.
3755
+ * @param opt - `true` for PowerPoint automatic styling, an {@link OptsChartGridLine}
3756
+ * to customize the line, or falsy / `{ style: 'none' }` to omit the element.
3757
+ */
3758
+ function createSerLinesElement(opt) {
3759
+ if (!opt) return "";
3760
+ if (opt === true) return "<c:serLines/>";
3761
+ if (opt.style === "none") return "";
3762
+ let strXml = "<c:serLines><c:spPr>";
3763
+ strXml += `<a:ln w="${valToPts(opt.size || DEF_CHART_GRIDLINE.size)}" cap="${createLineCap(opt.cap || DEF_CHART_GRIDLINE.cap)}">`;
3764
+ strXml += `<a:solidFill><a:srgbClr val="${opt.color || DEF_CHART_GRIDLINE.color}"/></a:solidFill>`;
3765
+ strXml += `<a:prstDash val="${opt.style || DEF_CHART_GRIDLINE.style}"/><a:round/>`;
3766
+ strXml += "</a:ln></c:spPr></c:serLines>";
3767
+ return strXml;
3768
+ }
3769
+ function makeCustomDLblXml(idx, text, opts) {
3770
+ const sz = Math.round((opts.dataLabelFontSize || 12) * 100);
3771
+ const bold = opts.dataLabelFontBold ? "1" : "0";
3772
+ const italic = opts.dataLabelFontItalic ? "1" : "0";
3773
+ const color = createColorElement(opts.dataLabelColor || "000000");
3774
+ const face = opts.dataLabelFontFace || "Arial";
3775
+ const lang = opts.lang || "en-US";
3776
+ 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>`;
3777
+ }
3778
+ /**
3779
+ * Build an `<a:ln>` border element from a per-data-point `BorderProps`.
3780
+ * @param border - point border style (`type`, `color`, `pt`)
3781
+ */
3782
+ function createChartBorderLine(border) {
3783
+ if (border.type === "none") return "<a:ln><a:noFill/></a:ln>";
3784
+ const dash = border.type === "dash" ? "dash" : "solid";
3785
+ 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>`;
3786
+ }
3787
+ /**
3788
+ * Build `<c:dPt>` entries for a series in the bar/line/area/scatter loops.
3789
+ *
3790
+ * Merges two sources into a single `c:dPt` per index so we never emit a
3791
+ * duplicate `<c:idx>` (which corrupts the chart):
3792
+ * - legacy single-series color-vary fills (bar/scatter), supplied via `varyColors`
3793
+ * - per-point `pointStyles` border/fill overrides
3794
+ *
3795
+ * Must be emitted in schema position *before* `c:dLbls` (CT_*Ser order).
3796
+ * RADAR is skipped: extra per-point markup historically corrupts the chart.
3797
+ *
3798
+ * @param chartType - series chart type
3799
+ * @param obj - series data (reads `values`, `pointStyles`)
3800
+ * @param opts - chart options (fill/shadow/lineSize context)
3801
+ * @param varyColors - color array when single-series color-vary applies, else `null`
3802
+ */
3803
+ function makeSeriesDataPointsXml(chartType, obj, opts, varyColors) {
3804
+ if (chartType === "radar") return "";
3805
+ const pointStyles = obj.pointStyles;
3806
+ if (!varyColors && !pointStyles?.length) return "";
3807
+ const isBar = chartType === "bar" || chartType === "bar3D";
3808
+ const isScatter = chartType === "scatter";
3809
+ let xml = "";
3810
+ obj.values.forEach((value, index) => {
3811
+ const ptStyle = pointStyles?.[index];
3812
+ const arrColors = varyColors ? value < 0 ? opts.invertedColors || opts.chartColors || BARCHART_COLORS : varyColors : null;
3813
+ const fillColor = ptStyle?.fill || (arrColors ? arrColors[index % arrColors.length] : null);
3814
+ const border = ptStyle?.border;
3815
+ if (!fillColor && !border) return;
3816
+ xml += "<c:dPt>";
3817
+ xml += `<c:idx val="${index}"/>`;
3818
+ if (isBar) xml += "<c:invertIfNegative val=\"0\"/>";
3819
+ xml += "<c:bubble3D val=\"0\"/>";
3820
+ xml += "<c:spPr>";
3821
+ if ((isBar || isScatter) && opts.lineSize === 0 && !border && !ptStyle?.fill) xml += "<a:ln><a:noFill/></a:ln>";
3822
+ else {
3823
+ if (fillColor) if (chartType === "bar3D") xml += `<a:ln><a:solidFill>${createColorElement(fillColor)}</a:solidFill></a:ln>`;
3824
+ else xml += `<a:solidFill>${createColorElement(fillColor)}</a:solidFill>`;
3825
+ if (border) xml += createChartBorderLine(border);
3826
+ }
3827
+ xml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW);
3828
+ xml += "</c:spPr>";
3829
+ xml += "</c:dPt>";
3830
+ });
3831
+ return xml;
3455
3832
  }
3456
3833
  //#endregion
3457
3834
  //#region src/gen-media.ts
@@ -3500,6 +3877,37 @@ function encodeSlideMediaRels(layout, runtime) {
3500
3877
  /**
3501
3878
  * PptxGenJS: XML Generation
3502
3879
  */
3880
+ const _warnedTextRangeMsgs = /* @__PURE__ */ new Set();
3881
+ function warnTextRangeOnce(msg) {
3882
+ if (_warnedTextRangeMsgs.has(msg)) return;
3883
+ _warnedTextRangeMsgs.add(msg);
3884
+ console.warn(msg);
3885
+ }
3886
+ /**
3887
+ * Clamp a font size (points) into ST_TextFontSize (1-4000pt) and return it in
3888
+ * hundredths of a point for the `sz` attribute. Out-of-range sizes make
3889
+ * PowerPoint report the package as needing repair (e.g. `sz` > 400000 or < 100).
3890
+ */
3891
+ function clampFontSizeSz(fontSizePts) {
3892
+ const raw = Math.round(fontSizePts * 100);
3893
+ const clamped = Math.min(4e5, Math.max(100, raw));
3894
+ if (clamped !== raw) warnTextRangeOnce(`Warning: fontSize ${fontSizePts} is outside the valid range 1-4000pt; using ${clamped / 100}.`);
3895
+ return clamped;
3896
+ }
3897
+ /** Clamp character spacing (points) into ST_TextPoint (-4000..4000pt); returns hundredths for the `spc` attribute. */
3898
+ function clampCharSpacingSpc(charSpacingPts) {
3899
+ const raw = Math.round(charSpacingPts * 100);
3900
+ const clamped = Math.min(4e5, Math.max(-4e5, raw));
3901
+ if (clamped !== raw) warnTextRangeOnce(`Warning: charSpacing ${charSpacingPts} is outside the valid range -4000..4000pt; using ${clamped / 100}.`);
3902
+ return clamped;
3903
+ }
3904
+ /** Clamp line spacing (points) into ST_TextSpacingPoint (0..1584pt); returns hundredths for `<a:spcPts val>`. */
3905
+ function clampLineSpacingPts(lineSpacingPts) {
3906
+ const raw = Math.round(lineSpacingPts * 100);
3907
+ const clamped = Math.min(158400, Math.max(0, raw));
3908
+ if (clamped !== raw) warnTextRangeOnce(`Warning: lineSpacing ${lineSpacingPts} is outside the valid range 0-1584pt; using ${clamped / 100}.`);
3909
+ return clamped;
3910
+ }
3503
3911
  const ImageSizingXml = {
3504
3912
  cover: function(imgSize, boxDim) {
3505
3913
  const imgRatio = imgSize.h / imgSize.w;
@@ -3524,6 +3932,15 @@ const ImageSizingXml = {
3524
3932
  const r = imgSize.w - (boxDim.x + boxDim.w);
3525
3933
  const t = boxDim.y;
3526
3934
  const b = imgSize.h - (boxDim.y + boxDim.h);
3935
+ if (l < 0 || r < 0 || t < 0 || b < 0) {
3936
+ const over = [
3937
+ l < 0 && `x (${l < 0 ? -l : 0} past left edge)`,
3938
+ r < 0 && `x+w (${-r} past right edge)`,
3939
+ t < 0 && `y (${-t} past top edge)`,
3940
+ b < 0 && `y+h (${-b} past bottom edge)`
3941
+ ].filter(Boolean).join(", ");
3942
+ throw new Error(`addImage sizing.type 'crop': crop window overflows image bounds — ${over}. Ensure x≥0, y≥0, x+w≤w, y+h≤h.`);
3943
+ }
3527
3944
  return `<a:srcRect l="${Math.round(1e5 * (l / imgSize.w))}" r="${Math.round(1e5 * (r / imgSize.w))}" t="${Math.round(1e5 * (t / imgSize.h))}" b="${Math.round(1e5 * (b / imgSize.h))}"/><a:stretch/>`;
3528
3945
  }
3529
3946
  };
@@ -3536,18 +3953,91 @@ const ImageSizingXml = {
3536
3953
  * @param {number} cy - shape height (EMU), used to scale `rectRadius`
3537
3954
  * @return {string} `<a:prstGeom>` XML
3538
3955
  */
3956
+ const RECT_RADIUS_ADJ1_SHAPES = new Set(["round2SameRect", "round2DiagRect"]);
3957
+ const SHAPE_LOCK_ATTRS = [
3958
+ "noGrp",
3959
+ "noSelect",
3960
+ "noRot",
3961
+ "noChangeAspect",
3962
+ "noMove",
3963
+ "noResize",
3964
+ "noEditPoints",
3965
+ "noAdjustHandles",
3966
+ "noChangeArrowheads",
3967
+ "noChangeShapeType",
3968
+ "noTextEdit"
3969
+ ];
3970
+ const PICTURE_LOCK_ATTRS = [
3971
+ "noGrp",
3972
+ "noSelect",
3973
+ "noRot",
3974
+ "noChangeAspect",
3975
+ "noMove",
3976
+ "noResize",
3977
+ "noEditPoints",
3978
+ "noAdjustHandles",
3979
+ "noChangeArrowheads",
3980
+ "noChangeShapeType",
3981
+ "noCrop"
3982
+ ];
3983
+ const GRAPHIC_FRAME_LOCK_ATTRS = [
3984
+ "noGrp",
3985
+ "noDrilldown",
3986
+ "noSelect",
3987
+ "noChangeAspect",
3988
+ "noMove",
3989
+ "noResize"
3990
+ ];
3991
+ /**
3992
+ * Serialize an object-lock element (`a:spLocks` / `a:picLocks` / `a:graphicFrameLocks`).
3993
+ * Only flags set to `true` AND valid for this element type are emitted; a flag set on an
3994
+ * unsupported element type is dropped with a warning (silent coercion is a footgun).
3995
+ * @param tag - locking element tag, e.g. `'a:spLocks'`
3996
+ * @param allowed - attribute names this element type supports, in desired emit order
3997
+ * @param locks - merged lock flags (callers fold any hard-coded default in first)
3998
+ * @param objectName - for the warning message
3999
+ * @returns the locking element string, or `''` when no applicable flag is set
4000
+ */
4001
+ function genXmlObjectLock(tag, allowed, locks, objectName) {
4002
+ if (!locks) return "";
4003
+ const lockMap = locks;
4004
+ 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.`);
4005
+ const attrs = allowed.filter((name) => lockMap[name] === true).map((name) => `${name}="1"`);
4006
+ return attrs.length > 0 ? `<${tag} ${attrs.join(" ")}/>` : "";
4007
+ }
3539
4008
  function genXmlPresetGeom(shapeName, options, cx, cy) {
3540
- let strXml = `<a:prstGeom prst="${shapeName}"><a:avLst>`;
3541
- if (options.rectRadius) strXml += `<a:gd name="adj" fmla="val ${Math.round(options.rectRadius * EMU * 1e5 / Math.min(cx, cy))}"/>`;
3542
- else if (options.angleRange) {
4009
+ 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.`);
4010
+ let avLst = "";
4011
+ const emittedAdjNames = /* @__PURE__ */ new Set();
4012
+ const emitGuide = (name, fmlaVal) => {
4013
+ avLst += `<a:gd name="${name}" fmla="val ${fmlaVal}"/>`;
4014
+ emittedAdjNames.add(name);
4015
+ };
4016
+ if (options.rectRadius) {
4017
+ const adjVal = Math.round(options.rectRadius * EMU * 1e5 / Math.min(cx, cy));
4018
+ if (RECT_RADIUS_ADJ1_SHAPES.has(shapeName)) {
4019
+ emitGuide("adj1", adjVal);
4020
+ emitGuide("adj2", 0);
4021
+ } else emitGuide("adj", adjVal);
4022
+ } else if (options.angleRange) {
3543
4023
  for (let i = 0; i < 2; i++) {
3544
4024
  const angle = options.angleRange[i];
3545
- strXml += `<a:gd name="adj${i + 1}" fmla="val ${convertRotationDegrees(angle)}" />`;
4025
+ emitGuide(`adj${i + 1}`, convertRotationDegrees(angle));
3546
4026
  }
3547
- if (options.arcThicknessRatio) strXml += `<a:gd name="adj3" fmla="val ${Math.round(options.arcThicknessRatio * 5e4)}" />`;
4027
+ if (options.arcThicknessRatio) emitGuide("adj3", Math.round(options.arcThicknessRatio * 5e4));
3548
4028
  }
3549
- strXml += "</a:avLst></a:prstGeom>";
3550
- return strXml;
4029
+ if (options.shapeAdjust) (Array.isArray(options.shapeAdjust) ? options.shapeAdjust : [options.shapeAdjust]).forEach((adj) => {
4030
+ if (!adj || typeof adj.name !== "string" || adj.name.length === 0 || typeof adj.value !== "number" || !isFinite(adj.value)) {
4031
+ console.warn(`Warning: shapeAdjust entry ${JSON.stringify(adj)} is invalid (needs { name:string, value:number }) and was ignored.`);
4032
+ return;
4033
+ }
4034
+ if (emittedAdjNames.has(adj.name)) {
4035
+ console.warn(`Warning: shapeAdjust "${adj.name}" was ignored because rectRadius/angleRange already set that handle.`);
4036
+ return;
4037
+ }
4038
+ emitGuide(adj.name, Math.round(adj.value * 1e5));
4039
+ });
4040
+ return `<a:prstGeom prst="${shapeName}"><a:avLst>${avLst}</a:avLst></a:prstGeom>`;
3551
4041
  }
3552
4042
  /**
3553
4043
  * Emit an `<a:custGeom>` for a freeform path built from `points`.
@@ -3600,6 +4090,45 @@ function genXmlCustGeom(points, cx, cy, layout) {
3600
4090
  }
3601
4091
  const PLACEHOLDER_TYPE_MAP = PLACEHOLDER_TYPES;
3602
4092
  /**
4093
+ * Emit the `<a:lnL>/<a:lnR>/<a:lnT>/<a:lnB>` border children of an `<a:tcPr>` for a table cell.
4094
+ * Shared by normal cells and the dummy span (`_hmerge`/`_vmerge`) cells so a merged region's
4095
+ * outer edges render with the same border as its origin cell (Issue #680).
4096
+ * @param {BorderProps[]} cellBorder - 4-tuple of border props in [top, right, bottom, left] order
4097
+ * @return {string} concatenated border element XML, in the LRTB document order PowerPoint expects
4098
+ */
4099
+ function genTableCellBorderXml(cellBorder) {
4100
+ let strXml = "";
4101
+ [
4102
+ {
4103
+ idx: 3,
4104
+ name: "lnL"
4105
+ },
4106
+ {
4107
+ idx: 1,
4108
+ name: "lnR"
4109
+ },
4110
+ {
4111
+ idx: 0,
4112
+ name: "lnT"
4113
+ },
4114
+ {
4115
+ idx: 2,
4116
+ name: "lnB"
4117
+ }
4118
+ ].forEach((obj) => {
4119
+ const border = cellBorder[obj.idx];
4120
+ if (!border) return;
4121
+ const cap = createLineCap(border.cap);
4122
+ if (border.type !== "none") {
4123
+ strXml += `<a:${obj.name} w="${valToPts(border.pt)}" cap="${cap}" cmpd="sng" algn="ctr">`;
4124
+ strXml += `<a:solidFill>${createColorElement(border.color)}</a:solidFill>`;
4125
+ 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"/>`;
4126
+ strXml += `</a:${obj.name}>`;
4127
+ } else strXml += `<a:${obj.name} w="0" cap="${cap}" cmpd="sng" algn="ctr"><a:noFill/></a:${obj.name}>`;
4128
+ });
4129
+ return strXml;
4130
+ }
4131
+ /**
3603
4132
  * Transforms a slide or slideLayout to resulting XML string - Creates `ppt/slide*.xml`
3604
4133
  * @param {PresSlideInternal|SlideLayoutInternal} slideObject - slide object created within createSlideObject
3605
4134
  * @return {string} XML string with <p:cSld> as the root
@@ -3658,9 +4187,16 @@ function slideObjectToXml(slide) {
3658
4187
  intColCnt += cellOpts?.colspan ? Number(cellOpts.colspan) : 1;
3659
4188
  });
3660
4189
  strXml = `<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="${intTableNum * slide._slideNum + 1}" name="${slideItemObj.options.objectName}" descr="${encodeXmlEntities(slideItemObj.options.altText || "")}"/>`;
3661
- 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>";
4190
+ strXml += `<p:cNvGraphicFramePr>${genXmlObjectLock("a:graphicFrameLocks", GRAPHIC_FRAME_LOCK_ATTRS, {
4191
+ noGrp: true,
4192
+ ...slideItemObj.options.objectLock
4193
+ }, 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>`;
3662
4194
  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>`;
3663
- strXml += "<a:graphic><a:graphicData uri=\"http://schemas.openxmlformats.org/drawingml/2006/table\"><a:tbl><a:tblPr/>";
4195
+ {
4196
+ 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\"" : "");
4197
+ const tblPr = objTabOpts.tableStyle ? `<a:tblPr${tblPrAttrs}><a:tableStyleId>${objTabOpts.tableStyle}</a:tableStyleId></a:tblPr>` : `<a:tblPr${tblPrAttrs}/>`;
4198
+ strXml += `<a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table"><a:tbl>${tblPr}`;
4199
+ }
3664
4200
  if (Array.isArray(objTabOpts.colW)) {
3665
4201
  strXml += "<a:tblGrid>";
3666
4202
  for (let col = 0; col < intColCnt; col++) {
@@ -3686,7 +4222,8 @@ function slideObjectToXml(slide) {
3686
4222
  return {
3687
4223
  _type: "tablecell",
3688
4224
  options: { rowspan },
3689
- _hmerge: true
4225
+ _hmerge: true,
4226
+ _spanOrigin: cell
3690
4227
  };
3691
4228
  });
3692
4229
  cells.splice(cIdx + 1, 0, ...vMergeCells);
@@ -3702,12 +4239,14 @@ function slideObjectToXml(slide) {
3702
4239
  const colspan = cell.options?.colspan;
3703
4240
  const _hmerge = cell._hmerge;
3704
4241
  if (rowspan && rowspan > 1) {
4242
+ const _spanOrigin = cell._spanOrigin || cell;
3705
4243
  const hMergeCell = {
3706
4244
  _type: "tablecell",
3707
4245
  options: { colspan },
3708
4246
  _rowContinue: rowspan - 1,
3709
4247
  _vmerge: true,
3710
- _hmerge
4248
+ _hmerge,
4249
+ _spanOrigin
3711
4250
  };
3712
4251
  nextRow.splice(cIdx, 0, hMergeCell);
3713
4252
  }
@@ -3717,7 +4256,7 @@ function slideObjectToXml(slide) {
3717
4256
  let intRowH = 0;
3718
4257
  if (Array.isArray(objTabOpts.rowH) && objTabOpts.rowH[rIdx]) intRowH = inch2Emu(Number(objTabOpts.rowH[rIdx]));
3719
4258
  else if (objTabOpts.rowH && !isNaN(Number(objTabOpts.rowH))) intRowH = inch2Emu(Number(objTabOpts.rowH));
3720
- 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);
4259
+ 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);
3721
4260
  strXml += `<a:tr h="${intRowH}">`;
3722
4261
  cells.forEach((cellObj) => {
3723
4262
  const cell = cellObj;
@@ -3730,7 +4269,17 @@ function slideObjectToXml(slide) {
3730
4269
  let cellSpanAttrStr = Object.entries(cellSpanAttrs).filter(([, v]) => !!v).map(([k, v]) => `${String(k)}="${String(v)}"`).join(" ");
3731
4270
  if (cellSpanAttrStr) cellSpanAttrStr = " " + cellSpanAttrStr;
3732
4271
  if (cell._hmerge || cell._vmerge) {
3733
- strXml += `<a:tc${cellSpanAttrStr}><a:tcPr/></a:tc>`;
4272
+ const origin = cell._spanOrigin;
4273
+ let spanPrXml = "";
4274
+ if (origin) {
4275
+ const originOpts = origin.options || {};
4276
+ const originBorder = Array.isArray(originOpts.border) ? originOpts.border : null;
4277
+ if (originBorder) spanPrXml += genTableCellBorderXml(originBorder);
4278
+ let spanFill = origin._optImp?.fill?.color ? origin._optImp.fill.color : origin._optImp?.fill && typeof origin._optImp.fill === "string" ? origin._optImp.fill : "";
4279
+ spanFill = spanFill || originOpts.fill ? originOpts.fill : "";
4280
+ if (spanFill) spanPrXml += genXmlColorSelection(spanFill);
4281
+ }
4282
+ strXml += `<a:tc${cellSpanAttrStr}><a:tcPr>${spanPrXml}</a:tcPr></a:tc>`;
3734
4283
  return;
3735
4284
  }
3736
4285
  const cellOpts = cell.options || {};
@@ -3774,32 +4323,7 @@ function slideObjectToXml(slide) {
3774
4323
  else cellMarginXml = ` marL="${inch2Emu(cellMargin[3])}" marR="${inch2Emu(cellMargin[1])}" marT="${inch2Emu(cellMargin[0])}" marB="${inch2Emu(cellMargin[2])}"`;
3775
4324
  strXml += `<a:tc${cellSpanAttrStr}>${genXmlTextBody(cell)}<a:tcPr${cellMarginXml}${cellValign}${cellTextDir}>`;
3776
4325
  const cellBorder = Array.isArray(cellOpts.border) ? cellOpts.border : null;
3777
- if (cellBorder) [
3778
- {
3779
- idx: 3,
3780
- name: "lnL"
3781
- },
3782
- {
3783
- idx: 1,
3784
- name: "lnR"
3785
- },
3786
- {
3787
- idx: 0,
3788
- name: "lnT"
3789
- },
3790
- {
3791
- idx: 2,
3792
- name: "lnB"
3793
- }
3794
- ].forEach((obj) => {
3795
- const border = cellBorder[obj.idx];
3796
- if (border.type !== "none") {
3797
- strXml += `<a:${obj.name} w="${valToPts(border.pt)}" cap="flat" cmpd="sng" algn="ctr">`;
3798
- strXml += `<a:solidFill>${createColorElement(border.color)}</a:solidFill>`;
3799
- 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"/>`;
3800
- strXml += `</a:${obj.name}>`;
3801
- } else strXml += `<a:${obj.name} w="0" cap="flat" cmpd="sng" algn="ctr"><a:noFill/></a:${obj.name}>`;
3802
- });
4326
+ if (cellBorder) strXml += genTableCellBorderXml(cellBorder);
3803
4327
  strXml += cellFill;
3804
4328
  strXml += " </a:tcPr>";
3805
4329
  strXml += " </a:tc>";
@@ -3818,10 +4342,10 @@ function slideObjectToXml(slide) {
3818
4342
  if (!slideItemObj.options.line && cy === 0) cy = EMU * .3;
3819
4343
  if (!slideItemObj.options._bodyProp) slideItemObj.options._bodyProp = {};
3820
4344
  if (slideItemObj.options.margin && Array.isArray(slideItemObj.options.margin)) {
3821
- slideItemObj.options._bodyProp.lIns = valToPts(slideItemObj.options.margin[0] || 0);
4345
+ slideItemObj.options._bodyProp.tIns = valToPts(slideItemObj.options.margin[0] || 0);
3822
4346
  slideItemObj.options._bodyProp.rIns = valToPts(slideItemObj.options.margin[1] || 0);
3823
4347
  slideItemObj.options._bodyProp.bIns = valToPts(slideItemObj.options.margin[2] || 0);
3824
- slideItemObj.options._bodyProp.tIns = valToPts(slideItemObj.options.margin[3] || 0);
4348
+ slideItemObj.options._bodyProp.lIns = valToPts(slideItemObj.options.margin[3] || 0);
3825
4349
  } else if (typeof slideItemObj.options.margin === "number") {
3826
4350
  slideItemObj.options._bodyProp.lIns = valToPts(slideItemObj.options.margin);
3827
4351
  slideItemObj.options._bodyProp.rIns = valToPts(slideItemObj.options.margin);
@@ -3833,7 +4357,11 @@ function slideObjectToXml(slide) {
3833
4357
  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) : ""}"/>`;
3834
4358
  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"/>`;
3835
4359
  strSlideXml += "</p:cNvPr>";
3836
- strSlideXml += "<p:cNvSpPr" + (slideItemObj.options?.isTextBox ? " txBox=\"1\"/>" : "/>");
4360
+ {
4361
+ const spLockXml = genXmlObjectLock("a:spLocks", SHAPE_LOCK_ATTRS, slideItemObj.options.objectLock, slideItemObj.options.objectName);
4362
+ strSlideXml += "<p:cNvSpPr" + (slideItemObj.options?.isTextBox ? " txBox=\"1\"" : "");
4363
+ strSlideXml += spLockXml ? `>${spLockXml}</p:cNvSpPr>` : "/>";
4364
+ }
3837
4365
  strSlideXml += `<p:nvPr>${slideItemObj._type === "placeholder" ? genXmlPlaceholder(slideItemObj) : genXmlPlaceholder(placeholderObj)}</p:nvPr>`;
3838
4366
  strSlideXml += "</p:nvSpPr><p:spPr>";
3839
4367
  strSlideXml += `<a:xfrm${locationAttr}>`;
@@ -3843,7 +4371,8 @@ function slideObjectToXml(slide) {
3843
4371
  else strSlideXml += genXmlPresetGeom(slideItemObj.shape, slideItemObj.options, cx, cy);
3844
4372
  strSlideXml += slideItemObj.options.fill ? genXmlColorSelection(slideItemObj.options.fill) : "<a:noFill/>";
3845
4373
  if (slideItemObj.options.line) {
3846
- strSlideXml += slideItemObj.options.line.width ? `<a:ln w="${valToPts(slideItemObj.options.line.width)}">` : "<a:ln>";
4374
+ const lnAttrs = (slideItemObj.options.line.width ? ` w="${lineWidthToEmu(slideItemObj.options.line.width)}"` : "") + (slideItemObj.options.line.cap ? ` cap="${createLineCap(slideItemObj.options.line.cap)}"` : "");
4375
+ strSlideXml += `<a:ln${lnAttrs}>`;
3847
4376
  if (slideItemObj.options.line.color) strSlideXml += genXmlColorSelection(slideItemObj.options.line);
3848
4377
  if (slideItemObj.options.line.dashType) strSlideXml += `<a:prstDash val="${slideItemObj.options.line.dashType}"/>`;
3849
4378
  if (slideItemObj.options.line.beginArrowType) strSlideXml += `<a:headEnd type="${slideItemObj.options.line.beginArrowType}"/>`;
@@ -3876,13 +4405,17 @@ function slideObjectToXml(slide) {
3876
4405
  if (slideItemObj.hyperlink?.url) strSlideXml += `<a:hlinkClick r:id="rId${slideItemObj.hyperlink._rId}" tooltip="${slideItemObj.hyperlink.tooltip ? encodeXmlEntities(slideItemObj.hyperlink.tooltip) : ""}"/>`;
3877
4406
  if (slideItemObj.hyperlink?.slide) strSlideXml += `<a:hlinkClick r:id="rId${slideItemObj.hyperlink._rId}" tooltip="${slideItemObj.hyperlink.tooltip ? encodeXmlEntities(slideItemObj.hyperlink.tooltip) : ""}" action="ppaction://hlinksldjump"/>`;
3878
4407
  strSlideXml += " </p:cNvPr>";
3879
- strSlideXml += " <p:cNvPicPr><a:picLocks noChangeAspect=\"1\"/></p:cNvPicPr>";
4408
+ strSlideXml += ` <p:cNvPicPr>${genXmlObjectLock("a:picLocks", PICTURE_LOCK_ATTRS, {
4409
+ noChangeAspect: true,
4410
+ ...slideItemObj.options.objectLock
4411
+ }, slideItemObj.options.objectName)}</p:cNvPicPr>`;
3880
4412
  strSlideXml += " <p:nvPr>" + genXmlPlaceholder(placeholderObj) + "</p:nvPr>";
3881
4413
  strSlideXml += " </p:nvPicPr>";
3882
4414
  strSlideXml += "<p:blipFill>";
3883
4415
  if ((slide._relsMedia || []).find((rel) => rel.rId === slideItemObj.imageRid)?.extn === "svg") {
3884
4416
  strSlideXml += `<a:blip r:embed="rId${slideItemObj.imageRid - 1}">`;
3885
4417
  strSlideXml += slideItemObj.options.transparency ? ` <a:alphaModFix amt="${Math.round((100 - slideItemObj.options.transparency) * 1e3)}"/>` : "";
4418
+ strSlideXml += slideItemObj.options.duotone ? `<a:duotone>${createColorElement(slideItemObj.options.duotone.shadow)}${createColorElement(slideItemObj.options.duotone.highlight)}</a:duotone>` : "";
3886
4419
  strSlideXml += " <a:extLst>";
3887
4420
  strSlideXml += " <a:ext uri=\"{96DAC541-7B7A-43D3-8B79-37D633B846F1}\">";
3888
4421
  strSlideXml += ` <asvg:svgBlip xmlns:asvg="http://schemas.microsoft.com/office/drawing/2016/SVG/main" r:embed="rId${slideItemObj.imageRid}"/>`;
@@ -3892,6 +4425,7 @@ function slideObjectToXml(slide) {
3892
4425
  } else {
3893
4426
  strSlideXml += `<a:blip r:embed="rId${slideItemObj.imageRid}">`;
3894
4427
  strSlideXml += slideItemObj.options.transparency ? `<a:alphaModFix amt="${Math.round((100 - slideItemObj.options.transparency) * 1e3)}"/>` : "";
4428
+ strSlideXml += slideItemObj.options.duotone ? `<a:duotone>${createColorElement(slideItemObj.options.duotone.shadow)}${createColorElement(slideItemObj.options.duotone.highlight)}</a:duotone>` : "";
3895
4429
  strSlideXml += "</a:blip>";
3896
4430
  }
3897
4431
  if (sizing?.type) {
@@ -3899,10 +4433,17 @@ function slideObjectToXml(slide) {
3899
4433
  const boxH = sizing.h ? getSmartParseNumber(sizing.h, "Y", slide._presLayout) : cy;
3900
4434
  const boxX = getSmartParseNumber(sizing.x || 0, "X", slide._presLayout);
3901
4435
  const boxY = getSmartParseNumber(sizing.y || 0, "Y", slide._presLayout);
3902
- strSlideXml += ImageSizingXml[sizing.type]({
4436
+ let cropSize = {
3903
4437
  w: imgWidth,
3904
4438
  h: imgHeight
3905
- }, {
4439
+ };
4440
+ if (sizing.type === "cover" || sizing.type === "contain") {
4441
+ const relData = (slide._relsMedia || []).find((rel) => rel.rId === slideItemObj.imageRid)?.data;
4442
+ const natural = typeof relData === "string" ? getImageSizeFromBase64(relData) : null;
4443
+ if (natural) cropSize = natural;
4444
+ 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.`);
4445
+ }
4446
+ strSlideXml += ImageSizingXml[sizing.type](cropSize, {
3906
4447
  w: boxW,
3907
4448
  h: boxH,
3908
4449
  x: boxX,
@@ -3942,7 +4483,7 @@ function slideObjectToXml(slide) {
3942
4483
  strSlideXml += "<p:pic>";
3943
4484
  strSlideXml += " <p:nvPicPr>";
3944
4485
  strSlideXml += `<p:cNvPr id="${slideItemObj.mediaRid + 2}" name="${slideItemObj.options.objectName}" descr="${encodeXmlEntities(slideItemObj.options.altText || "")}"/>`;
3945
- strSlideXml += " <p:cNvPicPr/>";
4486
+ strSlideXml += ` <p:cNvPicPr>${genXmlObjectLock("a:picLocks", PICTURE_LOCK_ATTRS, slideItemObj.options.objectLock, slideItemObj.options.objectName)}</p:cNvPicPr>`;
3946
4487
  strSlideXml += " <p:nvPr>";
3947
4488
  strSlideXml += ` <a:videoFile r:link="rId${slideItemObj.mediaRid}"/>`;
3948
4489
  strSlideXml += " </p:nvPr>";
@@ -3957,7 +4498,10 @@ function slideObjectToXml(slide) {
3957
4498
  strSlideXml += "<p:pic>";
3958
4499
  strSlideXml += " <p:nvPicPr>";
3959
4500
  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>`;
3960
- strSlideXml += " <p:cNvPicPr><a:picLocks noChangeAspect=\"1\"/></p:cNvPicPr>";
4501
+ strSlideXml += ` <p:cNvPicPr>${genXmlObjectLock("a:picLocks", PICTURE_LOCK_ATTRS, {
4502
+ noChangeAspect: true,
4503
+ ...slideItemObj.options.objectLock
4504
+ }, slideItemObj.options.objectName)}</p:cNvPicPr>`;
3961
4505
  strSlideXml += " <p:nvPr>";
3962
4506
  strSlideXml += ` <a:videoFile r:link="rId${slideItemObj.mediaRid}"/>`;
3963
4507
  strSlideXml += " <p:extLst>";
@@ -4021,7 +4565,7 @@ function slideObjectToXml(slide) {
4021
4565
  strSlideXml += "/>";
4022
4566
  strSlideXml += " <a:lstStyle><a:lvl1pPr>";
4023
4567
  if (slide._slideNumberProps.fontFace || slide._slideNumberProps.fontSize || slide._slideNumberProps.color) {
4024
- strSlideXml += `<a:defRPr sz="${Math.round((slide._slideNumberProps.fontSize || 12) * 100)}">`;
4568
+ strSlideXml += `<a:defRPr sz="${clampFontSizeSz(slide._slideNumberProps.fontSize || 12)}">`;
4025
4569
  if (slide._slideNumberProps.color) strSlideXml += genXmlColorSelection(slide._slideNumberProps.color);
4026
4570
  if (slide._slideNumberProps.fontFace) strSlideXml += `<a:latin typeface="${slide._slideNumberProps.fontFace}"/><a:ea typeface="${slide._slideNumberProps.fontFace}"/><a:cs typeface="${slide._slideNumberProps.fontFace}"/>`;
4027
4571
  strSlideXml += "</a:defRPr>";
@@ -4110,7 +4654,7 @@ function genXmlParagraphProperties(textObj, isDefault) {
4110
4654
  paragraphPropXml += "";
4111
4655
  break;
4112
4656
  }
4113
- if (textObj.options.lineSpacing) strXmlLnSpc = `<a:lnSpc><a:spcPts val="${Math.round(textObj.options.lineSpacing * 100)}"/></a:lnSpc>`;
4657
+ if (textObj.options.lineSpacing) strXmlLnSpc = `<a:lnSpc><a:spcPts val="${clampLineSpacingPts(textObj.options.lineSpacing)}"/></a:lnSpc>`;
4114
4658
  else if (textObj.options.lineSpacingMultiple) strXmlLnSpc = `<a:lnSpc><a:spcPct val="${Math.round(textObj.options.lineSpacingMultiple * 1e5)}"/></a:lnSpc>`;
4115
4659
  if (textObj.options.indentLevel && !isNaN(Number(textObj.options.indentLevel)) && textObj.options.indentLevel > 0) paragraphPropXml += ` lvl="${textObj.options.indentLevel}"`;
4116
4660
  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>`;
@@ -4164,7 +4708,7 @@ function genXmlTextRunProperties(opts, isDefault) {
4164
4708
  let runProps = "";
4165
4709
  const runPropsTag = isDefault ? "a:defRPr" : "a:rPr";
4166
4710
  runProps += "<" + runPropsTag + " lang=\"" + (opts.lang ? opts.lang : "en-US") + "\"" + (opts.lang ? " altLang=\"en-US\"" : "");
4167
- runProps += opts.fontSize ? ` sz="${Math.round(opts.fontSize * 100)}"` : "";
4711
+ runProps += opts.fontSize ? ` sz="${clampFontSizeSz(opts.fontSize)}"` : "";
4168
4712
  runProps += opts?.bold ? ` b="${opts.bold ? "1" : "0"}"` : "";
4169
4713
  runProps += opts?.italic ? ` i="${opts.italic ? "1" : "0"}"` : "";
4170
4714
  runProps += opts?.strike ? ` strike="${typeof opts.strike === "string" ? opts.strike : "sngStrike"}"` : "";
@@ -4175,17 +4719,23 @@ function genXmlTextRunProperties(opts, isDefault) {
4175
4719
  if (opts.baseline) runProps += ` baseline="${Math.round(opts.baseline * 50)}"`;
4176
4720
  else if (opts.subscript) runProps += " baseline=\"-40000\"";
4177
4721
  else if (opts.superscript) runProps += " baseline=\"30000\"";
4178
- runProps += opts.charSpacing ? ` spc="${Math.round(opts.charSpacing * 100)}" kern="0"` : "";
4722
+ runProps += opts.charSpacing ? ` spc="${clampCharSpacingSpc(opts.charSpacing)}" kern="0"` : "";
4179
4723
  runProps += " dirty=\"0\">";
4180
- if (opts.color || opts.fontFace || opts.outline || typeof opts.underline === "object" && opts.underline.color) {
4181
- if (opts.outline && typeof opts.outline === "object") runProps += `<a:ln w="${valToPts(opts.outline.size || .75)}">${genXmlColorSelection(opts.outline.color || "FFFFFF")}</a:ln>`;
4724
+ const hasShadow = !!opts.shadow && opts.shadow.type !== "none";
4725
+ if (opts.color || opts.fontFace || opts.outline || opts.glow || hasShadow || typeof opts.underline === "object" && opts.underline.color) {
4726
+ if (opts.outline && typeof opts.outline === "object") runProps += `<a:ln w="${lineWidthToEmu(opts.outline.size || .75)}">${genXmlColorSelection(opts.outline.color || "FFFFFF")}</a:ln>`;
4182
4727
  if (opts.color) runProps += genXmlColorSelection({
4183
4728
  color: opts.color,
4184
4729
  transparency: opts.transparency
4185
4730
  });
4731
+ if (opts.glow || hasShadow) {
4732
+ runProps += "<a:effectLst>";
4733
+ if (opts.glow) runProps += createGlowElement(opts.glow, DEF_TEXT_GLOW);
4734
+ if (hasShadow) runProps += createShadowElement$1(opts.shadow, DEF_TEXT_SHADOW);
4735
+ runProps += "</a:effectLst>";
4736
+ }
4186
4737
  if (opts.highlight) runProps += `<a:highlight>${createColorElement(opts.highlight)}</a:highlight>`;
4187
4738
  if (typeof opts.underline === "object" && opts.underline.color) runProps += `<a:uFill>${genXmlColorSelection(opts.underline.color)}</a:uFill>`;
4188
- if (opts.glow) runProps += `<a:effectLst>${createGlowElement(opts.glow, DEF_TEXT_GLOW)}</a:effectLst>`;
4189
4739
  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"/>`;
4190
4740
  }
4191
4741
  if (opts.hyperlink) {
@@ -4215,6 +4765,28 @@ function genXmlTextRun(textObj) {
4215
4765
  return `<a:r>${genXmlTextRunProperties(textObj.options, false)}<a:t>${encodeXmlEntities(String(textObj.text))}</a:t></a:r>`;
4216
4766
  }
4217
4767
  /**
4768
+ * Builds `<a:normAutofit>` with explicit fontScale/lnSpcReduction for "shrink text on overflow"
4769
+ * @param {TextFitShrinkProps} fit - shrink fit options
4770
+ * @return {string} XML string (`<a:normAutofit .../>`)
4771
+ * @see ECMA-376 CT_TextNormAutofit (attributes in 1000ths of a percent)
4772
+ */
4773
+ function genXmlNormAutofit(fit) {
4774
+ let attrs = "";
4775
+ const pct = (val, name) => {
4776
+ if (val === void 0 || val === null) return null;
4777
+ if (typeof val !== "number" || isNaN(val) || val < 0 || val > 100) {
4778
+ console.warn(`Warning: fit.${name} must be a number between 0 and 100 (percent); received ${String(val)} - attribute ignored.`);
4779
+ return null;
4780
+ }
4781
+ return Math.round(val * 1e3);
4782
+ };
4783
+ const fontScale = pct(fit.fontScale, "fontScale");
4784
+ if (fontScale !== null) attrs += ` fontScale="${fontScale}"`;
4785
+ const lnSpcReduction = pct(fit.lnSpcReduction, "lnSpcReduction");
4786
+ if (lnSpcReduction !== null) attrs += ` lnSpcReduction="${lnSpcReduction}"`;
4787
+ return `<a:normAutofit${attrs}/>`;
4788
+ }
4789
+ /**
4218
4790
  * Builds `<a:bodyPr></a:bodyPr>` tag for "genXmlTextBody()"
4219
4791
  * @param {ISlideObject | TableCell} slideObject - various options
4220
4792
  * @return {string} XML string
@@ -4227,6 +4799,8 @@ function genXmlBodyProperties(slideObject) {
4227
4799
  if (slideObject.options._bodyProp.tIns || slideObject.options._bodyProp.tIns === 0) bodyProperties += ` tIns="${slideObject.options._bodyProp.tIns}"`;
4228
4800
  if (slideObject.options._bodyProp.rIns || slideObject.options._bodyProp.rIns === 0) bodyProperties += ` rIns="${slideObject.options._bodyProp.rIns}"`;
4229
4801
  if (slideObject.options._bodyProp.bIns || slideObject.options._bodyProp.bIns === 0) bodyProperties += ` bIns="${slideObject.options._bodyProp.bIns}"`;
4802
+ if (slideObject.options._bodyProp.numCol) bodyProperties += ` numCol="${slideObject.options._bodyProp.numCol}"`;
4803
+ if (slideObject.options._bodyProp.spcCol) bodyProperties += ` spcCol="${slideObject.options._bodyProp.spcCol}"`;
4230
4804
  bodyProperties += " rtlCol=\"0\"";
4231
4805
  if (slideObject.options._bodyProp.anchor) bodyProperties += " anchor=\"" + slideObject.options._bodyProp.anchor + "\"";
4232
4806
  if (slideObject.options._bodyProp.vert) bodyProperties += " vert=\"" + slideObject.options._bodyProp.vert + "\"";
@@ -4237,9 +4811,11 @@ function genXmlBodyProperties(slideObject) {
4237
4811
  * @see: http://www.datypic.com/sc/ooxml/g-a_EG_TextAutofit.html
4238
4812
  */
4239
4813
  if (slideObject.options.fit) {
4240
- if (slideObject.options.fit === "none") bodyProperties += "";
4241
- else if (slideObject.options.fit === "shrink") bodyProperties += "<a:normAutofit/>";
4242
- else if (slideObject.options.fit === "resize") bodyProperties += "<a:spAutoFit/>";
4814
+ const fit = slideObject.options.fit;
4815
+ if (fit === "none") bodyProperties += "";
4816
+ else if (fit === "shrink") bodyProperties += "<a:normAutofit/>";
4817
+ else if (fit === "resize") bodyProperties += "<a:spAutoFit/>";
4818
+ else if (typeof fit === "object" && fit.type === "shrink") bodyProperties += genXmlNormAutofit(fit);
4243
4819
  }
4244
4820
  if (slideObject.options.shrinkText) bodyProperties += "<a:normAutofit/>";
4245
4821
  bodyProperties += slideObject.options._bodyProp.autoFit ? "<a:spAutoFit/>" : "";
@@ -4298,14 +4874,19 @@ function genXmlTextBody(slideObj) {
4298
4874
  itext.options = itext.options || opts || {};
4299
4875
  if (idx === 0 && itext.options && !itext.options.bullet && opts.bullet) itext.options.bullet = opts.bullet;
4300
4876
  if (typeof itext.text === "string" || typeof itext.text === "number") itext.text = itext.text.toString().replace(/\r*\n/g, "\r\n");
4301
- if (itext.text.includes("\r\n") && itext.text.match(/\n$/g) === null) itext.text.split("\r\n").forEach((line) => {
4302
- itext.options.breakLine = true;
4303
- arrTextObjects.push({
4304
- text: line,
4305
- options: itext.options
4877
+ if (itext.text.includes("\r\n") && itext.text.match(/\n$/g) === null) {
4878
+ const lines = itext.text.split("\r\n");
4879
+ lines.forEach((line, lineIdx) => {
4880
+ const isLast = lineIdx === lines.length - 1;
4881
+ arrTextObjects.push({
4882
+ text: line,
4883
+ options: {
4884
+ ...itext.options,
4885
+ breakLine: isLast ? itext.options.breakLine : true
4886
+ }
4887
+ });
4306
4888
  });
4307
- });
4308
- else arrTextObjects.push(itext);
4889
+ } else arrTextObjects.push(itext);
4309
4890
  });
4310
4891
  const arrLines = [];
4311
4892
  let arrTexts = [];
@@ -4370,13 +4951,13 @@ function genXmlTextBody(slideObj) {
4370
4951
  }
4371
4952
  });
4372
4953
  if (slideObj._type === "tablecell" && (opts.fontSize || opts.fontFace)) if (opts.fontFace) {
4373
- strSlideXml += `<a:endParaRPr lang="${opts.lang || "en-US"}"` + (opts.fontSize ? ` sz="${Math.round(opts.fontSize * 100)}"` : "") + " dirty=\"0\">";
4954
+ strSlideXml += `<a:endParaRPr lang="${opts.lang || "en-US"}"` + (opts.fontSize ? ` sz="${clampFontSizeSz(opts.fontSize)}"` : "") + " dirty=\"0\">";
4374
4955
  strSlideXml += `<a:latin typeface="${opts.fontFace}" charset="0"/>`;
4375
4956
  strSlideXml += `<a:ea typeface="${opts.fontFace}" charset="0"/>`;
4376
4957
  strSlideXml += `<a:cs typeface="${opts.fontFace}" charset="0"/>`;
4377
4958
  strSlideXml += "</a:endParaRPr>";
4378
- } else strSlideXml += `<a:endParaRPr lang="${opts.lang || "en-US"}"` + (opts.fontSize ? ` sz="${Math.round(opts.fontSize * 100)}"` : "") + " dirty=\"0\"/>";
4379
- else if (reqsClosingFontSize) strSlideXml += `<a:endParaRPr lang="${opts.lang || "en-US"}"` + (opts.fontSize ? ` sz="${Math.round(opts.fontSize * 100)}"` : "") + " dirty=\"0\"/>";
4959
+ } else strSlideXml += `<a:endParaRPr lang="${opts.lang || "en-US"}"` + (opts.fontSize ? ` sz="${clampFontSizeSz(opts.fontSize)}"` : "") + " dirty=\"0\"/>";
4960
+ else if (reqsClosingFontSize) strSlideXml += `<a:endParaRPr lang="${opts.lang || "en-US"}"` + (opts.fontSize ? ` sz="${clampFontSizeSz(opts.fontSize)}"` : "") + " dirty=\"0\"/>";
4380
4961
  else strSlideXml += `<a:endParaRPr lang="${opts.lang || "en-US"}" dirty="0"/>`;
4381
4962
  strSlideXml += "</a:p>";
4382
4963
  });
@@ -4407,7 +4988,7 @@ function genXmlPlaceholder(placeholderObj) {
4407
4988
  * @param {PresSlideInternal} masterSlide - master slide
4408
4989
  * @returns XML
4409
4990
  */
4410
- function makeXmlContTypes(slides, slideLayouts, masterSlide) {
4991
+ function makeXmlContTypes(slides, slideLayouts, masterSlide, hasCustomProps) {
4411
4992
  let strXml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n";
4412
4993
  strXml += "<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">";
4413
4994
  strXml += "<Default Extension=\"xml\" ContentType=\"application/xml\"/>";
@@ -4457,6 +5038,7 @@ function makeXmlContTypes(slides, slideLayouts, masterSlide) {
4457
5038
  });
4458
5039
  strXml += " <Override PartName=\"/docProps/core.xml\" ContentType=\"application/vnd.openxmlformats-package.core-properties+xml\"/>";
4459
5040
  strXml += " <Override PartName=\"/docProps/app.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.extended-properties+xml\"/>";
5041
+ if (hasCustomProps) strXml += " <Override PartName=\"/docProps/custom.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.custom-properties+xml\"/>";
4460
5042
  strXml += "</Types>";
4461
5043
  return strXml;
4462
5044
  }
@@ -4464,13 +5046,15 @@ function makeXmlContTypes(slides, slideLayouts, masterSlide) {
4464
5046
  * Creates `_rels/.rels`
4465
5047
  * @returns XML
4466
5048
  */
4467
- function makeXmlRootRels() {
4468
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\r
5049
+ function makeXmlRootRels(hasCustomProps) {
5050
+ let xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\r
4469
5051
  <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
4470
5052
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
4471
5053
  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
4472
- <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>
4473
- </Relationships>`;
5054
+ <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>`;
5055
+ if (hasCustomProps) xml += "\n <Relationship Id=\"rId4\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties\" Target=\"docProps/custom.xml\"/>";
5056
+ xml += "\n </Relationships>";
5057
+ return xml;
4474
5058
  }
4475
5059
  /**
4476
5060
  * Creates `docProps/app.xml`
@@ -4536,6 +5120,23 @@ function makeXmlCore(title, subject, author, revision) {
4536
5120
  <dcterms:modified xsi:type="dcterms:W3CDTF">${(/* @__PURE__ */ new Date()).toISOString().replace(/\.\d\d\dZ/, "Z")}</dcterms:modified>
4537
5121
  </cp:coreProperties>`;
4538
5122
  }
5123
+ const CUSTOM_PROPS_FMTID = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}";
5124
+ /**
5125
+ * Creates `docProps/custom.xml`
5126
+ * @param props - custom property name/value pairs
5127
+ * @returns XML
5128
+ */
5129
+ function makeXmlCustomProperties(props) {
5130
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\r
5131
+ <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">${props.map(({ name, value }, idx) => {
5132
+ let valueXml;
5133
+ if (typeof value === "boolean") valueXml = `<vt:bool>${value}</vt:bool>`;
5134
+ else if (value instanceof Date) valueXml = `<vt:filetime>${value.toISOString().replace(/\.\d{3}Z$/, "Z")}</vt:filetime>`;
5135
+ else if (typeof value === "number") valueXml = Number.isInteger(value) ? `<vt:i4>${value}</vt:i4>` : `<vt:r8>${value}</vt:r8>`;
5136
+ else valueXml = `<vt:lpwstr>${encodeXmlEntities(String(value))}</vt:lpwstr>`;
5137
+ return `<property fmtid="${CUSTOM_PROPS_FMTID}" pid="${idx + 2}" name="${encodeXmlEntities(name)}">${valueXml}</property>`;
5138
+ }).join("")}</Properties>`;
5139
+ }
4539
5140
  /**
4540
5141
  * Creates `ppt/_rels/presentation.xml.rels`
4541
5142
  * @param {PresSlideInternal[]} slides - Presenation Slides
@@ -4712,7 +5313,7 @@ function makeXmlTheme(pres) {
4712
5313
  */
4713
5314
  function makeXmlPresentation(pres) {
4714
5315
  let strXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\r
4715
- <p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" ${pres.rtlMode ? "rtl=\"1\"" : ""} saveSubsetFonts="1" autoCompressPictures="0">`;
5316
+ <p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" ${pres.rtlMode ? "rtl=\"1\"" : ""} saveSubsetFonts="1" autoCompressPictures="0"${pres.firstSlideNum !== 1 ? ` firstSlideNum="${pres.firstSlideNum}"` : ""}>`;
4716
5317
  strXml += "<p:sldMasterIdLst><p:sldMasterId id=\"2147483648\" r:id=\"rId1\"/></p:sldMasterIdLst>";
4717
5318
  strXml += `<p:notesMasterIdLst><p:notesMasterId r:id="rId${pres.slides.length + 2}"/></p:notesMasterIdLst>`;
4718
5319
  strXml += "<p:sldIdLst>";
@@ -4751,9 +5352,96 @@ function makeXmlPresProps() {
4751
5352
  * @see: http://openxmldeveloper.org/discussions/formats/f/13/p/2398/8107.aspx
4752
5353
  * @return {string} XML
4753
5354
  */
4754
- function makeXmlTableStyles() {
4755
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\r
4756
- <a:tblStyleLst xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" def="{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"/>`;
5355
+ function makeXmlTableStyles(tableStyles = []) {
5356
+ const open = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\r
5357
+ <a:tblStyleLst xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" def="{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"`;
5358
+ if (!tableStyles || tableStyles.length === 0) return `${open}/>`;
5359
+ let strXml = `${open}>`;
5360
+ tableStyles.forEach(({ guid, def }) => {
5361
+ strXml += `<a:tblStyle styleId="${guid}" styleName="${encodeXmlEntities(def.name)}">`;
5362
+ [
5363
+ ["wholeTbl", def.wholeTbl],
5364
+ ["band1H", def.band1H],
5365
+ ["band2H", def.band2H],
5366
+ ["band1V", def.band1V],
5367
+ ["band2V", def.band2V],
5368
+ ["lastCol", def.lastCol],
5369
+ ["firstCol", def.firstCol],
5370
+ ["lastRow", def.lastRow],
5371
+ ["firstRow", def.firstRow]
5372
+ ].forEach(([name, region]) => {
5373
+ if (region) strXml += genXmlTableStyleRegion(name, region);
5374
+ });
5375
+ strXml += "</a:tblStyle>";
5376
+ });
5377
+ strXml += "</a:tblStyleLst>";
5378
+ return strXml;
5379
+ }
5380
+ /**
5381
+ * Build one `CT_TablePartStyle` region (e.g. `firstRow`, `band1H`) for a custom table style.
5382
+ * Emits `tcTxStyle` (text) before `tcStyle` (cell fill/borders) per the schema sequence.
5383
+ * @param {string} name - region element name
5384
+ * @param {TableStyleRegionProps} region - region styling
5385
+ * @return {string} XML
5386
+ */
5387
+ function genXmlTableStyleRegion(name, region) {
5388
+ let xml = `<a:${name}>`;
5389
+ if (region.bold !== void 0 || region.italic !== void 0 || region.color) {
5390
+ const b = region.bold ? " b=\"on\"" : "";
5391
+ const i = region.italic ? " i=\"on\"" : "";
5392
+ xml += `<a:tcTxStyle${b}${i}><a:fontRef idx="minor"/>`;
5393
+ xml += region.color ? createColorElement(region.color) : "";
5394
+ xml += "</a:tcTxStyle>";
5395
+ }
5396
+ if (region.border !== void 0 || region.fill !== void 0) {
5397
+ xml += "<a:tcStyle>";
5398
+ if (region.border !== void 0) xml += genXmlTableStyleBorders(region.border);
5399
+ if (region.fill !== void 0) xml += `<a:fill><a:solidFill>${createColorElement(region.fill)}</a:solidFill></a:fill>`;
5400
+ xml += "</a:tcStyle>";
5401
+ }
5402
+ xml += `</a:${name}>`;
5403
+ return xml;
5404
+ }
5405
+ /**
5406
+ * Build the `tcBdr` border block for a custom table style region.
5407
+ * A single `BorderProps` styles all four sides plus the interior grid lines; a
5408
+ * TRBL array styles only the four outer sides. Sides are emitted in schema order.
5409
+ * @param {BorderProps | BorderProps[]} border - border definition
5410
+ * @return {string} XML
5411
+ */
5412
+ function genXmlTableStyleBorders(border) {
5413
+ let sides;
5414
+ if (Array.isArray(border)) {
5415
+ const [top, right, bottom, left] = border;
5416
+ sides = [
5417
+ ["left", left],
5418
+ ["right", right],
5419
+ ["top", top],
5420
+ ["bottom", bottom]
5421
+ ];
5422
+ } else sides = [
5423
+ ["left", border],
5424
+ ["right", border],
5425
+ ["top", border],
5426
+ ["bottom", border],
5427
+ ["insideH", border],
5428
+ ["insideV", border]
5429
+ ];
5430
+ let xml = "<a:tcBdr>";
5431
+ sides.forEach(([side, b]) => {
5432
+ if (!b) return;
5433
+ xml += `<a:${side}>`;
5434
+ if (b.type === "none") xml += "<a:ln><a:noFill/></a:ln>";
5435
+ else {
5436
+ xml += `<a:ln w="${lineWidthToEmu(b.pt ?? 1)}" cap="flat" cmpd="sng" algn="ctr">`;
5437
+ xml += `<a:solidFill>${createColorElement(b.color ?? "666666")}</a:solidFill>`;
5438
+ xml += `<a:prstDash val="${b.type === "dash" ? "sysDash" : "solid"}"/>`;
5439
+ xml += "</a:ln>";
5440
+ }
5441
+ xml += `</a:${side}>`;
5442
+ });
5443
+ xml += "</a:tcBdr>";
5444
+ return xml;
4757
5445
  }
4758
5446
  /**
4759
5447
  * Creates `ppt/viewProps.xml`
@@ -4823,7 +5511,7 @@ function makeXmlViewProps() {
4823
5511
  * @see https://docs.microsoft.com/en-us/office/open-xml/structure-of-a-presentationml-document
4824
5512
  * @see https://docs.microsoft.com/en-us/previous-versions/office/developer/office-2010/hh273476(v=office.14)
4825
5513
  */
4826
- const VERSION = "5.1.0";
5514
+ const VERSION = "5.3.0";
4827
5515
  function standardLayoutToPresLayout(layout) {
4828
5516
  return {
4829
5517
  name: layout.name,
@@ -4924,6 +5612,14 @@ var PptxGenJS = class {
4924
5612
  get title() {
4925
5613
  return this._title;
4926
5614
  }
5615
+ /** Slide number shown on the first slide (maps to firstSlideNum in presentation.xml) */
5616
+ _firstSlideNum;
5617
+ set firstSlideNum(value) {
5618
+ this._firstSlideNum = value;
5619
+ }
5620
+ get firstSlideNum() {
5621
+ return this._firstSlideNum;
5622
+ }
4927
5623
  /**
4928
5624
  * Whether Right-to-Left (RTL) mode is enabled
4929
5625
  * @type {boolean}
@@ -4950,6 +5646,9 @@ var PptxGenJS = class {
4950
5646
  get sections() {
4951
5647
  return this._sections;
4952
5648
  }
5649
+ /** custom document properties stored in docProps/custom.xml */
5650
+ _customProperties;
5651
+ _tableStyles;
4953
5652
  /** slide layout definition objects, used for generating slide layout files */
4954
5653
  _slideLayouts;
4955
5654
  get slideLayouts() {
@@ -4959,6 +5658,7 @@ var PptxGenJS = class {
4959
5658
  return {
4960
5659
  author: this.author,
4961
5660
  company: this.company,
5661
+ firstSlideNum: this.firstSlideNum,
4962
5662
  layout: this.layout,
4963
5663
  masterSlide: this._masterSlide,
4964
5664
  presLayout: this.presLayout,
@@ -5043,6 +5743,7 @@ var PptxGenJS = class {
5043
5743
  width: this.LAYOUTS[DEF_PRES_LAYOUT].width,
5044
5744
  height: this.LAYOUTS[DEF_PRES_LAYOUT].height
5045
5745
  };
5746
+ this._firstSlideNum = 1;
5046
5747
  this._rtlMode = false;
5047
5748
  this._slideLayouts = [{
5048
5749
  _margin: DEF_SLIDE_MARGIN_IN,
@@ -5058,6 +5759,8 @@ var PptxGenJS = class {
5058
5759
  }];
5059
5760
  this._slides = [];
5060
5761
  this._sections = [];
5762
+ this._customProperties = [];
5763
+ this._tableStyles = [];
5061
5764
  this._masterSlide = {
5062
5765
  addChart: null,
5063
5766
  addImage: null,
@@ -5086,7 +5789,8 @@ var PptxGenJS = class {
5086
5789
  */
5087
5790
  addNewSlide = (options) => {
5088
5791
  const nextOptions = options || {};
5089
- nextOptions.sectionTitle = this._sections.length > 0 && this._sections[this._sections.length - 1]._slides.some((slide) => slide._slideNum === this._slides[this._slides.length - 1]._slideNum) ? this._sections[this._sections.length - 1].title : null;
5792
+ const lastSlide = this._slides[this._slides.length - 1];
5793
+ nextOptions.sectionTitle = this._sections.find((sect) => sect._slides.some((s) => s._slideNum === lastSlide._slideNum))?.title ?? null;
5090
5794
  return this.addSlide(nextOptions);
5091
5795
  };
5092
5796
  /**
@@ -5139,6 +5843,18 @@ var PptxGenJS = class {
5139
5843
  });
5140
5844
  arrMediaPromises = arrMediaPromises.concat(encodeSlideMediaRels(this._masterSlide, this._runtime));
5141
5845
  return await Promise.all(arrMediaPromises).then(async () => {
5846
+ const canonicalMediaTargets = /* @__PURE__ */ new Map();
5847
+ for (const target of [
5848
+ ...this._slides,
5849
+ ...this._slideLayouts,
5850
+ this._masterSlide
5851
+ ]) for (const rel of target._relsMedia || []) {
5852
+ if (rel.type === "online" || rel.type === "hyperlink" || typeof rel.data !== "string" || !rel.data) continue;
5853
+ const key = (rel.extn || "") + "\0" + rel.data;
5854
+ const canonical = canonicalMediaTargets.get(key);
5855
+ if (canonical) rel.Target = canonical;
5856
+ else canonicalMediaTargets.set(key, rel.Target);
5857
+ }
5142
5858
  this._slides.forEach((slide) => {
5143
5859
  if (slide._slideLayout) addPlaceholdersToSlideLayouts(slide);
5144
5860
  });
@@ -5156,16 +5872,18 @@ var PptxGenJS = class {
5156
5872
  zip.folder("ppt/theme");
5157
5873
  zip.folder("ppt/notesMasters").folder("_rels");
5158
5874
  zip.folder("ppt/notesSlides").folder("_rels");
5159
- zip.file("[Content_Types].xml", makeXmlContTypes(this._slides, this._slideLayouts, this._masterSlide));
5160
- zip.file("_rels/.rels", makeXmlRootRels());
5875
+ const hasCustomProps = this._customProperties.length > 0;
5876
+ zip.file("[Content_Types].xml", makeXmlContTypes(this._slides, this._slideLayouts, this._masterSlide, hasCustomProps));
5877
+ zip.file("_rels/.rels", makeXmlRootRels(hasCustomProps));
5161
5878
  zip.file("docProps/app.xml", makeXmlApp(this._slides, this.company));
5162
5879
  zip.file("docProps/core.xml", makeXmlCore(this.title, this.subject, this.author, this.revision));
5880
+ if (hasCustomProps) zip.file("docProps/custom.xml", makeXmlCustomProperties(this._customProperties));
5163
5881
  zip.file("ppt/_rels/presentation.xml.rels", makeXmlPresentationRels(this._slides));
5164
5882
  zip.file("ppt/theme/theme1.xml", makeXmlTheme(this.internalPresentation));
5165
5883
  zip.file("ppt/theme/theme2.xml", makeXmlTheme(this.internalPresentation));
5166
5884
  zip.file("ppt/presentation.xml", makeXmlPresentation(this.internalPresentation));
5167
5885
  zip.file("ppt/presProps.xml", makeXmlPresProps());
5168
- zip.file("ppt/tableStyles.xml", makeXmlTableStyles());
5886
+ zip.file("ppt/tableStyles.xml", makeXmlTableStyles(this._tableStyles));
5169
5887
  zip.file("ppt/viewProps.xml", makeXmlViewProps());
5170
5888
  this._slideLayouts.forEach((layout, idx) => {
5171
5889
  zip.file(`ppt/slideLayouts/slideLayout${idx + 1}.xml`, makeXmlLayout(layout));
@@ -5245,6 +5963,19 @@ var PptxGenJS = class {
5245
5963
  return await this._runtime.writeFile(fileName, data);
5246
5964
  }
5247
5965
  /**
5966
+ * Set a custom document property stored in `docProps/custom.xml`.
5967
+ * Calling with the same name replaces the existing value.
5968
+ * @param name - property name
5969
+ * @param value - string, integer/float number, boolean, or Date
5970
+ */
5971
+ setCustomProperty(name, value) {
5972
+ this._customProperties = this._customProperties.filter((p) => p.name !== name);
5973
+ this._customProperties.push({
5974
+ name,
5975
+ value
5976
+ });
5977
+ }
5978
+ /**
5248
5979
  * Add a new Section to Presentation
5249
5980
  * @param {ISectionProps} section - section properties
5250
5981
  * @example pptx.addSection({ title:'Charts' });
@@ -5353,6 +6084,31 @@ var PptxGenJS = class {
5353
6084
  if (newLayout._slideNumberProps && !this._masterSlide._slideNumberProps) this._masterSlide._slideNumberProps = newLayout._slideNumberProps;
5354
6085
  }
5355
6086
  /**
6087
+ * Register a reusable custom table style and return its GUID.
6088
+ * The style is written to `ppt/tableStyles.xml` and is editable in PowerPoint's
6089
+ * Table Styles gallery. Pass the returned GUID as `TableProps.tableStyle`, and use
6090
+ * the `has*` flags (`hasHeader`, `hasBandedRows`, …) to activate the matching regions.
6091
+ * @param {TableStyleProps} props - custom table style definition (requires `name`)
6092
+ * @returns {string} braced GUID to use as `tableStyle`
6093
+ * @example
6094
+ * const brand = pptx.defineTableStyle({
6095
+ * name: 'Brand Banded',
6096
+ * firstRow: { fill:'1A2B3C', color:'FFFFFF', bold:true },
6097
+ * band1H: { fill:'EAF1F8' },
6098
+ * })
6099
+ * slide.addTable(rows, { tableStyle: brand, hasHeader:true, hasBandedRows:true })
6100
+ */
6101
+ defineTableStyle(props) {
6102
+ if (!props || typeof props !== "object") throw new Error("defineTableStyle() requires a `{ name, ... }` object argument");
6103
+ if (!props.name || typeof props.name !== "string") throw new Error("defineTableStyle() requires a non-empty `name`");
6104
+ const guid = `{${getUuid("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx").toUpperCase()}}`;
6105
+ this._tableStyles.push({
6106
+ guid,
6107
+ def: props
6108
+ });
6109
+ return guid;
6110
+ }
6111
+ /**
5356
6112
  * Reproduces an HTML table as a PowerPoint table - including column widths, style, etc. - creates 1 or more slides as needed
5357
6113
  * @param {string} eleId - table HTML element ID
5358
6114
  * @param {TableToSlidesProps} options - generation options
@@ -5364,4 +6120,4 @@ var PptxGenJS = class {
5364
6120
  //#endregion
5365
6121
  export { PptxGenJS as t };
5366
6122
 
5367
- //# sourceMappingURL=pptxgen-3gNqxx8n.js.map
6123
+ //# sourceMappingURL=pptxgen-B-mAxCRC.js.map