@jsamuel1/pptxgenjs 4.1.1 → 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.1.0 @ 2026-06-07T11:53:01.413Z */
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";
@@ -2480,6 +2481,90 @@ function addShapeDefinition(target, shapeName, opts) {
2480
2481
  // LAST: Add object to slide
2481
2482
  target._slideObjects.push(newObject);
2482
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
+ }
2483
2568
  /**
2484
2569
  * Adds a table object to a slide definition.
2485
2570
  * @param {PresSlide} target - slide object that the table should be added to
@@ -3163,6 +3248,27 @@ class Slide {
3163
3248
  addShapeDefinition(this, shapeName, options);
3164
3249
  return this;
3165
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
+ }
3166
3272
  /**
3167
3273
  * Add table to Slide
3168
3274
  * @param {TableRow[]} tableRows - table rows
@@ -5796,7 +5902,11 @@ function slideObjectToXml(slide) {
5796
5902
  }
5797
5903
  else {
5798
5904
  strSlideXml += '<a:prstGeom prst="' + slideItemObj.shape + '"><a:avLst>';
5799
- 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) {
5800
5910
  strSlideXml += `<a:gd name="adj" fmla="val ${Math.round((slideItemObj.options.rectRadius * EMU * 100000) / Math.min(cx, cy))}"/>`;
5801
5911
  }
5802
5912
  else if (slideItemObj.options.angleRange) {
@@ -5826,23 +5936,33 @@ function slideObjectToXml(slide) {
5826
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)
5827
5937
  strSlideXml += '</a:ln>';
5828
5938
  }
5829
- // EFFECTS > SHADOW: REF: @see http://officeopenxml.com/drwSp-effects.php
5830
- if (slideItemObj.options.shadow && slideItemObj.options.shadow.type !== 'none') {
5831
- // derive emit-time values into locals so we don't mutate the user's options.shadow
5832
- // (re-emission would otherwise re-convert pt→EMU and produce absurd values).
5833
- const sh = slideItemObj.options.shadow;
5834
- const shadowType = sh.type || 'outer';
5835
- const shadowBlur = valToPts(sh.blur || 8);
5836
- const shadowOffset = valToPts(sh.offset || 4);
5837
- const shadowAngle = Math.round((sh.angle || 270) * 60000);
5838
- const shadowOpacity = Math.round((sh.opacity || 0.75) * 100000);
5839
- const shadowColor = sh.color || DEF_TEXT_SHADOW.color;
5840
- strSlideXml += '<a:effectLst>';
5841
- strSlideXml += ` <a:${shadowType}Shdw ${shadowType === 'outer' ? 'sx="100000" sy="100000" kx="0" ky="0" algn="bl" rotWithShape="0"' : ''} blurRad="${shadowBlur}" dist="${shadowOffset}" dir="${shadowAngle}">`;
5842
- strSlideXml += ` <a:srgbClr val="${shadowColor}">`;
5843
- strSlideXml += ` <a:alpha val="${shadowOpacity}"/></a:srgbClr>`;
5844
- strSlideXml += ' </a:outerShdw>';
5845
- 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
+ }
5846
5966
  }
5847
5967
  /* TODO: FUTURE: Text wrapping (copied from MS-PPTX export)
5848
5968
  // Commented out b/c i'm not even sure this works - current code produces text that wraps in shapes and textboxes, so...
@@ -5990,6 +6110,20 @@ function slideObjectToXml(slide) {
5990
6110
  strSlideXml += ' </a:graphic>';
5991
6111
  strSlideXml += '</p:graphicFrame>';
5992
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;
5993
6127
  default:
5994
6128
  strSlideXml += '';
5995
6129
  break;
@@ -6061,6 +6195,47 @@ function slideObjectToXml(slide) {
6061
6195
  // LAST: Return
6062
6196
  return strSlideXml;
6063
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
+ }
6064
6239
  /**
6065
6240
  * Transforms slide relations to XML string.
6066
6241
  * Extra relations that are not dynamic can be passed using the 2nd arg (e.g. theme relation in master file).
@@ -7396,7 +7571,7 @@ function makeXmlViewProps() {
7396
7571
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
7397
7572
  * SOFTWARE.
7398
7573
  */
