dom-to-pptx 1.1.0 → 1.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.
@@ -199,11 +199,14 @@ class PPTXEmbedFonts {
199
199
 
200
200
  // src/utils.js
201
201
 
202
- /**
203
- * Checks if any parent element has overflow: hidden which would clip this element
204
- * @param {HTMLElement} node - The DOM node to check
205
- * @returns {boolean} - True if a parent has overflow-hidden or overflow-clip
206
- */
202
+ // canvas context for color normalization
203
+ let _ctx;
204
+ function getCtx() {
205
+ if (!_ctx) _ctx = document.createElement('canvas').getContext('2d', { willReadFrequently: true });
206
+ return _ctx;
207
+ }
208
+
209
+ // Checks if any parent element has overflow: hidden which would clip this element
207
210
  function isClippedByParent(node) {
208
211
  let parent = node.parentElement;
209
212
  while (parent && parent !== document.body) {
@@ -369,28 +372,58 @@ function generateCustomShapeSVG(w, h, color, opacity, radii) {
369
372
  return 'data:image/svg+xml;base64,' + btoa(svg);
370
373
  }
371
374
 
375
+ // --- REPLACE THE EXISTING parseColor FUNCTION ---
372
376
  function parseColor(str) {
373
- if (!str || str === 'transparent' || str.startsWith('rgba(0, 0, 0, 0)')) {
377
+ if (!str || str === 'transparent' || str.trim() === 'rgba(0, 0, 0, 0)') {
374
378
  return { hex: null, opacity: 0 };
375
379
  }
376
- if (str.startsWith('#')) {
377
- let hex = str.slice(1);
378
- if (hex.length === 3)
379
- hex = hex
380
- .split('')
381
- .map((c) => c + c)
382
- .join('');
383
- return { hex: hex.toUpperCase(), opacity: 1 };
380
+
381
+ const ctx = getCtx();
382
+ ctx.fillStyle = str;
383
+ // This forces the browser to resolve variables and convert formats (oklch -> rgb/hex)
384
+ const computed = ctx.fillStyle;
385
+
386
+ // 1. Handle Hex Output (e.g. #ff0000 or #ff0000ff)
387
+ if (computed.startsWith('#')) {
388
+ let hex = computed.slice(1); // Remove '#'
389
+ let opacity = 1;
390
+
391
+ // Expand shorthand #RGB -> #RRGGBB
392
+ if (hex.length === 3) {
393
+ hex = hex.split('').map(c => c + c).join('');
394
+ }
395
+ // Expand shorthand #RGBA -> #RRGGBBAA
396
+ else if (hex.length === 4) {
397
+ hex = hex.split('').map(c => c + c).join('');
398
+ }
399
+
400
+ // Handle 8-digit Hex (RRGGBBAA) - PptxGenJS fails if we send 8 digits
401
+ if (hex.length === 8) {
402
+ opacity = parseInt(hex.slice(6), 16) / 255;
403
+ hex = hex.slice(0, 6); // Keep only RRGGBB
404
+ }
405
+
406
+ return { hex: hex.toUpperCase(), opacity };
384
407
  }
385
- const match = str.match(/[\d.]+/g);
408
+
409
+ // 2. Handle RGB/RGBA Output (e.g. "rgb(55, 65, 81)" or "rgba(55, 65, 81, 1)")
410
+ const match = computed.match(/[\d.]+/g);
386
411
  if (match && match.length >= 3) {
387
412
  const r = parseInt(match[0]);
388
413
  const g = parseInt(match[1]);
389
414
  const b = parseInt(match[2]);
390
415
  const a = match.length > 3 ? parseFloat(match[3]) : 1;
391
- const hex = ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
416
+
417
+ // Bitwise shift to get Hex
418
+ const hex = ((1 << 24) + (r << 16) + (g << 8) + b)
419
+ .toString(16)
420
+ .slice(1)
421
+ .toUpperCase();
422
+
392
423
  return { hex, opacity: a };
393
424
  }
425
+
426
+ // Fallback (Parsing failed)
394
427
  return { hex: null, opacity: 0 };
395
428
  }
396
429
 
@@ -874,33 +907,30 @@ async function getAutoDetectedFonts(usedFamilies) {
874
907
 
875
908
  // src/image-processor.js
876
909
 
877
- async function getProcessedImage(src, targetW, targetH, radius) {
910
+ async function getProcessedImage(src, targetW, targetH, radius, objectFit = 'fill', objectPosition = '50% 50%') {
878
911
  return new Promise((resolve) => {
879
912
  const img = new Image();
880
- img.crossOrigin = 'Anonymous'; // Critical for canvas manipulation
913
+ img.crossOrigin = 'Anonymous';
881
914
 
882
915
  img.onload = () => {
883
916
  const canvas = document.createElement('canvas');
884
- // Double resolution for better quality
885
- const scale = 2;
917
+ const scale = 2; // Double resolution
886
918
  canvas.width = targetW * scale;
887
919
  canvas.height = targetH * scale;
888
920
  const ctx = canvas.getContext('2d');
889
921
  ctx.scale(scale, scale);
890
922
 
891
- // Normalize radius input to an object { tl, tr, br, bl }
923
+ // Normalize radius
892
924
  let r = { tl: 0, tr: 0, br: 0, bl: 0 };
893
925
  if (typeof radius === 'number') {
894
926
  r = { tl: radius, tr: radius, br: radius, bl: radius };
895
927
  } else if (typeof radius === 'object' && radius !== null) {
896
- r = { ...r, ...radius }; // Merge with defaults
928
+ r = { ...r, ...radius };
897
929
  }
898
930
 
899
- // 1. Draw the Mask (Custom Shape with specific corners)
931
+ // 1. Draw Mask
900
932
  ctx.beginPath();
901
-
902
- // Border Radius Clamping Logic (CSS Spec)
903
- // Prevents corners from overlapping if radii are too large for the container
933
+ // ... (radius clamping logic remains the same) ...
904
934
  const factor = Math.min(
905
935
  targetW / (r.tl + r.tr) || Infinity,
906
936
  targetH / (r.tr + r.br) || Infinity,
@@ -909,13 +939,9 @@ async function getProcessedImage(src, targetW, targetH, radius) {
909
939
  );
910
940
 
911
941
  if (factor < 1) {
912
- r.tl *= factor;
913
- r.tr *= factor;
914
- r.br *= factor;
915
- r.bl *= factor;
942
+ r.tl *= factor; r.tr *= factor; r.br *= factor; r.bl *= factor;
916
943
  }
917
944
 
918
- // Draw path: Top-Left -> Top-Right -> Bottom-Right -> Bottom-Left
919
945
  ctx.moveTo(r.tl, 0);
920
946
  ctx.lineTo(targetW - r.tr, 0);
921
947
  ctx.arcTo(targetW, 0, targetW, r.tr, r.tr);
@@ -925,22 +951,58 @@ async function getProcessedImage(src, targetW, targetH, radius) {
925
951
  ctx.arcTo(0, targetH, 0, targetH - r.bl, r.bl);
926
952
  ctx.lineTo(0, r.tl);
927
953
  ctx.arcTo(0, 0, r.tl, 0, r.tl);
928
-
929
954
  ctx.closePath();
930
955
  ctx.fillStyle = '#000';
931
956
  ctx.fill();
932
957
 
933
- // 2. Composite Source-In (Crops the next image draw to the mask)
958
+ // 2. Composite Source-In
934
959
  ctx.globalCompositeOperation = 'source-in';
935
960
 
936
- // 3. Draw Image (Object Cover Logic)
961
+ // 3. Draw Image with Object Fit logic
937
962
  const wRatio = targetW / img.width;
938
963
  const hRatio = targetH / img.height;
939
- const maxRatio = Math.max(wRatio, hRatio);
940
- const renderW = img.width * maxRatio;
941
- const renderH = img.height * maxRatio;
942
- const renderX = (targetW - renderW) / 2;
943
- const renderY = (targetH - renderH) / 2;
964
+ let renderW, renderH;
965
+
966
+ if (objectFit === 'contain') {
967
+ const fitScale = Math.min(wRatio, hRatio);
968
+ renderW = img.width * fitScale;
969
+ renderH = img.height * fitScale;
970
+ } else if (objectFit === 'cover') {
971
+ const coverScale = Math.max(wRatio, hRatio);
972
+ renderW = img.width * coverScale;
973
+ renderH = img.height * coverScale;
974
+ } else if (objectFit === 'none') {
975
+ renderW = img.width;
976
+ renderH = img.height;
977
+ } else if (objectFit === 'scale-down') {
978
+ const scaleDown = Math.min(1, Math.min(wRatio, hRatio));
979
+ renderW = img.width * scaleDown;
980
+ renderH = img.height * scaleDown;
981
+ } else {
982
+ // 'fill' (default)
983
+ renderW = targetW;
984
+ renderH = targetH;
985
+ }
986
+
987
+ // Handle Object Position (simplified parsing for "x% y%" or keywords)
988
+ let posX = 0.5; // Default center
989
+ let posY = 0.5;
990
+
991
+ const posParts = objectPosition.split(' ');
992
+ if (posParts.length > 0) {
993
+ const parsePos = (val) => {
994
+ if (val === 'left' || val === 'top') return 0;
995
+ if (val === 'center') return 0.5;
996
+ if (val === 'right' || val === 'bottom') return 1;
997
+ if (val.includes('%')) return parseFloat(val) / 100;
998
+ return 0.5; // fallback
999
+ };
1000
+ posX = parsePos(posParts[0]);
1001
+ posY = posParts.length > 1 ? parsePos(posParts[1]) : 0.5;
1002
+ }
1003
+
1004
+ const renderX = (targetW - renderW) * posX;
1005
+ const renderY = (targetH - renderH) * posY;
944
1006
 
945
1007
  ctx.drawImage(img, renderX, renderY, renderW, renderH);
946
1008
 
@@ -996,34 +1058,37 @@ async function exportToPptx(target, options = {}) {
996
1058
  await processSlide(root, slide, pptx);
997
1059
  }
998
1060
 
999
- // 3. Font Embedding Logic
1061
+ // 3. Font Embedding Logic
1000
1062
  let finalBlob;
1001
1063
  let fontsToEmbed = options.fonts || [];
1002
1064
 
1003
1065
  if (options.autoEmbedFonts) {
1004
1066
  // A. Scan DOM for used font families
1005
1067
  const usedFamilies = getUsedFontFamilies(elements);
1006
-
1068
+
1007
1069
  // B. Scan CSS for URLs matches
1008
1070
  const detectedFonts = await getAutoDetectedFonts(usedFamilies);
1009
-
1071
+
1010
1072
  // C. Merge (Avoid duplicates)
1011
- const explicitNames = new Set(fontsToEmbed.map(f => f.name));
1073
+ const explicitNames = new Set(fontsToEmbed.map((f) => f.name));
1012
1074
  for (const autoFont of detectedFonts) {
1013
- if (!explicitNames.has(autoFont.name)) {
1014
- fontsToEmbed.push(autoFont);
1015
- }
1075
+ if (!explicitNames.has(autoFont.name)) {
1076
+ fontsToEmbed.push(autoFont);
1077
+ }
1016
1078
  }
1017
-
1079
+
1018
1080
  if (detectedFonts.length > 0) {
1019
- console.log('Auto-detected fonts:', detectedFonts.map(f => f.name));
1081
+ console.log(
1082
+ 'Auto-detected fonts:',
1083
+ detectedFonts.map((f) => f.name)
1084
+ );
1020
1085
  }
1021
1086
  }
1022
1087
 
1023
1088
  if (fontsToEmbed.length > 0) {
1024
1089
  // Generate initial PPTX
1025
1090
  const initialBlob = await pptx.write({ outputType: 'blob' });
1026
-
1091
+
1027
1092
  // Load into Embedder
1028
1093
  const zip = await JSZip.loadAsync(initialBlob);
1029
1094
  const embedder = new PPTXEmbedFonts();
@@ -1035,7 +1100,7 @@ async function exportToPptx(target, options = {}) {
1035
1100
  const response = await fetch(fontCfg.url);
1036
1101
  if (!response.ok) throw new Error(`Failed to fetch ${fontCfg.url}`);
1037
1102
  const buffer = await response.arrayBuffer();
1038
-
1103
+
1039
1104
  // Infer type
1040
1105
  const ext = fontCfg.url.split('.').pop().split(/[?#]/)[0].toLowerCase();
1041
1106
  let type = 'ttf';
@@ -1174,11 +1239,8 @@ async function processSlide(root, slide, pptx) {
1174
1239
 
1175
1240
  /**
1176
1241
  * Optimized html2canvas wrapper
1177
- * Now strictly captures the node itself, not the root.
1178
- */
1179
- /**
1180
- * Optimized html2canvas wrapper
1181
- * Includes fix for cropped icons by adjusting styles in the cloned document.
1242
+ * Fixes icon clipping by adding padding in the clone to capture font bleed,
1243
+ * then scaling the result to fit the original bounding box.
1182
1244
  */
1183
1245
  async function elementToCanvasImage(node, widthPx, heightPx) {
1184
1246
  return new Promise((resolve) => {
@@ -1194,51 +1256,76 @@ async function elementToCanvasImage(node, widthPx, heightPx) {
1194
1256
  html2canvas(node, {
1195
1257
  backgroundColor: null,
1196
1258
  logging: false,
1197
- scale: 3, // Higher scale for sharper icons
1198
- useCORS: true, // critical for external fonts/images
1259
+ scale: 3, // High resolution capture
1260
+ useCORS: true,
1199
1261
  onclone: (clonedDoc) => {
1200
1262
  const clonedNode = clonedDoc.getElementById(tempId);
1201
1263
  if (clonedNode) {
1202
- // --- FIX: PREVENT ICON CLIPPING ---
1203
- // 1. Force overflow visible so glyphs bleeding out aren't cut
1204
- clonedNode.style.overflow = 'visible';
1205
-
1206
- // 2. Adjust alignment for Icons to prevent baseline clipping
1207
- // (Applies to <i>, <span>, or standard icon classes)
1264
+ // --- FIX FOR ICON CLIPPING ---
1208
1265
  const tag = clonedNode.tagName;
1209
- if (tag === 'I' || tag === 'SPAN' || clonedNode.className.includes('fa-')) {
1210
- // Flex center helps align the glyph exactly in the middle of the box
1211
- // preventing top/bottom cropping due to line-height mismatches.
1212
- clonedNode.style.display = 'inline-flex';
1266
+ // Detect icons: I tags, SPAN tags, or elements with icon classes
1267
+ const isIcon =
1268
+ tag === 'I' ||
1269
+ tag === 'SPAN' ||
1270
+ clonedNode.className.includes('fa-') ||
1271
+ clonedNode.className.includes('icon');
1272
+
1273
+ if (isIcon) {
1274
+ // 1. Use inline-flex to center the glyph content
1275
+ clonedNode.style.display = 'flex';
1213
1276
  clonedNode.style.justifyContent = 'center';
1214
1277
  clonedNode.style.alignItems = 'center';
1215
1278
 
1216
- // Remove margins that might offset the capture
1217
- clonedNode.style.margin = '0';
1279
+ // 2. Reset constraints that might crop content
1280
+ clonedNode.style.overflow = 'visible';
1281
+ clonedNode.style.lineHeight = 'normal';
1282
+
1283
+ // 3. Add padding to the capture area.
1284
+ // This ensures parts of the glyph sticking out of the bounding box (ascenders/descenders)
1285
+ // are captured in the canvas instead of being cropped.
1286
+ // 'content-box' ensures padding adds to the total width/height.
1287
+ clonedNode.style.boxSizing = 'content-box';
1288
+ clonedNode.style.padding = '10px';
1289
+
1290
+ // 4. Ensure the content box itself matches the original size (minimum)
1291
+ // so the icon doesn't collapse.
1292
+ clonedNode.style.minWidth = `${width}px`;
1293
+ clonedNode.style.minHeight = `${height}px`;
1218
1294
 
1219
- // Ensure the font fits
1220
- clonedNode.style.lineHeight = '1';
1221
- clonedNode.style.verticalAlign = 'middle';
1295
+ // Clear margins that might displace the capture
1296
+ clonedNode.style.margin = '0';
1222
1297
  }
1223
1298
  }
1224
1299
  },
1225
1300
  })
1226
1301
  .then((canvas) => {
1227
- // Restore the original ID
1302
+ // Restore ID
1228
1303
  if (originalId) node.id = originalId;
1229
1304
  else node.removeAttribute('id');
1230
1305
 
1306
+ // Create destination canvas with the EXACT original dimensions
1231
1307
  const destCanvas = document.createElement('canvas');
1232
1308
  destCanvas.width = width;
1233
1309
  destCanvas.height = height;
1234
1310
  const ctx = destCanvas.getContext('2d');
1235
1311
 
1236
- // Draw captured canvas.
1237
- // We simply draw it to fill the box. Since we centered it in 'onclone',
1238
- // the glyph should now be visible within the bounds.
1239
- ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, width, height);
1312
+ // --- SCALE TO FIT ---
1313
+ // The captured 'canvas' is now larger than 'destCanvas' because we added padding.
1314
+ // We draw the larger captured image into the smaller destination box.
1315
+ // This effectively "zooms out" slightly, ensuring the bleed is visible within the bounds.
1316
+ ctx.drawImage(
1317
+ canvas,
1318
+ 0,
1319
+ 0,
1320
+ canvas.width,
1321
+ canvas.height, // Source: Full captured size (with padding)
1322
+ 0,
1323
+ 0,
1324
+ width,
1325
+ height // Dest: Original requested size
1326
+ );
1240
1327
 
1241
- // --- Border Radius Clipping (Existing Logic) ---
1328
+ // --- Border Radius Clipping (Preserve existing logic) ---
1242
1329
  let tl = parseFloat(style.borderTopLeftRadius) || 0;
1243
1330
  let tr = parseFloat(style.borderTopRightRadius) || 0;
1244
1331
  let br = parseFloat(style.borderBottomRightRadius) || 0;
@@ -1450,6 +1537,9 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
1450
1537
  }
1451
1538
  }
1452
1539
 
1540
+ const objectFit = style.objectFit || 'fill'; // default CSS behavior is fill
1541
+ const objectPosition = style.objectPosition || '50% 50%';
1542
+
1453
1543
  const item = {
1454
1544
  type: 'image',
1455
1545
  zIndex,
@@ -1458,7 +1548,14 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
1458
1548
  };
1459
1549
 
1460
1550
  const job = async () => {
1461
- const processed = await getProcessedImage(node.src, widthPx, heightPx, radii);
1551
+ const processed = await getProcessedImage(
1552
+ node.src,
1553
+ widthPx,
1554
+ heightPx,
1555
+ radii,
1556
+ objectFit,
1557
+ objectPosition
1558
+ );
1462
1559
  if (processed) item.options.data = processed;
1463
1560
  else item.skip = true;
1464
1561
  };
@@ -1558,16 +1655,45 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
1558
1655
  const isList = style.display === 'list-item';
1559
1656
  if (isList) {
1560
1657
  const fontSizePt = parseFloat(style.fontSize) * 0.75 * config.scale;
1561
- const bulletShift = (parseFloat(style.fontSize) || 16) * PX_TO_INCH * config.scale * 1.5;
1562
- x -= bulletShift;
1563
- w += bulletShift;
1564
- textParts.push({
1565
- text: ' ',
1566
- options: {
1567
- color: parseColor(style.color).hex || '000000',
1568
- fontSize: fontSizePt,
1569
- },
1570
- });
1658
+ const listStyleType = style.listStyleType || 'disc';
1659
+ const listStylePos = style.listStylePosition || 'outside';
1660
+
1661
+ let marker = null;
1662
+
1663
+ // 1. Determine the marker character based on list-style-type
1664
+ if (listStyleType !== 'none') {
1665
+ if (listStyleType === 'decimal') {
1666
+ // Calculate index for ordered lists (1., 2., etc.)
1667
+ const index = Array.prototype.indexOf.call(node.parentNode.children, node) + 1;
1668
+ marker = `${index}.`;
1669
+ } else if (listStyleType === 'circle') {
1670
+ marker = '○';
1671
+ } else if (listStyleType === 'square') {
1672
+ marker = '■';
1673
+ } else {
1674
+ marker = '•'; // Default to disc
1675
+ }
1676
+ }
1677
+
1678
+ // 2. Apply alignment and add marker
1679
+ if (marker) {
1680
+ // Only shift the text box to the left if the bullet is OUTSIDE the content box.
1681
+ // Tailwind 'list-inside' puts the bullet inside the box, so we must NOT shift X.
1682
+ if (listStylePos === 'outside') {
1683
+ const bulletShift = (parseFloat(style.fontSize) || 16) * PX_TO_INCH * config.scale * 1.5;
1684
+ x -= bulletShift;
1685
+ w += bulletShift;
1686
+ }
1687
+
1688
+ // Add the bullet + 3 spaces for visual separation
1689
+ textParts.push({
1690
+ text: marker + ' ',
1691
+ options: {
1692
+ color: parseColor(style.color).hex || '000000',
1693
+ fontSize: fontSizePt,
1694
+ },
1695
+ });
1696
+ }
1571
1697
  }
1572
1698
 
1573
1699
  node.childNodes.forEach((child, index) => {
@@ -1647,6 +1773,7 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
1647
1773
  }
1648
1774
 
1649
1775
  if (textPayload) {
1776
+ textPayload.text[0].options.fontSize = Math.floor(textPayload.text[0]?.options?.fontSize)|| 12;
1650
1777
  items.push({
1651
1778
  type: 'text',
1652
1779
  zIndex: zIndex + 1,
@@ -1726,21 +1853,46 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
1726
1853
 
1727
1854
  if (hasShadow) shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
1728
1855
 
1729
- const borderRadius = parseFloat(style.borderRadius) || 0;
1730
- const aspectRatio = Math.max(widthPx, heightPx) / Math.min(widthPx, heightPx);
1731
- const isCircle = aspectRatio < 1.1 && borderRadius >= Math.min(widthPx, heightPx) / 2 - 1;
1856
+ // 1. Calculate dimensions first
1857
+ const minDimension = Math.min(widthPx, heightPx);
1858
+
1859
+ let rawRadius = parseFloat(style.borderRadius) || 0;
1860
+ const isPercentage = style.borderRadius && style.borderRadius.toString().includes('%');
1861
+
1862
+ // 2. Normalize radius to pixels
1863
+ let radiusPx = rawRadius;
1864
+ if (isPercentage) {
1865
+ radiusPx = (rawRadius / 100) * minDimension;
1866
+ }
1732
1867
 
1733
1868
  let shapeType = pptx.ShapeType.rect;
1734
- if (isCircle) shapeType = pptx.ShapeType.ellipse;
1735
- else if (borderRadius > 0) {
1869
+
1870
+ // 3. Determine Shape Logic
1871
+ const isSquare = Math.abs(widthPx - heightPx) < 1;
1872
+ const isFullyRound = radiusPx >= minDimension / 2;
1873
+
1874
+ // CASE A: It is an Ellipse if:
1875
+ // 1. It is explicitly "50%" (standard CSS way to make ovals/circles)
1876
+ // 2. OR it is a perfect square and fully rounded (a circle)
1877
+ if (isFullyRound && (isPercentage || isSquare)) {
1878
+ shapeType = pptx.ShapeType.ellipse;
1879
+ }
1880
+ // CASE B: It is a Rounded Rectangle (including "Pill" shapes)
1881
+ else if (radiusPx > 0) {
1736
1882
  shapeType = pptx.ShapeType.roundRect;
1737
- shapeOpts.rectRadius = Math.min(0.5, borderRadius / Math.min(widthPx, heightPx));
1883
+ let r = radiusPx / minDimension;
1884
+ if (r > 0.5) r = 0.5;
1885
+ if (minDimension < 100) r = r * 0.25; // Small size adjustment for small shapes
1886
+
1887
+ shapeOpts.rectRadius = r;
1738
1888
  }
1739
1889
 
1740
1890
  if (textPayload) {
1891
+ textPayload.text[0].options.fontSize = Math.floor(textPayload.text[0]?.options?.fontSize)|| 12;
1741
1892
  const textOptions = {
1742
1893
  shape: shapeType,
1743
1894
  ...shapeOpts,
1895
+ rotate: rotation,
1744
1896
  align: textPayload.align,
1745
1897
  valign: textPayload.valign,
1746
1898
  inset: textPayload.inset,