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