dom-to-pptx 1.0.1 → 1.0.2

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.
package/src/index.js CHANGED
@@ -1,253 +1,486 @@
1
- // src/index.js
2
- import PptxGenJS from "pptxgenjs";
3
- // FIX: Added generateGradientTextSVG
4
- import { parseColor, getTextStyle, isTextContainer, getVisibleShadow, generateGradientSVG, getRotation, svgToPng, getPadding, getSoftEdges, generateBlurredSVG } from "./utils.js";
5
- import { getProcessedImage } from "./image-processor.js";
6
-
7
- const PPI = 96;
8
- const PX_TO_INCH = 1 / PPI;
9
-
10
- export async function exportToPptx(elementOrSelector, options = {}) {
11
- const root = typeof elementOrSelector === "string"
12
- ? document.querySelector(elementOrSelector)
13
- : elementOrSelector;
14
-
15
- if (!root) throw new Error("Root element not found");
16
-
17
- const pptx = new PptxGenJS();
18
- pptx.layout = "LAYOUT_16x9";
19
- const slide = pptx.addSlide();
20
-
21
- const rootRect = root.getBoundingClientRect();
22
- const PPTX_WIDTH_IN = 10;
23
- const PPTX_HEIGHT_IN = 5.625;
24
- const contentWidthIn = rootRect.width * PX_TO_INCH;
25
- const contentHeightIn = rootRect.height * PX_TO_INCH;
26
- const scale = Math.min(PPTX_WIDTH_IN / contentWidthIn, PPTX_HEIGHT_IN / contentHeightIn);
27
-
28
- const layoutConfig = {
29
- rootX: rootRect.x, rootY: rootRect.y, scale: scale,
30
- offX: (PPTX_WIDTH_IN - contentWidthIn * scale) / 2,
31
- offY: (PPTX_HEIGHT_IN - contentHeightIn * scale) / 2,
32
- };
33
-
34
- const renderQueue = [];
35
- let domOrderCounter = 0;
36
-
37
- async function collect(node) {
38
- const order = domOrderCounter++;
39
- const result = await createRenderItem(node, layoutConfig, order, pptx);
40
- if (result) {
41
- if (result.items) renderQueue.push(...result.items);
42
- if (result.stopRecursion) return;
43
- }
44
- for (const child of node.children) await collect(child);
45
- }
46
-
47
- await collect(root);
48
-
49
- renderQueue.sort((a, b) => {
50
- if (a.zIndex !== b.zIndex) return a.zIndex - b.zIndex;
51
- return a.domOrder - b.domOrder;
52
- });
53
-
54
- for (const item of renderQueue) {
55
- if (item.type === 'shape') slide.addShape(item.shapeType, item.options);
56
- if (item.type === 'image') slide.addImage(item.options);
57
- if (item.type === 'text') slide.addText(item.textParts, item.options);
58
- }
59
-
60
- const fileName = options.fileName || "export.pptx";
61
- pptx.writeFile({ fileName });
62
- }
63
-
64
- async function createRenderItem(node, config, domOrder, pptx) {
65
- if (node.nodeType !== 1) return null;
66
- const style = window.getComputedStyle(node);
67
- if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return null;
68
-
69
- const rect = node.getBoundingClientRect();
70
- if (rect.width === 0 || rect.height === 0) return null;
71
-
72
- const zIndex = style.zIndex !== 'auto' ? parseInt(style.zIndex) : 0;
73
- const rotation = getRotation(style.transform);
74
- const elementOpacity = parseFloat(style.opacity);
75
-
76
- const widthPx = node.offsetWidth || rect.width;
77
- const heightPx = node.offsetHeight || rect.height;
78
-
79
- const unrotatedW = widthPx * PX_TO_INCH * config.scale;
80
- const unrotatedH = heightPx * PX_TO_INCH * config.scale;
81
- const centerX = rect.left + rect.width / 2;
82
- const centerY = rect.top + rect.height / 2;
83
-
84
- let x = config.offX + ((centerX - config.rootX) * PX_TO_INCH * config.scale) - (unrotatedW / 2);
85
- let y = config.offY + ((centerY - config.rootY) * PX_TO_INCH * config.scale) - (unrotatedH / 2);
86
- let w = unrotatedW;
87
- let h = unrotatedH;
88
-
89
- const items = [];
90
-
91
- // --- SVG & IMG HANDLERS ---
92
- if (node.nodeName.toUpperCase() === 'SVG') {
93
- const pngData = await svgToPng(node);
94
- if (pngData) items.push({ type: 'image', zIndex, domOrder, options: { data: pngData, x, y, w, h, rotate: rotation } });
95
- return { items, stopRecursion: true };
96
- }
97
- if (node.tagName === "IMG") {
98
- let borderRadius = parseFloat(style.borderRadius) || 0;
99
- if (borderRadius === 0) {
100
- const parentStyle = window.getComputedStyle(node.parentElement);
101
- if (parentStyle.overflow !== 'visible') borderRadius = parseFloat(parentStyle.borderRadius) || 0;
102
- }
103
- const processed = await getProcessedImage(node.src, widthPx, heightPx, borderRadius);
104
- if (processed) items.push({ type: 'image', zIndex, domOrder, options: { data: processed, x, y, w, h, rotate: rotation } });
105
- return { items, stopRecursion: true };
106
- }
107
-
108
- // --- PREPARE STYLES ---
109
- const bgColorObj = parseColor(style.backgroundColor);
110
- const bgClip = style.webkitBackgroundClip || style.backgroundClip;
111
- const isBgClipText = bgClip === 'text';
112
- const hasGradient = !isBgClipText && style.backgroundImage && style.backgroundImage.includes("linear-gradient");
113
-
114
- const borderColorObj = parseColor(style.borderColor);
115
- const borderWidth = parseFloat(style.borderWidth);
116
- const hasBorder = borderWidth > 0 && borderColorObj.hex;
117
- const shadowStr = style.boxShadow;
118
- const hasShadow = shadowStr && shadowStr !== "none";
119
- const borderRadius = parseFloat(style.borderRadius) || 0;
120
- const softEdge = getSoftEdges(style.filter, config.scale);
121
-
122
- let isImageWrapper = false;
123
- const imgChild = Array.from(node.children).find(c => c.tagName === 'IMG');
124
- if (imgChild) {
125
- const childW = imgChild.offsetWidth || imgChild.getBoundingClientRect().width;
126
- const childH = imgChild.offsetHeight || imgChild.getBoundingClientRect().height;
127
- if (childW >= widthPx - 2 && childH >= heightPx - 2) {
128
- isImageWrapper = true;
129
- }
130
- }
131
-
132
- // --- TEXT EXTRACTION ---
133
- let textPayload = null;
134
- const isText = isTextContainer(node);
135
-
136
- if (isText) {
137
- const textParts = [];
138
- const isList = style.display === 'list-item';
139
-
140
- if (isList) {
141
- const fontSizePt = parseFloat(style.fontSize) * 0.75 * config.scale;
142
- const bulletShift = (parseFloat(style.fontSize) || 16) * PX_TO_INCH * config.scale * 1.5;
143
- x -= bulletShift;
144
- w += bulletShift;
145
- textParts.push({ text: "• ", options: { color: parseColor(style.color).hex || '000000', fontSize: fontSizePt } });
146
- }
147
-
148
- node.childNodes.forEach((child, index) => {
149
- let textVal = child.nodeType === 3 ? child.nodeValue : child.textContent;
150
- let nodeStyle = child.nodeType === 1 ? window.getComputedStyle(child) : style;
151
- textVal = textVal.replace(/[\n\r\t]+/g, " ").replace(/\s{2,}/g, " ");
152
- if (index === 0 && !isList) textVal = textVal.trimStart();
153
- else if (index === 0) textVal = textVal.trimStart();
154
- if (index === node.childNodes.length - 1) textVal = textVal.trimEnd();
155
- if (nodeStyle.textTransform === "uppercase") textVal = textVal.toUpperCase();
156
- if (nodeStyle.textTransform === "lowercase") textVal = textVal.toLowerCase();
157
-
158
- if (textVal.length > 0) {
159
- textParts.push({ text: textVal, options: getTextStyle(nodeStyle, config.scale) });
160
- }
161
- });
162
-
163
- if (textParts.length > 0) {
164
- let align = style.textAlign || "left";
165
- if (align === "start") align = "left"; if (align === "end") align = "right";
166
-
167
- let valign = "top";
168
- // Standard Flex Alignment
169
- if (style.alignItems === "center") valign = "middle";
170
- if (style.justifyContent === "center" && style.display.includes("flex")) align = "center";
171
-
172
- // FIX: Improved Alignment Detection for "Button" Badges
173
- // If Top/Bottom Padding is equal, and it's a "Shape" (has bg), default to Middle
174
- const pt = parseFloat(style.paddingTop) || 0;
175
- const pb = parseFloat(style.paddingBottom) || 0;
176
- if (Math.abs(pt - pb) < 2 && bgColorObj.hex) {
177
- valign = "middle";
178
- }
179
-
180
- const padding = getPadding(style, config.scale);
181
- textPayload = { text: textParts, align, valign, inset: padding };
182
- }
183
- }
184
-
185
- // --- RENDER LOGIC ---
186
- if (hasGradient || (softEdge && bgColorObj.hex && !isImageWrapper)) {
187
- let bgData = null;
188
- let padIn = 0;
189
- if (softEdge) {
190
- const svgInfo = generateBlurredSVG(widthPx, heightPx, bgColorObj.hex, borderRadius, softEdge);
191
- bgData = svgInfo.data;
192
- padIn = svgInfo.padding * PX_TO_INCH * config.scale;
193
- } else {
194
- bgData = generateGradientSVG(widthPx, heightPx, style.backgroundImage, borderRadius, hasBorder ? { color: borderColorObj.hex, width: borderWidth } : null);
195
- }
196
-
197
- if (bgData) {
198
- items.push({
199
- type: 'image', zIndex, domOrder,
200
- options: { data: bgData, x: x - padIn, y: y - padIn, w: w + (padIn * 2), h: h + (padIn * 2), rotate: rotation }
201
- });
202
- }
203
-
204
- if (textPayload) {
205
- items.push({
206
- type: 'text', zIndex: zIndex + 1, domOrder,
207
- textParts: textPayload.text,
208
- options: {
209
- x, y, w, h, align: textPayload.align, valign: textPayload.valign, inset: textPayload.inset, rotate: rotation, margin: 0, wrap: true, autoFit: false
210
- }
211
- });
212
- }
213
- }
214
- else if ((bgColorObj.hex && !isImageWrapper) || hasBorder || hasShadow || textPayload) {
215
- const finalAlpha = elementOpacity * bgColorObj.opacity;
216
- const transparency = (1 - finalAlpha) * 100;
217
-
218
- const shapeOpts = {
219
- x, y, w, h, rotate: rotation,
220
- fill: (bgColorObj.hex && !isImageWrapper) ? { color: bgColorObj.hex, transparency: transparency } : { type: 'none' },
221
- line: hasBorder ? { color: borderColorObj.hex, width: borderWidth * 0.75 * config.scale } : null,
222
- };
223
-
224
- if (hasShadow) {
225
- const shadow = getVisibleShadow(shadowStr, config.scale);
226
- if (shadow) shapeOpts.shadow = shadow;
227
- if (shapeOpts.fill.type === 'none' && !hasBorder && style.backgroundColor) {
228
- shapeOpts.fill = { color: 'FFFFFF', transparency: 99 };
229
- }
230
- }
231
-
232
- const isCircle = borderRadius >= Math.min(widthPx, heightPx) / 2 - 1 && Math.abs(widthPx - heightPx) < 2;
233
- let shapeType = pptx.ShapeType.rect;
234
- if (isCircle) shapeType = pptx.ShapeType.ellipse;
235
- else if (borderRadius > 0) {
236
- shapeType = pptx.ShapeType.roundRect;
237
- shapeOpts.rectRadius = Math.min(1, borderRadius / (Math.min(widthPx, heightPx) / 2));
238
- }
239
-
240
- if (textPayload) {
241
- const textOptions = {
242
- shape: shapeType, ...shapeOpts,
243
- align: textPayload.align, valign: textPayload.valign, inset: textPayload.inset,
244
- margin: 0, wrap: true, autoFit: false
245
- };
246
- items.push({ type: 'text', zIndex, domOrder, textParts: textPayload.text, options: textOptions });
247
- } else {
248
- items.push({ type: 'shape', zIndex, domOrder, shapeType, options: shapeOpts });
249
- }
250
- }
251
-
252
- return { items, stopRecursion: !!textPayload };
253
- }
1
+ // src/index.js
2
+ import PptxGenJS from 'pptxgenjs';
3
+ import {
4
+ parseColor,
5
+ getTextStyle,
6
+ isTextContainer,
7
+ getVisibleShadow,
8
+ generateGradientSVG,
9
+ getRotation,
10
+ svgToPng,
11
+ getPadding,
12
+ getSoftEdges,
13
+ generateBlurredSVG,
14
+ getBorderInfo,
15
+ generateCompositeBorderSVG,
16
+ } from './utils.js';
17
+ import { getProcessedImage } from './image-processor.js';
18
+
19
+ const PPI = 96;
20
+ const PX_TO_INCH = 1 / PPI;
21
+
22
+ /**
23
+ * Main export function. Accepts single element or an array.
24
+ * @param {HTMLElement | string | Array<HTMLElement | string>} target - The root element(s) to convert.
25
+ * @param {Object} options - { fileName: string }
26
+ */
27
+ export async function exportToPptx(target, options = {}) {
28
+ const pptx = new PptxGenJS();
29
+ pptx.layout = 'LAYOUT_16x9';
30
+
31
+ // Standardize input to an array, ensuring single or multiple elements are handled consistently
32
+ const elements = Array.isArray(target) ? target : [target];
33
+
34
+ for (const el of elements) {
35
+ const root = typeof el === 'string' ? document.querySelector(el) : el;
36
+ if (!root) {
37
+ console.warn('Element not found, skipping slide:', el);
38
+ continue;
39
+ }
40
+
41
+ const slide = pptx.addSlide();
42
+ await processSlide(root, slide, pptx);
43
+ }
44
+
45
+ const fileName = options.fileName || 'export.pptx';
46
+ pptx.writeFile({ fileName });
47
+ }
48
+
49
+ /**
50
+ * Worker function to process a single DOM element into a single PPTX slide.
51
+ * @param {HTMLElement} root - The root element for this slide.
52
+ * @param {PptxGenJS.Slide} slide - The PPTX slide object to add content to.
53
+ * @param {PptxGenJS} pptx - The main PPTX instance.
54
+ */
55
+ async function processSlide(root, slide, pptx) {
56
+ const rootRect = root.getBoundingClientRect();
57
+ const PPTX_WIDTH_IN = 10;
58
+ const PPTX_HEIGHT_IN = 5.625;
59
+
60
+ const contentWidthIn = rootRect.width * PX_TO_INCH;
61
+ const contentHeightIn = rootRect.height * PX_TO_INCH;
62
+ const scale = Math.min(PPTX_WIDTH_IN / contentWidthIn, PPTX_HEIGHT_IN / contentHeightIn);
63
+
64
+ const layoutConfig = {
65
+ rootX: rootRect.x,
66
+ rootY: rootRect.y,
67
+ scale: scale,
68
+ offX: (PPTX_WIDTH_IN - contentWidthIn * scale) / 2,
69
+ offY: (PPTX_HEIGHT_IN - contentHeightIn * scale) / 2,
70
+ };
71
+
72
+ const renderQueue = [];
73
+ let domOrderCounter = 0;
74
+
75
+ async function collect(node) {
76
+ const order = domOrderCounter++; // Assign a DOM order for z-index tie-breaking
77
+ const result = await createRenderItem(node, layoutConfig, order, pptx);
78
+ if (result) {
79
+ if (result.items) renderQueue.push(...result.items); // Add any generated render items to the queue
80
+ if (result.stopRecursion) return; // Stop processing children if the item fully consumed the node
81
+ }
82
+ for (const child of node.children) await collect(child);
83
+ }
84
+
85
+ await collect(root);
86
+
87
+ renderQueue.sort((a, b) => {
88
+ if (a.zIndex !== b.zIndex) return a.zIndex - b.zIndex;
89
+ return a.domOrder - b.domOrder;
90
+ });
91
+
92
+ for (const item of renderQueue) {
93
+ if (item.type === 'shape') slide.addShape(item.shapeType, item.options);
94
+ if (item.type === 'image') slide.addImage(item.options);
95
+ if (item.type === 'text') slide.addText(item.textParts, item.options);
96
+ }
97
+ }
98
+
99
+ async function createRenderItem(node, config, domOrder, pptx) {
100
+ if (node.nodeType !== 1) return null;
101
+ const style = window.getComputedStyle(node);
102
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')
103
+ return null;
104
+
105
+ const rect = node.getBoundingClientRect();
106
+ if (rect.width === 0 || rect.height === 0) return null;
107
+
108
+ const zIndex = style.zIndex !== 'auto' ? parseInt(style.zIndex) : 0;
109
+ const rotation = getRotation(style.transform);
110
+ const elementOpacity = parseFloat(style.opacity);
111
+
112
+ const widthPx = node.offsetWidth || rect.width;
113
+ const heightPx = node.offsetHeight || rect.height;
114
+ const unrotatedW = widthPx * PX_TO_INCH * config.scale;
115
+ const unrotatedH = heightPx * PX_TO_INCH * config.scale;
116
+ const centerX = rect.left + rect.width / 2;
117
+ const centerY = rect.top + rect.height / 2;
118
+
119
+ let x = config.offX + (centerX - config.rootX) * PX_TO_INCH * config.scale - unrotatedW / 2;
120
+ let y = config.offY + (centerY - config.rootY) * PX_TO_INCH * config.scale - unrotatedH / 2;
121
+ let w = unrotatedW;
122
+ let h = unrotatedH;
123
+
124
+ const items = [];
125
+
126
+ // Image handling for SVG nodes directly
127
+ if (node.nodeName.toUpperCase() === 'SVG') {
128
+ const pngData = await svgToPng(node);
129
+ if (pngData)
130
+ items.push({
131
+ type: 'image',
132
+ zIndex,
133
+ domOrder,
134
+ options: { data: pngData, x, y, w, h, rotate: rotation },
135
+ });
136
+ return { items, stopRecursion: true };
137
+ }
138
+ // Image handling for <img> tags, including rounded corners
139
+ if (node.tagName === 'IMG') {
140
+ let borderRadius = parseFloat(style.borderRadius) || 0;
141
+ if (borderRadius === 0) {
142
+ const parentStyle = window.getComputedStyle(node.parentElement);
143
+ if (parentStyle.overflow !== 'visible')
144
+ borderRadius = parseFloat(parentStyle.borderRadius) || 0;
145
+ }
146
+ const processed = await getProcessedImage(node.src, widthPx, heightPx, borderRadius);
147
+ if (processed)
148
+ items.push({
149
+ type: 'image',
150
+ zIndex,
151
+ domOrder,
152
+ options: { data: processed, x, y, w, h, rotate: rotation },
153
+ });
154
+ return { items, stopRecursion: true };
155
+ }
156
+
157
+ const bgColorObj = parseColor(style.backgroundColor);
158
+ const bgClip = style.webkitBackgroundClip || style.backgroundClip;
159
+ const isBgClipText = bgClip === 'text';
160
+ const hasGradient =
161
+ !isBgClipText && style.backgroundImage && style.backgroundImage.includes('linear-gradient');
162
+
163
+ const borderColorObj = parseColor(style.borderColor);
164
+ const borderWidth = parseFloat(style.borderWidth);
165
+ const hasBorder = borderWidth > 0 && borderColorObj.hex;
166
+
167
+ const borderInfo = getBorderInfo(style, config.scale);
168
+ const hasUniformBorder = borderInfo.type === 'uniform';
169
+ const hasCompositeBorder = borderInfo.type === 'composite';
170
+
171
+ const shadowStr = style.boxShadow;
172
+ const hasShadow = shadowStr && shadowStr !== 'none';
173
+ const borderRadius = parseFloat(style.borderRadius) || 0;
174
+ const softEdge = getSoftEdges(style.filter, config.scale);
175
+
176
+ let isImageWrapper = false;
177
+ const imgChild = Array.from(node.children).find((c) => c.tagName === 'IMG');
178
+ if (imgChild) {
179
+ const childW = imgChild.offsetWidth || imgChild.getBoundingClientRect().width;
180
+ const childH = imgChild.offsetHeight || imgChild.getBoundingClientRect().height;
181
+ if (childW >= widthPx - 2 && childH >= heightPx - 2) isImageWrapper = true;
182
+ }
183
+
184
+ let textPayload = null;
185
+ const isText = isTextContainer(node);
186
+
187
+ if (isText) {
188
+ const textParts = [];
189
+ const isList = style.display === 'list-item';
190
+ if (isList) {
191
+ const fontSizePt = parseFloat(style.fontSize) * 0.75 * config.scale;
192
+ const bulletShift = (parseFloat(style.fontSize) || 16) * PX_TO_INCH * config.scale * 1.5;
193
+ x -= bulletShift;
194
+ w += bulletShift;
195
+ textParts.push({
196
+ text: '• ',
197
+ options: {
198
+ // Default bullet point styling
199
+ color: parseColor(style.color).hex || '000000',
200
+ fontSize: fontSizePt,
201
+ },
202
+ });
203
+ }
204
+
205
+ node.childNodes.forEach((child, index) => {
206
+ // Process text content, sanitizing whitespace and applying text transformations
207
+ let textVal = child.nodeType === 3 ? child.nodeValue : child.textContent;
208
+ let nodeStyle = child.nodeType === 1 ? window.getComputedStyle(child) : style;
209
+ textVal = textVal.replace(/[\n\r\t]+/g, ' ').replace(/\s{2,}/g, ' ');
210
+ if (index === 0 && !isList) textVal = textVal.trimStart();
211
+ else if (index === 0) textVal = textVal.trimStart();
212
+ if (index === node.childNodes.length - 1) textVal = textVal.trimEnd();
213
+ if (nodeStyle.textTransform === 'uppercase') textVal = textVal.toUpperCase();
214
+ if (nodeStyle.textTransform === 'lowercase') textVal = textVal.toLowerCase();
215
+
216
+ if (textVal.length > 0) {
217
+ textParts.push({
218
+ text: textVal,
219
+ options: getTextStyle(nodeStyle, config.scale),
220
+ });
221
+ }
222
+ });
223
+
224
+ if (textParts.length > 0) {
225
+ let align = style.textAlign || 'left';
226
+ if (align === 'start') align = 'left';
227
+ if (align === 'end') align = 'right';
228
+ let valign = 'top';
229
+ if (style.alignItems === 'center') valign = 'middle';
230
+ if (style.justifyContent === 'center' && style.display.includes('flex')) align = 'center';
231
+
232
+ const pt = parseFloat(style.paddingTop) || 0;
233
+ const pb = parseFloat(style.paddingBottom) || 0;
234
+ if (Math.abs(pt - pb) < 2 && bgColorObj.hex) valign = 'middle';
235
+
236
+ let padding = getPadding(style, config.scale);
237
+ if (align === 'center' && valign === 'middle') padding = [0, 0, 0, 0];
238
+
239
+ textPayload = { text: textParts, align, valign, inset: padding };
240
+ }
241
+ }
242
+
243
+ if (hasGradient || (softEdge && bgColorObj.hex && !isImageWrapper)) {
244
+ let bgData = null;
245
+ let padIn = 0;
246
+ if (softEdge) {
247
+ const svgInfo = generateBlurredSVG(widthPx, heightPx, bgColorObj.hex, borderRadius, softEdge);
248
+ bgData = svgInfo.data;
249
+ padIn = svgInfo.padding * PX_TO_INCH * config.scale;
250
+ } else {
251
+ bgData = generateGradientSVG(
252
+ widthPx,
253
+ heightPx,
254
+ style.backgroundImage,
255
+ borderRadius,
256
+ hasBorder ? { color: borderColorObj.hex, width: borderWidth } : null
257
+ );
258
+ }
259
+
260
+ if (bgData) {
261
+ items.push({
262
+ type: 'image',
263
+ zIndex,
264
+ domOrder,
265
+ options: {
266
+ data: bgData,
267
+ x: x - padIn,
268
+ y: y - padIn,
269
+ w: w + padIn * 2,
270
+ h: h + padIn * 2,
271
+ rotate: rotation,
272
+ },
273
+ });
274
+ }
275
+
276
+ if (textPayload) {
277
+ items.push({
278
+ type: 'text',
279
+ zIndex: zIndex + 1,
280
+ domOrder,
281
+ textParts: textPayload.text,
282
+ options: {
283
+ x,
284
+ y,
285
+ w,
286
+ h,
287
+ align: textPayload.align,
288
+ valign: textPayload.valign,
289
+ inset: textPayload.inset,
290
+ rotate: rotation,
291
+ margin: 0,
292
+ wrap: true,
293
+ autoFit: false,
294
+ },
295
+ });
296
+ }
297
+ if (hasCompositeBorder) {
298
+ // Add border shapes after the main background
299
+ const borderItems = createCompositeBorderItems(
300
+ borderInfo.sides,
301
+ x,
302
+ y,
303
+ w,
304
+ h,
305
+ config.scale,
306
+ zIndex,
307
+ domOrder
308
+ );
309
+ items.push(...borderItems);
310
+ }
311
+ } else if (
312
+ (bgColorObj.hex && !isImageWrapper) ||
313
+ hasUniformBorder ||
314
+ hasCompositeBorder ||
315
+ hasShadow ||
316
+ textPayload
317
+ ) {
318
+ const finalAlpha = elementOpacity * bgColorObj.opacity;
319
+ const transparency = (1 - finalAlpha) * 100;
320
+
321
+ const shapeOpts = {
322
+ x,
323
+ y,
324
+ w,
325
+ h,
326
+ rotate: rotation,
327
+ fill:
328
+ bgColorObj.hex && !isImageWrapper
329
+ ? { color: bgColorObj.hex, transparency: transparency }
330
+ : { type: 'none' },
331
+ // Only apply line if the border is uniform
332
+ line: hasUniformBorder ? borderInfo.options : null,
333
+ };
334
+
335
+ if (hasShadow) {
336
+ shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
337
+ }
338
+
339
+ const borderRadius = parseFloat(style.borderRadius) || 0;
340
+ const widthPx = node.offsetWidth || rect.width;
341
+ const heightPx = node.offsetHeight || rect.height;
342
+ const isCircle =
343
+ borderRadius >= Math.min(widthPx, heightPx) / 2 - 1 && Math.abs(widthPx - heightPx) < 2;
344
+
345
+ let shapeType = pptx.ShapeType.rect;
346
+ if (isCircle) shapeType = pptx.ShapeType.ellipse;
347
+ else if (borderRadius > 0) {
348
+ shapeType = pptx.ShapeType.roundRect;
349
+ shapeOpts.rectRadius = Math.min(1, borderRadius / (Math.min(widthPx, heightPx) / 2));
350
+ }
351
+
352
+ // MERGE TEXT INTO SHAPE (if text exists)
353
+ if (textPayload) {
354
+ const textOptions = {
355
+ shape: shapeType,
356
+ ...shapeOpts,
357
+ align: textPayload.align,
358
+ valign: textPayload.valign,
359
+ inset: textPayload.inset,
360
+ margin: 0,
361
+ wrap: true,
362
+ autoFit: false,
363
+ };
364
+ items.push({
365
+ type: 'text',
366
+ zIndex,
367
+ domOrder,
368
+ textParts: textPayload.text,
369
+ options: textOptions,
370
+ });
371
+ // If no text, just draw the shape
372
+ } else {
373
+ items.push({
374
+ type: 'shape',
375
+ zIndex,
376
+ domOrder,
377
+ shapeType,
378
+ options: shapeOpts,
379
+ });
380
+ }
381
+
382
+ // ADD COMPOSITE BORDERS (if they exist)
383
+ if (hasCompositeBorder) {
384
+ // Generate a single SVG image that contains all the rounded border sides
385
+ const borderSvgData = generateCompositeBorderSVG(
386
+ widthPx,
387
+ heightPx,
388
+ borderRadius,
389
+ borderInfo.sides
390
+ );
391
+
392
+ if (borderSvgData) {
393
+ items.push({
394
+ type: 'image',
395
+ zIndex: zIndex + 1,
396
+ domOrder,
397
+ options: {
398
+ data: borderSvgData,
399
+ x: x,
400
+ y: y,
401
+ w: w,
402
+ h: h,
403
+ rotate: rotation,
404
+ },
405
+ });
406
+ }
407
+ }
408
+ }
409
+
410
+ return { items, stopRecursion: !!textPayload };
411
+ }
412
+
413
+ /**
414
+ * Helper function to create individual border shapes
415
+ */
416
+ function createCompositeBorderItems(sides, x, y, w, h, scale, zIndex, domOrder) {
417
+ const items = [];
418
+ const pxToInch = 1 / 96;
419
+
420
+ // TOP BORDER
421
+ if (sides.top.width > 0) {
422
+ items.push({
423
+ type: 'shape',
424
+ zIndex: zIndex + 1,
425
+ domOrder,
426
+ shapeType: 'rect',
427
+ options: {
428
+ x: x,
429
+ y: y,
430
+ w: w,
431
+ h: sides.top.width * pxToInch * scale,
432
+ fill: { color: sides.top.color },
433
+ },
434
+ });
435
+ }
436
+ // RIGHT BORDER
437
+ if (sides.right.width > 0) {
438
+ items.push({
439
+ type: 'shape',
440
+ zIndex: zIndex + 1,
441
+ domOrder,
442
+ shapeType: 'rect',
443
+ options: {
444
+ x: x + w - sides.right.width * pxToInch * scale,
445
+ y: y,
446
+ w: sides.right.width * pxToInch * scale,
447
+ h: h,
448
+ fill: { color: sides.right.color },
449
+ },
450
+ });
451
+ }
452
+ // BOTTOM BORDER
453
+ if (sides.bottom.width > 0) {
454
+ items.push({
455
+ type: 'shape',
456
+ zIndex: zIndex + 1,
457
+ domOrder,
458
+ shapeType: 'rect',
459
+ options: {
460
+ x: x,
461
+ y: y + h - sides.bottom.width * pxToInch * scale,
462
+ w: w,
463
+ h: sides.bottom.width * pxToInch * scale,
464
+ fill: { color: sides.bottom.color },
465
+ },
466
+ });
467
+ }
468
+ // LEFT BORDER
469
+ if (sides.left.width > 0) {
470
+ items.push({
471
+ type: 'shape',
472
+ zIndex: zIndex + 1,
473
+ domOrder,
474
+ shapeType: 'rect',
475
+ options: {
476
+ x: x,
477
+ y: y,
478
+ w: sides.left.width * pxToInch * scale,
479
+ h: h,
480
+ fill: { color: sides.left.color },
481
+ },
482
+ });
483
+ }
484
+
485
+ return items;
486
+ }