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.
@@ -229,11 +229,14 @@ class PPTXEmbedFonts {
229
229
 
230
230
  // src/utils.js
231
231
 
232
- /**
233
- * Checks if any parent element has overflow: hidden which would clip this element
234
- * @param {HTMLElement} node - The DOM node to check
235
- * @returns {boolean} - True if a parent has overflow-hidden or overflow-clip
236
- */
232
+ // canvas context for color normalization
233
+ let _ctx;
234
+ function getCtx() {
235
+ if (!_ctx) _ctx = document.createElement('canvas').getContext('2d', { willReadFrequently: true });
236
+ return _ctx;
237
+ }
238
+
239
+ // Checks if any parent element has overflow: hidden which would clip this element
237
240
  function isClippedByParent(node) {
238
241
  let parent = node.parentElement;
239
242
  while (parent && parent !== document.body) {
@@ -399,28 +402,58 @@ function generateCustomShapeSVG(w, h, color, opacity, radii) {
399
402
  return 'data:image/svg+xml;base64,' + btoa(svg);
400
403
  }
401
404
 
405
+ // --- REPLACE THE EXISTING parseColor FUNCTION ---
402
406
  function parseColor(str) {
403
- if (!str || str === 'transparent' || str.startsWith('rgba(0, 0, 0, 0)')) {
407
+ if (!str || str === 'transparent' || str.trim() === 'rgba(0, 0, 0, 0)') {
404
408
  return { hex: null, opacity: 0 };
405
409
  }
406
- if (str.startsWith('#')) {
407
- let hex = str.slice(1);
408
- if (hex.length === 3)
409
- hex = hex
410
- .split('')
411
- .map((c) => c + c)
412
- .join('');
413
- return { hex: hex.toUpperCase(), opacity: 1 };
410
+
411
+ const ctx = getCtx();
412
+ ctx.fillStyle = str;
413
+ // This forces the browser to resolve variables and convert formats (oklch -> rgb/hex)
414
+ const computed = ctx.fillStyle;
415
+
416
+ // 1. Handle Hex Output (e.g. #ff0000 or #ff0000ff)
417
+ if (computed.startsWith('#')) {
418
+ let hex = computed.slice(1); // Remove '#'
419
+ let opacity = 1;
420
+
421
+ // Expand shorthand #RGB -> #RRGGBB
422
+ if (hex.length === 3) {
423
+ hex = hex.split('').map(c => c + c).join('');
424
+ }
425
+ // Expand shorthand #RGBA -> #RRGGBBAA
426
+ else if (hex.length === 4) {
427
+ hex = hex.split('').map(c => c + c).join('');
428
+ }
429
+
430
+ // Handle 8-digit Hex (RRGGBBAA) - PptxGenJS fails if we send 8 digits
431
+ if (hex.length === 8) {
432
+ opacity = parseInt(hex.slice(6), 16) / 255;
433
+ hex = hex.slice(0, 6); // Keep only RRGGBB
434
+ }
435
+
436
+ return { hex: hex.toUpperCase(), opacity };
414
437
  }
415
- const match = str.match(/[\d.]+/g);
438
+
439
+ // 2. Handle RGB/RGBA Output (e.g. "rgb(55, 65, 81)" or "rgba(55, 65, 81, 1)")
440
+ const match = computed.match(/[\d.]+/g);
416
441
  if (match && match.length >= 3) {
417
442
  const r = parseInt(match[0]);
418
443
  const g = parseInt(match[1]);
419
444
  const b = parseInt(match[2]);
420
445
  const a = match.length > 3 ? parseFloat(match[3]) : 1;
421
- const hex = ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
446
+
447
+ // Bitwise shift to get Hex
448
+ const hex = ((1 << 24) + (r << 16) + (g << 8) + b)
449
+ .toString(16)
450
+ .slice(1)
451
+ .toUpperCase();
452
+
422
453
  return { hex, opacity: a };
423
454
  }
455
+
456
+ // Fallback (Parsing failed)
424
457
  return { hex: null, opacity: 0 };
425
458
  }
426
459
 
@@ -904,33 +937,30 @@ async function getAutoDetectedFonts(usedFamilies) {
904
937
 
905
938
  // src/image-processor.js
906
939
 
907
- async function getProcessedImage(src, targetW, targetH, radius) {
940
+ async function getProcessedImage(src, targetW, targetH, radius, objectFit = 'fill', objectPosition = '50% 50%') {
908
941
  return new Promise((resolve) => {
909
942
  const img = new Image();
910
- img.crossOrigin = 'Anonymous'; // Critical for canvas manipulation
943
+ img.crossOrigin = 'Anonymous';
911
944
 
912
945
  img.onload = () => {
913
946
  const canvas = document.createElement('canvas');
914
- // Double resolution for better quality
915
- const scale = 2;
947
+ const scale = 2; // Double resolution
916
948
  canvas.width = targetW * scale;
917
949
  canvas.height = targetH * scale;
918
950
  const ctx = canvas.getContext('2d');
919
951
  ctx.scale(scale, scale);
920
952
 
921
- // Normalize radius input to an object { tl, tr, br, bl }
953
+ // Normalize radius
922
954
  let r = { tl: 0, tr: 0, br: 0, bl: 0 };
923
955
  if (typeof radius === 'number') {
924
956
  r = { tl: radius, tr: radius, br: radius, bl: radius };
925
957
  } else if (typeof radius === 'object' && radius !== null) {
926
- r = { ...r, ...radius }; // Merge with defaults
958
+ r = { ...r, ...radius };
927
959
  }
928
960
 
929
- // 1. Draw the Mask (Custom Shape with specific corners)
961
+ // 1. Draw Mask
930
962
  ctx.beginPath();
931
-
932
- // Border Radius Clamping Logic (CSS Spec)
933
- // Prevents corners from overlapping if radii are too large for the container
963
+ // ... (radius clamping logic remains the same) ...
934
964
  const factor = Math.min(
935
965
  targetW / (r.tl + r.tr) || Infinity,
936
966
  targetH / (r.tr + r.br) || Infinity,
@@ -939,13 +969,9 @@ async function getProcessedImage(src, targetW, targetH, radius) {
939
969
  );
940
970
 
941
971
  if (factor < 1) {
942
- r.tl *= factor;
943
- r.tr *= factor;
944
- r.br *= factor;
945
- r.bl *= factor;
972
+ r.tl *= factor; r.tr *= factor; r.br *= factor; r.bl *= factor;
946
973
  }
947
974
 
948
- // Draw path: Top-Left -> Top-Right -> Bottom-Right -> Bottom-Left
949
975
  ctx.moveTo(r.tl, 0);
950
976
  ctx.lineTo(targetW - r.tr, 0);
951
977
  ctx.arcTo(targetW, 0, targetW, r.tr, r.tr);
@@ -955,22 +981,58 @@ async function getProcessedImage(src, targetW, targetH, radius) {
955
981
  ctx.arcTo(0, targetH, 0, targetH - r.bl, r.bl);
956
982
  ctx.lineTo(0, r.tl);
957
983
  ctx.arcTo(0, 0, r.tl, 0, r.tl);
958
-
959
984
  ctx.closePath();
960
985
  ctx.fillStyle = '#000';
961
986
  ctx.fill();
962
987
 
963
- // 2. Composite Source-In (Crops the next image draw to the mask)
988
+ // 2. Composite Source-In
964
989
  ctx.globalCompositeOperation = 'source-in';
965
990
 
966
- // 3. Draw Image (Object Cover Logic)
991
+ // 3. Draw Image with Object Fit logic
967
992
  const wRatio = targetW / img.width;
968
993
  const hRatio = targetH / img.height;
969
- const maxRatio = Math.max(wRatio, hRatio);
970
- const renderW = img.width * maxRatio;
971
- const renderH = img.height * maxRatio;
972
- const renderX = (targetW - renderW) / 2;
973
- const renderY = (targetH - renderH) / 2;
994
+ let renderW, renderH;
995
+
996
+ if (objectFit === 'contain') {
997
+ const fitScale = Math.min(wRatio, hRatio);
998
+ renderW = img.width * fitScale;
999
+ renderH = img.height * fitScale;
1000
+ } else if (objectFit === 'cover') {
1001
+ const coverScale = Math.max(wRatio, hRatio);
1002
+ renderW = img.width * coverScale;
1003
+ renderH = img.height * coverScale;
1004
+ } else if (objectFit === 'none') {
1005
+ renderW = img.width;
1006
+ renderH = img.height;
1007
+ } else if (objectFit === 'scale-down') {
1008
+ const scaleDown = Math.min(1, Math.min(wRatio, hRatio));
1009
+ renderW = img.width * scaleDown;
1010
+ renderH = img.height * scaleDown;
1011
+ } else {
1012
+ // 'fill' (default)
1013
+ renderW = targetW;
1014
+ renderH = targetH;
1015
+ }
1016
+
1017
+ // Handle Object Position (simplified parsing for "x% y%" or keywords)
1018
+ let posX = 0.5; // Default center
1019
+ let posY = 0.5;
1020
+
1021
+ const posParts = objectPosition.split(' ');
1022
+ if (posParts.length > 0) {
1023
+ const parsePos = (val) => {
1024
+ if (val === 'left' || val === 'top') return 0;
1025
+ if (val === 'center') return 0.5;
1026
+ if (val === 'right' || val === 'bottom') return 1;
1027
+ if (val.includes('%')) return parseFloat(val) / 100;
1028
+ return 0.5; // fallback
1029
+ };
1030
+ posX = parsePos(posParts[0]);
1031
+ posY = posParts.length > 1 ? parsePos(posParts[1]) : 0.5;
1032
+ }
1033
+
1034
+ const renderX = (targetW - renderW) * posX;
1035
+ const renderY = (targetH - renderH) * posY;
974
1036
 
975
1037
  ctx.drawImage(img, renderX, renderY, renderW, renderH);
976
1038
 
@@ -1026,34 +1088,37 @@ async function exportToPptx(target, options = {}) {
1026
1088
  await processSlide(root, slide, pptx);
1027
1089
  }
1028
1090
 
1029
- // 3. Font Embedding Logic
1091
+ // 3. Font Embedding Logic
1030
1092
  let finalBlob;
1031
1093
  let fontsToEmbed = options.fonts || [];
1032
1094
 
1033
1095
  if (options.autoEmbedFonts) {
1034
1096
  // A. Scan DOM for used font families
1035
1097
  const usedFamilies = getUsedFontFamilies(elements);
1036
-
1098
+
1037
1099
  // B. Scan CSS for URLs matches
1038
1100
  const detectedFonts = await getAutoDetectedFonts(usedFamilies);
1039
-
1101
+
1040
1102
  // C. Merge (Avoid duplicates)
1041
- const explicitNames = new Set(fontsToEmbed.map(f => f.name));
1103
+ const explicitNames = new Set(fontsToEmbed.map((f) => f.name));
1042
1104
  for (const autoFont of detectedFonts) {
1043
- if (!explicitNames.has(autoFont.name)) {
1044
- fontsToEmbed.push(autoFont);
1045
- }
1105
+ if (!explicitNames.has(autoFont.name)) {
1106
+ fontsToEmbed.push(autoFont);
1107
+ }
1046
1108
  }
1047
-
1109
+
1048
1110
  if (detectedFonts.length > 0) {
1049
- console.log('Auto-detected fonts:', detectedFonts.map(f => f.name));
1111
+ console.log(
1112
+ 'Auto-detected fonts:',
1113
+ detectedFonts.map((f) => f.name)
1114
+ );
1050
1115
  }
1051
1116
  }
1052
1117
 
1053
1118
  if (fontsToEmbed.length > 0) {
1054
1119
  // Generate initial PPTX
1055
1120
  const initialBlob = await pptx.write({ outputType: 'blob' });
1056
-
1121
+
1057
1122
  // Load into Embedder
1058
1123
  const zip = await JSZip__default["default"].loadAsync(initialBlob);
1059
1124
  const embedder = new PPTXEmbedFonts();
@@ -1065,7 +1130,7 @@ async function exportToPptx(target, options = {}) {
1065
1130
  const response = await fetch(fontCfg.url);
1066
1131
  if (!response.ok) throw new Error(`Failed to fetch ${fontCfg.url}`);
1067
1132
  const buffer = await response.arrayBuffer();
1068
-
1133
+
1069
1134
  // Infer type
1070
1135
  const ext = fontCfg.url.split('.').pop().split(/[?#]/)[0].toLowerCase();
1071
1136
  let type = 'ttf';
@@ -1204,11 +1269,8 @@ async function processSlide(root, slide, pptx) {
1204
1269
 
1205
1270
  /**
1206
1271
  * Optimized html2canvas wrapper
1207
- * Now strictly captures the node itself, not the root.
1208
- */
1209
- /**
1210
- * Optimized html2canvas wrapper
1211
- * Includes fix for cropped icons by adjusting styles in the cloned document.
1272
+ * Fixes icon clipping by adding padding in the clone to capture font bleed,
1273
+ * then scaling the result to fit the original bounding box.
1212
1274
  */
1213
1275
  async function elementToCanvasImage(node, widthPx, heightPx) {
1214
1276
  return new Promise((resolve) => {
@@ -1224,51 +1286,76 @@ async function elementToCanvasImage(node, widthPx, heightPx) {
1224
1286
  html2canvas__default["default"](node, {
1225
1287
  backgroundColor: null,
1226
1288
  logging: false,
1227
- scale: 3, // Higher scale for sharper icons
1228
- useCORS: true, // critical for external fonts/images
1289
+ scale: 3, // High resolution capture
1290
+ useCORS: true,
1229
1291
  onclone: (clonedDoc) => {
1230
1292
  const clonedNode = clonedDoc.getElementById(tempId);
1231
1293
  if (clonedNode) {
1232
- // --- FIX: PREVENT ICON CLIPPING ---
1233
- // 1. Force overflow visible so glyphs bleeding out aren't cut
1234
- clonedNode.style.overflow = 'visible';
1235
-
1236
- // 2. Adjust alignment for Icons to prevent baseline clipping
1237
- // (Applies to <i>, <span>, or standard icon classes)
1294
+ // --- FIX FOR ICON CLIPPING ---
1238
1295
  const tag = clonedNode.tagName;
1239
- if (tag === 'I' || tag === 'SPAN' || clonedNode.className.includes('fa-')) {
1240
- // Flex center helps align the glyph exactly in the middle of the box
1241
- // preventing top/bottom cropping due to line-height mismatches.
1242
- clonedNode.style.display = 'inline-flex';
1296
+ // Detect icons: I tags, SPAN tags, or elements with icon classes
1297
+ const isIcon =
1298
+ tag === 'I' ||
1299
+ tag === 'SPAN' ||
1300
+ clonedNode.className.includes('fa-') ||
1301
+ clonedNode.className.includes('icon');
1302
+
1303
+ if (isIcon) {
1304
+ // 1. Use inline-flex to center the glyph content
1305
+ clonedNode.style.display = 'flex';
1243
1306
  clonedNode.style.justifyContent = 'center';
1244
1307
  clonedNode.style.alignItems = 'center';
1245
1308
 
1246
- // Remove margins that might offset the capture
1247
- clonedNode.style.margin = '0';
1309
+ // 2. Reset constraints that might crop content
1310
+ clonedNode.style.overflow = 'visible';
1311
+ clonedNode.style.lineHeight = 'normal';
1312
+
1313
+ // 3. Add padding to the capture area.
1314
+ // This ensures parts of the glyph sticking out of the bounding box (ascenders/descenders)
1315
+ // are captured in the canvas instead of being cropped.
1316
+ // 'content-box' ensures padding adds to the total width/height.
1317
+ clonedNode.style.boxSizing = 'content-box';
1318
+ clonedNode.style.padding = '10px';
1319
+
1320
+ // 4. Ensure the content box itself matches the original size (minimum)
1321
+ // so the icon doesn't collapse.
1322
+ clonedNode.style.minWidth = `${width}px`;
1323
+ clonedNode.style.minHeight = `${height}px`;
1248
1324
 
1249
- // Ensure the font fits
1250
- clonedNode.style.lineHeight = '1';
1251
- clonedNode.style.verticalAlign = 'middle';
1325
+ // Clear margins that might displace the capture
1326
+ clonedNode.style.margin = '0';
1252
1327
  }
1253
1328
  }
1254
1329
  },
1255
1330
  })
1256
1331
  .then((canvas) => {
1257
- // Restore the original ID
1332
+ // Restore ID
1258
1333
  if (originalId) node.id = originalId;
1259
1334
  else node.removeAttribute('id');
1260
1335
 
1336
+ // Create destination canvas with the EXACT original dimensions
1261
1337
  const destCanvas = document.createElement('canvas');
1262
1338
  destCanvas.width = width;
1263
1339
  destCanvas.height = height;
1264
1340
  const ctx = destCanvas.getContext('2d');
1265
1341
 
1266
- // Draw captured canvas.
1267
- // We simply draw it to fill the box. Since we centered it in 'onclone',
1268
- // the glyph should now be visible within the bounds.
1269
- ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, width, height);
1342
+ // --- SCALE TO FIT ---
1343
+ // The captured 'canvas' is now larger than 'destCanvas' because we added padding.
1344
+ // We draw the larger captured image into the smaller destination box.
1345
+ // This effectively "zooms out" slightly, ensuring the bleed is visible within the bounds.
1346
+ ctx.drawImage(
1347
+ canvas,
1348
+ 0,
1349
+ 0,
1350
+ canvas.width,
1351
+ canvas.height, // Source: Full captured size (with padding)
1352
+ 0,
1353
+ 0,
1354
+ width,
1355
+ height // Dest: Original requested size
1356
+ );
1270
1357
 
1271
- // --- Border Radius Clipping (Existing Logic) ---
1358
+ // --- Border Radius Clipping (Preserve existing logic) ---
1272
1359
  let tl = parseFloat(style.borderTopLeftRadius) || 0;
1273
1360
  let tr = parseFloat(style.borderTopRightRadius) || 0;
1274
1361
  let br = parseFloat(style.borderBottomRightRadius) || 0;
@@ -1480,6 +1567,9 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
1480
1567
  }
1481
1568
  }
1482
1569
 
1570
+ const objectFit = style.objectFit || 'fill'; // default CSS behavior is fill
1571
+ const objectPosition = style.objectPosition || '50% 50%';
1572
+
1483
1573
  const item = {
1484
1574
  type: 'image',
1485
1575
  zIndex,
@@ -1488,7 +1578,14 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
1488
1578
  };
1489
1579
 
1490
1580
  const job = async () => {
1491
- const processed = await getProcessedImage(node.src, widthPx, heightPx, radii);
1581
+ const processed = await getProcessedImage(
1582
+ node.src,
1583
+ widthPx,
1584
+ heightPx,
1585
+ radii,
1586
+ objectFit,
1587
+ objectPosition
1588
+ );
1492
1589
  if (processed) item.options.data = processed;
1493
1590
  else item.skip = true;
1494
1591
  };
@@ -1588,16 +1685,45 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
1588
1685
  const isList = style.display === 'list-item';
1589
1686
  if (isList) {
1590
1687
  const fontSizePt = parseFloat(style.fontSize) * 0.75 * config.scale;
1591
- const bulletShift = (parseFloat(style.fontSize) || 16) * PX_TO_INCH * config.scale * 1.5;
1592
- x -= bulletShift;
1593
- w += bulletShift;
1594
- textParts.push({
1595
- text: ' ',
1596
- options: {
1597
- color: parseColor(style.color).hex || '000000',
1598
- fontSize: fontSizePt,
1599
- },
1600
- });
1688
+ const listStyleType = style.listStyleType || 'disc';
1689
+ const listStylePos = style.listStylePosition || 'outside';
1690
+
1691
+ let marker = null;
1692
+
1693
+ // 1. Determine the marker character based on list-style-type
1694
+ if (listStyleType !== 'none') {
1695
+ if (listStyleType === 'decimal') {
1696
+ // Calculate index for ordered lists (1., 2., etc.)
1697
+ const index = Array.prototype.indexOf.call(node.parentNode.children, node) + 1;
1698
+ marker = `${index}.`;
1699
+ } else if (listStyleType === 'circle') {
1700
+ marker = '○';
1701
+ } else if (listStyleType === 'square') {
1702
+ marker = '■';
1703
+ } else {
1704
+ marker = '•'; // Default to disc
1705
+ }
1706
+ }
1707
+
1708
+ // 2. Apply alignment and add marker
1709
+ if (marker) {
1710
+ // Only shift the text box to the left if the bullet is OUTSIDE the content box.
1711
+ // Tailwind 'list-inside' puts the bullet inside the box, so we must NOT shift X.
1712
+ if (listStylePos === 'outside') {
1713
+ const bulletShift = (parseFloat(style.fontSize) || 16) * PX_TO_INCH * config.scale * 1.5;
1714
+ x -= bulletShift;
1715
+ w += bulletShift;
1716
+ }
1717
+
1718
+ // Add the bullet + 3 spaces for visual separation
1719
+ textParts.push({
1720
+ text: marker + ' ',
1721
+ options: {
1722
+ color: parseColor(style.color).hex || '000000',
1723
+ fontSize: fontSizePt,
1724
+ },
1725
+ });
1726
+ }
1601
1727
  }
1602
1728
 
1603
1729
  node.childNodes.forEach((child, index) => {
@@ -1677,6 +1803,7 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
1677
1803
  }
1678
1804
 
1679
1805
  if (textPayload) {
1806
+ textPayload.text[0].options.fontSize = Math.floor(textPayload.text[0]?.options?.fontSize)|| 12;
1680
1807
  items.push({
1681
1808
  type: 'text',
1682
1809
  zIndex: zIndex + 1,
@@ -1756,21 +1883,46 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
1756
1883
 
1757
1884
  if (hasShadow) shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
1758
1885
 
1759
- const borderRadius = parseFloat(style.borderRadius) || 0;
1760
- const aspectRatio = Math.max(widthPx, heightPx) / Math.min(widthPx, heightPx);
1761
- const isCircle = aspectRatio < 1.1 && borderRadius >= Math.min(widthPx, heightPx) / 2 - 1;
1886
+ // 1. Calculate dimensions first
1887
+ const minDimension = Math.min(widthPx, heightPx);
1888
+
1889
+ let rawRadius = parseFloat(style.borderRadius) || 0;
1890
+ const isPercentage = style.borderRadius && style.borderRadius.toString().includes('%');
1891
+
1892
+ // 2. Normalize radius to pixels
1893
+ let radiusPx = rawRadius;
1894
+ if (isPercentage) {
1895
+ radiusPx = (rawRadius / 100) * minDimension;
1896
+ }
1762
1897
 
1763
1898
  let shapeType = pptx.ShapeType.rect;
1764
- if (isCircle) shapeType = pptx.ShapeType.ellipse;
1765
- else if (borderRadius > 0) {
1899
+
1900
+ // 3. Determine Shape Logic
1901
+ const isSquare = Math.abs(widthPx - heightPx) < 1;
1902
+ const isFullyRound = radiusPx >= minDimension / 2;
1903
+
1904
+ // CASE A: It is an Ellipse if:
1905
+ // 1. It is explicitly "50%" (standard CSS way to make ovals/circles)
1906
+ // 2. OR it is a perfect square and fully rounded (a circle)
1907
+ if (isFullyRound && (isPercentage || isSquare)) {
1908
+ shapeType = pptx.ShapeType.ellipse;
1909
+ }
1910
+ // CASE B: It is a Rounded Rectangle (including "Pill" shapes)
1911
+ else if (radiusPx > 0) {
1766
1912
  shapeType = pptx.ShapeType.roundRect;
1767
- shapeOpts.rectRadius = Math.min(0.5, borderRadius / Math.min(widthPx, heightPx));
1913
+ let r = radiusPx / minDimension;
1914
+ if (r > 0.5) r = 0.5;
1915
+ if (minDimension < 100) r = r * 0.25; // Small size adjustment for small shapes
1916
+
1917
+ shapeOpts.rectRadius = r;
1768
1918
  }
1769
1919
 
1770
1920
  if (textPayload) {
1921
+ textPayload.text[0].options.fontSize = Math.floor(textPayload.text[0]?.options?.fontSize)|| 12;
1771
1922
  const textOptions = {
1772
1923
  shape: shapeType,
1773
1924
  ...shapeOpts,
1925
+ rotate: rotation,
1774
1926
  align: textPayload.align,
1775
1927
  valign: textPayload.valign,
1776
1928
  inset: textPayload.inset,