@jsamuel1/pptxgenjs 4.1.0 → 4.1.1

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.0 @ 2026-06-07T11:53:01.413Z */
2
2
  'use strict';
3
3
 
4
4
  var JSZip = require('jszip');
@@ -664,7 +664,8 @@ function getUuid(uuidFormat) {
664
664
  return uuidFormat.replace(/[xy]/g, function (c) {
665
665
  const r = (Math.random() * 16) | 0;
666
666
  const v = c === 'x' ? r : (r & 0x3) | 0x8;
667
- return v.toString(16);
667
+ // OOXML ST_Guid requires uppercase hex: \{[0-9A-F]{8}-...\}
668
+ return v.toString(16).toUpperCase();
668
669
  });
669
670
  }
670
671
  /**
@@ -925,6 +926,170 @@ function correctShadowOptions(ShadowProps) {
925
926
  }
926
927
  return ShadowProps;
927
928
  }
929
+ /**
930
+ * Convert an SVG `<path d="…">` definition to an OOXML custom geometry (`<a:custGeom>`).
931
+ *
932
+ * Supports the following SVG path commands (both absolute upper-case and relative lower-case):
933
+ * - `M`/`m` moveTo → `<a:moveTo>` (extra coordinate pairs become implicit `lineTo`)
934
+ * - `L`/`l` lineTo → `<a:lnTo>`
935
+ * - `H`/`h` horizontal lineTo → `<a:lnTo>`
936
+ * - `V`/`v` vertical lineTo → `<a:lnTo>`
937
+ * - `C`/`c` cubic Bézier → `<a:cubicBezTo>`
938
+ * - `Q`/`q` quadratic Bézier → `<a:quadBezTo>`
939
+ * - `Z`/`z` close path → `<a:close>`
940
+ *
941
+ * Relative commands are tracked against the current pen position and converted to absolute
942
+ * coordinates. Coordinates are scaled from the SVG viewBox into EMU via `914400 / width`, so the
943
+ * viewBox width maps to exactly 1 inch (914400 EMU) of path coordinate space. The shape's on-slide
944
+ * dimensions still come from the `<a:xfrm><a:ext>` set by the caller — the path coordinate system is
945
+ * stretched to fit it — so the absolute scale here only needs to be internally consistent.
946
+ *
947
+ * @param {string} svgPathD - the SVG path `d` attribute (e.g. `"M 0 0 L 12 0 L 6 12 Z"`)
948
+ * @param {number} width - SVG viewBox width
949
+ * @param {number} height - SVG viewBox height
950
+ * @returns {string} OOXML `<a:custGeom>…</a:custGeom>` string (empty string on invalid input)
951
+ * @see ECMA-376 §20.1.9.8 (custGeom) / §20.1.9.16 (path2D)
952
+ */
953
+ function svgPathToOoxml(svgPathD, width, height) {
954
+ var _a;
955
+ if (!svgPathD || typeof svgPathD !== 'string' || !(width > 0) || !(height > 0))
956
+ return '';
957
+ const scale = 914400 / width;
958
+ const pathW = Math.round(width * scale);
959
+ const pathH = Math.round(height * scale);
960
+ // Match each command letter followed by its (possibly empty) run of numeric arguments
961
+ const commandRegex = /([MmLlHhVvCcQqZz])([^MmLlHhVvCcQqZz]*)/g;
962
+ // Match numbers incl. decimals, leading sign, and scientific notation
963
+ const numberRegex = /-?\d*\.?\d+(?:[eE][-+]?\d+)?/g;
964
+ const sc = (v) => Math.round(v * scale);
965
+ let curX = 0;
966
+ let curY = 0;
967
+ let startX = 0;
968
+ let startY = 0;
969
+ let xml = '';
970
+ let match;
971
+ while ((match = commandRegex.exec(svgPathD)) !== null) {
972
+ const cmd = match[1];
973
+ const isRel = cmd >= 'a' && cmd <= 'z';
974
+ const upper = cmd.toUpperCase();
975
+ const args = ((_a = match[2].match(numberRegex)) !== null && _a !== void 0 ? _a : []).map(Number);
976
+ let i = 0;
977
+ switch (upper) {
978
+ case 'M': {
979
+ // First coordinate pair is a moveTo; any subsequent pairs are implicit lineTo (per SVG spec)
980
+ let first = true;
981
+ while (i + 1 < args.length) {
982
+ let x = args[i];
983
+ let y = args[i + 1];
984
+ if (isRel) {
985
+ x += curX;
986
+ y += curY;
987
+ }
988
+ curX = x;
989
+ curY = y;
990
+ if (first) {
991
+ startX = curX;
992
+ startY = curY;
993
+ xml += `<a:moveTo><a:pt x="${sc(curX)}" y="${sc(curY)}"/></a:moveTo>`;
994
+ first = false;
995
+ }
996
+ else {
997
+ xml += `<a:lnTo><a:pt x="${sc(curX)}" y="${sc(curY)}"/></a:lnTo>`;
998
+ }
999
+ i += 2;
1000
+ }
1001
+ break;
1002
+ }
1003
+ case 'L': {
1004
+ while (i + 1 < args.length) {
1005
+ let x = args[i];
1006
+ let y = args[i + 1];
1007
+ if (isRel) {
1008
+ x += curX;
1009
+ y += curY;
1010
+ }
1011
+ curX = x;
1012
+ curY = y;
1013
+ xml += `<a:lnTo><a:pt x="${sc(curX)}" y="${sc(curY)}"/></a:lnTo>`;
1014
+ i += 2;
1015
+ }
1016
+ break;
1017
+ }
1018
+ case 'H': {
1019
+ while (i < args.length) {
1020
+ let x = args[i];
1021
+ if (isRel)
1022
+ x += curX;
1023
+ curX = x;
1024
+ xml += `<a:lnTo><a:pt x="${sc(curX)}" y="${sc(curY)}"/></a:lnTo>`;
1025
+ i += 1;
1026
+ }
1027
+ break;
1028
+ }
1029
+ case 'V': {
1030
+ while (i < args.length) {
1031
+ let y = args[i];
1032
+ if (isRel)
1033
+ y += curY;
1034
+ curY = y;
1035
+ xml += `<a:lnTo><a:pt x="${sc(curX)}" y="${sc(curY)}"/></a:lnTo>`;
1036
+ i += 1;
1037
+ }
1038
+ break;
1039
+ }
1040
+ case 'C': {
1041
+ while (i + 5 < args.length) {
1042
+ let x1 = args[i];
1043
+ let y1 = args[i + 1];
1044
+ let x2 = args[i + 2];
1045
+ let y2 = args[i + 3];
1046
+ let x = args[i + 4];
1047
+ let y = args[i + 5];
1048
+ if (isRel) {
1049
+ x1 += curX;
1050
+ y1 += curY;
1051
+ x2 += curX;
1052
+ y2 += curY;
1053
+ x += curX;
1054
+ y += curY;
1055
+ }
1056
+ 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>`;
1057
+ curX = x;
1058
+ curY = y;
1059
+ i += 6;
1060
+ }
1061
+ break;
1062
+ }
1063
+ case 'Q': {
1064
+ while (i + 3 < args.length) {
1065
+ let x1 = args[i];
1066
+ let y1 = args[i + 1];
1067
+ let x = args[i + 2];
1068
+ let y = args[i + 3];
1069
+ if (isRel) {
1070
+ x1 += curX;
1071
+ y1 += curY;
1072
+ x += curX;
1073
+ y += curY;
1074
+ }
1075
+ xml += `<a:quadBezTo><a:pt x="${sc(x1)}" y="${sc(y1)}"/><a:pt x="${sc(x)}" y="${sc(y)}"/></a:quadBezTo>`;
1076
+ curX = x;
1077
+ curY = y;
1078
+ i += 4;
1079
+ }
1080
+ break;
1081
+ }
1082
+ case 'Z': {
1083
+ xml += '<a:close/>';
1084
+ // Pen returns to the start of the current subpath
1085
+ curX = startX;
1086
+ curY = startY;
1087
+ break;
1088
+ }
1089
+ }
1090
+ }
1091
+ 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>`;
1092
+ }
928
1093
 
929
1094
  /**
930
1095
  * PptxGenJS: Table Generation
@@ -2685,7 +2850,11 @@ function addTextDefinition(target, text, opts, isPlaceholder) {
2685
2850
  itemOpts.lineSpacingMultiple = itemOpts.lineSpacingMultiple && !isNaN(itemOpts.lineSpacingMultiple) ? itemOpts.lineSpacingMultiple : null;
2686
2851
  // D: Transform text options to bodyProperties as thats how we build XML
2687
2852
  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)
2853
+ // Back-compat: legacy `autoFit: true` ("resize shape to fit text") now maps to `fit: 'resize'`.
2854
+ // Routing through `fit` keeps a single code path (and avoids emitting `<a:spAutoFit/>` twice). @deprecated 3.3.0
2855
+ if (itemOpts.autoFit === true && !itemOpts.fit)
2856
+ itemOpts.fit = 'resize';
2857
+ itemOpts._bodyProp.autoFit = false; // DEPRECATED: (3.3.0) superseded by `fit` (see above)
2689
2858
  itemOpts._bodyProp.anchor = !itemOpts.placeholder ? TEXT_VALIGN.ctr : null; // VALS: [t,ctr,b]
2690
2859
  itemOpts._bodyProp.vert = itemOpts.vert || null; // VALS: [eaVert,horz,mongolianVert,vert,vert270,wordArtVert,wordArtVertRtl]
2691
2860
  itemOpts._bodyProp.wrap = typeof itemOpts.wrap === 'boolean' ? itemOpts.wrap : true;
@@ -5292,6 +5461,20 @@ function slideObjectToXml(slide) {
5292
5461
  if (placeholderObj.options.h || placeholderObj.options.h === 0)
5293
5462
  cy = getSmartParseNumber(placeholderObj.options.h, 'Y', slide._presLayout);
5294
5463
  }
5464
+ // Normalize negative extents: PPTX requires cx/cy >= 0 (ST_PositiveCoordinate); encode direction via flip
5465
+ // A shape drawn "backwards" (e.g. a line with negative w/h) otherwise emits an invalid `<a:ext>` that triggers PowerPoint repair.
5466
+ if (cx < 0) {
5467
+ x += cx;
5468
+ cx = Math.abs(cx);
5469
+ imgWidth = cx;
5470
+ slideItemObj.options.flipH = !slideItemObj.options.flipH;
5471
+ }
5472
+ if (cy < 0) {
5473
+ y += cy;
5474
+ cy = Math.abs(cy);
5475
+ imgHeight = cy;
5476
+ slideItemObj.options.flipV = !slideItemObj.options.flipV;
5477
+ }
5295
5478
  //
5296
5479
  if (slideItemObj.options.flipH)
5297
5480
  locationAttr += ' flipH="1"';
@@ -5562,7 +5745,11 @@ function slideObjectToXml(slide) {
5562
5745
  strSlideXml += `<a:xfrm${locationAttr}>`;
5563
5746
  strSlideXml += `<a:off x="${x}" y="${y}"/>`;
5564
5747
  strSlideXml += `<a:ext cx="${cx}" cy="${cy}"/></a:xfrm>`;
5565
- if (slideItemObj.shape === 'custGeom') {
5748
+ if (slideItemObj.options.svgPath) {
5749
+ // Feature 9: Convert an SVG path to OOXML custom geometry (<a:custGeom>) instead of a preset geometry.
5750
+ strSlideXml += svgPathToOoxml(slideItemObj.options.svgPath.d, slideItemObj.options.svgPath.viewBox.w, slideItemObj.options.svgPath.viewBox.h);
5751
+ }
5752
+ else if (slideItemObj.shape === 'custGeom') {
5566
5753
  strSlideXml += '<a:custGeom><a:avLst />';
5567
5754
  strSlideXml += '<a:gdLst>';
5568
5755
  strSlideXml += '</a:gdLst>';
@@ -6202,6 +6389,13 @@ function genXmlBodyProperties(slideObject) {
6202
6389
  bodyProperties += ' anchor="' + slideObject.options._bodyProp.anchor + '"'; // VALS: [t,ctr,b]
6203
6390
  if (slideObject.options._bodyProp.vert)
6204
6391
  bodyProperties += ' vert="' + slideObject.options._bodyProp.vert + '"'; // VALS: [eaVert,horz,mongolianVert,vert,vert270,wordArtVert,wordArtVertRtl]
6392
+ // D2: Multi-column text (numCol/spcCol attributes on <a:bodyPr>)
6393
+ // NOTE: must be appended as attributes BEFORE the opening tag is closed below (section E)
6394
+ if (slideObject.options.columns && slideObject.options.columns > 1) {
6395
+ bodyProperties += ` numCol="${Math.round(slideObject.options.columns)}"`;
6396
+ const spcColIn = typeof slideObject.options.columnSpacing === 'number' ? slideObject.options.columnSpacing : 0.5;
6397
+ bodyProperties += ` spcCol="${Math.round(spcColIn * EMU)}"`;
6398
+ }
6205
6399
  // E: Close <a:bodyPr element
6206
6400
  bodyProperties += '>';
6207
6401
  /**
@@ -6213,10 +6407,10 @@ function genXmlBodyProperties(slideObject) {
6213
6407
  // NOTE: Use of '<a:noAutofit/>' instead of '' causes issues in PPT-2013!
6214
6408
  if (slideObject.options.fit === 'none')
6215
6409
  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"
6410
+ // "Shrink text on overflow": emit a fixed `fontScale` (70%) so the text is scaled down to fit the shape.
6411
+ // NOTE: PowerPoint recalculates `fontScale` dynamically once the text/shape is edited; the emitted value is the initial scale.
6218
6412
  else if (slideObject.options.fit === 'shrink')
6219
- bodyProperties += '<a:normAutofit/>';
6413
+ bodyProperties += '<a:normAutofit fontScale="70000"/>';
6220
6414
  else if (slideObject.options.fit === 'resize')
6221
6415
  bodyProperties += '<a:spAutoFit/>';
6222
6416
  }
@@ -6744,14 +6938,15 @@ function genXmlAnimPayload(anim, spid, nextId) {
6744
6938
  }
6745
6939
  // flyIn ADDS a <p:anim> that translates the shape from offscreen to its final position.
6746
6940
  // 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"
6941
+ // left -> ppt_x, "#ppt_x-1slide" right -> ppt_x, "#ppt_x+1slide"
6942
+ // up -> ppt_y, "#ppt_y+1slide" down -> ppt_y, "#ppt_y-1slide"
6943
+ // The "1slide" offset starts the shape one full slide-width/height offscreen (PowerPoint-native Fly In).
6749
6944
  if (anim.type === 'flyIn') {
6750
6945
  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' },
6946
+ left: { attr: 'ppt_x', start: '#ppt_x-1slide' },
6947
+ right: { attr: 'ppt_x', start: '#ppt_x+1slide' },
6948
+ up: { attr: 'ppt_y', start: '#ppt_y+1slide' },
6949
+ down: { attr: 'ppt_y', start: '#ppt_y-1slide' },
6755
6950
  };
6756
6951
  const { attr, start } = (_b = flyMap[(_a = anim.direction) !== null && _a !== void 0 ? _a : 'left']) !== null && _b !== void 0 ? _b : flyMap.left;
6757
6952
  payload +=
@@ -1,4 +1,4 @@
1
- /* PptxGenJS 4.0.1 @ 2026-06-07T05:05:05.129Z */
1
+ /* PptxGenJS 4.1.0 @ 2026-06-07T11:53:01.419Z */
2
2
  import JSZip from 'jszip';
3
3
 
4
4
  /******************************************************************************
@@ -662,7 +662,8 @@ function getUuid(uuidFormat) {
662
662
  return uuidFormat.replace(/[xy]/g, function (c) {
663
663
  const r = (Math.random() * 16) | 0;
664
664
  const v = c === 'x' ? r : (r & 0x3) | 0x8;
665
- return v.toString(16);
665
+ // OOXML ST_Guid requires uppercase hex: \{[0-9A-F]{8}-...\}
666
+ return v.toString(16).toUpperCase();
666
667
  });
667
668
  }
668
669
  /**
@@ -923,6 +924,170 @@ function correctShadowOptions(ShadowProps) {
923
924
  }
924
925
  return ShadowProps;
925
926
  }
927
+ /**
928
+ * Convert an SVG `<path d="…">` definition to an OOXML custom geometry (`<a:custGeom>`).
929
+ *
930
+ * Supports the following SVG path commands (both absolute upper-case and relative lower-case):
931
+ * - `M`/`m` moveTo → `<a:moveTo>` (extra coordinate pairs become implicit `lineTo`)
932
+ * - `L`/`l` lineTo → `<a:lnTo>`
933
+ * - `H`/`h` horizontal lineTo → `<a:lnTo>`
934
+ * - `V`/`v` vertical lineTo → `<a:lnTo>`
935
+ * - `C`/`c` cubic Bézier → `<a:cubicBezTo>`
936
+ * - `Q`/`q` quadratic Bézier → `<a:quadBezTo>`
937
+ * - `Z`/`z` close path → `<a:close>`
938
+ *
939
+ * Relative commands are tracked against the current pen position and converted to absolute
940
+ * coordinates. Coordinates are scaled from the SVG viewBox into EMU via `914400 / width`, so the
941
+ * viewBox width maps to exactly 1 inch (914400 EMU) of path coordinate space. The shape's on-slide
942
+ * dimensions still come from the `<a:xfrm><a:ext>` set by the caller — the path coordinate system is
943
+ * stretched to fit it — so the absolute scale here only needs to be internally consistent.
944
+ *
945
+ * @param {string} svgPathD - the SVG path `d` attribute (e.g. `"M 0 0 L 12 0 L 6 12 Z"`)
946
+ * @param {number} width - SVG viewBox width
947
+ * @param {number} height - SVG viewBox height
948
+ * @returns {string} OOXML `<a:custGeom>…</a:custGeom>` string (empty string on invalid input)
949
+ * @see ECMA-376 §20.1.9.8 (custGeom) / §20.1.9.16 (path2D)
950
+ */
951
+ function svgPathToOoxml(svgPathD, width, height) {
952
+ var _a;
953
+ if (!svgPathD || typeof svgPathD !== 'string' || !(width > 0) || !(height > 0))
954
+ return '';
955
+ const scale = 914400 / width;
956
+ const pathW = Math.round(width * scale);
957
+ const pathH = Math.round(height * scale);
958
+ // Match each command letter followed by its (possibly empty) run of numeric arguments
959
+ const commandRegex = /([MmLlHhVvCcQqZz])([^MmLlHhVvCcQqZz]*)/g;
960
+ // Match numbers incl. decimals, leading sign, and scientific notation
961
+ const numberRegex = /-?\d*\.?\d+(?:[eE][-+]?\d+)?/g;
962
+ const sc = (v) => Math.round(v * scale);
963
+ let curX = 0;
964
+ let curY = 0;
965
+ let startX = 0;
966
+ let startY = 0;
967
+ let xml = '';
968
+ let match;
969
+ while ((match = commandRegex.exec(svgPathD)) !== null) {
970
+ const cmd = match[1];
971
+ const isRel = cmd >= 'a' && cmd <= 'z';
972
+ const upper = cmd.toUpperCase();
973
+ const args = ((_a = match[2].match(numberRegex)) !== null && _a !== void 0 ? _a : []).map(Number);
974
+ let i = 0;
975
+ switch (upper) {
976
+ case 'M': {
977
+ // First coordinate pair is a moveTo; any subsequent pairs are implicit lineTo (per SVG spec)
978
+ let first = true;
979
+ while (i + 1 < args.length) {
980
+ let x = args[i];
981
+ let y = args[i + 1];
982
+ if (isRel) {
983
+ x += curX;
984
+ y += curY;
985
+ }
986
+ curX = x;
987
+ curY = y;
988
+ if (first) {
989
+ startX = curX;
990
+ startY = curY;
991
+ xml += `<a:moveTo><a:pt x="${sc(curX)}" y="${sc(curY)}"/></a:moveTo>`;
992
+ first = false;
993
+ }
994
+ else {
995
+ xml += `<a:lnTo><a:pt x="${sc(curX)}" y="${sc(curY)}"/></a:lnTo>`;
996
+ }
997
+ i += 2;
998
+ }
999
+ break;
1000
+ }
1001
+ case 'L': {
1002
+ while (i + 1 < args.length) {
1003
+ let x = args[i];
1004
+ let y = args[i + 1];
1005
+ if (isRel) {
1006
+ x += curX;
1007
+ y += curY;
1008
+ }
1009
+ curX = x;
1010
+ curY = y;
1011
+ xml += `<a:lnTo><a:pt x="${sc(curX)}" y="${sc(curY)}"/></a:lnTo>`;
1012
+ i += 2;
1013
+ }
1014
+ break;
1015
+ }
1016
+ case 'H': {
1017
+ while (i < args.length) {
1018
+ let x = args[i];
1019
+ if (isRel)
1020
+ x += curX;
1021
+ curX = x;
1022
+ xml += `<a:lnTo><a:pt x="${sc(curX)}" y="${sc(curY)}"/></a:lnTo>`;
1023
+ i += 1;
1024
+ }
1025
+ break;
1026
+ }
1027
+ case 'V': {
1028
+ while (i < args.length) {
1029
+ let y = args[i];
1030
+ if (isRel)
1031
+ y += curY;
1032
+ curY = y;
1033
+ xml += `<a:lnTo><a:pt x="${sc(curX)}" y="${sc(curY)}"/></a:lnTo>`;
1034
+ i += 1;
1035
+ }
1036
+ break;
1037
+ }
1038
+ case 'C': {
1039
+ while (i + 5 < args.length) {
1040
+ let x1 = args[i];
1041
+ let y1 = args[i + 1];
1042
+ let x2 = args[i + 2];
1043
+ let y2 = args[i + 3];
1044
+ let x = args[i + 4];
1045
+ let y = args[i + 5];
1046
+ if (isRel) {
1047
+ x1 += curX;
1048
+ y1 += curY;
1049
+ x2 += curX;
1050
+ y2 += curY;
1051
+ x += curX;
1052
+ y += curY;
1053
+ }
1054
+ 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>`;
1055
+ curX = x;
1056
+ curY = y;
1057
+ i += 6;
1058
+ }
1059
+ break;
1060
+ }
1061
+ case 'Q': {
1062
+ while (i + 3 < args.length) {
1063
+ let x1 = args[i];
1064
+ let y1 = args[i + 1];
1065
+ let x = args[i + 2];
1066
+ let y = args[i + 3];
1067
+ if (isRel) {
1068
+ x1 += curX;
1069
+ y1 += curY;
1070
+ x += curX;
1071
+ y += curY;
1072
+ }
1073
+ xml += `<a:quadBezTo><a:pt x="${sc(x1)}" y="${sc(y1)}"/><a:pt x="${sc(x)}" y="${sc(y)}"/></a:quadBezTo>`;
1074
+ curX = x;
1075
+ curY = y;
1076
+ i += 4;
1077
+ }
1078
+ break;
1079
+ }
1080
+ case 'Z': {
1081
+ xml += '<a:close/>';
1082
+ // Pen returns to the start of the current subpath
1083
+ curX = startX;
1084
+ curY = startY;
1085
+ break;
1086
+ }
1087
+ }
1088
+ }
1089
+ 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>`;
1090
+ }
926
1091
 
927
1092
  /**
928
1093
  * PptxGenJS: Table Generation
@@ -2683,7 +2848,11 @@ function addTextDefinition(target, text, opts, isPlaceholder) {
2683
2848
  itemOpts.lineSpacingMultiple = itemOpts.lineSpacingMultiple && !isNaN(itemOpts.lineSpacingMultiple) ? itemOpts.lineSpacingMultiple : null;
2684
2849
  // D: Transform text options to bodyProperties as thats how we build XML
2685
2850
  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)
2851
+ // Back-compat: legacy `autoFit: true` ("resize shape to fit text") now maps to `fit: 'resize'`.
2852
+ // Routing through `fit` keeps a single code path (and avoids emitting `<a:spAutoFit/>` twice). @deprecated 3.3.0
2853
+ if (itemOpts.autoFit === true && !itemOpts.fit)
2854
+ itemOpts.fit = 'resize';
2855
+ itemOpts._bodyProp.autoFit = false; // DEPRECATED: (3.3.0) superseded by `fit` (see above)
2687
2856
  itemOpts._bodyProp.anchor = !itemOpts.placeholder ? TEXT_VALIGN.ctr : null; // VALS: [t,ctr,b]
2688
2857
  itemOpts._bodyProp.vert = itemOpts.vert || null; // VALS: [eaVert,horz,mongolianVert,vert,vert270,wordArtVert,wordArtVertRtl]
2689
2858
  itemOpts._bodyProp.wrap = typeof itemOpts.wrap === 'boolean' ? itemOpts.wrap : true;
@@ -5290,6 +5459,20 @@ function slideObjectToXml(slide) {
5290
5459
  if (placeholderObj.options.h || placeholderObj.options.h === 0)
5291
5460
  cy = getSmartParseNumber(placeholderObj.options.h, 'Y', slide._presLayout);
5292
5461
  }
5462
+ // Normalize negative extents: PPTX requires cx/cy >= 0 (ST_PositiveCoordinate); encode direction via flip
5463
+ // A shape drawn "backwards" (e.g. a line with negative w/h) otherwise emits an invalid `<a:ext>` that triggers PowerPoint repair.
5464
+ if (cx < 0) {
5465
+ x += cx;
5466
+ cx = Math.abs(cx);
5467
+ imgWidth = cx;
5468
+ slideItemObj.options.flipH = !slideItemObj.options.flipH;
5469
+ }
5470
+ if (cy < 0) {
5471
+ y += cy;
5472
+ cy = Math.abs(cy);
5473
+ imgHeight = cy;
5474
+ slideItemObj.options.flipV = !slideItemObj.options.flipV;
5475
+ }
5293
5476
  //
5294
5477
  if (slideItemObj.options.flipH)
5295
5478
  locationAttr += ' flipH="1"';
@@ -5560,7 +5743,11 @@ function slideObjectToXml(slide) {
5560
5743
  strSlideXml += `<a:xfrm${locationAttr}>`;
5561
5744
  strSlideXml += `<a:off x="${x}" y="${y}"/>`;
5562
5745
  strSlideXml += `<a:ext cx="${cx}" cy="${cy}"/></a:xfrm>`;
5563
- if (slideItemObj.shape === 'custGeom') {
5746
+ if (slideItemObj.options.svgPath) {
5747
+ // Feature 9: Convert an SVG path to OOXML custom geometry (<a:custGeom>) instead of a preset geometry.
5748
+ strSlideXml += svgPathToOoxml(slideItemObj.options.svgPath.d, slideItemObj.options.svgPath.viewBox.w, slideItemObj.options.svgPath.viewBox.h);
5749
+ }
5750
+ else if (slideItemObj.shape === 'custGeom') {
5564
5751
  strSlideXml += '<a:custGeom><a:avLst />';
5565
5752
  strSlideXml += '<a:gdLst>';
5566
5753
  strSlideXml += '</a:gdLst>';
@@ -6200,6 +6387,13 @@ function genXmlBodyProperties(slideObject) {
6200
6387
  bodyProperties += ' anchor="' + slideObject.options._bodyProp.anchor + '"'; // VALS: [t,ctr,b]
6201
6388
  if (slideObject.options._bodyProp.vert)
6202
6389
  bodyProperties += ' vert="' + slideObject.options._bodyProp.vert + '"'; // VALS: [eaVert,horz,mongolianVert,vert,vert270,wordArtVert,wordArtVertRtl]
6390
+ // D2: Multi-column text (numCol/spcCol attributes on <a:bodyPr>)
6391
+ // NOTE: must be appended as attributes BEFORE the opening tag is closed below (section E)
6392
+ if (slideObject.options.columns && slideObject.options.columns > 1) {
6393
+ bodyProperties += ` numCol="${Math.round(slideObject.options.columns)}"`;
6394
+ const spcColIn = typeof slideObject.options.columnSpacing === 'number' ? slideObject.options.columnSpacing : 0.5;
6395
+ bodyProperties += ` spcCol="${Math.round(spcColIn * EMU)}"`;
6396
+ }
6203
6397
  // E: Close <a:bodyPr element
6204
6398
  bodyProperties += '>';
6205
6399
  /**
@@ -6211,10 +6405,10 @@ function genXmlBodyProperties(slideObject) {
6211
6405
  // NOTE: Use of '<a:noAutofit/>' instead of '' causes issues in PPT-2013!
6212
6406
  if (slideObject.options.fit === 'none')
6213
6407
  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"
6408
+ // "Shrink text on overflow": emit a fixed `fontScale` (70%) so the text is scaled down to fit the shape.
6409
+ // NOTE: PowerPoint recalculates `fontScale` dynamically once the text/shape is edited; the emitted value is the initial scale.
6216
6410
  else if (slideObject.options.fit === 'shrink')
6217
- bodyProperties += '<a:normAutofit/>';
6411
+ bodyProperties += '<a:normAutofit fontScale="70000"/>';
6218
6412
  else if (slideObject.options.fit === 'resize')
6219
6413
  bodyProperties += '<a:spAutoFit/>';
6220
6414
  }
@@ -6742,14 +6936,15 @@ function genXmlAnimPayload(anim, spid, nextId) {
6742
6936
  }
6743
6937
  // flyIn ADDS a <p:anim> that translates the shape from offscreen to its final position.
6744
6938
  // 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"
6939
+ // left -> ppt_x, "#ppt_x-1slide" right -> ppt_x, "#ppt_x+1slide"
6940
+ // up -> ppt_y, "#ppt_y+1slide" down -> ppt_y, "#ppt_y-1slide"
6941
+ // The "1slide" offset starts the shape one full slide-width/height offscreen (PowerPoint-native Fly In).
6747
6942
  if (anim.type === 'flyIn') {
6748
6943
  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' },
6944
+ left: { attr: 'ppt_x', start: '#ppt_x-1slide' },
6945
+ right: { attr: 'ppt_x', start: '#ppt_x+1slide' },
6946
+ up: { attr: 'ppt_y', start: '#ppt_y+1slide' },
6947
+ down: { attr: 'ppt_y', start: '#ppt_y-1slide' },
6753
6948
  };
6754
6949
  const { attr, start } = (_b = flyMap[(_a = anim.direction) !== null && _a !== void 0 ? _a : 'left']) !== null && _b !== void 0 ? _b : flyMap.left;
6755
6950
  payload +=