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.
@@ -62410,11 +62410,14 @@
62410
62410
 
62411
62411
  // src/utils.js
62412
62412
 
62413
- /**
62414
- * Checks if any parent element has overflow: hidden which would clip this element
62415
- * @param {HTMLElement} node - The DOM node to check
62416
- * @returns {boolean} - True if a parent has overflow-hidden or overflow-clip
62417
- */
62413
+ // canvas context for color normalization
62414
+ let _ctx;
62415
+ function getCtx() {
62416
+ if (!_ctx) _ctx = document.createElement('canvas').getContext('2d', { willReadFrequently: true });
62417
+ return _ctx;
62418
+ }
62419
+
62420
+ // Checks if any parent element has overflow: hidden which would clip this element
62418
62421
  function isClippedByParent(node) {
62419
62422
  let parent = node.parentElement;
62420
62423
  while (parent && parent !== document.body) {
@@ -62580,28 +62583,58 @@
62580
62583
  return 'data:image/svg+xml;base64,' + btoa(svg);
62581
62584
  }
62582
62585
 
62586
+ // --- REPLACE THE EXISTING parseColor FUNCTION ---
62583
62587
  function parseColor(str) {
62584
- if (!str || str === 'transparent' || str.startsWith('rgba(0, 0, 0, 0)')) {
62588
+ if (!str || str === 'transparent' || str.trim() === 'rgba(0, 0, 0, 0)') {
62585
62589
  return { hex: null, opacity: 0 };
62586
62590
  }
62587
- if (str.startsWith('#')) {
62588
- let hex = str.slice(1);
62589
- if (hex.length === 3)
62590
- hex = hex
62591
- .split('')
62592
- .map((c) => c + c)
62593
- .join('');
62594
- return { hex: hex.toUpperCase(), opacity: 1 };
62591
+
62592
+ const ctx = getCtx();
62593
+ ctx.fillStyle = str;
62594
+ // This forces the browser to resolve variables and convert formats (oklch -> rgb/hex)
62595
+ const computed = ctx.fillStyle;
62596
+
62597
+ // 1. Handle Hex Output (e.g. #ff0000 or #ff0000ff)
62598
+ if (computed.startsWith('#')) {
62599
+ let hex = computed.slice(1); // Remove '#'
62600
+ let opacity = 1;
62601
+
62602
+ // Expand shorthand #RGB -> #RRGGBB
62603
+ if (hex.length === 3) {
62604
+ hex = hex.split('').map(c => c + c).join('');
62605
+ }
62606
+ // Expand shorthand #RGBA -> #RRGGBBAA
62607
+ else if (hex.length === 4) {
62608
+ hex = hex.split('').map(c => c + c).join('');
62609
+ }
62610
+
62611
+ // Handle 8-digit Hex (RRGGBBAA) - PptxGenJS fails if we send 8 digits
62612
+ if (hex.length === 8) {
62613
+ opacity = parseInt(hex.slice(6), 16) / 255;
62614
+ hex = hex.slice(0, 6); // Keep only RRGGBB
62615
+ }
62616
+
62617
+ return { hex: hex.toUpperCase(), opacity };
62595
62618
  }
62596
- const match = str.match(/[\d.]+/g);
62619
+
62620
+ // 2. Handle RGB/RGBA Output (e.g. "rgb(55, 65, 81)" or "rgba(55, 65, 81, 1)")
62621
+ const match = computed.match(/[\d.]+/g);
62597
62622
  if (match && match.length >= 3) {
62598
62623
  const r = parseInt(match[0]);
62599
62624
  const g = parseInt(match[1]);
62600
62625
  const b = parseInt(match[2]);
62601
62626
  const a = match.length > 3 ? parseFloat(match[3]) : 1;
62602
- const hex = ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
62627
+
62628
+ // Bitwise shift to get Hex
62629
+ const hex = ((1 << 24) + (r << 16) + (g << 8) + b)
62630
+ .toString(16)
62631
+ .slice(1)
62632
+ .toUpperCase();
62633
+
62603
62634
  return { hex, opacity: a };
62604
62635
  }
62636
+
62637
+ // Fallback (Parsing failed)
62605
62638
  return { hex: null, opacity: 0 };
62606
62639
  }
62607
62640
 
@@ -63085,33 +63118,30 @@
63085
63118
 
63086
63119
  // src/image-processor.js
63087
63120
 
63088
- async function getProcessedImage(src, targetW, targetH, radius) {
63121
+ async function getProcessedImage(src, targetW, targetH, radius, objectFit = 'fill', objectPosition = '50% 50%') {
63089
63122
  return new Promise((resolve) => {
63090
63123
  const img = new Image();
63091
- img.crossOrigin = 'Anonymous'; // Critical for canvas manipulation
63124
+ img.crossOrigin = 'Anonymous';
63092
63125
 
63093
63126
  img.onload = () => {
63094
63127
  const canvas = document.createElement('canvas');
63095
- // Double resolution for better quality
63096
- const scale = 2;
63128
+ const scale = 2; // Double resolution
63097
63129
  canvas.width = targetW * scale;
63098
63130
  canvas.height = targetH * scale;
63099
63131
  const ctx = canvas.getContext('2d');
63100
63132
  ctx.scale(scale, scale);
63101
63133
 
63102
- // Normalize radius input to an object { tl, tr, br, bl }
63134
+ // Normalize radius
63103
63135
  let r = { tl: 0, tr: 0, br: 0, bl: 0 };
63104
63136
  if (typeof radius === 'number') {
63105
63137
  r = { tl: radius, tr: radius, br: radius, bl: radius };
63106
63138
  } else if (typeof radius === 'object' && radius !== null) {
63107
- r = { ...r, ...radius }; // Merge with defaults
63139
+ r = { ...r, ...radius };
63108
63140
  }
63109
63141
 
63110
- // 1. Draw the Mask (Custom Shape with specific corners)
63142
+ // 1. Draw Mask
63111
63143
  ctx.beginPath();
63112
-
63113
- // Border Radius Clamping Logic (CSS Spec)
63114
- // Prevents corners from overlapping if radii are too large for the container
63144
+ // ... (radius clamping logic remains the same) ...
63115
63145
  const factor = Math.min(
63116
63146
  targetW / (r.tl + r.tr) || Infinity,
63117
63147
  targetH / (r.tr + r.br) || Infinity,
@@ -63120,13 +63150,9 @@
63120
63150
  );
63121
63151
 
63122
63152
  if (factor < 1) {
63123
- r.tl *= factor;
63124
- r.tr *= factor;
63125
- r.br *= factor;
63126
- r.bl *= factor;
63153
+ r.tl *= factor; r.tr *= factor; r.br *= factor; r.bl *= factor;
63127
63154
  }
63128
63155
 
63129
- // Draw path: Top-Left -> Top-Right -> Bottom-Right -> Bottom-Left
63130
63156
  ctx.moveTo(r.tl, 0);
63131
63157
  ctx.lineTo(targetW - r.tr, 0);
63132
63158
  ctx.arcTo(targetW, 0, targetW, r.tr, r.tr);
@@ -63136,22 +63162,58 @@
63136
63162
  ctx.arcTo(0, targetH, 0, targetH - r.bl, r.bl);
63137
63163
  ctx.lineTo(0, r.tl);
63138
63164
  ctx.arcTo(0, 0, r.tl, 0, r.tl);
63139
-
63140
63165
  ctx.closePath();
63141
63166
  ctx.fillStyle = '#000';
63142
63167
  ctx.fill();
63143
63168
 
63144
- // 2. Composite Source-In (Crops the next image draw to the mask)
63169
+ // 2. Composite Source-In
63145
63170
  ctx.globalCompositeOperation = 'source-in';
63146
63171
 
63147
- // 3. Draw Image (Object Cover Logic)
63172
+ // 3. Draw Image with Object Fit logic
63148
63173
  const wRatio = targetW / img.width;
63149
63174
  const hRatio = targetH / img.height;
63150
- const maxRatio = Math.max(wRatio, hRatio);
63151
- const renderW = img.width * maxRatio;
63152
- const renderH = img.height * maxRatio;
63153
- const renderX = (targetW - renderW) / 2;
63154
- const renderY = (targetH - renderH) / 2;
63175
+ let renderW, renderH;
63176
+
63177
+ if (objectFit === 'contain') {
63178
+ const fitScale = Math.min(wRatio, hRatio);
63179
+ renderW = img.width * fitScale;
63180
+ renderH = img.height * fitScale;
63181
+ } else if (objectFit === 'cover') {
63182
+ const coverScale = Math.max(wRatio, hRatio);
63183
+ renderW = img.width * coverScale;
63184
+ renderH = img.height * coverScale;
63185
+ } else if (objectFit === 'none') {
63186
+ renderW = img.width;
63187
+ renderH = img.height;
63188
+ } else if (objectFit === 'scale-down') {
63189
+ const scaleDown = Math.min(1, Math.min(wRatio, hRatio));
63190
+ renderW = img.width * scaleDown;
63191
+ renderH = img.height * scaleDown;
63192
+ } else {
63193
+ // 'fill' (default)
63194
+ renderW = targetW;
63195
+ renderH = targetH;
63196
+ }
63197
+
63198
+ // Handle Object Position (simplified parsing for "x% y%" or keywords)
63199
+ let posX = 0.5; // Default center
63200
+ let posY = 0.5;
63201
+
63202
+ const posParts = objectPosition.split(' ');
63203
+ if (posParts.length > 0) {
63204
+ const parsePos = (val) => {
63205
+ if (val === 'left' || val === 'top') return 0;
63206
+ if (val === 'center') return 0.5;
63207
+ if (val === 'right' || val === 'bottom') return 1;
63208
+ if (val.includes('%')) return parseFloat(val) / 100;
63209
+ return 0.5; // fallback
63210
+ };
63211
+ posX = parsePos(posParts[0]);
63212
+ posY = posParts.length > 1 ? parsePos(posParts[1]) : 0.5;
63213
+ }
63214
+
63215
+ const renderX = (targetW - renderW) * posX;
63216
+ const renderY = (targetH - renderH) * posY;
63155
63217
 
63156
63218
  ctx.drawImage(img, renderX, renderY, renderW, renderH);
63157
63219
 
@@ -63207,34 +63269,37 @@
63207
63269
  await processSlide(root, slide, pptx);
63208
63270
  }
63209
63271
 
63210
- // 3. Font Embedding Logic
63272
+ // 3. Font Embedding Logic
63211
63273
  let finalBlob;
63212
63274
  let fontsToEmbed = options.fonts || [];
63213
63275
 
63214
63276
  if (options.autoEmbedFonts) {
63215
63277
  // A. Scan DOM for used font families
63216
63278
  const usedFamilies = getUsedFontFamilies(elements);
63217
-
63279
+
63218
63280
  // B. Scan CSS for URLs matches
63219
63281
  const detectedFonts = await getAutoDetectedFonts(usedFamilies);
63220
-
63282
+
63221
63283
  // C. Merge (Avoid duplicates)
63222
- const explicitNames = new Set(fontsToEmbed.map(f => f.name));
63284
+ const explicitNames = new Set(fontsToEmbed.map((f) => f.name));
63223
63285
  for (const autoFont of detectedFonts) {
63224
- if (!explicitNames.has(autoFont.name)) {
63225
- fontsToEmbed.push(autoFont);
63226
- }
63286
+ if (!explicitNames.has(autoFont.name)) {
63287
+ fontsToEmbed.push(autoFont);
63288
+ }
63227
63289
  }
63228
-
63290
+
63229
63291
  if (detectedFonts.length > 0) {
63230
- console.log('Auto-detected fonts:', detectedFonts.map(f => f.name));
63292
+ console.log(
63293
+ 'Auto-detected fonts:',
63294
+ detectedFonts.map((f) => f.name)
63295
+ );
63231
63296
  }
63232
63297
  }
63233
63298
 
63234
63299
  if (fontsToEmbed.length > 0) {
63235
63300
  // Generate initial PPTX
63236
63301
  const initialBlob = await pptx.write({ outputType: 'blob' });
63237
-
63302
+
63238
63303
  // Load into Embedder
63239
63304
  const zip = await JSZip.loadAsync(initialBlob);
63240
63305
  const embedder = new PPTXEmbedFonts();
@@ -63246,7 +63311,7 @@
63246
63311
  const response = await fetch(fontCfg.url);
63247
63312
  if (!response.ok) throw new Error(`Failed to fetch ${fontCfg.url}`);
63248
63313
  const buffer = await response.arrayBuffer();
63249
-
63314
+
63250
63315
  // Infer type
63251
63316
  const ext = fontCfg.url.split('.').pop().split(/[?#]/)[0].toLowerCase();
63252
63317
  let type = 'ttf';
@@ -63385,11 +63450,8 @@
63385
63450
 
63386
63451
  /**
63387
63452
  * Optimized html2canvas wrapper
63388
- * Now strictly captures the node itself, not the root.
63389
- */
63390
- /**
63391
- * Optimized html2canvas wrapper
63392
- * Includes fix for cropped icons by adjusting styles in the cloned document.
63453
+ * Fixes icon clipping by adding padding in the clone to capture font bleed,
63454
+ * then scaling the result to fit the original bounding box.
63393
63455
  */
63394
63456
  async function elementToCanvasImage(node, widthPx, heightPx) {
63395
63457
  return new Promise((resolve) => {
@@ -63405,51 +63467,76 @@
63405
63467
  html2canvas(node, {
63406
63468
  backgroundColor: null,
63407
63469
  logging: false,
63408
- scale: 3, // Higher scale for sharper icons
63409
- useCORS: true, // critical for external fonts/images
63470
+ scale: 3, // High resolution capture
63471
+ useCORS: true,
63410
63472
  onclone: (clonedDoc) => {
63411
63473
  const clonedNode = clonedDoc.getElementById(tempId);
63412
63474
  if (clonedNode) {
63413
- // --- FIX: PREVENT ICON CLIPPING ---
63414
- // 1. Force overflow visible so glyphs bleeding out aren't cut
63415
- clonedNode.style.overflow = 'visible';
63416
-
63417
- // 2. Adjust alignment for Icons to prevent baseline clipping
63418
- // (Applies to <i>, <span>, or standard icon classes)
63475
+ // --- FIX FOR ICON CLIPPING ---
63419
63476
  const tag = clonedNode.tagName;
63420
- if (tag === 'I' || tag === 'SPAN' || clonedNode.className.includes('fa-')) {
63421
- // Flex center helps align the glyph exactly in the middle of the box
63422
- // preventing top/bottom cropping due to line-height mismatches.
63423
- clonedNode.style.display = 'inline-flex';
63477
+ // Detect icons: I tags, SPAN tags, or elements with icon classes
63478
+ const isIcon =
63479
+ tag === 'I' ||
63480
+ tag === 'SPAN' ||
63481
+ clonedNode.className.includes('fa-') ||
63482
+ clonedNode.className.includes('icon');
63483
+
63484
+ if (isIcon) {
63485
+ // 1. Use inline-flex to center the glyph content
63486
+ clonedNode.style.display = 'flex';
63424
63487
  clonedNode.style.justifyContent = 'center';
63425
63488
  clonedNode.style.alignItems = 'center';
63426
63489
 
63427
- // Remove margins that might offset the capture
63428
- clonedNode.style.margin = '0';
63490
+ // 2. Reset constraints that might crop content
63491
+ clonedNode.style.overflow = 'visible';
63492
+ clonedNode.style.lineHeight = 'normal';
63493
+
63494
+ // 3. Add padding to the capture area.
63495
+ // This ensures parts of the glyph sticking out of the bounding box (ascenders/descenders)
63496
+ // are captured in the canvas instead of being cropped.
63497
+ // 'content-box' ensures padding adds to the total width/height.
63498
+ clonedNode.style.boxSizing = 'content-box';
63499
+ clonedNode.style.padding = '10px';
63500
+
63501
+ // 4. Ensure the content box itself matches the original size (minimum)
63502
+ // so the icon doesn't collapse.
63503
+ clonedNode.style.minWidth = `${width}px`;
63504
+ clonedNode.style.minHeight = `${height}px`;
63429
63505
 
63430
- // Ensure the font fits
63431
- clonedNode.style.lineHeight = '1';
63432
- clonedNode.style.verticalAlign = 'middle';
63506
+ // Clear margins that might displace the capture
63507
+ clonedNode.style.margin = '0';
63433
63508
  }
63434
63509
  }
63435
63510
  },
63436
63511
  })
63437
63512
  .then((canvas) => {
63438
- // Restore the original ID
63513
+ // Restore ID
63439
63514
  if (originalId) node.id = originalId;
63440
63515
  else node.removeAttribute('id');
63441
63516
 
63517
+ // Create destination canvas with the EXACT original dimensions
63442
63518
  const destCanvas = document.createElement('canvas');
63443
63519
  destCanvas.width = width;
63444
63520
  destCanvas.height = height;
63445
63521
  const ctx = destCanvas.getContext('2d');
63446
63522
 
63447
- // Draw captured canvas.
63448
- // We simply draw it to fill the box. Since we centered it in 'onclone',
63449
- // the glyph should now be visible within the bounds.
63450
- ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, width, height);
63523
+ // --- SCALE TO FIT ---
63524
+ // The captured 'canvas' is now larger than 'destCanvas' because we added padding.
63525
+ // We draw the larger captured image into the smaller destination box.
63526
+ // This effectively "zooms out" slightly, ensuring the bleed is visible within the bounds.
63527
+ ctx.drawImage(
63528
+ canvas,
63529
+ 0,
63530
+ 0,
63531
+ canvas.width,
63532
+ canvas.height, // Source: Full captured size (with padding)
63533
+ 0,
63534
+ 0,
63535
+ width,
63536
+ height // Dest: Original requested size
63537
+ );
63451
63538
 
63452
- // --- Border Radius Clipping (Existing Logic) ---
63539
+ // --- Border Radius Clipping (Preserve existing logic) ---
63453
63540
  let tl = parseFloat(style.borderTopLeftRadius) || 0;
63454
63541
  let tr = parseFloat(style.borderTopRightRadius) || 0;
63455
63542
  let br = parseFloat(style.borderBottomRightRadius) || 0;
@@ -63661,6 +63748,9 @@
63661
63748
  }
63662
63749
  }
63663
63750
 
63751
+ const objectFit = style.objectFit || 'fill'; // default CSS behavior is fill
63752
+ const objectPosition = style.objectPosition || '50% 50%';
63753
+
63664
63754
  const item = {
63665
63755
  type: 'image',
63666
63756
  zIndex,
@@ -63669,7 +63759,14 @@
63669
63759
  };
63670
63760
 
63671
63761
  const job = async () => {
63672
- const processed = await getProcessedImage(node.src, widthPx, heightPx, radii);
63762
+ const processed = await getProcessedImage(
63763
+ node.src,
63764
+ widthPx,
63765
+ heightPx,
63766
+ radii,
63767
+ objectFit,
63768
+ objectPosition
63769
+ );
63673
63770
  if (processed) item.options.data = processed;
63674
63771
  else item.skip = true;
63675
63772
  };
@@ -63769,16 +63866,45 @@
63769
63866
  const isList = style.display === 'list-item';
63770
63867
  if (isList) {
63771
63868
  const fontSizePt = parseFloat(style.fontSize) * 0.75 * config.scale;
63772
- const bulletShift = (parseFloat(style.fontSize) || 16) * PX_TO_INCH * config.scale * 1.5;
63773
- x -= bulletShift;
63774
- w += bulletShift;
63775
- textParts.push({
63776
- text: ' ',
63777
- options: {
63778
- color: parseColor(style.color).hex || '000000',
63779
- fontSize: fontSizePt,
63780
- },
63781
- });
63869
+ const listStyleType = style.listStyleType || 'disc';
63870
+ const listStylePos = style.listStylePosition || 'outside';
63871
+
63872
+ let marker = null;
63873
+
63874
+ // 1. Determine the marker character based on list-style-type
63875
+ if (listStyleType !== 'none') {
63876
+ if (listStyleType === 'decimal') {
63877
+ // Calculate index for ordered lists (1., 2., etc.)
63878
+ const index = Array.prototype.indexOf.call(node.parentNode.children, node) + 1;
63879
+ marker = `${index}.`;
63880
+ } else if (listStyleType === 'circle') {
63881
+ marker = '○';
63882
+ } else if (listStyleType === 'square') {
63883
+ marker = '■';
63884
+ } else {
63885
+ marker = '•'; // Default to disc
63886
+ }
63887
+ }
63888
+
63889
+ // 2. Apply alignment and add marker
63890
+ if (marker) {
63891
+ // Only shift the text box to the left if the bullet is OUTSIDE the content box.
63892
+ // Tailwind 'list-inside' puts the bullet inside the box, so we must NOT shift X.
63893
+ if (listStylePos === 'outside') {
63894
+ const bulletShift = (parseFloat(style.fontSize) || 16) * PX_TO_INCH * config.scale * 1.5;
63895
+ x -= bulletShift;
63896
+ w += bulletShift;
63897
+ }
63898
+
63899
+ // Add the bullet + 3 spaces for visual separation
63900
+ textParts.push({
63901
+ text: marker + ' ',
63902
+ options: {
63903
+ color: parseColor(style.color).hex || '000000',
63904
+ fontSize: fontSizePt,
63905
+ },
63906
+ });
63907
+ }
63782
63908
  }
63783
63909
 
63784
63910
  node.childNodes.forEach((child, index) => {
@@ -63858,6 +63984,7 @@
63858
63984
  }
63859
63985
 
63860
63986
  if (textPayload) {
63987
+ textPayload.text[0].options.fontSize = Math.floor(textPayload.text[0]?.options?.fontSize)|| 12;
63861
63988
  items.push({
63862
63989
  type: 'text',
63863
63990
  zIndex: zIndex + 1,
@@ -63937,21 +64064,46 @@
63937
64064
 
63938
64065
  if (hasShadow) shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
63939
64066
 
63940
- const borderRadius = parseFloat(style.borderRadius) || 0;
63941
- const aspectRatio = Math.max(widthPx, heightPx) / Math.min(widthPx, heightPx);
63942
- const isCircle = aspectRatio < 1.1 && borderRadius >= Math.min(widthPx, heightPx) / 2 - 1;
64067
+ // 1. Calculate dimensions first
64068
+ const minDimension = Math.min(widthPx, heightPx);
64069
+
64070
+ let rawRadius = parseFloat(style.borderRadius) || 0;
64071
+ const isPercentage = style.borderRadius && style.borderRadius.toString().includes('%');
64072
+
64073
+ // 2. Normalize radius to pixels
64074
+ let radiusPx = rawRadius;
64075
+ if (isPercentage) {
64076
+ radiusPx = (rawRadius / 100) * minDimension;
64077
+ }
63943
64078
 
63944
64079
  let shapeType = pptx.ShapeType.rect;
63945
- if (isCircle) shapeType = pptx.ShapeType.ellipse;
63946
- else if (borderRadius > 0) {
64080
+
64081
+ // 3. Determine Shape Logic
64082
+ const isSquare = Math.abs(widthPx - heightPx) < 1;
64083
+ const isFullyRound = radiusPx >= minDimension / 2;
64084
+
64085
+ // CASE A: It is an Ellipse if:
64086
+ // 1. It is explicitly "50%" (standard CSS way to make ovals/circles)
64087
+ // 2. OR it is a perfect square and fully rounded (a circle)
64088
+ if (isFullyRound && (isPercentage || isSquare)) {
64089
+ shapeType = pptx.ShapeType.ellipse;
64090
+ }
64091
+ // CASE B: It is a Rounded Rectangle (including "Pill" shapes)
64092
+ else if (radiusPx > 0) {
63947
64093
  shapeType = pptx.ShapeType.roundRect;
63948
- shapeOpts.rectRadius = Math.min(0.5, borderRadius / Math.min(widthPx, heightPx));
64094
+ let r = radiusPx / minDimension;
64095
+ if (r > 0.5) r = 0.5;
64096
+ if (minDimension < 100) r = r * 0.25; // Small size adjustment for small shapes
64097
+
64098
+ shapeOpts.rectRadius = r;
63949
64099
  }
63950
64100
 
63951
64101
  if (textPayload) {
64102
+ textPayload.text[0].options.fontSize = Math.floor(textPayload.text[0]?.options?.fontSize)|| 12;
63952
64103
  const textOptions = {
63953
64104
  shape: shapeType,
63954
64105
  ...shapeOpts,
64106
+ rotate: rotation,
63955
64107
  align: textPayload.align,
63956
64108
  valign: textPayload.valign,
63957
64109
  inset: textPayload.inset,