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