7399
- const VERSION = '4.0.1';
7574
+ const VERSION = '4.1.1';
7400
7575
  class PptxGenJS {
7401
7576
  set layout(value) {
7402
7577
  const newLayout = this.LAYOUTS[value];
@@ -1,4 +1,4 @@
1
- /* PptxGenJS 4.1.0 @ 2026-06-07T11:53:01.419Z */
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";
@@ -2478,6 +2479,90 @@ function addShapeDefinition(target, shapeName, opts) {
2478
2479
  // LAST: Add object to slide
2479
2480
  target._slideObjects.push(newObject);
2480
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
+ }
2481
2566
  /**
2482
2567
  * Adds a table object to a slide definition.
2483
2568
  * @param {PresSlide} target - slide object that the table should be added to
@@ -3161,6 +3246,27 @@ class Slide {
3161
3246
  addShapeDefinition(this, shapeName, options);
3162
3247
  return this;
3163
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
+ }
3164
3270
  /**
3165
3271
  * Add table to Slide
3166
3272
  * @param {TableRow[]} tableRows - table rows
@@ -5794,7 +5900,11 @@ function slideObjectToXml(slide) {
5794
5900
  }
5795
5901
  else {
5796
5902
  strSlideXml += '<a:prstGeom prst="' + slideItemObj.shape + '"><a:avLst>';
5797
- 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) {
5798
5908
  strSlideXml += `<a:gd name="adj" fmla="val ${Math.round((slideItemObj.options.rectRadius * EMU * 100000) / Math.min(cx, cy))}"/>`;
5799
5909
  }
5800
5910
  else if (slideItemObj.options.angleRange) {
@@ -5824,23 +5934,33 @@ function slideObjectToXml(slide) {
5824
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)
5825
5935
  strSlideXml += '</a:ln>';
5826
5936
  }
5827
- // EFFECTS > SHADOW: REF: @see http://officeopenxml.com/drwSp-effects.php
5828
- if (slideItemObj.options.shadow && slideItemObj.options.shadow.type !== 'none') {
5829
- // derive emit-time values into locals so we don't mutate the user's options.shadow
5830
- // (re-emission would otherwise re-convert pt→EMU and produce absurd values).
5831
- const sh = slideItemObj.options.shadow;
5832
- const shadowType = sh.type || 'outer';
5833
- const shadowBlur = valToPts(sh.blur || 8);
5834
- const shadowOffset = valToPts(sh.offset || 4);
5835
- const shadowAngle = Math.round((sh.angle || 270) * 60000);
5836
- const shadowOpacity = Math.round((sh.opacity || 0.75) * 100000);
5837
- const shadowColor = sh.color || DEF_TEXT_SHADOW.color;
5838
- strSlideXml += '<a:effectLst>';
5839
- strSlideXml += ` <a:${shadowType}Shdw ${shadowType === 'outer' ? 'sx="100000" sy="100000" kx="0" ky="0" algn="bl" rotWithShape="0"' : ''} blurRad="${shadowBlur}" dist="${shadowOffset}" dir="${shadowAngle}">`;
5840
- strSlideXml += ` <a:srgbClr val="${shadowColor}">`;
5841
- strSlideXml += ` <a:alpha val="${shadowOpacity}"/></a:srgbClr>`;
5842
- strSlideXml += ' </a:outerShdw>';
5843
- 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
+ }
5844
5964
  }
5845
5965
  /* TODO: FUTURE: Text wrapping (copied from MS-PPTX export)
5846
5966
  // Commented out b/c i'm not even sure this works - current code produces text that wraps in shapes and textboxes, so...
@@ -5988,6 +6108,20 @@ function slideObjectToXml(slide) {
5988
6108
  strSlideXml += ' </a:graphic>';
5989
6109
  strSlideXml += '</p:graphicFrame>';
5990
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;
5991
6125
  default:
5992
6126
  strSlideXml += '';
5993
6127
  break;
@@ -6059,6 +6193,47 @@ function slideObjectToXml(slide) {
6059
6193
  // LAST: Return
6060
6194
  return strSlideXml;
6061
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
+ }
6062
6237
  /**
6063
6238
  * Transforms slide relations to XML string.
6064
6239
  * Extra relations that are not dynamic can be passed using the 2nd arg (e.g. theme relation in master file).
@@ -7394,7 +7569,7 @@ function makeXmlViewProps() {
7394
7569
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
7395
7570
  * SOFTWARE.
7396
7571
  */
7397
- const VERSION = '4.0.1';
7572
+ const VERSION = '4.1.1';
7398
7573
  class PptxGenJS {
7399
7574
  set layout(value) {
7400
7575
  const newLayout = this.LAYOUTS[value];