@jsamuel1/pptxgenjs 4.1.0 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- /* PptxGenJS 4.0.1 @ 2026-06-07T05:05:05.124Z */
1
+ /* PptxGenJS 4.1.1 @ 2026-06-07T21:17:30.867Z */
2
2
  'use strict';
3
3
 
4
4
  var JSZip = require('jszip');
@@ -581,6 +581,7 @@ var MASTER_OBJECTS;
581
581
  var SLIDE_OBJECT_TYPES;
582
582
  (function (SLIDE_OBJECT_TYPES) {
583
583
  SLIDE_OBJECT_TYPES["chart"] = "chart";
584
+ SLIDE_OBJECT_TYPES["group"] = "group";
584
585
  SLIDE_OBJECT_TYPES["hyperlink"] = "hyperlink";
585
586
  SLIDE_OBJECT_TYPES["image"] = "image";
586
587
  SLIDE_OBJECT_TYPES["media"] = "media";
@@ -664,7 +665,8 @@ function getUuid(uuidFormat) {
664
665
  return uuidFormat.replace(/[xy]/g, function (c) {
665
666
  const r = (Math.random() * 16) | 0;
666
667
  const v = c === 'x' ? r : (r & 0x3) | 0x8;
667
- return v.toString(16);
668
+ // OOXML ST_Guid requires uppercase hex: \{[0-9A-F]{8}-...\}
669
+ return v.toString(16).toUpperCase();
668
670
  });
669
671
  }
670
672
  /**
@@ -925,6 +927,170 @@ function correctShadowOptions(ShadowProps) {
925
927
  }
926
928
  return ShadowProps;
927
929
  }
930
+ /**
931
+ * Convert an SVG `<path d="…">` definition to an OOXML custom geometry (`<a:custGeom>`).
932
+ *
933
+ * Supports the following SVG path commands (both absolute upper-case and relative lower-case):
934
+ * - `M`/`m` moveTo → `<a:moveTo>` (extra coordinate pairs become implicit `lineTo`)
935
+ * - `L`/`l` lineTo → `<a:lnTo>`
936
+ * - `H`/`h` horizontal lineTo → `<a:lnTo>`
937
+ * - `V`/`v` vertical lineTo → `<a:lnTo>`
938
+ * - `C`/`c` cubic Bézier → `<a:cubicBezTo>`
939
+ * - `Q`/`q` quadratic Bézier → `<a:quadBezTo>`
940
+ * - `Z`/`z` close path → `<a:close>`
941
+ *
942
+ * Relative commands are tracked against the current pen position and converted to absolute
943
+ * coordinates. Coordinates are scaled from the SVG viewBox into EMU via `914400 / width`, so the
944
+ * viewBox width maps to exactly 1 inch (914400 EMU) of path coordinate space. The shape's on-slide
945
+ * dimensions still come from the `<a:xfrm><a:ext>` set by the caller — the path coordinate system is
946
+ * stretched to fit it — so the absolute scale here only needs to be internally consistent.
947
+ *
948
+ * @param {string} svgPathD - the SVG path `d` attribute (e.g. `"M 0 0 L 12 0 L 6 12 Z"`)
949
+ * @param {number} width - SVG viewBox width
950
+ * @param {number} height - SVG viewBox height
951
+ * @returns {string} OOXML `<a:custGeom>…</a:custGeom>` string (empty string on invalid input)
952
+ * @see ECMA-376 §20.1.9.8 (custGeom) / §20.1.9.16 (path2D)
953
+ */
954
+ function svgPathToOoxml(svgPathD, width, height) {
955
+ var _a;
956
+ if (!svgPathD || typeof svgPathD !== 'string' || !(width > 0) || !(height > 0))
957
+ return '';
958
+ const scale = 914400 / width;
959
+ const pathW = Math.round(width * scale);
960
+ const pathH = Math.round(height * scale);
961
+ // Match each command letter followed by its (possibly empty) run of numeric arguments
962
+ const commandRegex = /([MmLlHhVvCcQqZz])([^MmLlHhVvCcQqZz]*)/g;
963
+ // Match numbers incl. decimals, leading sign, and scientific notation
964
+ const numberRegex = /-?\d*\.?\d+(?:[eE][-+]?\d+)?/g;
965
+ const sc = (v) => Math.round(v * scale);
966
+ let curX = 0;
967
+ let curY = 0;
968
+ let startX = 0;
969
+ let startY = 0;
970
+ let xml = '';
971
+ let match;
972
+ while ((match = commandRegex.exec(svgPathD)) !== null) {
973
+ const cmd = match[1];
974
+ const isRel = cmd >= 'a' && cmd <= 'z';
975
+ const upper = cmd.toUpperCase();
976
+ const args = ((_a = match[2].match(numberRegex)) !== null && _a !== void 0 ? _a : []).map(Number);
977
+ let i = 0;
978
+ switch (upper) {
979
+ case 'M': {
980
+ // First coordinate pair is a moveTo; any subsequent pairs are implicit lineTo (per SVG spec)
981
+ let first = true;
982
+ while (i + 1 < args.length) {
983
+ let x = args[i];
984
+ let y = args[i + 1];
985
+ if (isRel) {
986
+ x += curX;
987
+ y += curY;
988
+ }
989
+ curX = x;
990
+ curY = y;
991
+ if (first) {
992
+ startX = curX;
993
+ startY = curY;
994
+ xml += `<a:moveTo><a:pt x="${sc(curX)}" y="${sc(curY)}"/></a:moveTo>`;
995
+ first = false;
996
+ }
997
+ else {
998
+ xml += `<a:lnTo><a:pt x="${sc(curX)}" y="${sc(curY)}"/></a:lnTo>`;
999
+ }
1000
+ i += 2;
1001
+ }
1002
+ break;
1003
+ }
1004
+ case 'L': {
1005
+ while (i + 1 < args.length) {
1006
+ let x = args[i];
1007
+ let y = args[i + 1];
1008
+ if (isRel) {
1009
+ x += curX;
1010
+ y += curY;
1011
+ }
1012
+ curX = x;
1013
+ curY = y;
1014
+ xml += `<a:lnTo><a:pt x="${sc(curX)}" y="${sc(curY)}"/></a:lnTo>`;
1015
+ i += 2;
1016
+ }
1017
+ break;
1018
+ }
1019
+ case 'H': {
1020
+ while (i < args.length) {
1021
+ let x = args[i];
1022
+ if (isRel)
1023
+ x += curX;
1024
+ curX = x;
1025
+ xml += `<a:lnTo><a:pt x="${sc(curX)}" y="${sc(curY)}"/></a:lnTo>`;
1026
+ i += 1;
1027
+ }
1028
+ break;
1029
+ }
1030
+ case 'V': {
1031
+ while (i < args.length) {
1032
+ let y = args[i];
1033
+ if (isRel)
1034
+ y += curY;
1035
+ curY = y;
1036
+ xml += `<a:lnTo><a:pt x="${sc(curX)}" y="${sc(curY)}"/></a:lnTo>`;
1037
+ i += 1;
1038
+ }
1039
+ break;
1040
+ }
1041
+ case 'C': {
1042
+ while (i + 5 < args.length) {
1043
+ let x1 = args[i];
1044
+ let y1 = args[i + 1];
1045
+ let x2 = args[i + 2];
1046
+ let y2 = args[i + 3];
1047
+ let x = args[i + 4];
1048
+ let y = args[i + 5];
1049
+ if (isRel) {
1050
+ x1 += curX;
1051
+ y1 += curY;
1052
+ x2 += curX;
1053
+ y2 += curY;
1054
+ x += curX;
1055
+ y += curY;
1056
+ }
1057
+ xml += `<a:cubicBezTo><a:pt x="${sc(x1)}" y="${sc(y1)}"/><a:pt x="${sc(x2)}" y="${sc(y2)}"/><a:pt x="${sc(x)}" y="${sc(y)}"/></a:cubicBezTo>`;
1058
+ curX = x;
1059
+ curY = y;
1060
+ i += 6;
1061
+ }
1062
+ break;
1063
+ }
1064
+ case 'Q': {
1065
+ while (i + 3 < args.length) {
1066
+ let x1 = args[i];
1067
+ let y1 = args[i + 1];
1068
+ let x = args[i + 2];
1069
+ let y = args[i + 3];
1070
+ if (isRel) {
1071
+ x1 += curX;
1072
+ y1 += curY;
1073
+ x += curX;
1074
+ y += curY;
1075
+ }
1076
+ xml += `<a:quadBezTo><a:pt x="${sc(x1)}" y="${sc(y1)}"/><a:pt x="${sc(x)}" y="${sc(y)}"/></a:quadBezTo>`;
1077
+ curX = x;
1078
+ curY = y;
1079
+ i += 4;
1080
+ }
1081
+ break;
1082
+ }
1083
+ case 'Z': {
1084
+ xml += '<a:close/>';
1085
+ // Pen returns to the start of the current subpath
1086
+ curX = startX;
1087
+ curY = startY;
1088
+ break;
1089
+ }
1090
+ }
1091
+ }
1092
+ return `<a:custGeom><a:avLst/><a:gdLst/><a:ahLst/><a:cxnLst/><a:rect l="l" t="t" r="r" b="b"/><a:pathLst><a:path w="${pathW}" h="${pathH}">${xml}</a:path></a:pathLst></a:custGeom>`;
1093
+ }
928
1094
 
929
1095
  /**
930
1096
  * PptxGenJS: Table Generation
@@ -2315,6 +2481,90 @@ function addShapeDefinition(target, shapeName, opts) {
2315
2481
  // LAST: Add object to slide
2316
2482
  target._slideObjects.push(newObject);
2317
2483
  }
2484
+ /**
2485
+ * Feature 7: Adds a rounded-rectangle callout/badge to a slide definition.
2486
+ * Thin sugar over `addTextDefinition` with `shape:'roundRect'`, centred text, and a
2487
+ * corner-radius `adj` value computed from `cornerRadius` (inches) per:
2488
+ * `adj = Math.round((cornerRadius / (h / 2)) * 50000)`.
2489
+ * @param {PresSlide} target slide object that the callout should be added to
2490
+ * @param {CalloutProps} opts callout options
2491
+ */
2492
+ function addCalloutDefinition(target, opts) {
2493
+ const options = typeof opts === 'object' ? opts : {};
2494
+ const h = options.h !== undefined ? Number(options.h) : 0.4;
2495
+ const w = options.w !== undefined ? options.w : 1.5;
2496
+ const cornerRadius = options.cornerRadius !== undefined ? options.cornerRadius : 0.1;
2497
+ // Map inches -> OOXML `adj` (percentage of half-shortest-side × 1000). Guard divide-by-zero.
2498
+ const calloutAdj = h > 0 ? Math.round((cornerRadius / (h / 2)) * 50000) : 0;
2499
+ const fill = options.fill !== undefined ? options.fill : '7C3AED';
2500
+ const textOpts = {
2501
+ shape: SHAPE_TYPE.ROUNDED_RECTANGLE,
2502
+ x: options.x !== undefined ? options.x : 1,
2503
+ y: options.y !== undefined ? options.y : 1,
2504
+ w,
2505
+ h,
2506
+ fill: typeof fill === 'string' ? { color: fill } : fill,
2507
+ color: options.fontColor !== undefined ? options.fontColor : 'FFFFFF',
2508
+ fontSize: options.fontSize !== undefined ? options.fontSize : 12,
2509
+ bold: options.fontBold !== undefined ? options.fontBold : true,
2510
+ align: options.align || 'center',
2511
+ valign: options.valign || 'middle',
2512
+ _calloutAdj: calloutAdj,
2513
+ };
2514
+ if (options.objectName)
2515
+ textOpts.objectName = options.objectName;
2516
+ addTextDefinition(target, [{ text: options.text || '', options: null }], textOpts, false);
2517
+ }
2518
+ /**
2519
+ * Feature 6: Adds a shape group to a slide definition and returns a group handle.
2520
+ * The group emits a `<p:grpSp>` whose `<a:xfrm>` carries the absolute position/size plus
2521
+ * `chOff="0,0"`/`chExt` equal to the extent — so child shapes/text use coordinates relative
2522
+ * to the group origin (1:1 scale). Children are added via the returned object's
2523
+ * `addShape()` / `addText()`, which reuse the existing shape/text intake but push onto the
2524
+ * group's private child array instead of the slide.
2525
+ * @param {PresSlide} target slide the group should be added to
2526
+ * @param {GroupProps} opts group position/size options
2527
+ * @return {SlideGroup} group handle exposing `addShape` / `addText`
2528
+ */
2529
+ function addGroupDefinition(target, opts) {
2530
+ const options = typeof opts === 'object' ? opts : {};
2531
+ const grpObjects = [];
2532
+ const groupObj = {
2533
+ _type: SLIDE_OBJECT_TYPES.group,
2534
+ options: {
2535
+ x: options.x !== undefined ? options.x : 0,
2536
+ y: options.y !== undefined ? options.y : 0,
2537
+ w: options.w !== undefined ? options.w : 0,
2538
+ h: options.h !== undefined ? options.h : 0,
2539
+ objectName: options.objectName ? encodeXmlEntities(options.objectName) : `Group ${target._slideObjects.filter(obj => obj._type === SLIDE_OBJECT_TYPES.group).length + 1}`,
2540
+ },
2541
+ _grpObjects: grpObjects,
2542
+ };
2543
+ target._slideObjects.push(groupObj);
2544
+ // Proxy target: existing intake fns push onto the group's child array but reuse the parent
2545
+ // slide's rels/layout/color so child shapes/text render identically to top-level ones.
2546
+ const childTarget = {
2547
+ _slideObjects: grpObjects,
2548
+ _rels: target._rels,
2549
+ _relsChart: target._relsChart,
2550
+ _relsMedia: target._relsMedia,
2551
+ _slideLayout: target._slideLayout,
2552
+ _presLayout: target._presLayout,
2553
+ color: target.color,
2554
+ };
2555
+ const group = {
2556
+ addShape(shapeName, shapeOpts) {
2557
+ addShapeDefinition(childTarget, shapeName, (shapeOpts || {}));
2558
+ return group;
2559
+ },
2560
+ addText(text, textOpts) {
2561
+ const textParam = typeof text === 'string' || typeof text === 'number' ? [{ text, options: textOpts }] : text;
2562
+ addTextDefinition(childTarget, textParam, (textOpts || {}), false);
2563
+ return group;
2564
+ },
2565
+ };
2566
+ return group;
2567
+ }
2318
2568
  /**
2319
2569
  * Adds a table object to a slide definition.
2320
2570
  * @param {PresSlide} target - slide object that the table should be added to
@@ -2685,7 +2935,11 @@ function addTextDefinition(target, text, opts, isPlaceholder) {
2685
2935
  itemOpts.lineSpacingMultiple = itemOpts.lineSpacingMultiple && !isNaN(itemOpts.lineSpacingMultiple) ? itemOpts.lineSpacingMultiple : null;
2686
2936
  // D: Transform text options to bodyProperties as thats how we build XML
2687
2937
  itemOpts._bodyProp = itemOpts._bodyProp || {};
2688
- itemOpts._bodyProp.autoFit = itemOpts.autoFit || false; // DEPRECATED: (3.3.0) If true, shape will collapse to text size (Fit To shape)
2938
+ // Back-compat: legacy `autoFit: true` ("resize shape to fit text") now maps to `fit: 'resize'`.
2939
+ // Routing through `fit` keeps a single code path (and avoids emitting `<a:spAutoFit/>` twice). @deprecated 3.3.0
2940
+ if (itemOpts.autoFit === true && !itemOpts.fit)
2941
+ itemOpts.fit = 'resize';
2942
+ itemOpts._bodyProp.autoFit = false; // DEPRECATED: (3.3.0) superseded by `fit` (see above)
2689
2943
  itemOpts._bodyProp.anchor = !itemOpts.placeholder ? TEXT_VALIGN.ctr : null; // VALS: [t,ctr,b]
2690
2944
  itemOpts._bodyProp.vert = itemOpts.vert || null; // VALS: [eaVert,horz,mongolianVert,vert,vert270,wordArtVert,wordArtVertRtl]
2691
2945
  itemOpts._bodyProp.wrap = typeof itemOpts.wrap === 'boolean' ? itemOpts.wrap : true;
@@ -2994,6 +3248,27 @@ class Slide {
2994
3248
  addShapeDefinition(this, shapeName, options);
2995
3249
  return this;
2996
3250
  }
3251
+ /**
3252
+ * Add a shape group to Slide (Feature 6).
3253
+ * Returns a group handle whose `addShape()`/`addText()` use coordinates relative to the
3254
+ * group origin; the group emits a `<p:grpSp>` with an absolute `<a:xfrm>` + `chOff`/`chExt`.
3255
+ * @param {GroupProps} options - group position/size options
3256
+ * @return {SlideGroup} group handle exposing `addShape` / `addText`
3257
+ */
3258
+ addGroup(options) {
3259
+ return addGroupDefinition(this, options);
3260
+ }
3261
+ /**
3262
+ * Add a rounded-rectangle callout/badge to Slide (Feature 7).
3263
+ * Sugar over `addShape('roundRect', …)` with centred text and an `adj`
3264
+ * corner-radius derived from `cornerRadius` (inches).
3265
+ * @param {CalloutProps} options - callout options
3266
+ * @return {Slide} this Slide
3267
+ */
3268
+ addCallout(options) {
3269
+ addCalloutDefinition(this, options);
3270
+ return this;
3271
+ }
2997
3272
  /**
2998
3273
  * Add table to Slide
2999
3274
  * @param {TableRow[]} tableRows - table rows
@@ -5292,6 +5567,20 @@ function slideObjectToXml(slide) {
5292
5567
  if (placeholderObj.options.h || placeholderObj.options.h === 0)
5293
5568
  cy = getSmartParseNumber(placeholderObj.options.h, 'Y', slide._presLayout);
5294
5569
  }
5570
+ // Normalize negative extents: PPTX requires cx/cy >= 0 (ST_PositiveCoordinate); encode direction via flip
5571
+ // A shape drawn "backwards" (e.g. a line with negative w/h) otherwise emits an invalid `<a:ext>` that triggers PowerPoint repair.
5572
+ if (cx < 0) {
5573
+ x += cx;
5574
+ cx = Math.abs(cx);
5575
+ imgWidth = cx;
5576
+ slideItemObj.options.flipH = !slideItemObj.options.flipH;
5577
+ }
5578
+ if (cy < 0) {
5579
+ y += cy;
5580
+ cy = Math.abs(cy);
5581
+ imgHeight = cy;
5582
+ slideItemObj.options.flipV = !slideItemObj.options.flipV;
5583
+ }
5295
5584
  //
5296
5585
  if (slideItemObj.options.flipH)
5297
5586
  locationAttr += ' flipH="1"';
@@ -5562,7 +5851,11 @@ function slideObjectToXml(slide) {
5562
5851
  strSlideXml += `<a:xfrm${locationAttr}>`;
5563
5852
  strSlideXml += `<a:off x="${x}" y="${y}"/>`;
5564
5853
  strSlideXml += `<a:ext cx="${cx}" cy="${cy}"/></a:xfrm>`;
5565
- if (slideItemObj.shape === 'custGeom') {
5854
+ if (slideItemObj.options.svgPath) {
5855
+ // Feature 9: Convert an SVG path to OOXML custom geometry (<a:custGeom>) instead of a preset geometry.
5856
+ strSlideXml += svgPathToOoxml(slideItemObj.options.svgPath.d, slideItemObj.options.svgPath.viewBox.w, slideItemObj.options.svgPath.viewBox.h);
5857
+ }
5858
+ else if (slideItemObj.shape === 'custGeom') {
5566
5859
  strSlideXml += '<a:custGeom><a:avLst />';
5567
5860
  strSlideXml += '<a:gdLst>';
5568
5861
  strSlideXml += '</a:gdLst>';
@@ -5609,7 +5902,11 @@ function slideObjectToXml(slide) {
5609
5902
  }
5610
5903
  else {
5611
5904
  strSlideXml += '<a:prstGeom prst="' + slideItemObj.shape + '"><a:avLst>';
5612
- if (slideItemObj.options.rectRadius) {
5905
+ if (slideItemObj.options._calloutAdj !== undefined && slideItemObj.options._calloutAdj !== null) {
5906
+ // Feature 7: addCallout() supplies a pre-computed `adj` value; emit it verbatim.
5907
+ strSlideXml += `<a:gd name="adj" fmla="val ${slideItemObj.options._calloutAdj}"/>`;
5908
+ }
5909
+ else if (slideItemObj.options.rectRadius) {
5613
5910
  strSlideXml += `<a:gd name="adj" fmla="val ${Math.round((slideItemObj.options.rectRadius * EMU * 100000) / Math.min(cx, cy))}"/>`;
5614
5911
  }
5615
5912
  else if (slideItemObj.options.angleRange) {
@@ -5639,23 +5936,33 @@ function slideObjectToXml(slide) {
5639
5936
  // FUTURE: `endArrowSize` < a: headEnd type = "arrow" w = "lg" len = "lg" /> 'sm' | 'med' | 'lg'(values are 1 - 9, making a 3x3 grid of w / len possibilities)
5640
5937
  strSlideXml += '</a:ln>';
5641
5938
  }
5642
- // EFFECTS > SHADOW: REF: @see http://officeopenxml.com/drwSp-effects.php
5643
- if (slideItemObj.options.shadow && slideItemObj.options.shadow.type !== 'none') {
5644
- // derive emit-time values into locals so we don't mutate the user's options.shadow
5645
- // (re-emission would otherwise re-convert pt→EMU and produce absurd values).
5646
- const sh = slideItemObj.options.shadow;
5647
- const shadowType = sh.type || 'outer';
5648
- const shadowBlur = valToPts(sh.blur || 8);
5649
- const shadowOffset = valToPts(sh.offset || 4);
5650
- const shadowAngle = Math.round((sh.angle || 270) * 60000);
5651
- const shadowOpacity = Math.round((sh.opacity || 0.75) * 100000);
5652
- const shadowColor = sh.color || DEF_TEXT_SHADOW.color;
5653
- strSlideXml += '<a:effectLst>';
5654
- strSlideXml += ` <a:${shadowType}Shdw ${shadowType === 'outer' ? 'sx="100000" sy="100000" kx="0" ky="0" algn="bl" rotWithShape="0"' : ''} blurRad="${shadowBlur}" dist="${shadowOffset}" dir="${shadowAngle}">`;
5655
- strSlideXml += ` <a:srgbClr val="${shadowColor}">`;
5656
- strSlideXml += ` <a:alpha val="${shadowOpacity}"/></a:srgbClr>`;
5657
- strSlideXml += ' </a:outerShdw>';
5658
- strSlideXml += '</a:effectLst>';
5939
+ // EFFECTS > SHADOW + GLOW (Feature 10): REF: @see http://officeopenxml.com/drwSp-effects.php
5940
+ // Both effects share a single <a:effectLst>; emit it once if either is present.
5941
+ {
5942
+ const hasShadow = !!(slideItemObj.options.shadow && slideItemObj.options.shadow.type !== 'none');
5943
+ const hasGlow = !!slideItemObj.options.glow;
5944
+ if (hasShadow || hasGlow) {
5945
+ strSlideXml += '<a:effectLst>';
5946
+ if (hasShadow) {
5947
+ // derive emit-time values into locals so we don't mutate the user's options.shadow
5948
+ // (re-emission would otherwise re-convert pt→EMU and produce absurd values).
5949
+ const sh = slideItemObj.options.shadow;
5950
+ const shadowType = sh.type || 'outer';
5951
+ const shadowBlur = valToPts(sh.blur || 8);
5952
+ const shadowOffset = valToPts(sh.offset || 4);
5953
+ const shadowAngle = Math.round((sh.angle || 270) * 60000);
5954
+ const shadowOpacity = Math.round((sh.opacity || 0.75) * 100000);
5955
+ const shadowColor = sh.color || DEF_TEXT_SHADOW.color;
5956
+ strSlideXml += `<a:${shadowType}Shdw ${shadowType === 'outer' ? 'sx="100000" sy="100000" kx="0" ky="0" algn="bl" rotWithShape="0"' : ''} blurRad="${shadowBlur}" dist="${shadowOffset}" dir="${shadowAngle}">`;
5957
+ strSlideXml += `<a:srgbClr val="${shadowColor}">`;
5958
+ strSlideXml += `<a:alpha val="${shadowOpacity}"/></a:srgbClr>`;
5959
+ strSlideXml += `</a:${shadowType}Shdw>`;
5960
+ }
5961
+ if (hasGlow) {
5962
+ strSlideXml += createGlowElement(slideItemObj.options.glow, DEF_TEXT_GLOW);
5963
+ }
5964
+ strSlideXml += '</a:effectLst>';
5965
+ }
5659
5966
  }
5660
5967
  /* TODO: FUTURE: Text wrapping (copied from MS-PPTX export)
5661
5968
  // Commented out b/c i'm not even sure this works - current code produces text that wraps in shapes and textboxes, so...
@@ -5803,6 +6110,20 @@ function slideObjectToXml(slide) {
5803
6110
  strSlideXml += ' </a:graphic>';
5804
6111
  strSlideXml += '</p:graphicFrame>';
5805
6112
  break;
6113
+ case SLIDE_OBJECT_TYPES.group:
6114
+ // Feature 6: nested shape group. The `<a:xfrm>` carries the absolute position/size;
6115
+ // `chOff="0,0"` + `chExt`=ext gives a 1:1 child coordinate space, so children use
6116
+ // coordinates relative to the group origin. Children reuse the standard shape/text
6117
+ // emitters via `genGroupChildrenXml`.
6118
+ strSlideXml += '<p:grpSp>';
6119
+ strSlideXml += `<p:nvGrpSpPr><p:cNvPr id="${idx + 2}" name="${slideItemObj.options.objectName || `Group ${idx + 1}`}"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>`;
6120
+ strSlideXml += `<p:grpSpPr><a:xfrm${locationAttr}>`;
6121
+ strSlideXml += `<a:off x="${x}" y="${y}"/><a:ext cx="${cx}" cy="${cy}"/>`;
6122
+ strSlideXml += `<a:chOff x="0" y="0"/><a:chExt cx="${cx}" cy="${cy}"/>`;
6123
+ strSlideXml += '</a:xfrm></p:grpSpPr>';
6124
+ strSlideXml += genGroupChildrenXml(slide, slideItemObj._grpObjects, idx);
6125
+ strSlideXml += '</p:grpSp>';
6126
+ break;
5806
6127
  default:
5807
6128
  strSlideXml += '';
5808
6129
  break;
@@ -5874,6 +6195,47 @@ function slideObjectToXml(slide) {
5874
6195
  // LAST: Return
5875
6196
  return strSlideXml;
5876
6197
  }
6198
+ /**
6199
+ * Feature 6: Render a shape group's child objects as the inner markup of a `<p:grpSp>`.
6200
+ * Reuses the full slide emitter (`slideObjectToXml`) on the same slide with its object list
6201
+ * temporarily swapped to the group's children (and slide-number footer suppressed), then
6202
+ * extracts just the `<p:spTree>` child markup (everything after the root `</p:grpSpPr>` up to
6203
+ * `</p:spTree>`). Child `<p:cNvPr>` ids are offset to stay unique within the slide part so
6204
+ * PowerPoint does not flag the file for repair.
6205
+ * @param {PresSlide | SlideLayout} slide - parent slide (provides layout/rels/presLayout)
6206
+ * @param {ISlideObject[]} grpObjects - the group's child slide objects
6207
+ * @param {number} groupIdx - the group's index within the slide (used to namespace child ids)
6208
+ * @return {string} child markup to nest inside `<p:grpSp>`
6209
+ */
6210
+ function genGroupChildrenXml(slide, grpObjects, groupIdx) {
6211
+ if (!grpObjects || grpObjects.length === 0)
6212
+ return '';
6213
+ // Temporarily render the children through the normal slide pipeline. Background is emitted
6214
+ // before `<p:spTree>` (outside the extracted region) so it is harmless; the slide-number
6215
+ // footer is emitted inside `<p:spTree>`, so suppress it during the recursive render.
6216
+ const savedObjects = slide._slideObjects;
6217
+ const savedSlideNum = slide._slideNumberProps;
6218
+ slide._slideObjects = grpObjects;
6219
+ slide._slideNumberProps = null;
6220
+ let fullXml;
6221
+ try {
6222
+ fullXml = slideObjectToXml(slide);
6223
+ }
6224
+ finally {
6225
+ slide._slideObjects = savedObjects;
6226
+ slide._slideNumberProps = savedSlideNum;
6227
+ }
6228
+ const startMarker = '</p:grpSpPr>';
6229
+ const startIdx = fullXml.indexOf(startMarker);
6230
+ const endIdx = fullXml.lastIndexOf('</p:spTree>');
6231
+ if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx)
6232
+ return '';
6233
+ let childXml = fullXml.substring(startIdx + startMarker.length, endIdx);
6234
+ // Keep child cNvPr ids unique within the slide part (avoid PowerPoint "needs repair").
6235
+ const idBase = (groupIdx + 1) * 1000;
6236
+ childXml = childXml.replace(/<p:cNvPr id="(\d+)"/g, (_m, n) => `<p:cNvPr id="${idBase + Number(n)}"`);
6237
+ return childXml;
6238
+ }
5877
6239
  /**
5878
6240
  * Transforms slide relations to XML string.
5879
6241
  * Extra relations that are not dynamic can be passed using the 2nd arg (e.g. theme relation in master file).
@@ -6202,6 +6564,13 @@ function genXmlBodyProperties(slideObject) {
6202
6564
  bodyProperties += ' anchor="' + slideObject.options._bodyProp.anchor + '"'; // VALS: [t,ctr,b]
6203
6565
  if (slideObject.options._bodyProp.vert)
6204
6566
  bodyProperties += ' vert="' + slideObject.options._bodyProp.vert + '"'; // VALS: [eaVert,horz,mongolianVert,vert,vert270,wordArtVert,wordArtVertRtl]
6567
+ // D2: Multi-column text (numCol/spcCol attributes on <a:bodyPr>)
6568
+ // NOTE: must be appended as attributes BEFORE the opening tag is closed below (section E)
6569
+ if (slideObject.options.columns && slideObject.options.columns > 1) {
6570
+ bodyProperties += ` numCol="${Math.round(slideObject.options.columns)}"`;
6571
+ const spcColIn = typeof slideObject.options.columnSpacing === 'number' ? slideObject.options.columnSpacing : 0.5;
6572
+ bodyProperties += ` spcCol="${Math.round(spcColIn * EMU)}"`;
6573
+ }
6205
6574
  // E: Close <a:bodyPr element
6206
6575
  bodyProperties += '>';
6207
6576
  /**
@@ -6213,10 +6582,10 @@ function genXmlBodyProperties(slideObject) {
6213
6582
  // NOTE: Use of '<a:noAutofit/>' instead of '' causes issues in PPT-2013!
6214
6583
  if (slideObject.options.fit === 'none')
6215
6584
  bodyProperties += '';
6216
- // NOTE: Shrink does not work automatically - PowerPoint calculates the `fontScale` value dynamically upon resize
6217
- // else if (slideObject.options.fit === 'shrink') bodyProperties += '<a:normAutofit fontScale="85000" lnSpcReduction="20000"/>' // MS-PPT > Format shape > Text Options: "Shrink text on overflow"
6585
+ // "Shrink text on overflow": emit a fixed `fontScale` (70%) so the text is scaled down to fit the shape.
6586
+ // NOTE: PowerPoint recalculates `fontScale` dynamically once the text/shape is edited; the emitted value is the initial scale.
6218
6587
  else if (slideObject.options.fit === 'shrink')
6219
- bodyProperties += '<a:normAutofit/>';
6588
+ bodyProperties += '<a:normAutofit fontScale="70000"/>';
6220
6589
  else if (slideObject.options.fit === 'resize')
6221
6590
  bodyProperties += '<a:spAutoFit/>';
6222
6591
  }
@@ -6744,14 +7113,15 @@ function genXmlAnimPayload(anim, spid, nextId) {
6744
7113
  }
6745
7114
  // flyIn ADDS a <p:anim> that translates the shape from offscreen to its final position.
6746
7115
  // direction (TransitionDirection: left|right|up|down) -> animated attr + tm="0" start formula:
6747
- // left -> ppt_x, "0-#ppt_w/2" right -> ppt_x, "1+#ppt_w/2"
6748
- // up -> ppt_y, "0-#ppt_h/2" down -> ppt_y, "1+#ppt_h/2"
7116
+ // left -> ppt_x, "#ppt_x-1slide" right -> ppt_x, "#ppt_x+1slide"
7117
+ // up -> ppt_y, "#ppt_y+1slide" down -> ppt_y, "#ppt_y-1slide"
7118
+ // The "1slide" offset starts the shape one full slide-width/height offscreen (PowerPoint-native Fly In).
6749
7119
  if (anim.type === 'flyIn') {
6750
7120
  const flyMap = {
6751
- left: { attr: 'ppt_x', start: '0-#ppt_w/2' },
6752
- right: { attr: 'ppt_x', start: '1+#ppt_w/2' },
6753
- up: { attr: 'ppt_y', start: '0-#ppt_h/2' },
6754
- down: { attr: 'ppt_y', start: '1+#ppt_h/2' },
7121
+ left: { attr: 'ppt_x', start: '#ppt_x-1slide' },
7122
+ right: { attr: 'ppt_x', start: '#ppt_x+1slide' },
7123
+ up: { attr: 'ppt_y', start: '#ppt_y+1slide' },
7124
+ down: { attr: 'ppt_y', start: '#ppt_y-1slide' },
6755
7125
  };
6756
7126
  const { attr, start } = (_b = flyMap[(_a = anim.direction) !== null && _a !== void 0 ? _a : 'left']) !== null && _b !== void 0 ? _b : flyMap.left;
6757
7127
  payload +=
@@ -7201,7 +7571,7 @@ function makeXmlViewProps() {
7201
7571
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
7202
7572
  * SOFTWARE.
7203
7573
  */
7204
- const VERSION = '4.0.1';
7574
+ const VERSION = '4.1.1';
7205
7575
  class PptxGenJS {
7206
7576
  set layout(value) {
7207
7577
  const newLayout = this.LAYOUTS[value];