dom-to-pptx 1.0.5 → 1.0.6

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,8 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
3
5
  var PptxGenJSImport = require('pptxgenjs');
4
6
 
5
- function _interopNamespaceDefault(e) {
7
+ function _interopNamespace(e) {
8
+ if (e && e.__esModule) return e;
6
9
  var n = Object.create(null);
7
10
  if (e) {
8
11
  Object.keys(e).forEach(function (k) {
@@ -15,11 +18,11 @@ function _interopNamespaceDefault(e) {
15
18
  }
16
19
  });
17
20
  }
18
- n.default = e;
21
+ n["default"] = e;
19
22
  return Object.freeze(n);
20
23
  }
21
24
 
22
- var PptxGenJSImport__namespace = /*#__PURE__*/_interopNamespaceDefault(PptxGenJSImport);
25
+ var PptxGenJSImport__namespace = /*#__PURE__*/_interopNamespace(PptxGenJSImport);
23
26
 
24
27
  /*!
25
28
  * html2canvas 1.4.1 <https://html2canvas.hertzen.com>
@@ -74,7 +77,7 @@ function __awaiter(thisArg, _arguments, P, generator) {
74
77
  function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
75
78
  function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
76
79
  function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
77
- step((generator = generator.apply(thisArg, [])).next());
80
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
78
81
  });
79
82
  }
80
83
 
@@ -107,7 +110,7 @@ function __generator(thisArg, body) {
107
110
  }
108
111
 
109
112
  function __spreadArray(to, from, pack) {
110
- if (arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
113
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
111
114
  if (ar || !(i in from)) {
112
115
  if (!ar) ar = Array.prototype.slice.call(from, 0, i);
113
116
  ar[i] = from[i];
@@ -7841,1172 +7844,1202 @@ var parseBackgroundColor = function (context, element, backgroundColorOverride)
7841
7844
  : defaultBackgroundColor;
7842
7845
  };
7843
7846
 
7844
- // src/utils.js
7845
-
7846
- /**
7847
- * Checks if any parent element has overflow: hidden which would clip this element
7848
- * @param {HTMLElement} node - The DOM node to check
7849
- * @returns {boolean} - True if a parent has overflow-hidden or overflow-clip
7850
- */
7851
- function isClippedByParent(node) {
7852
- let parent = node.parentElement;
7853
- while (parent && parent !== document.body) {
7854
- const style = window.getComputedStyle(parent);
7855
- const overflow = style.overflow;
7856
- if (overflow === 'hidden' || overflow === 'clip') {
7857
- return true;
7858
- }
7859
- parent = parent.parentElement;
7860
- }
7861
- return false;
7862
- }
7863
-
7864
- // Helper to save gradient text
7865
- function getGradientFallbackColor(bgImage) {
7866
- if (!bgImage) return null;
7867
- const hexMatch = bgImage.match(/#(?:[0-9a-fA-F]{3}){1,2}/);
7868
- if (hexMatch) return hexMatch[0];
7869
- const rgbMatch = bgImage.match(/rgba?\(.*?\)/);
7870
- if (rgbMatch) return rgbMatch[0];
7871
- return null;
7872
- }
7873
-
7874
- function mapDashType(style) {
7875
- if (style === 'dashed') return 'dash';
7876
- if (style === 'dotted') return 'dot';
7877
- return 'solid';
7878
- }
7879
-
7880
- /**
7881
- * Analyzes computed border styles and determines the rendering strategy.
7882
- */
7883
- function getBorderInfo(style, scale) {
7884
- const top = {
7885
- width: parseFloat(style.borderTopWidth) || 0,
7886
- style: style.borderTopStyle,
7887
- color: parseColor(style.borderTopColor).hex,
7888
- };
7889
- const right = {
7890
- width: parseFloat(style.borderRightWidth) || 0,
7891
- style: style.borderRightStyle,
7892
- color: parseColor(style.borderRightColor).hex,
7893
- };
7894
- const bottom = {
7895
- width: parseFloat(style.borderBottomWidth) || 0,
7896
- style: style.borderBottomStyle,
7897
- color: parseColor(style.borderBottomColor).hex,
7898
- };
7899
- const left = {
7900
- width: parseFloat(style.borderLeftWidth) || 0,
7901
- style: style.borderLeftStyle,
7902
- color: parseColor(style.borderLeftColor).hex,
7903
- };
7904
-
7905
- const hasAnyBorder = top.width > 0 || right.width > 0 || bottom.width > 0 || left.width > 0;
7906
- if (!hasAnyBorder) return { type: 'none' };
7907
-
7908
- // Check if all sides are uniform
7909
- const isUniform =
7910
- top.width === right.width &&
7911
- top.width === bottom.width &&
7912
- top.width === left.width &&
7913
- top.style === right.style &&
7914
- top.style === bottom.style &&
7915
- top.style === left.style &&
7916
- top.color === right.color &&
7917
- top.color === bottom.color &&
7918
- top.color === left.color;
7919
-
7920
- if (isUniform) {
7921
- return {
7922
- type: 'uniform',
7923
- options: {
7924
- width: top.width * 0.75 * scale,
7925
- color: top.color,
7926
- transparency: (1 - parseColor(style.borderTopColor).opacity) * 100,
7927
- dashType: mapDashType(top.style),
7928
- },
7929
- };
7930
- } else {
7931
- return {
7932
- type: 'composite',
7933
- sides: { top, right, bottom, left },
7934
- };
7935
- }
7936
- }
7937
-
7938
- /**
7939
- * Generates an SVG image for composite borders that respects border-radius.
7940
- */
7941
- function generateCompositeBorderSVG(w, h, radius, sides) {
7942
- radius = radius / 2; // Adjust for SVG rendering
7943
- const clipId = 'clip_' + Math.random().toString(36).substr(2, 9);
7944
- let borderRects = '';
7945
-
7946
- if (sides.top.width > 0 && sides.top.color) {
7947
- borderRects += `<rect x="0" y="0" width="${w}" height="${sides.top.width}" fill="#${sides.top.color}" />`;
7948
- }
7949
- if (sides.right.width > 0 && sides.right.color) {
7950
- borderRects += `<rect x="${w - sides.right.width}" y="0" width="${sides.right.width}" height="${h}" fill="#${sides.right.color}" />`;
7951
- }
7952
- if (sides.bottom.width > 0 && sides.bottom.color) {
7953
- borderRects += `<rect x="0" y="${h - sides.bottom.width}" width="${w}" height="${sides.bottom.width}" fill="#${sides.bottom.color}" />`;
7954
- }
7955
- if (sides.left.width > 0 && sides.left.color) {
7956
- borderRects += `<rect x="0" y="0" width="${sides.left.width}" height="${h}" fill="#${sides.left.color}" />`;
7957
- }
7958
-
7959
- const svg = `
7960
- <svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
7961
- <defs>
7962
- <clipPath id="${clipId}">
7963
- <rect x="0" y="0" width="${w}" height="${h}" rx="${radius}" ry="${radius}" />
7964
- </clipPath>
7965
- </defs>
7966
- <g clip-path="url(#${clipId})">
7967
- ${borderRects}
7968
- </g>
7969
- </svg>`;
7970
-
7971
- return 'data:image/svg+xml;base64,' + btoa(svg);
7972
- }
7973
-
7974
- /**
7975
- * Generates an SVG data URL for a solid shape with non-uniform corner radii.
7976
- */
7977
- function generateCustomShapeSVG(w, h, color, opacity, radii) {
7978
- let { tl, tr, br, bl } = radii;
7979
-
7980
- // Clamp radii using CSS spec logic (avoid overlap)
7981
- const factor = Math.min(
7982
- (w / (tl + tr)) || Infinity,
7983
- (h / (tr + br)) || Infinity,
7984
- (w / (br + bl)) || Infinity,
7985
- (h / (bl + tl)) || Infinity
7986
- );
7987
-
7988
- if (factor < 1) {
7989
- tl *= factor; tr *= factor; br *= factor; bl *= factor;
7990
- }
7991
-
7992
- const path = `
7993
- M ${tl} 0
7994
- L ${w - tr} 0
7995
- A ${tr} ${tr} 0 0 1 ${w} ${tr}
7996
- L ${w} ${h - br}
7997
- A ${br} ${br} 0 0 1 ${w - br} ${h}
7998
- L ${bl} ${h}
7999
- A ${bl} ${bl} 0 0 1 0 ${h - bl}
8000
- L 0 ${tl}
8001
- A ${tl} ${tl} 0 0 1 ${tl} 0
8002
- Z
8003
- `;
8004
-
8005
- const svg = `
8006
- <svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
8007
- <path d="${path}" fill="#${color}" fill-opacity="${opacity}" />
8008
- </svg>`;
8009
-
8010
- return 'data:image/svg+xml;base64,' + btoa(svg);
8011
- }
8012
-
8013
- function parseColor(str) {
8014
- if (!str || str === 'transparent' || str.startsWith('rgba(0, 0, 0, 0)')) {
8015
- return { hex: null, opacity: 0 };
8016
- }
8017
- if (str.startsWith('#')) {
8018
- let hex = str.slice(1);
8019
- if (hex.length === 3)
8020
- hex = hex.split('').map((c) => c + c).join('');
8021
- return { hex: hex.toUpperCase(), opacity: 1 };
8022
- }
8023
- const match = str.match(/[\d.]+/g);
8024
- if (match && match.length >= 3) {
8025
- const r = parseInt(match[0]);
8026
- const g = parseInt(match[1]);
8027
- const b = parseInt(match[2]);
8028
- const a = match.length > 3 ? parseFloat(match[3]) : 1;
8029
- const hex = ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
8030
- return { hex, opacity: a };
8031
- }
8032
- return { hex: null, opacity: 0 };
8033
- }
8034
-
8035
- function getPadding(style, scale) {
8036
- const pxToInch = 1 / 96;
8037
- return [
8038
- (parseFloat(style.paddingTop) || 0) * pxToInch * scale,
8039
- (parseFloat(style.paddingRight) || 0) * pxToInch * scale,
8040
- (parseFloat(style.paddingBottom) || 0) * pxToInch * scale,
8041
- (parseFloat(style.paddingLeft) || 0) * pxToInch * scale,
8042
- ];
8043
- }
8044
-
8045
- function getSoftEdges(filterStr, scale) {
8046
- if (!filterStr || filterStr === 'none') return null;
8047
- const match = filterStr.match(/blur\(([\d.]+)px\)/);
8048
- if (match) return parseFloat(match[1]) * 0.75 * scale;
8049
- return null;
8050
- }
8051
-
8052
- function getTextStyle(style, scale) {
8053
- let colorObj = parseColor(style.color);
8054
-
8055
- const bgClip = style.webkitBackgroundClip || style.backgroundClip;
8056
- if (colorObj.opacity === 0 && bgClip === 'text') {
8057
- const fallback = getGradientFallbackColor(style.backgroundImage);
8058
- if (fallback) colorObj = parseColor(fallback);
8059
- }
8060
-
8061
- return {
8062
- color: colorObj.hex || '000000',
8063
- fontFace: style.fontFamily.split(',')[0].replace(/['"]/g, ''),
8064
- fontSize: parseFloat(style.fontSize) * 0.75 * scale,
8065
- bold: parseInt(style.fontWeight) >= 600,
8066
- italic: style.fontStyle === 'italic',
8067
- underline: style.textDecoration.includes('underline'),
8068
- };
8069
- }
8070
-
8071
- /**
8072
- * Determines if a given DOM node is primarily a text container.
8073
- */
8074
- function isTextContainer(node) {
8075
- const hasText = node.textContent.trim().length > 0;
8076
- if (!hasText) return false;
8077
-
8078
- const children = Array.from(node.children);
8079
- if (children.length === 0) return true;
8080
-
8081
- // Check if children are purely inline text formatting or visual shapes
8082
- const isSafeInline = (el) => {
8083
- const style = window.getComputedStyle(el);
8084
- const display = style.display;
8085
-
8086
- // If it's a standard inline element
8087
- const isInlineTag = ['SPAN', 'B', 'STRONG', 'EM', 'I', 'A', 'SMALL'].includes(el.tagName);
8088
- const isInlineDisplay = display.includes('inline');
8089
-
8090
- if (!isInlineTag && !isInlineDisplay) return false;
8091
-
8092
- // Check if element is a shape (visual object without text)
8093
- // If an element is empty but has a visible background/border, it's a shape (like a dot).
8094
- // We must return false so the parent isn't treated as a text-only container.
8095
- const hasContent = el.textContent.trim().length > 0;
8096
- const bgColor = parseColor(style.backgroundColor);
8097
- const hasVisibleBg = bgColor.hex && bgColor.opacity > 0;
8098
- const hasBorder = parseFloat(style.borderWidth) > 0 && parseColor(style.borderColor).opacity > 0;
8099
-
8100
- if (!hasContent && (hasVisibleBg || hasBorder)) {
8101
- return false;
8102
- }
8103
-
8104
- return true;
8105
- };
8106
-
8107
- return children.every(isSafeInline);
8108
- }
8109
-
8110
- function getRotation(transformStr) {
8111
- if (!transformStr || transformStr === 'none') return 0;
8112
- const values = transformStr.split('(')[1].split(')')[0].split(',');
8113
- if (values.length < 4) return 0;
8114
- const a = parseFloat(values[0]);
8115
- const b = parseFloat(values[1]);
8116
- return Math.round(Math.atan2(b, a) * (180 / Math.PI));
8117
- }
8118
-
8119
- function svgToPng(node) {
8120
- return new Promise((resolve) => {
8121
- const clone = node.cloneNode(true);
8122
- const rect = node.getBoundingClientRect();
8123
- const width = rect.width || 300;
8124
- const height = rect.height || 150;
8125
-
8126
- function inlineStyles(source, target) {
8127
- const computed = window.getComputedStyle(source);
8128
- const properties = [
8129
- 'fill', 'stroke', 'stroke-width', 'stroke-linecap',
8130
- 'stroke-linejoin', 'opacity', 'font-family', 'font-size', 'font-weight',
8131
- ];
8132
-
8133
- if (computed.fill === 'none') target.setAttribute('fill', 'none');
8134
- else if (computed.fill) target.style.fill = computed.fill;
8135
-
8136
- if (computed.stroke === 'none') target.setAttribute('stroke', 'none');
8137
- else if (computed.stroke) target.style.stroke = computed.stroke;
8138
-
8139
- properties.forEach((prop) => {
8140
- if (prop !== 'fill' && prop !== 'stroke') {
8141
- const val = computed[prop];
8142
- if (val && val !== 'auto') target.style[prop] = val;
8143
- }
8144
- });
8145
-
8146
- for (let i = 0; i < source.children.length; i++) {
8147
- if (target.children[i]) inlineStyles(source.children[i], target.children[i]);
8148
- }
8149
- }
8150
-
8151
- inlineStyles(node, clone);
8152
- clone.setAttribute('width', width);
8153
- clone.setAttribute('height', height);
8154
- clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
8155
-
8156
- const xml = new XMLSerializer().serializeToString(clone);
8157
- const svgUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(xml)}`;
8158
- const img = new Image();
8159
- img.crossOrigin = 'Anonymous';
8160
- img.onload = () => {
8161
- const canvas = document.createElement('canvas');
8162
- const scale = 3;
8163
- canvas.width = width * scale;
8164
- canvas.height = height * scale;
8165
- const ctx = canvas.getContext('2d');
8166
- ctx.scale(scale, scale);
8167
- ctx.drawImage(img, 0, 0, width, height);
8168
- resolve(canvas.toDataURL('image/png'));
8169
- };
8170
- img.onerror = () => resolve(null);
8171
- img.src = svgUrl;
8172
- });
8173
- }
8174
-
8175
- function getVisibleShadow(shadowStr, scale) {
8176
- if (!shadowStr || shadowStr === 'none') return null;
8177
- const shadows = shadowStr.split(/,(?![^()]*\))/);
8178
- for (let s of shadows) {
8179
- s = s.trim();
8180
- if (s.startsWith('rgba(0, 0, 0, 0)')) continue;
8181
- const match = s.match(
8182
- /(rgba?\([^)]+\)|#[0-9a-fA-F]+)\s+(-?[\d.]+)px\s+(-?[\d.]+)px\s+([\d.]+)px/
8183
- );
8184
- if (match) {
8185
- const colorStr = match[1];
8186
- const x = parseFloat(match[2]);
8187
- const y = parseFloat(match[3]);
8188
- const blur = parseFloat(match[4]);
8189
- const distance = Math.sqrt(x * x + y * y);
8190
- let angle = Math.atan2(y, x) * (180 / Math.PI);
8191
- if (angle < 0) angle += 360;
8192
- const colorObj = parseColor(colorStr);
8193
- return {
8194
- type: 'outer',
8195
- angle: angle,
8196
- blur: blur * 0.75 * scale,
8197
- offset: distance * 0.75 * scale,
8198
- color: colorObj.hex || '000000',
8199
- opacity: colorObj.opacity,
8200
- };
8201
- }
8202
- }
8203
- return null;
8204
- }
8205
-
8206
- function generateGradientSVG(w, h, bgString, radius, border) {
8207
- try {
8208
- const match = bgString.match(/linear-gradient\((.*)\)/);
8209
- if (!match) return null;
8210
- const content = match[1];
8211
- const parts = content.split(/,(?![^()]*\))/).map((p) => p.trim());
8212
-
8213
- let x1 = '0%', y1 = '0%', x2 = '0%', y2 = '100%';
8214
- let stopsStartIdx = 0;
8215
- if (parts[0].includes('to right')) {
8216
- x1 = '0%'; x2 = '100%'; y2 = '0%'; stopsStartIdx = 1;
8217
- } else if (parts[0].includes('to left')) {
8218
- x1 = '100%'; x2 = '0%'; y2 = '0%'; stopsStartIdx = 1;
8219
- } else if (parts[0].includes('to top')) {
8220
- y1 = '100%'; y2 = '0%'; stopsStartIdx = 1;
8221
- } else if (parts[0].includes('to bottom')) {
8222
- y1 = '0%'; y2 = '100%'; stopsStartIdx = 1;
8223
- }
8224
-
8225
- let stopsXML = '';
8226
- const stopParts = parts.slice(stopsStartIdx);
8227
- stopParts.forEach((part, idx) => {
8228
- let color = part;
8229
- let offset = Math.round((idx / (stopParts.length - 1)) * 100) + '%';
8230
- const posMatch = part.match(/(.*?)\s+(\d+(\.\d+)?%?)$/);
8231
- if (posMatch) {
8232
- color = posMatch[1];
8233
- offset = posMatch[2];
8234
- }
8235
- let opacity = 1;
8236
- if (color.includes('rgba')) {
8237
- const rgba = color.match(/[\d.]+/g);
8238
- if (rgba && rgba.length > 3) {
8239
- opacity = rgba[3];
8240
- color = `rgb(${rgba[0]},${rgba[1]},${rgba[2]})`;
8241
- }
8242
- }
8243
- stopsXML += `<stop offset="${offset}" stop-color="${color}" stop-opacity="${opacity}"/>`;
8244
- });
8245
-
8246
- let strokeAttr = '';
8247
- if (border) {
8248
- strokeAttr = `stroke="#${border.color}" stroke-width="${border.width}"`;
8249
- }
8250
-
8251
- const svg = `
8252
- <svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
8253
- <defs><linearGradient id="grad" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}">${stopsXML}</linearGradient></defs>
8254
- <rect x="0" y="0" width="${w}" height="${h}" rx="${radius}" ry="${radius}" fill="url(#grad)" ${strokeAttr} />
8255
- </svg>`;
8256
- return 'data:image/svg+xml;base64,' + btoa(svg);
8257
- } catch {
8258
- return null;
8259
- }
8260
- }
8261
-
8262
- function generateBlurredSVG(w, h, color, radius, blurPx) {
8263
- const padding = blurPx * 3;
8264
- const fullW = w + padding * 2;
8265
- const fullH = h + padding * 2;
8266
- const x = padding;
8267
- const y = padding;
8268
- let shapeTag = '';
8269
- const isCircle = radius >= Math.min(w, h) / 2 - 1 && Math.abs(w - h) < 2;
8270
-
8271
- if (isCircle) {
8272
- const cx = x + w / 2;
8273
- const cy = y + h / 2;
8274
- const rx = w / 2;
8275
- const ry = h / 2;
8276
- shapeTag = `<ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}" fill="#${color}" filter="url(#f1)" />`;
8277
- } else {
8278
- shapeTag = `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${radius}" ry="${radius}" fill="#${color}" filter="url(#f1)" />`;
8279
- }
8280
-
8281
- const svg = `
8282
- <svg xmlns="http://www.w3.org/2000/svg" width="${fullW}" height="${fullH}" viewBox="0 0 ${fullW} ${fullH}">
8283
- <defs>
8284
- <filter id="f1" x="-50%" y="-50%" width="200%" height="200%">
8285
- <feGaussianBlur in="SourceGraphic" stdDeviation="${blurPx}" />
8286
- </filter>
8287
- </defs>
8288
- ${shapeTag}
8289
- </svg>`;
8290
-
8291
- return {
8292
- data: 'data:image/svg+xml;base64,' + btoa(svg),
8293
- padding: padding,
8294
- };
7847
+ // src/utils.js
7848
+
7849
+ /**
7850
+ * Checks if any parent element has overflow: hidden which would clip this element
7851
+ * @param {HTMLElement} node - The DOM node to check
7852
+ * @returns {boolean} - True if a parent has overflow-hidden or overflow-clip
7853
+ */
7854
+ function isClippedByParent(node) {
7855
+ let parent = node.parentElement;
7856
+ while (parent && parent !== document.body) {
7857
+ const style = window.getComputedStyle(parent);
7858
+ const overflow = style.overflow;
7859
+ if (overflow === 'hidden' || overflow === 'clip') {
7860
+ return true;
7861
+ }
7862
+ parent = parent.parentElement;
7863
+ }
7864
+ return false;
8295
7865
  }
8296
7866
 
8297
- // src/image-processor.js
8298
-
8299
- async function getProcessedImage(src, targetW, targetH, radius) {
8300
- return new Promise((resolve) => {
8301
- const img = new Image();
8302
- img.crossOrigin = 'Anonymous'; // Critical for canvas manipulation
8303
-
8304
- img.onload = () => {
8305
- const canvas = document.createElement('canvas');
8306
- // Double resolution for better quality
8307
- const scale = 2;
8308
- canvas.width = targetW * scale;
8309
- canvas.height = targetH * scale;
8310
- const ctx = canvas.getContext('2d');
8311
- ctx.scale(scale, scale);
8312
-
8313
- // Normalize radius input to an object { tl, tr, br, bl }
8314
- let r = { tl: 0, tr: 0, br: 0, bl: 0 };
8315
- if (typeof radius === 'number') {
8316
- r = { tl: radius, tr: radius, br: radius, bl: radius };
8317
- } else if (typeof radius === 'object' && radius !== null) {
8318
- r = { ...r, ...radius }; // Merge with defaults
8319
- }
8320
-
8321
- // 1. Draw the Mask (Custom Shape with specific corners)
8322
- ctx.beginPath();
8323
-
8324
- // Border Radius Clamping Logic (CSS Spec)
8325
- // Prevents corners from overlapping if radii are too large for the container
8326
- const factor = Math.min(
8327
- (targetW / (r.tl + r.tr)) || Infinity,
8328
- (targetH / (r.tr + r.br)) || Infinity,
8329
- (targetW / (r.br + r.bl)) || Infinity,
8330
- (targetH / (r.bl + r.tl)) || Infinity
8331
- );
8332
-
8333
- if (factor < 1) {
8334
- r.tl *= factor; r.tr *= factor; r.br *= factor; r.bl *= factor;
8335
- }
8336
-
8337
- // Draw path: Top-Left -> Top-Right -> Bottom-Right -> Bottom-Left
8338
- ctx.moveTo(r.tl, 0);
8339
- ctx.lineTo(targetW - r.tr, 0);
8340
- ctx.arcTo(targetW, 0, targetW, r.tr, r.tr);
8341
- ctx.lineTo(targetW, targetH - r.br);
8342
- ctx.arcTo(targetW, targetH, targetW - r.br, targetH, r.br);
8343
- ctx.lineTo(r.bl, targetH);
8344
- ctx.arcTo(0, targetH, 0, targetH - r.bl, r.bl);
8345
- ctx.lineTo(0, r.tl);
8346
- ctx.arcTo(0, 0, r.tl, 0, r.tl);
8347
-
8348
- ctx.closePath();
8349
- ctx.fillStyle = '#000';
8350
- ctx.fill();
8351
-
8352
- // 2. Composite Source-In (Crops the next image draw to the mask)
8353
- ctx.globalCompositeOperation = 'source-in';
8354
-
8355
- // 3. Draw Image (Object Cover Logic)
8356
- const wRatio = targetW / img.width;
8357
- const hRatio = targetH / img.height;
8358
- const maxRatio = Math.max(wRatio, hRatio);
8359
- const renderW = img.width * maxRatio;
8360
- const renderH = img.height * maxRatio;
8361
- const renderX = (targetW - renderW) / 2;
8362
- const renderY = (targetH - renderH) / 2;
8363
-
8364
- ctx.drawImage(img, renderX, renderY, renderW, renderH);
8365
-
8366
- resolve(canvas.toDataURL('image/png'));
8367
- };
8368
-
8369
- img.onerror = () => resolve(null);
8370
- img.src = src;
8371
- });
7867
+ // Helper to save gradient text
7868
+ function getGradientFallbackColor(bgImage) {
7869
+ if (!bgImage) return null;
7870
+ const hexMatch = bgImage.match(/#(?:[0-9a-fA-F]{3}){1,2}/);
7871
+ if (hexMatch) return hexMatch[0];
7872
+ const rgbMatch = bgImage.match(/rgba?\(.*?\)/);
7873
+ if (rgbMatch) return rgbMatch[0];
7874
+ return null;
8372
7875
  }
8373
7876
 
8374
- // src/index.js
8375
-
8376
- // Normalize import
8377
- const PptxGenJS = PptxGenJSImport__namespace?.default ?? PptxGenJSImport__namespace;
8378
-
8379
- const PPI = 96;
8380
- const PX_TO_INCH = 1 / PPI;
8381
-
8382
- /**
8383
- * Main export function. Accepts single element or an array.
8384
- * @param {HTMLElement | string | Array<HTMLElement | string>} target - The root element(s) to convert.
8385
- * @param {Object} options - { fileName: string }
8386
- */
8387
- async function exportToPptx(target, options = {}) {
8388
- const resolvePptxConstructor = (pkg) => {
8389
- if (!pkg) return null;
8390
- if (typeof pkg === 'function') return pkg;
8391
- if (pkg && typeof pkg.default === 'function') return pkg.default;
8392
- if (pkg && typeof pkg.PptxGenJS === 'function') return pkg.PptxGenJS;
8393
- if (pkg && pkg.PptxGenJS && typeof pkg.PptxGenJS.default === 'function')
8394
- return pkg.PptxGenJS.default;
8395
- return null;
8396
- };
8397
-
8398
- const PptxConstructor = resolvePptxConstructor(PptxGenJS);
8399
- if (!PptxConstructor) throw new Error('PptxGenJS constructor not found.');
8400
- const pptx = new PptxConstructor();
8401
- pptx.layout = 'LAYOUT_16x9';
8402
-
8403
- const elements = Array.isArray(target) ? target : [target];
8404
-
8405
- for (const el of elements) {
8406
- const root = typeof el === 'string' ? document.querySelector(el) : el;
8407
- if (!root) {
8408
- console.warn('Element not found, skipping slide:', el);
8409
- continue;
8410
- }
8411
- const slide = pptx.addSlide();
8412
- await processSlide(root, slide, pptx);
8413
- }
8414
-
8415
- const fileName = options.fileName || 'export.pptx';
8416
- pptx.writeFile({ fileName });
8417
- }
8418
-
8419
- /**
8420
- * Worker function to process a single DOM element into a single PPTX slide.
8421
- * @param {HTMLElement} root - The root element for this slide.
8422
- * @param {PptxGenJS.Slide} slide - The PPTX slide object to add content to.
8423
- * @param {PptxGenJS} pptx - The main PPTX instance.
8424
- */
8425
- async function processSlide(root, slide, pptx) {
8426
- const rootRect = root.getBoundingClientRect();
8427
- const PPTX_WIDTH_IN = 10;
8428
- const PPTX_HEIGHT_IN = 5.625;
8429
-
8430
- const contentWidthIn = rootRect.width * PX_TO_INCH;
8431
- const contentHeightIn = rootRect.height * PX_TO_INCH;
8432
- const scale = Math.min(PPTX_WIDTH_IN / contentWidthIn, PPTX_HEIGHT_IN / contentHeightIn);
8433
-
8434
- const layoutConfig = {
8435
- rootX: rootRect.x,
8436
- rootY: rootRect.y,
8437
- scale: scale,
8438
- offX: (PPTX_WIDTH_IN - contentWidthIn * scale) / 2,
8439
- offY: (PPTX_HEIGHT_IN - contentHeightIn * scale) / 2,
8440
- };
8441
-
8442
- const renderQueue = [];
8443
- let domOrderCounter = 0;
8444
-
8445
- async function collect(node) {
8446
- const order = domOrderCounter++;
8447
- const result = await createRenderItem(node, { ...layoutConfig, root }, order, pptx);
8448
- if (result) {
8449
- if (result.items) renderQueue.push(...result.items);
8450
- if (result.stopRecursion) return;
8451
- }
8452
- for (const child of node.children) await collect(child);
8453
- }
8454
-
8455
- await collect(root);
8456
-
8457
- renderQueue.sort((a, b) => {
8458
- if (a.zIndex !== b.zIndex) return a.zIndex - b.zIndex;
8459
- return a.domOrder - b.domOrder;
8460
- });
8461
-
8462
- for (const item of renderQueue) {
8463
- if (item.type === 'shape') slide.addShape(item.shapeType, item.options);
8464
- if (item.type === 'image') slide.addImage(item.options);
8465
- if (item.type === 'text') slide.addText(item.textParts, item.options);
8466
- }
8467
- }
8468
-
8469
- async function elementToCanvasImage(node, widthPx, heightPx, root) {
8470
- return new Promise((resolve) => {
8471
- const width = Math.ceil(widthPx);
8472
- const height = Math.ceil(heightPx);
8473
-
8474
- if (width <= 0 || height <= 0) {
8475
- resolve(null);
8476
- return;
8477
- }
8478
-
8479
- const style = window.getComputedStyle(node);
8480
-
8481
- html2canvas(root, {
8482
- width: root.scrollWidth,
8483
- height: root.scrollHeight,
8484
- useCORS: true,
8485
- allowTaint: true,
8486
- backgroundColor: null,
8487
- })
8488
- .then((canvas) => {
8489
- const rootCanvas = canvas;
8490
- const nodeRect = node.getBoundingClientRect();
8491
- const rootRect = root.getBoundingClientRect();
8492
- const sourceX = nodeRect.left - rootRect.left;
8493
- const sourceY = nodeRect.top - rootRect.top;
8494
-
8495
- const destCanvas = document.createElement('canvas');
8496
- destCanvas.width = width;
8497
- destCanvas.height = height;
8498
- const ctx = destCanvas.getContext('2d');
8499
-
8500
- ctx.drawImage(rootCanvas, sourceX, sourceY, width, height, 0, 0, width, height);
8501
-
8502
- // Parse radii
8503
- let tl = parseFloat(style.borderTopLeftRadius) || 0;
8504
- let tr = parseFloat(style.borderTopRightRadius) || 0;
8505
- let br = parseFloat(style.borderBottomRightRadius) || 0;
8506
- let bl = parseFloat(style.borderBottomLeftRadius) || 0;
8507
-
8508
- const f = Math.min(
8509
- width / (tl + tr) || Infinity,
8510
- height / (tr + br) || Infinity,
8511
- width / (br + bl) || Infinity,
8512
- height / (bl + tl) || Infinity
8513
- );
8514
-
8515
- if (f < 1) {
8516
- tl *= f;
8517
- tr *= f;
8518
- br *= f;
8519
- bl *= f;
8520
- }
8521
-
8522
- ctx.globalCompositeOperation = 'destination-in';
8523
- ctx.beginPath();
8524
- ctx.moveTo(tl, 0);
8525
- ctx.lineTo(width - tr, 0);
8526
- ctx.arcTo(width, 0, width, tr, tr);
8527
- ctx.lineTo(width, height - br);
8528
- ctx.arcTo(width, height, width - br, height, br);
8529
- ctx.lineTo(bl, height);
8530
- ctx.arcTo(0, height, 0, height - bl, bl);
8531
- ctx.lineTo(0, tl);
8532
- ctx.arcTo(0, 0, tl, 0, tl);
8533
- ctx.closePath();
8534
- ctx.fill();
8535
-
8536
- resolve(destCanvas.toDataURL('image/png'));
8537
- })
8538
- .catch(() => resolve(null));
8539
- });
8540
- }
8541
-
8542
- async function createRenderItem(node, config, domOrder, pptx) {
8543
- if (node.nodeType !== 1) return null;
8544
- const style = window.getComputedStyle(node);
8545
- if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')
8546
- return null;
8547
-
8548
- const rect = node.getBoundingClientRect();
8549
- if (rect.width < 0.5 || rect.height < 0.5) return null;
8550
-
8551
- const zIndex = style.zIndex !== 'auto' ? parseInt(style.zIndex) : 0;
8552
- const rotation = getRotation(style.transform);
8553
- const elementOpacity = parseFloat(style.opacity);
8554
-
8555
- const widthPx = node.offsetWidth || rect.width;
8556
- const heightPx = node.offsetHeight || rect.height;
8557
- const unrotatedW = widthPx * PX_TO_INCH * config.scale;
8558
- const unrotatedH = heightPx * PX_TO_INCH * config.scale;
8559
- const centerX = rect.left + rect.width / 2;
8560
- const centerY = rect.top + rect.height / 2;
8561
-
8562
- let x = config.offX + (centerX - config.rootX) * PX_TO_INCH * config.scale - unrotatedW / 2;
8563
- let y = config.offY + (centerY - config.rootY) * PX_TO_INCH * config.scale - unrotatedH / 2;
8564
- let w = unrotatedW;
8565
- let h = unrotatedH;
8566
-
8567
- const items = [];
8568
-
8569
- if (node.nodeName.toUpperCase() === 'SVG') {
8570
- const pngData = await svgToPng(node);
8571
- if (pngData)
8572
- items.push({
8573
- type: 'image',
8574
- zIndex,
8575
- domOrder,
8576
- options: { data: pngData, x, y, w, h, rotate: rotation },
8577
- });
8578
- return { items, stopRecursion: true };
8579
- }
8580
-
8581
- // --- UPDATED IMG BLOCK START ---
8582
- if (node.tagName === 'IMG') {
8583
- // Extract individual corner radii
8584
- let radii = {
8585
- tl: parseFloat(style.borderTopLeftRadius) || 0,
8586
- tr: parseFloat(style.borderTopRightRadius) || 0,
8587
- br: parseFloat(style.borderBottomRightRadius) || 0,
8588
- bl: parseFloat(style.borderBottomLeftRadius) || 0,
8589
- };
8590
-
8591
- const hasAnyRadius = radii.tl > 0 || radii.tr > 0 || radii.br > 0 || radii.bl > 0;
8592
-
8593
- // Fallback: Check parent if image has no specific radius but parent clips it
8594
- if (!hasAnyRadius) {
8595
- const parent = node.parentElement;
8596
- const parentStyle = window.getComputedStyle(parent);
8597
- if (parentStyle.overflow !== 'visible') {
8598
- const pRadii = {
8599
- tl: parseFloat(parentStyle.borderTopLeftRadius) || 0,
8600
- tr: parseFloat(parentStyle.borderTopRightRadius) || 0,
8601
- br: parseFloat(parentStyle.borderBottomRightRadius) || 0,
8602
- bl: parseFloat(parentStyle.borderBottomLeftRadius) || 0,
8603
- };
8604
- // Simple heuristic: If image takes up full size of parent, inherit radii.
8605
- // For complex grids (like slide-1), this blindly applies parent radius.
8606
- // In a perfect world, we'd calculate intersection, but for now we apply parent radius
8607
- // if the image is close to the parent's size, effectively masking it.
8608
- const pRect = parent.getBoundingClientRect();
8609
- if (Math.abs(pRect.width - rect.width) < 5 && Math.abs(pRect.height - rect.height) < 5) {
8610
- radii = pRadii;
8611
- }
8612
- }
8613
- }
8614
-
8615
- const processed = await getProcessedImage(node.src, widthPx, heightPx, radii);
8616
- if (processed)
8617
- items.push({
8618
- type: 'image',
8619
- zIndex,
8620
- domOrder,
8621
- options: { data: processed, x, y, w, h, rotate: rotation },
8622
- });
8623
- return { items, stopRecursion: true };
8624
- }
8625
- // --- UPDATED IMG BLOCK END ---
8626
-
8627
- // Radii processing for Divs/Shapes
8628
- const borderRadiusValue = parseFloat(style.borderRadius) || 0;
8629
- const borderBottomLeftRadius = parseFloat(style.borderBottomLeftRadius) || 0;
8630
- const borderBottomRightRadius = parseFloat(style.borderBottomRightRadius) || 0;
8631
- const borderTopLeftRadius = parseFloat(style.borderTopLeftRadius) || 0;
8632
- const borderTopRightRadius = parseFloat(style.borderTopRightRadius) || 0;
8633
-
8634
- const hasPartialBorderRadius =
8635
- (borderBottomLeftRadius > 0 && borderBottomLeftRadius !== borderRadiusValue) ||
8636
- (borderBottomRightRadius > 0 && borderBottomRightRadius !== borderRadiusValue) ||
8637
- (borderTopLeftRadius > 0 && borderTopLeftRadius !== borderRadiusValue) ||
8638
- (borderTopRightRadius > 0 && borderTopRightRadius !== borderRadiusValue) ||
8639
- (borderRadiusValue === 0 &&
8640
- (borderBottomLeftRadius ||
8641
- borderBottomRightRadius ||
8642
- borderTopLeftRadius ||
8643
- borderTopRightRadius));
8644
-
8645
- // Allow clipped elements to be rendered via canvas
8646
- if (hasPartialBorderRadius && isClippedByParent(node)) {
8647
- const marginLeft = parseFloat(style.marginLeft) || 0;
8648
- const marginTop = parseFloat(style.marginTop) || 0;
8649
- x += marginLeft * PX_TO_INCH * config.scale;
8650
- y += marginTop * PX_TO_INCH * config.scale;
8651
-
8652
- const canvasImageData = await elementToCanvasImage(node, widthPx, heightPx, config.root);
8653
- if (canvasImageData) {
8654
- items.push({
8655
- type: 'image',
8656
- zIndex,
8657
- domOrder,
8658
- options: { data: canvasImageData, x, y, w, h, rotate: rotation },
8659
- });
8660
- return { items, stopRecursion: true };
8661
- }
8662
- }
8663
-
8664
- const bgColorObj = parseColor(style.backgroundColor);
8665
- const bgClip = style.webkitBackgroundClip || style.backgroundClip;
8666
- const isBgClipText = bgClip === 'text';
8667
- const hasGradient =
8668
- !isBgClipText && style.backgroundImage && style.backgroundImage.includes('linear-gradient');
8669
-
8670
- const borderColorObj = parseColor(style.borderColor);
8671
- const borderWidth = parseFloat(style.borderWidth);
8672
- const hasBorder = borderWidth > 0 && borderColorObj.hex;
8673
-
8674
- const borderInfo = getBorderInfo(style, config.scale);
8675
- const hasUniformBorder = borderInfo.type === 'uniform';
8676
- const hasCompositeBorder = borderInfo.type === 'composite';
8677
-
8678
- const shadowStr = style.boxShadow;
8679
- const hasShadow = shadowStr && shadowStr !== 'none';
8680
- const softEdge = getSoftEdges(style.filter, config.scale);
8681
-
8682
- let isImageWrapper = false;
8683
- const imgChild = Array.from(node.children).find((c) => c.tagName === 'IMG');
8684
- if (imgChild) {
8685
- const childW = imgChild.offsetWidth || imgChild.getBoundingClientRect().width;
8686
- const childH = imgChild.offsetHeight || imgChild.getBoundingClientRect().height;
8687
- if (childW >= widthPx - 2 && childH >= heightPx - 2) isImageWrapper = true;
8688
- }
8689
-
8690
- let textPayload = null;
8691
- const isText = isTextContainer(node);
8692
-
8693
- if (isText) {
8694
- const textParts = [];
8695
- const isList = style.display === 'list-item';
8696
- if (isList) {
8697
- const fontSizePt = parseFloat(style.fontSize) * 0.75 * config.scale;
8698
- const bulletShift = (parseFloat(style.fontSize) || 16) * PX_TO_INCH * config.scale * 1.5;
8699
- x -= bulletShift;
8700
- w += bulletShift;
8701
- textParts.push({
8702
- text: '• ',
8703
- options: {
8704
- color: parseColor(style.color).hex || '000000',
8705
- fontSize: fontSizePt,
8706
- },
8707
- });
8708
- }
8709
-
8710
- node.childNodes.forEach((child, index) => {
8711
- let textVal = child.nodeType === 3 ? child.nodeValue : child.textContent;
8712
- let nodeStyle = child.nodeType === 1 ? window.getComputedStyle(child) : style;
8713
- textVal = textVal.replace(/[\n\r\t]+/g, ' ').replace(/\s{2,}/g, ' ');
8714
- if (index === 0 && !isList) textVal = textVal.trimStart();
8715
- else if (index === 0) textVal = textVal.trimStart();
8716
- if (index === node.childNodes.length - 1) textVal = textVal.trimEnd();
8717
- if (nodeStyle.textTransform === 'uppercase') textVal = textVal.toUpperCase();
8718
- if (nodeStyle.textTransform === 'lowercase') textVal = textVal.toLowerCase();
8719
-
8720
- if (textVal.length > 0) {
8721
- textParts.push({
8722
- text: textVal,
8723
- options: getTextStyle(nodeStyle, config.scale),
8724
- });
8725
- }
8726
- });
8727
-
8728
- if (textParts.length > 0) {
8729
- let align = style.textAlign || 'left';
8730
- if (align === 'start') align = 'left';
8731
- if (align === 'end') align = 'right';
8732
- let valign = 'top';
8733
- if (style.alignItems === 'center') valign = 'middle';
8734
- if (style.justifyContent === 'center' && style.display.includes('flex')) align = 'center';
8735
-
8736
- const pt = parseFloat(style.paddingTop) || 0;
8737
- const pb = parseFloat(style.paddingBottom) || 0;
8738
- if (Math.abs(pt - pb) < 2 && bgColorObj.hex) valign = 'middle';
8739
-
8740
- let padding = getPadding(style, config.scale);
8741
- if (align === 'center' && valign === 'middle') padding = [0, 0, 0, 0];
8742
-
8743
- textPayload = { text: textParts, align, valign, inset: padding };
8744
- }
8745
- }
8746
-
8747
- if (hasGradient || (softEdge && bgColorObj.hex && !isImageWrapper)) {
8748
- let bgData = null;
8749
- let padIn = 0;
8750
- if (softEdge) {
8751
- const svgInfo = generateBlurredSVG(
8752
- widthPx,
8753
- heightPx,
8754
- bgColorObj.hex,
8755
- borderRadiusValue,
8756
- softEdge
8757
- );
8758
- bgData = svgInfo.data;
8759
- padIn = svgInfo.padding * PX_TO_INCH * config.scale;
8760
- } else {
8761
- bgData = generateGradientSVG(
8762
- widthPx,
8763
- heightPx,
8764
- style.backgroundImage,
8765
- borderRadiusValue,
8766
- hasBorder ? { color: borderColorObj.hex, width: borderWidth } : null
8767
- );
8768
- }
8769
-
8770
- if (bgData) {
8771
- items.push({
8772
- type: 'image',
8773
- zIndex,
8774
- domOrder,
8775
- options: {
8776
- data: bgData,
8777
- x: x - padIn,
8778
- y: y - padIn,
8779
- w: w + padIn * 2,
8780
- h: h + padIn * 2,
8781
- rotate: rotation,
8782
- },
8783
- });
8784
- }
8785
-
8786
- if (textPayload) {
8787
- items.push({
8788
- type: 'text',
8789
- zIndex: zIndex + 1,
8790
- domOrder,
8791
- textParts: textPayload.text,
8792
- options: {
8793
- x,
8794
- y,
8795
- w,
8796
- h,
8797
- align: textPayload.align,
8798
- valign: textPayload.valign,
8799
- inset: textPayload.inset,
8800
- rotate: rotation,
8801
- margin: 0,
8802
- wrap: true,
8803
- autoFit: false,
8804
- },
8805
- });
8806
- }
8807
- if (hasCompositeBorder) {
8808
- // Add border shapes after the main background
8809
- const borderItems = createCompositeBorderItems(
8810
- borderInfo.sides,
8811
- x,
8812
- y,
8813
- w,
8814
- h,
8815
- config.scale,
8816
- zIndex,
8817
- domOrder
8818
- );
8819
- items.push(...borderItems);
8820
- }
8821
- } else if (
8822
- (bgColorObj.hex && !isImageWrapper) ||
8823
- hasUniformBorder ||
8824
- hasCompositeBorder ||
8825
- hasShadow ||
8826
- textPayload
8827
- ) {
8828
- const finalAlpha = elementOpacity * bgColorObj.opacity;
8829
- const transparency = (1 - finalAlpha) * 100;
8830
- const useSolidFill = bgColorObj.hex && !isImageWrapper;
8831
-
8832
- if (hasPartialBorderRadius && useSolidFill && !textPayload) {
8833
- const shapeSvg = generateCustomShapeSVG(
8834
- widthPx,
8835
- heightPx,
8836
- bgColorObj.hex,
8837
- bgColorObj.opacity,
8838
- {
8839
- tl: parseFloat(style.borderTopLeftRadius) || 0,
8840
- tr: parseFloat(style.borderTopRightRadius) || 0,
8841
- br: parseFloat(style.borderBottomRightRadius) || 0,
8842
- bl: parseFloat(style.borderBottomLeftRadius) || 0,
8843
- }
8844
- );
8845
-
8846
- items.push({
8847
- type: 'image',
8848
- zIndex,
8849
- domOrder,
8850
- options: {
8851
- data: shapeSvg,
8852
- x,
8853
- y,
8854
- w,
8855
- h,
8856
- rotate: rotation,
8857
- },
8858
- });
8859
- } else {
8860
- const shapeOpts = {
8861
- x,
8862
- y,
8863
- w,
8864
- h,
8865
- rotate: rotation,
8866
- fill: useSolidFill
8867
- ? { color: bgColorObj.hex, transparency: transparency }
8868
- : { type: 'none' },
8869
- line: hasUniformBorder ? borderInfo.options : null,
8870
- };
8871
-
8872
- if (hasShadow) {
8873
- shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
8874
- }
8875
-
8876
- const borderRadius = parseFloat(style.borderRadius) || 0;
8877
- const aspectRatio = Math.max(widthPx, heightPx) / Math.min(widthPx, heightPx);
8878
- const isCircle = aspectRatio < 1.1 && borderRadius >= Math.min(widthPx, heightPx) / 2 - 1;
8879
-
8880
- let shapeType = pptx.ShapeType.rect;
8881
- if (isCircle) shapeType = pptx.ShapeType.ellipse;
8882
- else if (borderRadius > 0) {
8883
- shapeType = pptx.ShapeType.roundRect;
8884
- shapeOpts.rectRadius = Math.min(0.5, borderRadius / Math.min(widthPx, heightPx));
8885
- }
8886
-
8887
- if (textPayload) {
8888
- const textOptions = {
8889
- shape: shapeType,
8890
- ...shapeOpts,
8891
- align: textPayload.align,
8892
- valign: textPayload.valign,
8893
- inset: textPayload.inset,
8894
- margin: 0,
8895
- wrap: true,
8896
- autoFit: false,
8897
- };
8898
- items.push({
8899
- type: 'text',
8900
- zIndex,
8901
- domOrder,
8902
- textParts: textPayload.text,
8903
- options: textOptions,
8904
- });
8905
- } else if (!hasPartialBorderRadius) {
8906
- items.push({
8907
- type: 'shape',
8908
- zIndex,
8909
- domOrder,
8910
- shapeType,
8911
- options: shapeOpts,
8912
- });
8913
- }
8914
- }
8915
-
8916
- if (hasCompositeBorder) {
8917
- const borderSvgData = generateCompositeBorderSVG(
8918
- widthPx,
8919
- heightPx,
8920
- borderRadiusValue,
8921
- borderInfo.sides
8922
- );
8923
- if (borderSvgData) {
8924
- items.push({
8925
- type: 'image',
8926
- zIndex: zIndex + 1,
8927
- domOrder,
8928
- options: { data: borderSvgData, x, y, w, h, rotate: rotation },
8929
- });
8930
- }
8931
- }
8932
- }
8933
-
8934
- return { items, stopRecursion: !!textPayload };
8935
- }
8936
-
8937
- /**
8938
- * Helper function to create individual border shapes
8939
- */
8940
- function createCompositeBorderItems(sides, x, y, w, h, scale, zIndex, domOrder) {
8941
- const items = [];
8942
- const pxToInch = 1 / 96;
8943
-
8944
- // TOP BORDER
8945
- if (sides.top.width > 0) {
8946
- items.push({
8947
- type: 'shape',
8948
- zIndex: zIndex + 1,
8949
- domOrder,
8950
- shapeType: 'rect',
8951
- options: {
8952
- x: x,
8953
- y: y,
8954
- w: w,
8955
- h: sides.top.width * pxToInch * scale,
8956
- fill: { color: sides.top.color },
8957
- },
8958
- });
8959
- }
8960
- // RIGHT BORDER
8961
- if (sides.right.width > 0) {
8962
- items.push({
8963
- type: 'shape',
8964
- zIndex: zIndex + 1,
8965
- domOrder,
8966
- shapeType: 'rect',
8967
- options: {
8968
- x: x + w - sides.right.width * pxToInch * scale,
8969
- y: y,
8970
- w: sides.right.width * pxToInch * scale,
8971
- h: h,
8972
- fill: { color: sides.right.color },
8973
- },
8974
- });
8975
- }
8976
- // BOTTOM BORDER
8977
- if (sides.bottom.width > 0) {
8978
- items.push({
8979
- type: 'shape',
8980
- zIndex: zIndex + 1,
8981
- domOrder,
8982
- shapeType: 'rect',
8983
- options: {
8984
- x: x,
8985
- y: y + h - sides.bottom.width * pxToInch * scale,
8986
- w: w,
8987
- h: sides.bottom.width * pxToInch * scale,
8988
- fill: { color: sides.bottom.color },
8989
- },
8990
- });
8991
- }
8992
- // LEFT BORDER
8993
- if (sides.left.width > 0) {
8994
- items.push({
8995
- type: 'shape',
8996
- zIndex: zIndex + 1,
8997
- domOrder,
8998
- shapeType: 'rect',
8999
- options: {
9000
- x: x,
9001
- y: y,
9002
- w: sides.left.width * pxToInch * scale,
9003
- h: h,
9004
- fill: { color: sides.left.color },
9005
- },
9006
- });
9007
- }
9008
-
9009
- return items;
7877
+ function mapDashType(style) {
7878
+ if (style === 'dashed') return 'dash';
7879
+ if (style === 'dotted') return 'dot';
7880
+ return 'solid';
7881
+ }
7882
+
7883
+ /**
7884
+ * Analyzes computed border styles and determines the rendering strategy.
7885
+ */
7886
+ function getBorderInfo(style, scale) {
7887
+ const top = {
7888
+ width: parseFloat(style.borderTopWidth) || 0,
7889
+ style: style.borderTopStyle,
7890
+ color: parseColor(style.borderTopColor).hex,
7891
+ };
7892
+ const right = {
7893
+ width: parseFloat(style.borderRightWidth) || 0,
7894
+ style: style.borderRightStyle,
7895
+ color: parseColor(style.borderRightColor).hex,
7896
+ };
7897
+ const bottom = {
7898
+ width: parseFloat(style.borderBottomWidth) || 0,
7899
+ style: style.borderBottomStyle,
7900
+ color: parseColor(style.borderBottomColor).hex,
7901
+ };
7902
+ const left = {
7903
+ width: parseFloat(style.borderLeftWidth) || 0,
7904
+ style: style.borderLeftStyle,
7905
+ color: parseColor(style.borderLeftColor).hex,
7906
+ };
7907
+
7908
+ const hasAnyBorder = top.width > 0 || right.width > 0 || bottom.width > 0 || left.width > 0;
7909
+ if (!hasAnyBorder) return { type: 'none' };
7910
+
7911
+ // Check if all sides are uniform
7912
+ const isUniform =
7913
+ top.width === right.width &&
7914
+ top.width === bottom.width &&
7915
+ top.width === left.width &&
7916
+ top.style === right.style &&
7917
+ top.style === bottom.style &&
7918
+ top.style === left.style &&
7919
+ top.color === right.color &&
7920
+ top.color === bottom.color &&
7921
+ top.color === left.color;
7922
+
7923
+ if (isUniform) {
7924
+ return {
7925
+ type: 'uniform',
7926
+ options: {
7927
+ width: top.width * 0.75 * scale,
7928
+ color: top.color,
7929
+ transparency: (1 - parseColor(style.borderTopColor).opacity) * 100,
7930
+ dashType: mapDashType(top.style),
7931
+ },
7932
+ };
7933
+ } else {
7934
+ return {
7935
+ type: 'composite',
7936
+ sides: { top, right, bottom, left },
7937
+ };
7938
+ }
7939
+ }
7940
+
7941
+ /**
7942
+ * Generates an SVG image for composite borders that respects border-radius.
7943
+ */
7944
+ function generateCompositeBorderSVG(w, h, radius, sides) {
7945
+ radius = radius / 2; // Adjust for SVG rendering
7946
+ const clipId = 'clip_' + Math.random().toString(36).substr(2, 9);
7947
+ let borderRects = '';
7948
+
7949
+ if (sides.top.width > 0 && sides.top.color) {
7950
+ borderRects += `<rect x="0" y="0" width="${w}" height="${sides.top.width}" fill="#${sides.top.color}" />`;
7951
+ }
7952
+ if (sides.right.width > 0 && sides.right.color) {
7953
+ borderRects += `<rect x="${w - sides.right.width}" y="0" width="${sides.right.width}" height="${h}" fill="#${sides.right.color}" />`;
7954
+ }
7955
+ if (sides.bottom.width > 0 && sides.bottom.color) {
7956
+ borderRects += `<rect x="0" y="${h - sides.bottom.width}" width="${w}" height="${sides.bottom.width}" fill="#${sides.bottom.color}" />`;
7957
+ }
7958
+ if (sides.left.width > 0 && sides.left.color) {
7959
+ borderRects += `<rect x="0" y="0" width="${sides.left.width}" height="${h}" fill="#${sides.left.color}" />`;
7960
+ }
7961
+
7962
+ const svg = `
7963
+ <svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
7964
+ <defs>
7965
+ <clipPath id="${clipId}">
7966
+ <rect x="0" y="0" width="${w}" height="${h}" rx="${radius}" ry="${radius}" />
7967
+ </clipPath>
7968
+ </defs>
7969
+ <g clip-path="url(#${clipId})">
7970
+ ${borderRects}
7971
+ </g>
7972
+ </svg>`;
7973
+
7974
+ return 'data:image/svg+xml;base64,' + btoa(svg);
7975
+ }
7976
+
7977
+ /**
7978
+ * Generates an SVG data URL for a solid shape with non-uniform corner radii.
7979
+ */
7980
+ function generateCustomShapeSVG(w, h, color, opacity, radii) {
7981
+ let { tl, tr, br, bl } = radii;
7982
+
7983
+ // Clamp radii using CSS spec logic (avoid overlap)
7984
+ const factor = Math.min(
7985
+ w / (tl + tr) || Infinity,
7986
+ h / (tr + br) || Infinity,
7987
+ w / (br + bl) || Infinity,
7988
+ h / (bl + tl) || Infinity
7989
+ );
7990
+
7991
+ if (factor < 1) {
7992
+ tl *= factor;
7993
+ tr *= factor;
7994
+ br *= factor;
7995
+ bl *= factor;
7996
+ }
7997
+
7998
+ const path = `
7999
+ M ${tl} 0
8000
+ L ${w - tr} 0
8001
+ A ${tr} ${tr} 0 0 1 ${w} ${tr}
8002
+ L ${w} ${h - br}
8003
+ A ${br} ${br} 0 0 1 ${w - br} ${h}
8004
+ L ${bl} ${h}
8005
+ A ${bl} ${bl} 0 0 1 0 ${h - bl}
8006
+ L 0 ${tl}
8007
+ A ${tl} ${tl} 0 0 1 ${tl} 0
8008
+ Z
8009
+ `;
8010
+
8011
+ const svg = `
8012
+ <svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
8013
+ <path d="${path}" fill="#${color}" fill-opacity="${opacity}" />
8014
+ </svg>`;
8015
+
8016
+ return 'data:image/svg+xml;base64,' + btoa(svg);
8017
+ }
8018
+
8019
+ function parseColor(str) {
8020
+ if (!str || str === 'transparent' || str.startsWith('rgba(0, 0, 0, 0)')) {
8021
+ return { hex: null, opacity: 0 };
8022
+ }
8023
+ if (str.startsWith('#')) {
8024
+ let hex = str.slice(1);
8025
+ if (hex.length === 3)
8026
+ hex = hex
8027
+ .split('')
8028
+ .map((c) => c + c)
8029
+ .join('');
8030
+ return { hex: hex.toUpperCase(), opacity: 1 };
8031
+ }
8032
+ const match = str.match(/[\d.]+/g);
8033
+ if (match && match.length >= 3) {
8034
+ const r = parseInt(match[0]);
8035
+ const g = parseInt(match[1]);
8036
+ const b = parseInt(match[2]);
8037
+ const a = match.length > 3 ? parseFloat(match[3]) : 1;
8038
+ const hex = ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
8039
+ return { hex, opacity: a };
8040
+ }
8041
+ return { hex: null, opacity: 0 };
8042
+ }
8043
+
8044
+ function getPadding(style, scale) {
8045
+ const pxToInch = 1 / 96;
8046
+ return [
8047
+ (parseFloat(style.paddingTop) || 0) * pxToInch * scale,
8048
+ (parseFloat(style.paddingRight) || 0) * pxToInch * scale,
8049
+ (parseFloat(style.paddingBottom) || 0) * pxToInch * scale,
8050
+ (parseFloat(style.paddingLeft) || 0) * pxToInch * scale,
8051
+ ];
8052
+ }
8053
+
8054
+ function getSoftEdges(filterStr, scale) {
8055
+ if (!filterStr || filterStr === 'none') return null;
8056
+ const match = filterStr.match(/blur\(([\d.]+)px\)/);
8057
+ if (match) return parseFloat(match[1]) * 0.75 * scale;
8058
+ return null;
8059
+ }
8060
+
8061
+ function getTextStyle(style, scale) {
8062
+ let colorObj = parseColor(style.color);
8063
+
8064
+ const bgClip = style.webkitBackgroundClip || style.backgroundClip;
8065
+ if (colorObj.opacity === 0 && bgClip === 'text') {
8066
+ const fallback = getGradientFallbackColor(style.backgroundImage);
8067
+ if (fallback) colorObj = parseColor(fallback);
8068
+ }
8069
+
8070
+ return {
8071
+ color: colorObj.hex || '000000',
8072
+ fontFace: style.fontFamily.split(',')[0].replace(/['"]/g, ''),
8073
+ fontSize: parseFloat(style.fontSize) * 0.75 * scale,
8074
+ bold: parseInt(style.fontWeight) >= 600,
8075
+ italic: style.fontStyle === 'italic',
8076
+ underline: style.textDecoration.includes('underline'),
8077
+ };
8078
+ }
8079
+
8080
+ /**
8081
+ * Determines if a given DOM node is primarily a text container.
8082
+ */
8083
+ function isTextContainer(node) {
8084
+ const hasText = node.textContent.trim().length > 0;
8085
+ if (!hasText) return false;
8086
+
8087
+ const children = Array.from(node.children);
8088
+ if (children.length === 0) return true;
8089
+
8090
+ // Check if children are purely inline text formatting or visual shapes
8091
+ const isSafeInline = (el) => {
8092
+ const style = window.getComputedStyle(el);
8093
+ const display = style.display;
8094
+
8095
+ // If it's a standard inline element
8096
+ const isInlineTag = ['SPAN', 'B', 'STRONG', 'EM', 'I', 'A', 'SMALL'].includes(el.tagName);
8097
+ const isInlineDisplay = display.includes('inline');
8098
+
8099
+ if (!isInlineTag && !isInlineDisplay) return false;
8100
+
8101
+ // Check if element is a shape (visual object without text)
8102
+ // If an element is empty but has a visible background/border, it's a shape (like a dot).
8103
+ // We must return false so the parent isn't treated as a text-only container.
8104
+ const hasContent = el.textContent.trim().length > 0;
8105
+ const bgColor = parseColor(style.backgroundColor);
8106
+ const hasVisibleBg = bgColor.hex && bgColor.opacity > 0;
8107
+ const hasBorder =
8108
+ parseFloat(style.borderWidth) > 0 && parseColor(style.borderColor).opacity > 0;
8109
+
8110
+ if (!hasContent && (hasVisibleBg || hasBorder)) {
8111
+ return false;
8112
+ }
8113
+
8114
+ return true;
8115
+ };
8116
+
8117
+ return children.every(isSafeInline);
8118
+ }
8119
+
8120
+ function getRotation(transformStr) {
8121
+ if (!transformStr || transformStr === 'none') return 0;
8122
+ const values = transformStr.split('(')[1].split(')')[0].split(',');
8123
+ if (values.length < 4) return 0;
8124
+ const a = parseFloat(values[0]);
8125
+ const b = parseFloat(values[1]);
8126
+ return Math.round(Math.atan2(b, a) * (180 / Math.PI));
8127
+ }
8128
+
8129
+ function svgToPng(node) {
8130
+ return new Promise((resolve) => {
8131
+ const clone = node.cloneNode(true);
8132
+ const rect = node.getBoundingClientRect();
8133
+ const width = rect.width || 300;
8134
+ const height = rect.height || 150;
8135
+
8136
+ function inlineStyles(source, target) {
8137
+ const computed = window.getComputedStyle(source);
8138
+ const properties = [
8139
+ 'fill',
8140
+ 'stroke',
8141
+ 'stroke-width',
8142
+ 'stroke-linecap',
8143
+ 'stroke-linejoin',
8144
+ 'opacity',
8145
+ 'font-family',
8146
+ 'font-size',
8147
+ 'font-weight',
8148
+ ];
8149
+
8150
+ if (computed.fill === 'none') target.setAttribute('fill', 'none');
8151
+ else if (computed.fill) target.style.fill = computed.fill;
8152
+
8153
+ if (computed.stroke === 'none') target.setAttribute('stroke', 'none');
8154
+ else if (computed.stroke) target.style.stroke = computed.stroke;
8155
+
8156
+ properties.forEach((prop) => {
8157
+ if (prop !== 'fill' && prop !== 'stroke') {
8158
+ const val = computed[prop];
8159
+ if (val && val !== 'auto') target.style[prop] = val;
8160
+ }
8161
+ });
8162
+
8163
+ for (let i = 0; i < source.children.length; i++) {
8164
+ if (target.children[i]) inlineStyles(source.children[i], target.children[i]);
8165
+ }
8166
+ }
8167
+
8168
+ inlineStyles(node, clone);
8169
+ clone.setAttribute('width', width);
8170
+ clone.setAttribute('height', height);
8171
+ clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
8172
+
8173
+ const xml = new XMLSerializer().serializeToString(clone);
8174
+ const svgUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(xml)}`;
8175
+ const img = new Image();
8176
+ img.crossOrigin = 'Anonymous';
8177
+ img.onload = () => {
8178
+ const canvas = document.createElement('canvas');
8179
+ const scale = 3;
8180
+ canvas.width = width * scale;
8181
+ canvas.height = height * scale;
8182
+ const ctx = canvas.getContext('2d');
8183
+ ctx.scale(scale, scale);
8184
+ ctx.drawImage(img, 0, 0, width, height);
8185
+ resolve(canvas.toDataURL('image/png'));
8186
+ };
8187
+ img.onerror = () => resolve(null);
8188
+ img.src = svgUrl;
8189
+ });
8190
+ }
8191
+
8192
+ function getVisibleShadow(shadowStr, scale) {
8193
+ if (!shadowStr || shadowStr === 'none') return null;
8194
+ const shadows = shadowStr.split(/,(?![^()]*\))/);
8195
+ for (let s of shadows) {
8196
+ s = s.trim();
8197
+ if (s.startsWith('rgba(0, 0, 0, 0)')) continue;
8198
+ const match = s.match(
8199
+ /(rgba?\([^)]+\)|#[0-9a-fA-F]+)\s+(-?[\d.]+)px\s+(-?[\d.]+)px\s+([\d.]+)px/
8200
+ );
8201
+ if (match) {
8202
+ const colorStr = match[1];
8203
+ const x = parseFloat(match[2]);
8204
+ const y = parseFloat(match[3]);
8205
+ const blur = parseFloat(match[4]);
8206
+ const distance = Math.sqrt(x * x + y * y);
8207
+ let angle = Math.atan2(y, x) * (180 / Math.PI);
8208
+ if (angle < 0) angle += 360;
8209
+ const colorObj = parseColor(colorStr);
8210
+ return {
8211
+ type: 'outer',
8212
+ angle: angle,
8213
+ blur: blur * 0.75 * scale,
8214
+ offset: distance * 0.75 * scale,
8215
+ color: colorObj.hex || '000000',
8216
+ opacity: colorObj.opacity,
8217
+ };
8218
+ }
8219
+ }
8220
+ return null;
8221
+ }
8222
+
8223
+ function generateGradientSVG(w, h, bgString, radius, border) {
8224
+ try {
8225
+ const match = bgString.match(/linear-gradient\((.*)\)/);
8226
+ if (!match) return null;
8227
+ const content = match[1];
8228
+ const parts = content.split(/,(?![^()]*\))/).map((p) => p.trim());
8229
+
8230
+ let x1 = '0%',
8231
+ y1 = '0%',
8232
+ x2 = '0%',
8233
+ y2 = '100%';
8234
+ let stopsStartIdx = 0;
8235
+ if (parts[0].includes('to right')) {
8236
+ x1 = '0%';
8237
+ x2 = '100%';
8238
+ y2 = '0%';
8239
+ stopsStartIdx = 1;
8240
+ } else if (parts[0].includes('to left')) {
8241
+ x1 = '100%';
8242
+ x2 = '0%';
8243
+ y2 = '0%';
8244
+ stopsStartIdx = 1;
8245
+ } else if (parts[0].includes('to top')) {
8246
+ y1 = '100%';
8247
+ y2 = '0%';
8248
+ stopsStartIdx = 1;
8249
+ } else if (parts[0].includes('to bottom')) {
8250
+ y1 = '0%';
8251
+ y2 = '100%';
8252
+ stopsStartIdx = 1;
8253
+ }
8254
+
8255
+ let stopsXML = '';
8256
+ const stopParts = parts.slice(stopsStartIdx);
8257
+ stopParts.forEach((part, idx) => {
8258
+ let color = part;
8259
+ let offset = Math.round((idx / (stopParts.length - 1)) * 100) + '%';
8260
+ const posMatch = part.match(/(.*?)\s+(\d+(\.\d+)?%?)$/);
8261
+ if (posMatch) {
8262
+ color = posMatch[1];
8263
+ offset = posMatch[2];
8264
+ }
8265
+ let opacity = 1;
8266
+ if (color.includes('rgba')) {
8267
+ const rgba = color.match(/[\d.]+/g);
8268
+ if (rgba && rgba.length > 3) {
8269
+ opacity = rgba[3];
8270
+ color = `rgb(${rgba[0]},${rgba[1]},${rgba[2]})`;
8271
+ }
8272
+ }
8273
+ stopsXML += `<stop offset="${offset}" stop-color="${color}" stop-opacity="${opacity}"/>`;
8274
+ });
8275
+
8276
+ let strokeAttr = '';
8277
+ if (border) {
8278
+ strokeAttr = `stroke="#${border.color}" stroke-width="${border.width}"`;
8279
+ }
8280
+
8281
+ const svg = `
8282
+ <svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
8283
+ <defs><linearGradient id="grad" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}">${stopsXML}</linearGradient></defs>
8284
+ <rect x="0" y="0" width="${w}" height="${h}" rx="${radius}" ry="${radius}" fill="url(#grad)" ${strokeAttr} />
8285
+ </svg>`;
8286
+ return 'data:image/svg+xml;base64,' + btoa(svg);
8287
+ } catch {
8288
+ return null;
8289
+ }
8290
+ }
8291
+
8292
+ function generateBlurredSVG(w, h, color, radius, blurPx) {
8293
+ const padding = blurPx * 3;
8294
+ const fullW = w + padding * 2;
8295
+ const fullH = h + padding * 2;
8296
+ const x = padding;
8297
+ const y = padding;
8298
+ let shapeTag = '';
8299
+ const isCircle = radius >= Math.min(w, h) / 2 - 1 && Math.abs(w - h) < 2;
8300
+
8301
+ if (isCircle) {
8302
+ const cx = x + w / 2;
8303
+ const cy = y + h / 2;
8304
+ const rx = w / 2;
8305
+ const ry = h / 2;
8306
+ shapeTag = `<ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}" fill="#${color}" filter="url(#f1)" />`;
8307
+ } else {
8308
+ shapeTag = `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${radius}" ry="${radius}" fill="#${color}" filter="url(#f1)" />`;
8309
+ }
8310
+
8311
+ const svg = `
8312
+ <svg xmlns="http://www.w3.org/2000/svg" width="${fullW}" height="${fullH}" viewBox="0 0 ${fullW} ${fullH}">
8313
+ <defs>
8314
+ <filter id="f1" x="-50%" y="-50%" width="200%" height="200%">
8315
+ <feGaussianBlur in="SourceGraphic" stdDeviation="${blurPx}" />
8316
+ </filter>
8317
+ </defs>
8318
+ ${shapeTag}
8319
+ </svg>`;
8320
+
8321
+ return {
8322
+ data: 'data:image/svg+xml;base64,' + btoa(svg),
8323
+ padding: padding,
8324
+ };
8325
+ }
8326
+
8327
+ // src/image-processor.js
8328
+
8329
+ async function getProcessedImage(src, targetW, targetH, radius) {
8330
+ return new Promise((resolve) => {
8331
+ const img = new Image();
8332
+ img.crossOrigin = 'Anonymous'; // Critical for canvas manipulation
8333
+
8334
+ img.onload = () => {
8335
+ const canvas = document.createElement('canvas');
8336
+ // Double resolution for better quality
8337
+ const scale = 2;
8338
+ canvas.width = targetW * scale;
8339
+ canvas.height = targetH * scale;
8340
+ const ctx = canvas.getContext('2d');
8341
+ ctx.scale(scale, scale);
8342
+
8343
+ // Normalize radius input to an object { tl, tr, br, bl }
8344
+ let r = { tl: 0, tr: 0, br: 0, bl: 0 };
8345
+ if (typeof radius === 'number') {
8346
+ r = { tl: radius, tr: radius, br: radius, bl: radius };
8347
+ } else if (typeof radius === 'object' && radius !== null) {
8348
+ r = { ...r, ...radius }; // Merge with defaults
8349
+ }
8350
+
8351
+ // 1. Draw the Mask (Custom Shape with specific corners)
8352
+ ctx.beginPath();
8353
+
8354
+ // Border Radius Clamping Logic (CSS Spec)
8355
+ // Prevents corners from overlapping if radii are too large for the container
8356
+ const factor = Math.min(
8357
+ targetW / (r.tl + r.tr) || Infinity,
8358
+ targetH / (r.tr + r.br) || Infinity,
8359
+ targetW / (r.br + r.bl) || Infinity,
8360
+ targetH / (r.bl + r.tl) || Infinity
8361
+ );
8362
+
8363
+ if (factor < 1) {
8364
+ r.tl *= factor;
8365
+ r.tr *= factor;
8366
+ r.br *= factor;
8367
+ r.bl *= factor;
8368
+ }
8369
+
8370
+ // Draw path: Top-Left -> Top-Right -> Bottom-Right -> Bottom-Left
8371
+ ctx.moveTo(r.tl, 0);
8372
+ ctx.lineTo(targetW - r.tr, 0);
8373
+ ctx.arcTo(targetW, 0, targetW, r.tr, r.tr);
8374
+ ctx.lineTo(targetW, targetH - r.br);
8375
+ ctx.arcTo(targetW, targetH, targetW - r.br, targetH, r.br);
8376
+ ctx.lineTo(r.bl, targetH);
8377
+ ctx.arcTo(0, targetH, 0, targetH - r.bl, r.bl);
8378
+ ctx.lineTo(0, r.tl);
8379
+ ctx.arcTo(0, 0, r.tl, 0, r.tl);
8380
+
8381
+ ctx.closePath();
8382
+ ctx.fillStyle = '#000';
8383
+ ctx.fill();
8384
+
8385
+ // 2. Composite Source-In (Crops the next image draw to the mask)
8386
+ ctx.globalCompositeOperation = 'source-in';
8387
+
8388
+ // 3. Draw Image (Object Cover Logic)
8389
+ const wRatio = targetW / img.width;
8390
+ const hRatio = targetH / img.height;
8391
+ const maxRatio = Math.max(wRatio, hRatio);
8392
+ const renderW = img.width * maxRatio;
8393
+ const renderH = img.height * maxRatio;
8394
+ const renderX = (targetW - renderW) / 2;
8395
+ const renderY = (targetH - renderH) / 2;
8396
+
8397
+ ctx.drawImage(img, renderX, renderY, renderW, renderH);
8398
+
8399
+ resolve(canvas.toDataURL('image/png'));
8400
+ };
8401
+
8402
+ img.onerror = () => resolve(null);
8403
+ img.src = src;
8404
+ });
8405
+ }
8406
+
8407
+ // src/index.js
8408
+
8409
+ // Normalize import
8410
+ const PptxGenJS = PptxGenJSImport__namespace?.default ?? PptxGenJSImport__namespace;
8411
+
8412
+ const PPI = 96;
8413
+ const PX_TO_INCH = 1 / PPI;
8414
+
8415
+ /**
8416
+ * Main export function. Accepts single element or an array.
8417
+ * @param {HTMLElement | string | Array<HTMLElement | string>} target - The root element(s) to convert.
8418
+ * @param {Object} options - { fileName: string }
8419
+ */
8420
+ async function exportToPptx(target, options = {}) {
8421
+ const resolvePptxConstructor = (pkg) => {
8422
+ if (!pkg) return null;
8423
+ if (typeof pkg === 'function') return pkg;
8424
+ if (pkg && typeof pkg.default === 'function') return pkg.default;
8425
+ if (pkg && typeof pkg.PptxGenJS === 'function') return pkg.PptxGenJS;
8426
+ if (pkg && pkg.PptxGenJS && typeof pkg.PptxGenJS.default === 'function')
8427
+ return pkg.PptxGenJS.default;
8428
+ return null;
8429
+ };
8430
+
8431
+ const PptxConstructor = resolvePptxConstructor(PptxGenJS);
8432
+ if (!PptxConstructor) throw new Error('PptxGenJS constructor not found.');
8433
+ const pptx = new PptxConstructor();
8434
+ pptx.layout = 'LAYOUT_16x9';
8435
+
8436
+ const elements = Array.isArray(target) ? target : [target];
8437
+
8438
+ for (const el of elements) {
8439
+ const root = typeof el === 'string' ? document.querySelector(el) : el;
8440
+ if (!root) {
8441
+ console.warn('Element not found, skipping slide:', el);
8442
+ continue;
8443
+ }
8444
+ const slide = pptx.addSlide();
8445
+ await processSlide(root, slide, pptx);
8446
+ }
8447
+
8448
+ const fileName = options.fileName || 'export.pptx';
8449
+ pptx.writeFile({ fileName });
8450
+ }
8451
+
8452
+ /**
8453
+ * Worker function to process a single DOM element into a single PPTX slide.
8454
+ * @param {HTMLElement} root - The root element for this slide.
8455
+ * @param {PptxGenJS.Slide} slide - The PPTX slide object to add content to.
8456
+ * @param {PptxGenJS} pptx - The main PPTX instance.
8457
+ */
8458
+ async function processSlide(root, slide, pptx) {
8459
+ const rootRect = root.getBoundingClientRect();
8460
+ const PPTX_WIDTH_IN = 10;
8461
+ const PPTX_HEIGHT_IN = 5.625;
8462
+
8463
+ const contentWidthIn = rootRect.width * PX_TO_INCH;
8464
+ const contentHeightIn = rootRect.height * PX_TO_INCH;
8465
+ const scale = Math.min(PPTX_WIDTH_IN / contentWidthIn, PPTX_HEIGHT_IN / contentHeightIn);
8466
+
8467
+ const layoutConfig = {
8468
+ rootX: rootRect.x,
8469
+ rootY: rootRect.y,
8470
+ scale: scale,
8471
+ offX: (PPTX_WIDTH_IN - contentWidthIn * scale) / 2,
8472
+ offY: (PPTX_HEIGHT_IN - contentHeightIn * scale) / 2,
8473
+ };
8474
+
8475
+ const renderQueue = [];
8476
+ let domOrderCounter = 0;
8477
+
8478
+ async function collect(node) {
8479
+ const order = domOrderCounter++;
8480
+ const result = await createRenderItem(node, { ...layoutConfig, root }, order, pptx);
8481
+ if (result) {
8482
+ if (result.items) renderQueue.push(...result.items);
8483
+ if (result.stopRecursion) return;
8484
+ }
8485
+ for (const child of node.children) await collect(child);
8486
+ }
8487
+
8488
+ await collect(root);
8489
+
8490
+ renderQueue.sort((a, b) => {
8491
+ if (a.zIndex !== b.zIndex) return a.zIndex - b.zIndex;
8492
+ return a.domOrder - b.domOrder;
8493
+ });
8494
+
8495
+ for (const item of renderQueue) {
8496
+ if (item.type === 'shape') slide.addShape(item.shapeType, item.options);
8497
+ if (item.type === 'image') slide.addImage(item.options);
8498
+ if (item.type === 'text') slide.addText(item.textParts, item.options);
8499
+ }
8500
+ }
8501
+
8502
+ async function elementToCanvasImage(node, widthPx, heightPx, root) {
8503
+ return new Promise((resolve) => {
8504
+ const width = Math.ceil(widthPx);
8505
+ const height = Math.ceil(heightPx);
8506
+
8507
+ if (width <= 0 || height <= 0) {
8508
+ resolve(null);
8509
+ return;
8510
+ }
8511
+
8512
+ const style = window.getComputedStyle(node);
8513
+
8514
+ html2canvas(root, {
8515
+ width: root.scrollWidth,
8516
+ height: root.scrollHeight,
8517
+ useCORS: true,
8518
+ allowTaint: true,
8519
+ backgroundColor: null,
8520
+ })
8521
+ .then((canvas) => {
8522
+ const rootCanvas = canvas;
8523
+ const nodeRect = node.getBoundingClientRect();
8524
+ const rootRect = root.getBoundingClientRect();
8525
+ const sourceX = nodeRect.left - rootRect.left;
8526
+ const sourceY = nodeRect.top - rootRect.top;
8527
+
8528
+ const destCanvas = document.createElement('canvas');
8529
+ destCanvas.width = width;
8530
+ destCanvas.height = height;
8531
+ const ctx = destCanvas.getContext('2d');
8532
+
8533
+ ctx.drawImage(rootCanvas, sourceX, sourceY, width, height, 0, 0, width, height);
8534
+
8535
+ // Parse radii
8536
+ let tl = parseFloat(style.borderTopLeftRadius) || 0;
8537
+ let tr = parseFloat(style.borderTopRightRadius) || 0;
8538
+ let br = parseFloat(style.borderBottomRightRadius) || 0;
8539
+ let bl = parseFloat(style.borderBottomLeftRadius) || 0;
8540
+
8541
+ const f = Math.min(
8542
+ width / (tl + tr) || Infinity,
8543
+ height / (tr + br) || Infinity,
8544
+ width / (br + bl) || Infinity,
8545
+ height / (bl + tl) || Infinity
8546
+ );
8547
+
8548
+ if (f < 1) {
8549
+ tl *= f;
8550
+ tr *= f;
8551
+ br *= f;
8552
+ bl *= f;
8553
+ }
8554
+
8555
+ ctx.globalCompositeOperation = 'destination-in';
8556
+ ctx.beginPath();
8557
+ ctx.moveTo(tl, 0);
8558
+ ctx.lineTo(width - tr, 0);
8559
+ ctx.arcTo(width, 0, width, tr, tr);
8560
+ ctx.lineTo(width, height - br);
8561
+ ctx.arcTo(width, height, width - br, height, br);
8562
+ ctx.lineTo(bl, height);
8563
+ ctx.arcTo(0, height, 0, height - bl, bl);
8564
+ ctx.lineTo(0, tl);
8565
+ ctx.arcTo(0, 0, tl, 0, tl);
8566
+ ctx.closePath();
8567
+ ctx.fill();
8568
+
8569
+ resolve(destCanvas.toDataURL('image/png'));
8570
+ })
8571
+ .catch(() => resolve(null));
8572
+ });
8573
+ }
8574
+
8575
+ async function createRenderItem(node, config, domOrder, pptx) {
8576
+ if (node.nodeType !== 1) return null;
8577
+ const style = window.getComputedStyle(node);
8578
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')
8579
+ return null;
8580
+
8581
+ const rect = node.getBoundingClientRect();
8582
+ if (rect.width < 0.5 || rect.height < 0.5) return null;
8583
+
8584
+ const zIndex = style.zIndex !== 'auto' ? parseInt(style.zIndex) : 0;
8585
+ const rotation = getRotation(style.transform);
8586
+ const elementOpacity = parseFloat(style.opacity);
8587
+
8588
+ const widthPx = node.offsetWidth || rect.width;
8589
+ const heightPx = node.offsetHeight || rect.height;
8590
+ const unrotatedW = widthPx * PX_TO_INCH * config.scale;
8591
+ const unrotatedH = heightPx * PX_TO_INCH * config.scale;
8592
+ const centerX = rect.left + rect.width / 2;
8593
+ const centerY = rect.top + rect.height / 2;
8594
+
8595
+ let x = config.offX + (centerX - config.rootX) * PX_TO_INCH * config.scale - unrotatedW / 2;
8596
+ let y = config.offY + (centerY - config.rootY) * PX_TO_INCH * config.scale - unrotatedH / 2;
8597
+ let w = unrotatedW;
8598
+ let h = unrotatedH;
8599
+
8600
+ const items = [];
8601
+
8602
+ if (node.nodeName.toUpperCase() === 'SVG') {
8603
+ const pngData = await svgToPng(node);
8604
+ if (pngData)
8605
+ items.push({
8606
+ type: 'image',
8607
+ zIndex,
8608
+ domOrder,
8609
+ options: { data: pngData, x, y, w, h, rotate: rotation },
8610
+ });
8611
+ return { items, stopRecursion: true };
8612
+ }
8613
+
8614
+ // --- UPDATED IMG BLOCK START ---
8615
+ if (node.tagName === 'IMG') {
8616
+ // Extract individual corner radii
8617
+ let radii = {
8618
+ tl: parseFloat(style.borderTopLeftRadius) || 0,
8619
+ tr: parseFloat(style.borderTopRightRadius) || 0,
8620
+ br: parseFloat(style.borderBottomRightRadius) || 0,
8621
+ bl: parseFloat(style.borderBottomLeftRadius) || 0,
8622
+ };
8623
+
8624
+ const hasAnyRadius = radii.tl > 0 || radii.tr > 0 || radii.br > 0 || radii.bl > 0;
8625
+
8626
+ // Fallback: Check parent if image has no specific radius but parent clips it
8627
+ if (!hasAnyRadius) {
8628
+ const parent = node.parentElement;
8629
+ const parentStyle = window.getComputedStyle(parent);
8630
+ if (parentStyle.overflow !== 'visible') {
8631
+ const pRadii = {
8632
+ tl: parseFloat(parentStyle.borderTopLeftRadius) || 0,
8633
+ tr: parseFloat(parentStyle.borderTopRightRadius) || 0,
8634
+ br: parseFloat(parentStyle.borderBottomRightRadius) || 0,
8635
+ bl: parseFloat(parentStyle.borderBottomLeftRadius) || 0,
8636
+ };
8637
+ // Simple heuristic: If image takes up full size of parent, inherit radii.
8638
+ // For complex grids (like slide-1), this blindly applies parent radius.
8639
+ // In a perfect world, we'd calculate intersection, but for now we apply parent radius
8640
+ // if the image is close to the parent's size, effectively masking it.
8641
+ const pRect = parent.getBoundingClientRect();
8642
+ if (Math.abs(pRect.width - rect.width) < 5 && Math.abs(pRect.height - rect.height) < 5) {
8643
+ radii = pRadii;
8644
+ }
8645
+ }
8646
+ }
8647
+
8648
+ const processed = await getProcessedImage(node.src, widthPx, heightPx, radii);
8649
+ if (processed)
8650
+ items.push({
8651
+ type: 'image',
8652
+ zIndex,
8653
+ domOrder,
8654
+ options: { data: processed, x, y, w, h, rotate: rotation },
8655
+ });
8656
+ return { items, stopRecursion: true };
8657
+ }
8658
+ // --- UPDATED IMG BLOCK END ---
8659
+
8660
+ // Radii processing for Divs/Shapes
8661
+ const borderRadiusValue = parseFloat(style.borderRadius) || 0;
8662
+ const borderBottomLeftRadius = parseFloat(style.borderBottomLeftRadius) || 0;
8663
+ const borderBottomRightRadius = parseFloat(style.borderBottomRightRadius) || 0;
8664
+ const borderTopLeftRadius = parseFloat(style.borderTopLeftRadius) || 0;
8665
+ const borderTopRightRadius = parseFloat(style.borderTopRightRadius) || 0;
8666
+
8667
+ const hasPartialBorderRadius =
8668
+ (borderBottomLeftRadius > 0 && borderBottomLeftRadius !== borderRadiusValue) ||
8669
+ (borderBottomRightRadius > 0 && borderBottomRightRadius !== borderRadiusValue) ||
8670
+ (borderTopLeftRadius > 0 && borderTopLeftRadius !== borderRadiusValue) ||
8671
+ (borderTopRightRadius > 0 && borderTopRightRadius !== borderRadiusValue) ||
8672
+ (borderRadiusValue === 0 &&
8673
+ (borderBottomLeftRadius ||
8674
+ borderBottomRightRadius ||
8675
+ borderTopLeftRadius ||
8676
+ borderTopRightRadius));
8677
+
8678
+ // Allow clipped elements to be rendered via canvas
8679
+ if (hasPartialBorderRadius && isClippedByParent(node)) {
8680
+ const marginLeft = parseFloat(style.marginLeft) || 0;
8681
+ const marginTop = parseFloat(style.marginTop) || 0;
8682
+ x += marginLeft * PX_TO_INCH * config.scale;
8683
+ y += marginTop * PX_TO_INCH * config.scale;
8684
+
8685
+ const canvasImageData = await elementToCanvasImage(node, widthPx, heightPx, config.root);
8686
+ if (canvasImageData) {
8687
+ items.push({
8688
+ type: 'image',
8689
+ zIndex,
8690
+ domOrder,
8691
+ options: { data: canvasImageData, x, y, w, h, rotate: rotation },
8692
+ });
8693
+ return { items, stopRecursion: true };
8694
+ }
8695
+ }
8696
+
8697
+ const bgColorObj = parseColor(style.backgroundColor);
8698
+ const bgClip = style.webkitBackgroundClip || style.backgroundClip;
8699
+ const isBgClipText = bgClip === 'text';
8700
+ const hasGradient =
8701
+ !isBgClipText && style.backgroundImage && style.backgroundImage.includes('linear-gradient');
8702
+
8703
+ const borderColorObj = parseColor(style.borderColor);
8704
+ const borderWidth = parseFloat(style.borderWidth);
8705
+ const hasBorder = borderWidth > 0 && borderColorObj.hex;
8706
+
8707
+ const borderInfo = getBorderInfo(style, config.scale);
8708
+ const hasUniformBorder = borderInfo.type === 'uniform';
8709
+ const hasCompositeBorder = borderInfo.type === 'composite';
8710
+
8711
+ const shadowStr = style.boxShadow;
8712
+ const hasShadow = shadowStr && shadowStr !== 'none';
8713
+ const softEdge = getSoftEdges(style.filter, config.scale);
8714
+
8715
+ let isImageWrapper = false;
8716
+ const imgChild = Array.from(node.children).find((c) => c.tagName === 'IMG');
8717
+ if (imgChild) {
8718
+ const childW = imgChild.offsetWidth || imgChild.getBoundingClientRect().width;
8719
+ const childH = imgChild.offsetHeight || imgChild.getBoundingClientRect().height;
8720
+ if (childW >= widthPx - 2 && childH >= heightPx - 2) isImageWrapper = true;
8721
+ }
8722
+
8723
+ let textPayload = null;
8724
+ const isText = isTextContainer(node);
8725
+
8726
+ if (isText) {
8727
+ const textParts = [];
8728
+ const isList = style.display === 'list-item';
8729
+ if (isList) {
8730
+ const fontSizePt = parseFloat(style.fontSize) * 0.75 * config.scale;
8731
+ const bulletShift = (parseFloat(style.fontSize) || 16) * PX_TO_INCH * config.scale * 1.5;
8732
+ x -= bulletShift;
8733
+ w += bulletShift;
8734
+ textParts.push({
8735
+ text: '• ',
8736
+ options: {
8737
+ color: parseColor(style.color).hex || '000000',
8738
+ fontSize: fontSizePt,
8739
+ },
8740
+ });
8741
+ }
8742
+
8743
+ node.childNodes.forEach((child, index) => {
8744
+ let textVal = child.nodeType === 3 ? child.nodeValue : child.textContent;
8745
+ let nodeStyle = child.nodeType === 1 ? window.getComputedStyle(child) : style;
8746
+ textVal = textVal.replace(/[\n\r\t]+/g, ' ').replace(/\s{2,}/g, ' ');
8747
+ if (index === 0 && !isList) textVal = textVal.trimStart();
8748
+ else if (index === 0) textVal = textVal.trimStart();
8749
+ if (index === node.childNodes.length - 1) textVal = textVal.trimEnd();
8750
+ if (nodeStyle.textTransform === 'uppercase') textVal = textVal.toUpperCase();
8751
+ if (nodeStyle.textTransform === 'lowercase') textVal = textVal.toLowerCase();
8752
+
8753
+ if (textVal.length > 0) {
8754
+ textParts.push({
8755
+ text: textVal,
8756
+ options: getTextStyle(nodeStyle, config.scale),
8757
+ });
8758
+ }
8759
+ });
8760
+
8761
+ if (textParts.length > 0) {
8762
+ let align = style.textAlign || 'left';
8763
+ if (align === 'start') align = 'left';
8764
+ if (align === 'end') align = 'right';
8765
+ let valign = 'top';
8766
+ if (style.alignItems === 'center') valign = 'middle';
8767
+ if (style.justifyContent === 'center' && style.display.includes('flex')) align = 'center';
8768
+
8769
+ const pt = parseFloat(style.paddingTop) || 0;
8770
+ const pb = parseFloat(style.paddingBottom) || 0;
8771
+ if (Math.abs(pt - pb) < 2 && bgColorObj.hex) valign = 'middle';
8772
+
8773
+ let padding = getPadding(style, config.scale);
8774
+ if (align === 'center' && valign === 'middle') padding = [0, 0, 0, 0];
8775
+
8776
+ textPayload = { text: textParts, align, valign, inset: padding };
8777
+ }
8778
+ }
8779
+
8780
+ if (hasGradient || (softEdge && bgColorObj.hex && !isImageWrapper)) {
8781
+ let bgData = null;
8782
+ let padIn = 0;
8783
+ if (softEdge) {
8784
+ const svgInfo = generateBlurredSVG(
8785
+ widthPx,
8786
+ heightPx,
8787
+ bgColorObj.hex,
8788
+ borderRadiusValue,
8789
+ softEdge
8790
+ );
8791
+ bgData = svgInfo.data;
8792
+ padIn = svgInfo.padding * PX_TO_INCH * config.scale;
8793
+ } else {
8794
+ bgData = generateGradientSVG(
8795
+ widthPx,
8796
+ heightPx,
8797
+ style.backgroundImage,
8798
+ borderRadiusValue,
8799
+ hasBorder ? { color: borderColorObj.hex, width: borderWidth } : null
8800
+ );
8801
+ }
8802
+
8803
+ if (bgData) {
8804
+ items.push({
8805
+ type: 'image',
8806
+ zIndex,
8807
+ domOrder,
8808
+ options: {
8809
+ data: bgData,
8810
+ x: x - padIn,
8811
+ y: y - padIn,
8812
+ w: w + padIn * 2,
8813
+ h: h + padIn * 2,
8814
+ rotate: rotation,
8815
+ },
8816
+ });
8817
+ }
8818
+
8819
+ if (textPayload) {
8820
+ items.push({
8821
+ type: 'text',
8822
+ zIndex: zIndex + 1,
8823
+ domOrder,
8824
+ textParts: textPayload.text,
8825
+ options: {
8826
+ x,
8827
+ y,
8828
+ w,
8829
+ h,
8830
+ align: textPayload.align,
8831
+ valign: textPayload.valign,
8832
+ inset: textPayload.inset,
8833
+ rotate: rotation,
8834
+ margin: 0,
8835
+ wrap: true,
8836
+ autoFit: false,
8837
+ },
8838
+ });
8839
+ }
8840
+ if (hasCompositeBorder) {
8841
+ // Add border shapes after the main background
8842
+ const borderItems = createCompositeBorderItems(
8843
+ borderInfo.sides,
8844
+ x,
8845
+ y,
8846
+ w,
8847
+ h,
8848
+ config.scale,
8849
+ zIndex,
8850
+ domOrder
8851
+ );
8852
+ items.push(...borderItems);
8853
+ }
8854
+ } else if (
8855
+ (bgColorObj.hex && !isImageWrapper) ||
8856
+ hasUniformBorder ||
8857
+ hasCompositeBorder ||
8858
+ hasShadow ||
8859
+ textPayload
8860
+ ) {
8861
+ const finalAlpha = elementOpacity * bgColorObj.opacity;
8862
+ const transparency = (1 - finalAlpha) * 100;
8863
+ const useSolidFill = bgColorObj.hex && !isImageWrapper;
8864
+
8865
+ if (hasPartialBorderRadius && useSolidFill && !textPayload) {
8866
+ const shapeSvg = generateCustomShapeSVG(
8867
+ widthPx,
8868
+ heightPx,
8869
+ bgColorObj.hex,
8870
+ bgColorObj.opacity,
8871
+ {
8872
+ tl: parseFloat(style.borderTopLeftRadius) || 0,
8873
+ tr: parseFloat(style.borderTopRightRadius) || 0,
8874
+ br: parseFloat(style.borderBottomRightRadius) || 0,
8875
+ bl: parseFloat(style.borderBottomLeftRadius) || 0,
8876
+ }
8877
+ );
8878
+
8879
+ items.push({
8880
+ type: 'image',
8881
+ zIndex,
8882
+ domOrder,
8883
+ options: {
8884
+ data: shapeSvg,
8885
+ x,
8886
+ y,
8887
+ w,
8888
+ h,
8889
+ rotate: rotation,
8890
+ },
8891
+ });
8892
+ } else {
8893
+ const shapeOpts = {
8894
+ x,
8895
+ y,
8896
+ w,
8897
+ h,
8898
+ rotate: rotation,
8899
+ fill: useSolidFill
8900
+ ? { color: bgColorObj.hex, transparency: transparency }
8901
+ : { type: 'none' },
8902
+ line: hasUniformBorder ? borderInfo.options : null,
8903
+ };
8904
+
8905
+ if (hasShadow) {
8906
+ shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
8907
+ }
8908
+
8909
+ const borderRadius = parseFloat(style.borderRadius) || 0;
8910
+ const aspectRatio = Math.max(widthPx, heightPx) / Math.min(widthPx, heightPx);
8911
+ const isCircle = aspectRatio < 1.1 && borderRadius >= Math.min(widthPx, heightPx) / 2 - 1;
8912
+
8913
+ let shapeType = pptx.ShapeType.rect;
8914
+ if (isCircle) shapeType = pptx.ShapeType.ellipse;
8915
+ else if (borderRadius > 0) {
8916
+ shapeType = pptx.ShapeType.roundRect;
8917
+ shapeOpts.rectRadius = Math.min(0.5, borderRadius / Math.min(widthPx, heightPx));
8918
+ }
8919
+
8920
+ if (textPayload) {
8921
+ const textOptions = {
8922
+ shape: shapeType,
8923
+ ...shapeOpts,
8924
+ align: textPayload.align,
8925
+ valign: textPayload.valign,
8926
+ inset: textPayload.inset,
8927
+ margin: 0,
8928
+ wrap: true,
8929
+ autoFit: false,
8930
+ };
8931
+ items.push({
8932
+ type: 'text',
8933
+ zIndex,
8934
+ domOrder,
8935
+ textParts: textPayload.text,
8936
+ options: textOptions,
8937
+ });
8938
+ } else if (!hasPartialBorderRadius) {
8939
+ items.push({
8940
+ type: 'shape',
8941
+ zIndex,
8942
+ domOrder,
8943
+ shapeType,
8944
+ options: shapeOpts,
8945
+ });
8946
+ }
8947
+ }
8948
+
8949
+ if (hasCompositeBorder) {
8950
+ const borderSvgData = generateCompositeBorderSVG(
8951
+ widthPx,
8952
+ heightPx,
8953
+ borderRadiusValue,
8954
+ borderInfo.sides
8955
+ );
8956
+ if (borderSvgData) {
8957
+ items.push({
8958
+ type: 'image',
8959
+ zIndex: zIndex + 1,
8960
+ domOrder,
8961
+ options: { data: borderSvgData, x, y, w, h, rotate: rotation },
8962
+ });
8963
+ }
8964
+ }
8965
+ }
8966
+
8967
+ return { items, stopRecursion: !!textPayload };
8968
+ }
8969
+
8970
+ /**
8971
+ * Helper function to create individual border shapes
8972
+ */
8973
+ function createCompositeBorderItems(sides, x, y, w, h, scale, zIndex, domOrder) {
8974
+ const items = [];
8975
+ const pxToInch = 1 / 96;
8976
+
8977
+ // TOP BORDER
8978
+ if (sides.top.width > 0) {
8979
+ items.push({
8980
+ type: 'shape',
8981
+ zIndex: zIndex + 1,
8982
+ domOrder,
8983
+ shapeType: 'rect',
8984
+ options: {
8985
+ x: x,
8986
+ y: y,
8987
+ w: w,
8988
+ h: sides.top.width * pxToInch * scale,
8989
+ fill: { color: sides.top.color },
8990
+ },
8991
+ });
8992
+ }
8993
+ // RIGHT BORDER
8994
+ if (sides.right.width > 0) {
8995
+ items.push({
8996
+ type: 'shape',
8997
+ zIndex: zIndex + 1,
8998
+ domOrder,
8999
+ shapeType: 'rect',
9000
+ options: {
9001
+ x: x + w - sides.right.width * pxToInch * scale,
9002
+ y: y,
9003
+ w: sides.right.width * pxToInch * scale,
9004
+ h: h,
9005
+ fill: { color: sides.right.color },
9006
+ },
9007
+ });
9008
+ }
9009
+ // BOTTOM BORDER
9010
+ if (sides.bottom.width > 0) {
9011
+ items.push({
9012
+ type: 'shape',
9013
+ zIndex: zIndex + 1,
9014
+ domOrder,
9015
+ shapeType: 'rect',
9016
+ options: {
9017
+ x: x,
9018
+ y: y + h - sides.bottom.width * pxToInch * scale,
9019
+ w: w,
9020
+ h: sides.bottom.width * pxToInch * scale,
9021
+ fill: { color: sides.bottom.color },
9022
+ },
9023
+ });
9024
+ }
9025
+ // LEFT BORDER
9026
+ if (sides.left.width > 0) {
9027
+ items.push({
9028
+ type: 'shape',
9029
+ zIndex: zIndex + 1,
9030
+ domOrder,
9031
+ shapeType: 'rect',
9032
+ options: {
9033
+ x: x,
9034
+ y: y,
9035
+ w: sides.left.width * pxToInch * scale,
9036
+ h: h,
9037
+ fill: { color: sides.left.color },
9038
+ },
9039
+ });
9040
+ }
9041
+
9042
+ return items;
9010
9043
  }
9011
9044
 
9012
9045
  exports.exportToPptx = exportToPptx;