dom-to-pptx 1.0.4 → 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.
- package/CHANGELOG.md +22 -18
- package/Readme.md +288 -249
- package/SUPPORTED.md +50 -50
- package/dist/dom-to-pptx.bundle.js +30193 -31091
- package/dist/dom-to-pptx.cjs +1478 -23576
- package/dist/dom-to-pptx.min.js +9044 -31141
- package/dist/dom-to-pptx.mjs +1458 -23578
- package/package.json +73 -73
- package/rollup.config.js +56 -44
- package/src/image-processor.js +79 -76
- package/src/index.js +657 -657
- package/src/utils.js +479 -452
package/src/index.js
CHANGED
|
@@ -1,657 +1,657 @@
|
|
|
1
|
-
// src/index.js
|
|
2
|
-
import * as PptxGenJSImport from 'pptxgenjs';
|
|
3
|
-
import html2canvas from 'html2canvas';
|
|
4
|
-
|
|
5
|
-
// Normalize import
|
|
6
|
-
const PptxGenJS = PptxGenJSImport?.default ?? PptxGenJSImport;
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
parseColor,
|
|
10
|
-
getTextStyle,
|
|
11
|
-
isTextContainer,
|
|
12
|
-
getVisibleShadow,
|
|
13
|
-
generateGradientSVG,
|
|
14
|
-
getRotation,
|
|
15
|
-
svgToPng,
|
|
16
|
-
getPadding,
|
|
17
|
-
getSoftEdges,
|
|
18
|
-
generateBlurredSVG,
|
|
19
|
-
getBorderInfo,
|
|
20
|
-
generateCompositeBorderSVG,
|
|
21
|
-
isClippedByParent,
|
|
22
|
-
generateCustomShapeSVG,
|
|
23
|
-
} from './utils.js';
|
|
24
|
-
import { getProcessedImage } from './image-processor.js';
|
|
25
|
-
|
|
26
|
-
const PPI = 96;
|
|
27
|
-
const PX_TO_INCH = 1 / PPI;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Main export function. Accepts single element or an array.
|
|
31
|
-
* @param {HTMLElement | string | Array<HTMLElement | string>} target - The root element(s) to convert.
|
|
32
|
-
* @param {Object} options - { fileName: string }
|
|
33
|
-
*/
|
|
34
|
-
export async function exportToPptx(target, options = {}) {
|
|
35
|
-
const resolvePptxConstructor = (pkg) => {
|
|
36
|
-
if (!pkg) return null;
|
|
37
|
-
if (typeof pkg === 'function') return pkg;
|
|
38
|
-
if (pkg && typeof pkg.default === 'function') return pkg.default;
|
|
39
|
-
if (pkg && typeof pkg.PptxGenJS === 'function') return pkg.PptxGenJS;
|
|
40
|
-
if (pkg && pkg.PptxGenJS && typeof pkg.PptxGenJS.default === 'function')
|
|
41
|
-
return pkg.PptxGenJS.default;
|
|
42
|
-
return null;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const PptxConstructor = resolvePptxConstructor(PptxGenJS);
|
|
46
|
-
if (!PptxConstructor) throw new Error('PptxGenJS constructor not found.');
|
|
47
|
-
const pptx = new PptxConstructor();
|
|
48
|
-
pptx.layout = 'LAYOUT_16x9';
|
|
49
|
-
|
|
50
|
-
const elements = Array.isArray(target) ? target : [target];
|
|
51
|
-
|
|
52
|
-
for (const el of elements) {
|
|
53
|
-
const root = typeof el === 'string' ? document.querySelector(el) : el;
|
|
54
|
-
if (!root) {
|
|
55
|
-
console.warn('Element not found, skipping slide:', el);
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
const slide = pptx.addSlide();
|
|
59
|
-
await processSlide(root, slide, pptx);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const fileName = options.fileName || 'export.pptx';
|
|
63
|
-
pptx.writeFile({ fileName });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Worker function to process a single DOM element into a single PPTX slide.
|
|
68
|
-
* @param {HTMLElement} root - The root element for this slide.
|
|
69
|
-
* @param {PptxGenJS.Slide} slide - The PPTX slide object to add content to.
|
|
70
|
-
* @param {PptxGenJS} pptx - The main PPTX instance.
|
|
71
|
-
*/
|
|
72
|
-
async function processSlide(root, slide, pptx) {
|
|
73
|
-
const rootRect = root.getBoundingClientRect();
|
|
74
|
-
const PPTX_WIDTH_IN = 10;
|
|
75
|
-
const PPTX_HEIGHT_IN = 5.625;
|
|
76
|
-
|
|
77
|
-
const contentWidthIn = rootRect.width * PX_TO_INCH;
|
|
78
|
-
const contentHeightIn = rootRect.height * PX_TO_INCH;
|
|
79
|
-
const scale = Math.min(PPTX_WIDTH_IN / contentWidthIn, PPTX_HEIGHT_IN / contentHeightIn);
|
|
80
|
-
|
|
81
|
-
const layoutConfig = {
|
|
82
|
-
rootX: rootRect.x,
|
|
83
|
-
rootY: rootRect.y,
|
|
84
|
-
scale: scale,
|
|
85
|
-
offX: (PPTX_WIDTH_IN - contentWidthIn * scale) / 2,
|
|
86
|
-
offY: (PPTX_HEIGHT_IN - contentHeightIn * scale) / 2,
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const renderQueue = [];
|
|
90
|
-
let domOrderCounter = 0;
|
|
91
|
-
|
|
92
|
-
async function collect(node) {
|
|
93
|
-
const order = domOrderCounter++;
|
|
94
|
-
const result = await createRenderItem(node, { ...layoutConfig, root }, order, pptx);
|
|
95
|
-
if (result) {
|
|
96
|
-
if (result.items) renderQueue.push(...result.items);
|
|
97
|
-
if (result.stopRecursion) return;
|
|
98
|
-
}
|
|
99
|
-
for (const child of node.children) await collect(child);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
await collect(root);
|
|
103
|
-
|
|
104
|
-
renderQueue.sort((a, b) => {
|
|
105
|
-
if (a.zIndex !== b.zIndex) return a.zIndex - b.zIndex;
|
|
106
|
-
return a.domOrder - b.domOrder;
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
for (const item of renderQueue) {
|
|
110
|
-
if (item.type === 'shape') slide.addShape(item.shapeType, item.options);
|
|
111
|
-
if (item.type === 'image') slide.addImage(item.options);
|
|
112
|
-
if (item.type === 'text') slide.addText(item.textParts, item.options);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function elementToCanvasImage(node, widthPx, heightPx, root) {
|
|
117
|
-
return new Promise((resolve) => {
|
|
118
|
-
const width = Math.ceil(widthPx);
|
|
119
|
-
const height = Math.ceil(heightPx);
|
|
120
|
-
|
|
121
|
-
if (width <= 0 || height <= 0) {
|
|
122
|
-
resolve(null);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const style = window.getComputedStyle(node);
|
|
127
|
-
|
|
128
|
-
html2canvas(root, {
|
|
129
|
-
width: root.scrollWidth,
|
|
130
|
-
height: root.scrollHeight,
|
|
131
|
-
useCORS: true,
|
|
132
|
-
allowTaint: true,
|
|
133
|
-
backgroundColor: null,
|
|
134
|
-
})
|
|
135
|
-
.then((canvas) => {
|
|
136
|
-
const rootCanvas = canvas;
|
|
137
|
-
const nodeRect = node.getBoundingClientRect();
|
|
138
|
-
const rootRect = root.getBoundingClientRect();
|
|
139
|
-
const sourceX = nodeRect.left - rootRect.left;
|
|
140
|
-
const sourceY = nodeRect.top - rootRect.top;
|
|
141
|
-
|
|
142
|
-
const destCanvas = document.createElement('canvas');
|
|
143
|
-
destCanvas.width = width;
|
|
144
|
-
destCanvas.height = height;
|
|
145
|
-
const ctx = destCanvas.getContext('2d');
|
|
146
|
-
|
|
147
|
-
ctx.drawImage(rootCanvas, sourceX, sourceY, width, height, 0, 0, width, height);
|
|
148
|
-
|
|
149
|
-
// Parse radii
|
|
150
|
-
let tl = parseFloat(style.borderTopLeftRadius) || 0;
|
|
151
|
-
let tr = parseFloat(style.borderTopRightRadius) || 0;
|
|
152
|
-
let br = parseFloat(style.borderBottomRightRadius) || 0;
|
|
153
|
-
let bl = parseFloat(style.borderBottomLeftRadius) || 0;
|
|
154
|
-
|
|
155
|
-
const f = Math.min(
|
|
156
|
-
width / (tl + tr) || Infinity,
|
|
157
|
-
height / (tr + br) || Infinity,
|
|
158
|
-
width / (br + bl) || Infinity,
|
|
159
|
-
height / (bl + tl) || Infinity
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
if (f < 1) {
|
|
163
|
-
tl *= f;
|
|
164
|
-
tr *= f;
|
|
165
|
-
br *= f;
|
|
166
|
-
bl *= f;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
ctx.globalCompositeOperation = 'destination-in';
|
|
170
|
-
ctx.beginPath();
|
|
171
|
-
ctx.moveTo(tl, 0);
|
|
172
|
-
ctx.lineTo(width - tr, 0);
|
|
173
|
-
ctx.arcTo(width, 0, width, tr, tr);
|
|
174
|
-
ctx.lineTo(width, height - br);
|
|
175
|
-
ctx.arcTo(width, height, width - br, height, br);
|
|
176
|
-
ctx.lineTo(bl, height);
|
|
177
|
-
ctx.arcTo(0, height, 0, height - bl, bl);
|
|
178
|
-
ctx.lineTo(0, tl);
|
|
179
|
-
ctx.arcTo(0, 0, tl, 0, tl);
|
|
180
|
-
ctx.closePath();
|
|
181
|
-
ctx.fill();
|
|
182
|
-
|
|
183
|
-
resolve(destCanvas.toDataURL('image/png'));
|
|
184
|
-
})
|
|
185
|
-
.catch(() => resolve(null));
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async function createRenderItem(node, config, domOrder, pptx) {
|
|
190
|
-
if (node.nodeType !== 1) return null;
|
|
191
|
-
const style = window.getComputedStyle(node);
|
|
192
|
-
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')
|
|
193
|
-
return null;
|
|
194
|
-
|
|
195
|
-
const rect = node.getBoundingClientRect();
|
|
196
|
-
if (rect.width < 0.5 || rect.height < 0.5) return null;
|
|
197
|
-
|
|
198
|
-
const zIndex = style.zIndex !== 'auto' ? parseInt(style.zIndex) : 0;
|
|
199
|
-
const rotation = getRotation(style.transform);
|
|
200
|
-
const elementOpacity = parseFloat(style.opacity);
|
|
201
|
-
|
|
202
|
-
const widthPx = node.offsetWidth || rect.width;
|
|
203
|
-
const heightPx = node.offsetHeight || rect.height;
|
|
204
|
-
const unrotatedW = widthPx * PX_TO_INCH * config.scale;
|
|
205
|
-
const unrotatedH = heightPx * PX_TO_INCH * config.scale;
|
|
206
|
-
const centerX = rect.left + rect.width / 2;
|
|
207
|
-
const centerY = rect.top + rect.height / 2;
|
|
208
|
-
|
|
209
|
-
let x = config.offX + (centerX - config.rootX) * PX_TO_INCH * config.scale - unrotatedW / 2;
|
|
210
|
-
let y = config.offY + (centerY - config.rootY) * PX_TO_INCH * config.scale - unrotatedH / 2;
|
|
211
|
-
let w = unrotatedW;
|
|
212
|
-
let h = unrotatedH;
|
|
213
|
-
|
|
214
|
-
const items = [];
|
|
215
|
-
|
|
216
|
-
if (node.nodeName.toUpperCase() === 'SVG') {
|
|
217
|
-
const pngData = await svgToPng(node);
|
|
218
|
-
if (pngData)
|
|
219
|
-
items.push({
|
|
220
|
-
type: 'image',
|
|
221
|
-
zIndex,
|
|
222
|
-
domOrder,
|
|
223
|
-
options: { data: pngData, x, y, w, h, rotate: rotation },
|
|
224
|
-
});
|
|
225
|
-
return { items, stopRecursion: true };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// --- UPDATED IMG BLOCK START ---
|
|
229
|
-
if (node.tagName === 'IMG') {
|
|
230
|
-
// Extract individual corner radii
|
|
231
|
-
let radii = {
|
|
232
|
-
tl: parseFloat(style.borderTopLeftRadius) || 0,
|
|
233
|
-
tr: parseFloat(style.borderTopRightRadius) || 0,
|
|
234
|
-
br: parseFloat(style.borderBottomRightRadius) || 0,
|
|
235
|
-
bl: parseFloat(style.borderBottomLeftRadius) || 0,
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
const hasAnyRadius = radii.tl > 0 || radii.tr > 0 || radii.br > 0 || radii.bl > 0;
|
|
239
|
-
|
|
240
|
-
// Fallback: Check parent if image has no specific radius but parent clips it
|
|
241
|
-
if (!hasAnyRadius) {
|
|
242
|
-
const parent = node.parentElement;
|
|
243
|
-
const parentStyle = window.getComputedStyle(parent);
|
|
244
|
-
if (parentStyle.overflow !== 'visible') {
|
|
245
|
-
const pRadii = {
|
|
246
|
-
tl: parseFloat(parentStyle.borderTopLeftRadius) || 0,
|
|
247
|
-
tr: parseFloat(parentStyle.borderTopRightRadius) || 0,
|
|
248
|
-
br: parseFloat(parentStyle.borderBottomRightRadius) || 0,
|
|
249
|
-
bl: parseFloat(parentStyle.borderBottomLeftRadius) || 0,
|
|
250
|
-
};
|
|
251
|
-
// Simple heuristic: If image takes up full size of parent, inherit radii.
|
|
252
|
-
// For complex grids (like slide-1), this blindly applies parent radius.
|
|
253
|
-
// In a perfect world, we'd calculate intersection, but for now we apply parent radius
|
|
254
|
-
// if the image is close to the parent's size, effectively masking it.
|
|
255
|
-
const pRect = parent.getBoundingClientRect();
|
|
256
|
-
if (Math.abs(pRect.width - rect.width) < 5 && Math.abs(pRect.height - rect.height) < 5) {
|
|
257
|
-
radii = pRadii;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const processed = await getProcessedImage(node.src, widthPx, heightPx, radii);
|
|
263
|
-
if (processed)
|
|
264
|
-
items.push({
|
|
265
|
-
type: 'image',
|
|
266
|
-
zIndex,
|
|
267
|
-
domOrder,
|
|
268
|
-
options: { data: processed, x, y, w, h, rotate: rotation },
|
|
269
|
-
});
|
|
270
|
-
return { items, stopRecursion: true };
|
|
271
|
-
}
|
|
272
|
-
// --- UPDATED IMG BLOCK END ---
|
|
273
|
-
|
|
274
|
-
// Radii processing for Divs/Shapes
|
|
275
|
-
const borderRadiusValue = parseFloat(style.borderRadius) || 0;
|
|
276
|
-
const borderBottomLeftRadius = parseFloat(style.borderBottomLeftRadius) || 0;
|
|
277
|
-
const borderBottomRightRadius = parseFloat(style.borderBottomRightRadius) || 0;
|
|
278
|
-
const borderTopLeftRadius = parseFloat(style.borderTopLeftRadius) || 0;
|
|
279
|
-
const borderTopRightRadius = parseFloat(style.borderTopRightRadius) || 0;
|
|
280
|
-
|
|
281
|
-
const hasPartialBorderRadius =
|
|
282
|
-
(borderBottomLeftRadius > 0 && borderBottomLeftRadius !== borderRadiusValue) ||
|
|
283
|
-
(borderBottomRightRadius > 0 && borderBottomRightRadius !== borderRadiusValue) ||
|
|
284
|
-
(borderTopLeftRadius > 0 && borderTopLeftRadius !== borderRadiusValue) ||
|
|
285
|
-
(borderTopRightRadius > 0 && borderTopRightRadius !== borderRadiusValue) ||
|
|
286
|
-
(borderRadiusValue === 0 &&
|
|
287
|
-
(borderBottomLeftRadius ||
|
|
288
|
-
borderBottomRightRadius ||
|
|
289
|
-
borderTopLeftRadius ||
|
|
290
|
-
borderTopRightRadius));
|
|
291
|
-
|
|
292
|
-
// Allow clipped elements to be rendered via canvas
|
|
293
|
-
if (hasPartialBorderRadius && isClippedByParent(node)) {
|
|
294
|
-
const marginLeft = parseFloat(style.marginLeft) || 0;
|
|
295
|
-
const marginTop = parseFloat(style.marginTop) || 0;
|
|
296
|
-
x += marginLeft * PX_TO_INCH * config.scale;
|
|
297
|
-
y += marginTop * PX_TO_INCH * config.scale;
|
|
298
|
-
|
|
299
|
-
const canvasImageData = await elementToCanvasImage(node, widthPx, heightPx, config.root);
|
|
300
|
-
if (canvasImageData) {
|
|
301
|
-
items.push({
|
|
302
|
-
type: 'image',
|
|
303
|
-
zIndex,
|
|
304
|
-
domOrder,
|
|
305
|
-
options: { data: canvasImageData, x, y, w, h, rotate: rotation },
|
|
306
|
-
});
|
|
307
|
-
return { items, stopRecursion: true };
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const bgColorObj = parseColor(style.backgroundColor);
|
|
312
|
-
const bgClip = style.webkitBackgroundClip || style.backgroundClip;
|
|
313
|
-
const isBgClipText = bgClip === 'text';
|
|
314
|
-
const hasGradient =
|
|
315
|
-
!isBgClipText && style.backgroundImage && style.backgroundImage.includes('linear-gradient');
|
|
316
|
-
|
|
317
|
-
const borderColorObj = parseColor(style.borderColor);
|
|
318
|
-
const borderWidth = parseFloat(style.borderWidth);
|
|
319
|
-
const hasBorder = borderWidth > 0 && borderColorObj.hex;
|
|
320
|
-
|
|
321
|
-
const borderInfo = getBorderInfo(style, config.scale);
|
|
322
|
-
const hasUniformBorder = borderInfo.type === 'uniform';
|
|
323
|
-
const hasCompositeBorder = borderInfo.type === 'composite';
|
|
324
|
-
|
|
325
|
-
const shadowStr = style.boxShadow;
|
|
326
|
-
const hasShadow = shadowStr && shadowStr !== 'none';
|
|
327
|
-
const softEdge = getSoftEdges(style.filter, config.scale);
|
|
328
|
-
|
|
329
|
-
let isImageWrapper = false;
|
|
330
|
-
const imgChild = Array.from(node.children).find((c) => c.tagName === 'IMG');
|
|
331
|
-
if (imgChild) {
|
|
332
|
-
const childW = imgChild.offsetWidth || imgChild.getBoundingClientRect().width;
|
|
333
|
-
const childH = imgChild.offsetHeight || imgChild.getBoundingClientRect().height;
|
|
334
|
-
if (childW >= widthPx - 2 && childH >= heightPx - 2) isImageWrapper = true;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
let textPayload = null;
|
|
338
|
-
const isText = isTextContainer(node);
|
|
339
|
-
|
|
340
|
-
if (isText) {
|
|
341
|
-
const textParts = [];
|
|
342
|
-
const isList = style.display === 'list-item';
|
|
343
|
-
if (isList) {
|
|
344
|
-
const fontSizePt = parseFloat(style.fontSize) * 0.75 * config.scale;
|
|
345
|
-
const bulletShift = (parseFloat(style.fontSize) || 16) * PX_TO_INCH * config.scale * 1.5;
|
|
346
|
-
x -= bulletShift;
|
|
347
|
-
w += bulletShift;
|
|
348
|
-
textParts.push({
|
|
349
|
-
text: '• ',
|
|
350
|
-
options: {
|
|
351
|
-
color: parseColor(style.color).hex || '000000',
|
|
352
|
-
fontSize: fontSizePt,
|
|
353
|
-
},
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
node.childNodes.forEach((child, index) => {
|
|
358
|
-
let textVal = child.nodeType === 3 ? child.nodeValue : child.textContent;
|
|
359
|
-
let nodeStyle = child.nodeType === 1 ? window.getComputedStyle(child) : style;
|
|
360
|
-
textVal = textVal.replace(/[\n\r\t]+/g, ' ').replace(/\s{2,}/g, ' ');
|
|
361
|
-
if (index === 0 && !isList) textVal = textVal.trimStart();
|
|
362
|
-
else if (index === 0) textVal = textVal.trimStart();
|
|
363
|
-
if (index === node.childNodes.length - 1) textVal = textVal.trimEnd();
|
|
364
|
-
if (nodeStyle.textTransform === 'uppercase') textVal = textVal.toUpperCase();
|
|
365
|
-
if (nodeStyle.textTransform === 'lowercase') textVal = textVal.toLowerCase();
|
|
366
|
-
|
|
367
|
-
if (textVal.length > 0) {
|
|
368
|
-
textParts.push({
|
|
369
|
-
text: textVal,
|
|
370
|
-
options: getTextStyle(nodeStyle, config.scale),
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
if (textParts.length > 0) {
|
|
376
|
-
let align = style.textAlign || 'left';
|
|
377
|
-
if (align === 'start') align = 'left';
|
|
378
|
-
if (align === 'end') align = 'right';
|
|
379
|
-
let valign = 'top';
|
|
380
|
-
if (style.alignItems === 'center') valign = 'middle';
|
|
381
|
-
if (style.justifyContent === 'center' && style.display.includes('flex')) align = 'center';
|
|
382
|
-
|
|
383
|
-
const pt = parseFloat(style.paddingTop) || 0;
|
|
384
|
-
const pb = parseFloat(style.paddingBottom) || 0;
|
|
385
|
-
if (Math.abs(pt - pb) < 2 && bgColorObj.hex) valign = 'middle';
|
|
386
|
-
|
|
387
|
-
let padding = getPadding(style, config.scale);
|
|
388
|
-
if (align === 'center' && valign === 'middle') padding = [0, 0, 0, 0];
|
|
389
|
-
|
|
390
|
-
textPayload = { text: textParts, align, valign, inset: padding };
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (hasGradient || (softEdge && bgColorObj.hex && !isImageWrapper)) {
|
|
395
|
-
let bgData = null;
|
|
396
|
-
let padIn = 0;
|
|
397
|
-
if (softEdge) {
|
|
398
|
-
const svgInfo = generateBlurredSVG(
|
|
399
|
-
widthPx,
|
|
400
|
-
heightPx,
|
|
401
|
-
bgColorObj.hex,
|
|
402
|
-
borderRadiusValue,
|
|
403
|
-
softEdge
|
|
404
|
-
);
|
|
405
|
-
bgData = svgInfo.data;
|
|
406
|
-
padIn = svgInfo.padding * PX_TO_INCH * config.scale;
|
|
407
|
-
} else {
|
|
408
|
-
bgData = generateGradientSVG(
|
|
409
|
-
widthPx,
|
|
410
|
-
heightPx,
|
|
411
|
-
style.backgroundImage,
|
|
412
|
-
borderRadiusValue,
|
|
413
|
-
hasBorder ? { color: borderColorObj.hex, width: borderWidth } : null
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (bgData) {
|
|
418
|
-
items.push({
|
|
419
|
-
type: 'image',
|
|
420
|
-
zIndex,
|
|
421
|
-
domOrder,
|
|
422
|
-
options: {
|
|
423
|
-
data: bgData,
|
|
424
|
-
x: x - padIn,
|
|
425
|
-
y: y - padIn,
|
|
426
|
-
w: w + padIn * 2,
|
|
427
|
-
h: h + padIn * 2,
|
|
428
|
-
rotate: rotation,
|
|
429
|
-
},
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (textPayload) {
|
|
434
|
-
items.push({
|
|
435
|
-
type: 'text',
|
|
436
|
-
zIndex: zIndex + 1,
|
|
437
|
-
domOrder,
|
|
438
|
-
textParts: textPayload.text,
|
|
439
|
-
options: {
|
|
440
|
-
x,
|
|
441
|
-
y,
|
|
442
|
-
w,
|
|
443
|
-
h,
|
|
444
|
-
align: textPayload.align,
|
|
445
|
-
valign: textPayload.valign,
|
|
446
|
-
inset: textPayload.inset,
|
|
447
|
-
rotate: rotation,
|
|
448
|
-
margin: 0,
|
|
449
|
-
wrap: true,
|
|
450
|
-
autoFit: false,
|
|
451
|
-
},
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
if (hasCompositeBorder) {
|
|
455
|
-
// Add border shapes after the main background
|
|
456
|
-
const borderItems = createCompositeBorderItems(
|
|
457
|
-
borderInfo.sides,
|
|
458
|
-
x,
|
|
459
|
-
y,
|
|
460
|
-
w,
|
|
461
|
-
h,
|
|
462
|
-
config.scale,
|
|
463
|
-
zIndex,
|
|
464
|
-
domOrder
|
|
465
|
-
);
|
|
466
|
-
items.push(...borderItems);
|
|
467
|
-
}
|
|
468
|
-
} else if (
|
|
469
|
-
(bgColorObj.hex && !isImageWrapper) ||
|
|
470
|
-
hasUniformBorder ||
|
|
471
|
-
hasCompositeBorder ||
|
|
472
|
-
hasShadow ||
|
|
473
|
-
textPayload
|
|
474
|
-
) {
|
|
475
|
-
const finalAlpha = elementOpacity * bgColorObj.opacity;
|
|
476
|
-
const transparency = (1 - finalAlpha) * 100;
|
|
477
|
-
const useSolidFill = bgColorObj.hex && !isImageWrapper;
|
|
478
|
-
|
|
479
|
-
if (hasPartialBorderRadius && useSolidFill && !textPayload) {
|
|
480
|
-
const shapeSvg = generateCustomShapeSVG(
|
|
481
|
-
widthPx,
|
|
482
|
-
heightPx,
|
|
483
|
-
bgColorObj.hex,
|
|
484
|
-
bgColorObj.opacity,
|
|
485
|
-
{
|
|
486
|
-
tl: parseFloat(style.borderTopLeftRadius) || 0,
|
|
487
|
-
tr: parseFloat(style.borderTopRightRadius) || 0,
|
|
488
|
-
br: parseFloat(style.borderBottomRightRadius) || 0,
|
|
489
|
-
bl: parseFloat(style.borderBottomLeftRadius) || 0,
|
|
490
|
-
}
|
|
491
|
-
);
|
|
492
|
-
|
|
493
|
-
items.push({
|
|
494
|
-
type: 'image',
|
|
495
|
-
zIndex,
|
|
496
|
-
domOrder,
|
|
497
|
-
options: {
|
|
498
|
-
data: shapeSvg,
|
|
499
|
-
x,
|
|
500
|
-
y,
|
|
501
|
-
w,
|
|
502
|
-
h,
|
|
503
|
-
rotate: rotation,
|
|
504
|
-
},
|
|
505
|
-
});
|
|
506
|
-
} else {
|
|
507
|
-
const shapeOpts = {
|
|
508
|
-
x,
|
|
509
|
-
y,
|
|
510
|
-
w,
|
|
511
|
-
h,
|
|
512
|
-
rotate: rotation,
|
|
513
|
-
fill: useSolidFill
|
|
514
|
-
? { color: bgColorObj.hex, transparency: transparency }
|
|
515
|
-
: { type: 'none' },
|
|
516
|
-
line: hasUniformBorder ? borderInfo.options : null,
|
|
517
|
-
};
|
|
518
|
-
|
|
519
|
-
if (hasShadow) {
|
|
520
|
-
shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
const borderRadius = parseFloat(style.borderRadius) || 0;
|
|
524
|
-
const aspectRatio = Math.max(widthPx, heightPx) / Math.min(widthPx, heightPx);
|
|
525
|
-
const isCircle = aspectRatio < 1.1 && borderRadius >= Math.min(widthPx, heightPx) / 2 - 1;
|
|
526
|
-
|
|
527
|
-
let shapeType = pptx.ShapeType.rect;
|
|
528
|
-
if (isCircle) shapeType = pptx.ShapeType.ellipse;
|
|
529
|
-
else if (borderRadius > 0) {
|
|
530
|
-
shapeType = pptx.ShapeType.roundRect;
|
|
531
|
-
shapeOpts.rectRadius = Math.min(0.5, borderRadius / Math.min(widthPx, heightPx));
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
if (textPayload) {
|
|
535
|
-
const textOptions = {
|
|
536
|
-
shape: shapeType,
|
|
537
|
-
...shapeOpts,
|
|
538
|
-
align: textPayload.align,
|
|
539
|
-
valign: textPayload.valign,
|
|
540
|
-
inset: textPayload.inset,
|
|
541
|
-
margin: 0,
|
|
542
|
-
wrap: true,
|
|
543
|
-
autoFit: false,
|
|
544
|
-
};
|
|
545
|
-
items.push({
|
|
546
|
-
type: 'text',
|
|
547
|
-
zIndex,
|
|
548
|
-
domOrder,
|
|
549
|
-
textParts: textPayload.text,
|
|
550
|
-
options: textOptions,
|
|
551
|
-
});
|
|
552
|
-
} else if (!hasPartialBorderRadius) {
|
|
553
|
-
items.push({
|
|
554
|
-
type: 'shape',
|
|
555
|
-
zIndex,
|
|
556
|
-
domOrder,
|
|
557
|
-
shapeType,
|
|
558
|
-
options: shapeOpts,
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if (hasCompositeBorder) {
|
|
564
|
-
const borderSvgData = generateCompositeBorderSVG(
|
|
565
|
-
widthPx,
|
|
566
|
-
heightPx,
|
|
567
|
-
borderRadiusValue,
|
|
568
|
-
borderInfo.sides
|
|
569
|
-
);
|
|
570
|
-
if (borderSvgData) {
|
|
571
|
-
items.push({
|
|
572
|
-
type: 'image',
|
|
573
|
-
zIndex: zIndex + 1,
|
|
574
|
-
domOrder,
|
|
575
|
-
options: { data: borderSvgData, x, y, w, h, rotate: rotation },
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
return { items, stopRecursion: !!textPayload };
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* Helper function to create individual border shapes
|
|
586
|
-
*/
|
|
587
|
-
function createCompositeBorderItems(sides, x, y, w, h, scale, zIndex, domOrder) {
|
|
588
|
-
const items = [];
|
|
589
|
-
const pxToInch = 1 / 96;
|
|
590
|
-
|
|
591
|
-
// TOP BORDER
|
|
592
|
-
if (sides.top.width > 0) {
|
|
593
|
-
items.push({
|
|
594
|
-
type: 'shape',
|
|
595
|
-
zIndex: zIndex + 1,
|
|
596
|
-
domOrder,
|
|
597
|
-
shapeType: 'rect',
|
|
598
|
-
options: {
|
|
599
|
-
x: x,
|
|
600
|
-
y: y,
|
|
601
|
-
w: w,
|
|
602
|
-
h: sides.top.width * pxToInch * scale,
|
|
603
|
-
fill: { color: sides.top.color },
|
|
604
|
-
},
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
// RIGHT BORDER
|
|
608
|
-
if (sides.right.width > 0) {
|
|
609
|
-
items.push({
|
|
610
|
-
type: 'shape',
|
|
611
|
-
zIndex: zIndex + 1,
|
|
612
|
-
domOrder,
|
|
613
|
-
shapeType: 'rect',
|
|
614
|
-
options: {
|
|
615
|
-
x: x + w - sides.right.width * pxToInch * scale,
|
|
616
|
-
y: y,
|
|
617
|
-
w: sides.right.width * pxToInch * scale,
|
|
618
|
-
h: h,
|
|
619
|
-
fill: { color: sides.right.color },
|
|
620
|
-
},
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
// BOTTOM BORDER
|
|
624
|
-
if (sides.bottom.width > 0) {
|
|
625
|
-
items.push({
|
|
626
|
-
type: 'shape',
|
|
627
|
-
zIndex: zIndex + 1,
|
|
628
|
-
domOrder,
|
|
629
|
-
shapeType: 'rect',
|
|
630
|
-
options: {
|
|
631
|
-
x: x,
|
|
632
|
-
y: y + h - sides.bottom.width * pxToInch * scale,
|
|
633
|
-
w: w,
|
|
634
|
-
h: sides.bottom.width * pxToInch * scale,
|
|
635
|
-
fill: { color: sides.bottom.color },
|
|
636
|
-
},
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
// LEFT BORDER
|
|
640
|
-
if (sides.left.width > 0) {
|
|
641
|
-
items.push({
|
|
642
|
-
type: 'shape',
|
|
643
|
-
zIndex: zIndex + 1,
|
|
644
|
-
domOrder,
|
|
645
|
-
shapeType: 'rect',
|
|
646
|
-
options: {
|
|
647
|
-
x: x,
|
|
648
|
-
y: y,
|
|
649
|
-
w: sides.left.width * pxToInch * scale,
|
|
650
|
-
h: h,
|
|
651
|
-
fill: { color: sides.left.color },
|
|
652
|
-
},
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
return items;
|
|
657
|
-
}
|
|
1
|
+
// src/index.js
|
|
2
|
+
import * as PptxGenJSImport from 'pptxgenjs';
|
|
3
|
+
import html2canvas from 'html2canvas';
|
|
4
|
+
|
|
5
|
+
// Normalize import
|
|
6
|
+
const PptxGenJS = PptxGenJSImport?.default ?? PptxGenJSImport;
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
parseColor,
|
|
10
|
+
getTextStyle,
|
|
11
|
+
isTextContainer,
|
|
12
|
+
getVisibleShadow,
|
|
13
|
+
generateGradientSVG,
|
|
14
|
+
getRotation,
|
|
15
|
+
svgToPng,
|
|
16
|
+
getPadding,
|
|
17
|
+
getSoftEdges,
|
|
18
|
+
generateBlurredSVG,
|
|
19
|
+
getBorderInfo,
|
|
20
|
+
generateCompositeBorderSVG,
|
|
21
|
+
isClippedByParent,
|
|
22
|
+
generateCustomShapeSVG,
|
|
23
|
+
} from './utils.js';
|
|
24
|
+
import { getProcessedImage } from './image-processor.js';
|
|
25
|
+
|
|
26
|
+
const PPI = 96;
|
|
27
|
+
const PX_TO_INCH = 1 / PPI;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Main export function. Accepts single element or an array.
|
|
31
|
+
* @param {HTMLElement | string | Array<HTMLElement | string>} target - The root element(s) to convert.
|
|
32
|
+
* @param {Object} options - { fileName: string }
|
|
33
|
+
*/
|
|
34
|
+
export async function exportToPptx(target, options = {}) {
|
|
35
|
+
const resolvePptxConstructor = (pkg) => {
|
|
36
|
+
if (!pkg) return null;
|
|
37
|
+
if (typeof pkg === 'function') return pkg;
|
|
38
|
+
if (pkg && typeof pkg.default === 'function') return pkg.default;
|
|
39
|
+
if (pkg && typeof pkg.PptxGenJS === 'function') return pkg.PptxGenJS;
|
|
40
|
+
if (pkg && pkg.PptxGenJS && typeof pkg.PptxGenJS.default === 'function')
|
|
41
|
+
return pkg.PptxGenJS.default;
|
|
42
|
+
return null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const PptxConstructor = resolvePptxConstructor(PptxGenJS);
|
|
46
|
+
if (!PptxConstructor) throw new Error('PptxGenJS constructor not found.');
|
|
47
|
+
const pptx = new PptxConstructor();
|
|
48
|
+
pptx.layout = 'LAYOUT_16x9';
|
|
49
|
+
|
|
50
|
+
const elements = Array.isArray(target) ? target : [target];
|
|
51
|
+
|
|
52
|
+
for (const el of elements) {
|
|
53
|
+
const root = typeof el === 'string' ? document.querySelector(el) : el;
|
|
54
|
+
if (!root) {
|
|
55
|
+
console.warn('Element not found, skipping slide:', el);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const slide = pptx.addSlide();
|
|
59
|
+
await processSlide(root, slide, pptx);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const fileName = options.fileName || 'export.pptx';
|
|
63
|
+
pptx.writeFile({ fileName });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Worker function to process a single DOM element into a single PPTX slide.
|
|
68
|
+
* @param {HTMLElement} root - The root element for this slide.
|
|
69
|
+
* @param {PptxGenJS.Slide} slide - The PPTX slide object to add content to.
|
|
70
|
+
* @param {PptxGenJS} pptx - The main PPTX instance.
|
|
71
|
+
*/
|
|
72
|
+
async function processSlide(root, slide, pptx) {
|
|
73
|
+
const rootRect = root.getBoundingClientRect();
|
|
74
|
+
const PPTX_WIDTH_IN = 10;
|
|
75
|
+
const PPTX_HEIGHT_IN = 5.625;
|
|
76
|
+
|
|
77
|
+
const contentWidthIn = rootRect.width * PX_TO_INCH;
|
|
78
|
+
const contentHeightIn = rootRect.height * PX_TO_INCH;
|
|
79
|
+
const scale = Math.min(PPTX_WIDTH_IN / contentWidthIn, PPTX_HEIGHT_IN / contentHeightIn);
|
|
80
|
+
|
|
81
|
+
const layoutConfig = {
|
|
82
|
+
rootX: rootRect.x,
|
|
83
|
+
rootY: rootRect.y,
|
|
84
|
+
scale: scale,
|
|
85
|
+
offX: (PPTX_WIDTH_IN - contentWidthIn * scale) / 2,
|
|
86
|
+
offY: (PPTX_HEIGHT_IN - contentHeightIn * scale) / 2,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const renderQueue = [];
|
|
90
|
+
let domOrderCounter = 0;
|
|
91
|
+
|
|
92
|
+
async function collect(node) {
|
|
93
|
+
const order = domOrderCounter++;
|
|
94
|
+
const result = await createRenderItem(node, { ...layoutConfig, root }, order, pptx);
|
|
95
|
+
if (result) {
|
|
96
|
+
if (result.items) renderQueue.push(...result.items);
|
|
97
|
+
if (result.stopRecursion) return;
|
|
98
|
+
}
|
|
99
|
+
for (const child of node.children) await collect(child);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await collect(root);
|
|
103
|
+
|
|
104
|
+
renderQueue.sort((a, b) => {
|
|
105
|
+
if (a.zIndex !== b.zIndex) return a.zIndex - b.zIndex;
|
|
106
|
+
return a.domOrder - b.domOrder;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
for (const item of renderQueue) {
|
|
110
|
+
if (item.type === 'shape') slide.addShape(item.shapeType, item.options);
|
|
111
|
+
if (item.type === 'image') slide.addImage(item.options);
|
|
112
|
+
if (item.type === 'text') slide.addText(item.textParts, item.options);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function elementToCanvasImage(node, widthPx, heightPx, root) {
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
const width = Math.ceil(widthPx);
|
|
119
|
+
const height = Math.ceil(heightPx);
|
|
120
|
+
|
|
121
|
+
if (width <= 0 || height <= 0) {
|
|
122
|
+
resolve(null);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const style = window.getComputedStyle(node);
|
|
127
|
+
|
|
128
|
+
html2canvas(root, {
|
|
129
|
+
width: root.scrollWidth,
|
|
130
|
+
height: root.scrollHeight,
|
|
131
|
+
useCORS: true,
|
|
132
|
+
allowTaint: true,
|
|
133
|
+
backgroundColor: null,
|
|
134
|
+
})
|
|
135
|
+
.then((canvas) => {
|
|
136
|
+
const rootCanvas = canvas;
|
|
137
|
+
const nodeRect = node.getBoundingClientRect();
|
|
138
|
+
const rootRect = root.getBoundingClientRect();
|
|
139
|
+
const sourceX = nodeRect.left - rootRect.left;
|
|
140
|
+
const sourceY = nodeRect.top - rootRect.top;
|
|
141
|
+
|
|
142
|
+
const destCanvas = document.createElement('canvas');
|
|
143
|
+
destCanvas.width = width;
|
|
144
|
+
destCanvas.height = height;
|
|
145
|
+
const ctx = destCanvas.getContext('2d');
|
|
146
|
+
|
|
147
|
+
ctx.drawImage(rootCanvas, sourceX, sourceY, width, height, 0, 0, width, height);
|
|
148
|
+
|
|
149
|
+
// Parse radii
|
|
150
|
+
let tl = parseFloat(style.borderTopLeftRadius) || 0;
|
|
151
|
+
let tr = parseFloat(style.borderTopRightRadius) || 0;
|
|
152
|
+
let br = parseFloat(style.borderBottomRightRadius) || 0;
|
|
153
|
+
let bl = parseFloat(style.borderBottomLeftRadius) || 0;
|
|
154
|
+
|
|
155
|
+
const f = Math.min(
|
|
156
|
+
width / (tl + tr) || Infinity,
|
|
157
|
+
height / (tr + br) || Infinity,
|
|
158
|
+
width / (br + bl) || Infinity,
|
|
159
|
+
height / (bl + tl) || Infinity
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (f < 1) {
|
|
163
|
+
tl *= f;
|
|
164
|
+
tr *= f;
|
|
165
|
+
br *= f;
|
|
166
|
+
bl *= f;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
ctx.globalCompositeOperation = 'destination-in';
|
|
170
|
+
ctx.beginPath();
|
|
171
|
+
ctx.moveTo(tl, 0);
|
|
172
|
+
ctx.lineTo(width - tr, 0);
|
|
173
|
+
ctx.arcTo(width, 0, width, tr, tr);
|
|
174
|
+
ctx.lineTo(width, height - br);
|
|
175
|
+
ctx.arcTo(width, height, width - br, height, br);
|
|
176
|
+
ctx.lineTo(bl, height);
|
|
177
|
+
ctx.arcTo(0, height, 0, height - bl, bl);
|
|
178
|
+
ctx.lineTo(0, tl);
|
|
179
|
+
ctx.arcTo(0, 0, tl, 0, tl);
|
|
180
|
+
ctx.closePath();
|
|
181
|
+
ctx.fill();
|
|
182
|
+
|
|
183
|
+
resolve(destCanvas.toDataURL('image/png'));
|
|
184
|
+
})
|
|
185
|
+
.catch(() => resolve(null));
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function createRenderItem(node, config, domOrder, pptx) {
|
|
190
|
+
if (node.nodeType !== 1) return null;
|
|
191
|
+
const style = window.getComputedStyle(node);
|
|
192
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')
|
|
193
|
+
return null;
|
|
194
|
+
|
|
195
|
+
const rect = node.getBoundingClientRect();
|
|
196
|
+
if (rect.width < 0.5 || rect.height < 0.5) return null;
|
|
197
|
+
|
|
198
|
+
const zIndex = style.zIndex !== 'auto' ? parseInt(style.zIndex) : 0;
|
|
199
|
+
const rotation = getRotation(style.transform);
|
|
200
|
+
const elementOpacity = parseFloat(style.opacity);
|
|
201
|
+
|
|
202
|
+
const widthPx = node.offsetWidth || rect.width;
|
|
203
|
+
const heightPx = node.offsetHeight || rect.height;
|
|
204
|
+
const unrotatedW = widthPx * PX_TO_INCH * config.scale;
|
|
205
|
+
const unrotatedH = heightPx * PX_TO_INCH * config.scale;
|
|
206
|
+
const centerX = rect.left + rect.width / 2;
|
|
207
|
+
const centerY = rect.top + rect.height / 2;
|
|
208
|
+
|
|
209
|
+
let x = config.offX + (centerX - config.rootX) * PX_TO_INCH * config.scale - unrotatedW / 2;
|
|
210
|
+
let y = config.offY + (centerY - config.rootY) * PX_TO_INCH * config.scale - unrotatedH / 2;
|
|
211
|
+
let w = unrotatedW;
|
|
212
|
+
let h = unrotatedH;
|
|
213
|
+
|
|
214
|
+
const items = [];
|
|
215
|
+
|
|
216
|
+
if (node.nodeName.toUpperCase() === 'SVG') {
|
|
217
|
+
const pngData = await svgToPng(node);
|
|
218
|
+
if (pngData)
|
|
219
|
+
items.push({
|
|
220
|
+
type: 'image',
|
|
221
|
+
zIndex,
|
|
222
|
+
domOrder,
|
|
223
|
+
options: { data: pngData, x, y, w, h, rotate: rotation },
|
|
224
|
+
});
|
|
225
|
+
return { items, stopRecursion: true };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- UPDATED IMG BLOCK START ---
|
|
229
|
+
if (node.tagName === 'IMG') {
|
|
230
|
+
// Extract individual corner radii
|
|
231
|
+
let radii = {
|
|
232
|
+
tl: parseFloat(style.borderTopLeftRadius) || 0,
|
|
233
|
+
tr: parseFloat(style.borderTopRightRadius) || 0,
|
|
234
|
+
br: parseFloat(style.borderBottomRightRadius) || 0,
|
|
235
|
+
bl: parseFloat(style.borderBottomLeftRadius) || 0,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const hasAnyRadius = radii.tl > 0 || radii.tr > 0 || radii.br > 0 || radii.bl > 0;
|
|
239
|
+
|
|
240
|
+
// Fallback: Check parent if image has no specific radius but parent clips it
|
|
241
|
+
if (!hasAnyRadius) {
|
|
242
|
+
const parent = node.parentElement;
|
|
243
|
+
const parentStyle = window.getComputedStyle(parent);
|
|
244
|
+
if (parentStyle.overflow !== 'visible') {
|
|
245
|
+
const pRadii = {
|
|
246
|
+
tl: parseFloat(parentStyle.borderTopLeftRadius) || 0,
|
|
247
|
+
tr: parseFloat(parentStyle.borderTopRightRadius) || 0,
|
|
248
|
+
br: parseFloat(parentStyle.borderBottomRightRadius) || 0,
|
|
249
|
+
bl: parseFloat(parentStyle.borderBottomLeftRadius) || 0,
|
|
250
|
+
};
|
|
251
|
+
// Simple heuristic: If image takes up full size of parent, inherit radii.
|
|
252
|
+
// For complex grids (like slide-1), this blindly applies parent radius.
|
|
253
|
+
// In a perfect world, we'd calculate intersection, but for now we apply parent radius
|
|
254
|
+
// if the image is close to the parent's size, effectively masking it.
|
|
255
|
+
const pRect = parent.getBoundingClientRect();
|
|
256
|
+
if (Math.abs(pRect.width - rect.width) < 5 && Math.abs(pRect.height - rect.height) < 5) {
|
|
257
|
+
radii = pRadii;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const processed = await getProcessedImage(node.src, widthPx, heightPx, radii);
|
|
263
|
+
if (processed)
|
|
264
|
+
items.push({
|
|
265
|
+
type: 'image',
|
|
266
|
+
zIndex,
|
|
267
|
+
domOrder,
|
|
268
|
+
options: { data: processed, x, y, w, h, rotate: rotation },
|
|
269
|
+
});
|
|
270
|
+
return { items, stopRecursion: true };
|
|
271
|
+
}
|
|
272
|
+
// --- UPDATED IMG BLOCK END ---
|
|
273
|
+
|
|
274
|
+
// Radii processing for Divs/Shapes
|
|
275
|
+
const borderRadiusValue = parseFloat(style.borderRadius) || 0;
|
|
276
|
+
const borderBottomLeftRadius = parseFloat(style.borderBottomLeftRadius) || 0;
|
|
277
|
+
const borderBottomRightRadius = parseFloat(style.borderBottomRightRadius) || 0;
|
|
278
|
+
const borderTopLeftRadius = parseFloat(style.borderTopLeftRadius) || 0;
|
|
279
|
+
const borderTopRightRadius = parseFloat(style.borderTopRightRadius) || 0;
|
|
280
|
+
|
|
281
|
+
const hasPartialBorderRadius =
|
|
282
|
+
(borderBottomLeftRadius > 0 && borderBottomLeftRadius !== borderRadiusValue) ||
|
|
283
|
+
(borderBottomRightRadius > 0 && borderBottomRightRadius !== borderRadiusValue) ||
|
|
284
|
+
(borderTopLeftRadius > 0 && borderTopLeftRadius !== borderRadiusValue) ||
|
|
285
|
+
(borderTopRightRadius > 0 && borderTopRightRadius !== borderRadiusValue) ||
|
|
286
|
+
(borderRadiusValue === 0 &&
|
|
287
|
+
(borderBottomLeftRadius ||
|
|
288
|
+
borderBottomRightRadius ||
|
|
289
|
+
borderTopLeftRadius ||
|
|
290
|
+
borderTopRightRadius));
|
|
291
|
+
|
|
292
|
+
// Allow clipped elements to be rendered via canvas
|
|
293
|
+
if (hasPartialBorderRadius && isClippedByParent(node)) {
|
|
294
|
+
const marginLeft = parseFloat(style.marginLeft) || 0;
|
|
295
|
+
const marginTop = parseFloat(style.marginTop) || 0;
|
|
296
|
+
x += marginLeft * PX_TO_INCH * config.scale;
|
|
297
|
+
y += marginTop * PX_TO_INCH * config.scale;
|
|
298
|
+
|
|
299
|
+
const canvasImageData = await elementToCanvasImage(node, widthPx, heightPx, config.root);
|
|
300
|
+
if (canvasImageData) {
|
|
301
|
+
items.push({
|
|
302
|
+
type: 'image',
|
|
303
|
+
zIndex,
|
|
304
|
+
domOrder,
|
|
305
|
+
options: { data: canvasImageData, x, y, w, h, rotate: rotation },
|
|
306
|
+
});
|
|
307
|
+
return { items, stopRecursion: true };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const bgColorObj = parseColor(style.backgroundColor);
|
|
312
|
+
const bgClip = style.webkitBackgroundClip || style.backgroundClip;
|
|
313
|
+
const isBgClipText = bgClip === 'text';
|
|
314
|
+
const hasGradient =
|
|
315
|
+
!isBgClipText && style.backgroundImage && style.backgroundImage.includes('linear-gradient');
|
|
316
|
+
|
|
317
|
+
const borderColorObj = parseColor(style.borderColor);
|
|
318
|
+
const borderWidth = parseFloat(style.borderWidth);
|
|
319
|
+
const hasBorder = borderWidth > 0 && borderColorObj.hex;
|
|
320
|
+
|
|
321
|
+
const borderInfo = getBorderInfo(style, config.scale);
|
|
322
|
+
const hasUniformBorder = borderInfo.type === 'uniform';
|
|
323
|
+
const hasCompositeBorder = borderInfo.type === 'composite';
|
|
324
|
+
|
|
325
|
+
const shadowStr = style.boxShadow;
|
|
326
|
+
const hasShadow = shadowStr && shadowStr !== 'none';
|
|
327
|
+
const softEdge = getSoftEdges(style.filter, config.scale);
|
|
328
|
+
|
|
329
|
+
let isImageWrapper = false;
|
|
330
|
+
const imgChild = Array.from(node.children).find((c) => c.tagName === 'IMG');
|
|
331
|
+
if (imgChild) {
|
|
332
|
+
const childW = imgChild.offsetWidth || imgChild.getBoundingClientRect().width;
|
|
333
|
+
const childH = imgChild.offsetHeight || imgChild.getBoundingClientRect().height;
|
|
334
|
+
if (childW >= widthPx - 2 && childH >= heightPx - 2) isImageWrapper = true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let textPayload = null;
|
|
338
|
+
const isText = isTextContainer(node);
|
|
339
|
+
|
|
340
|
+
if (isText) {
|
|
341
|
+
const textParts = [];
|
|
342
|
+
const isList = style.display === 'list-item';
|
|
343
|
+
if (isList) {
|
|
344
|
+
const fontSizePt = parseFloat(style.fontSize) * 0.75 * config.scale;
|
|
345
|
+
const bulletShift = (parseFloat(style.fontSize) || 16) * PX_TO_INCH * config.scale * 1.5;
|
|
346
|
+
x -= bulletShift;
|
|
347
|
+
w += bulletShift;
|
|
348
|
+
textParts.push({
|
|
349
|
+
text: '• ',
|
|
350
|
+
options: {
|
|
351
|
+
color: parseColor(style.color).hex || '000000',
|
|
352
|
+
fontSize: fontSizePt,
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
node.childNodes.forEach((child, index) => {
|
|
358
|
+
let textVal = child.nodeType === 3 ? child.nodeValue : child.textContent;
|
|
359
|
+
let nodeStyle = child.nodeType === 1 ? window.getComputedStyle(child) : style;
|
|
360
|
+
textVal = textVal.replace(/[\n\r\t]+/g, ' ').replace(/\s{2,}/g, ' ');
|
|
361
|
+
if (index === 0 && !isList) textVal = textVal.trimStart();
|
|
362
|
+
else if (index === 0) textVal = textVal.trimStart();
|
|
363
|
+
if (index === node.childNodes.length - 1) textVal = textVal.trimEnd();
|
|
364
|
+
if (nodeStyle.textTransform === 'uppercase') textVal = textVal.toUpperCase();
|
|
365
|
+
if (nodeStyle.textTransform === 'lowercase') textVal = textVal.toLowerCase();
|
|
366
|
+
|
|
367
|
+
if (textVal.length > 0) {
|
|
368
|
+
textParts.push({
|
|
369
|
+
text: textVal,
|
|
370
|
+
options: getTextStyle(nodeStyle, config.scale),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
if (textParts.length > 0) {
|
|
376
|
+
let align = style.textAlign || 'left';
|
|
377
|
+
if (align === 'start') align = 'left';
|
|
378
|
+
if (align === 'end') align = 'right';
|
|
379
|
+
let valign = 'top';
|
|
380
|
+
if (style.alignItems === 'center') valign = 'middle';
|
|
381
|
+
if (style.justifyContent === 'center' && style.display.includes('flex')) align = 'center';
|
|
382
|
+
|
|
383
|
+
const pt = parseFloat(style.paddingTop) || 0;
|
|
384
|
+
const pb = parseFloat(style.paddingBottom) || 0;
|
|
385
|
+
if (Math.abs(pt - pb) < 2 && bgColorObj.hex) valign = 'middle';
|
|
386
|
+
|
|
387
|
+
let padding = getPadding(style, config.scale);
|
|
388
|
+
if (align === 'center' && valign === 'middle') padding = [0, 0, 0, 0];
|
|
389
|
+
|
|
390
|
+
textPayload = { text: textParts, align, valign, inset: padding };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (hasGradient || (softEdge && bgColorObj.hex && !isImageWrapper)) {
|
|
395
|
+
let bgData = null;
|
|
396
|
+
let padIn = 0;
|
|
397
|
+
if (softEdge) {
|
|
398
|
+
const svgInfo = generateBlurredSVG(
|
|
399
|
+
widthPx,
|
|
400
|
+
heightPx,
|
|
401
|
+
bgColorObj.hex,
|
|
402
|
+
borderRadiusValue,
|
|
403
|
+
softEdge
|
|
404
|
+
);
|
|
405
|
+
bgData = svgInfo.data;
|
|
406
|
+
padIn = svgInfo.padding * PX_TO_INCH * config.scale;
|
|
407
|
+
} else {
|
|
408
|
+
bgData = generateGradientSVG(
|
|
409
|
+
widthPx,
|
|
410
|
+
heightPx,
|
|
411
|
+
style.backgroundImage,
|
|
412
|
+
borderRadiusValue,
|
|
413
|
+
hasBorder ? { color: borderColorObj.hex, width: borderWidth } : null
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (bgData) {
|
|
418
|
+
items.push({
|
|
419
|
+
type: 'image',
|
|
420
|
+
zIndex,
|
|
421
|
+
domOrder,
|
|
422
|
+
options: {
|
|
423
|
+
data: bgData,
|
|
424
|
+
x: x - padIn,
|
|
425
|
+
y: y - padIn,
|
|
426
|
+
w: w + padIn * 2,
|
|
427
|
+
h: h + padIn * 2,
|
|
428
|
+
rotate: rotation,
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (textPayload) {
|
|
434
|
+
items.push({
|
|
435
|
+
type: 'text',
|
|
436
|
+
zIndex: zIndex + 1,
|
|
437
|
+
domOrder,
|
|
438
|
+
textParts: textPayload.text,
|
|
439
|
+
options: {
|
|
440
|
+
x,
|
|
441
|
+
y,
|
|
442
|
+
w,
|
|
443
|
+
h,
|
|
444
|
+
align: textPayload.align,
|
|
445
|
+
valign: textPayload.valign,
|
|
446
|
+
inset: textPayload.inset,
|
|
447
|
+
rotate: rotation,
|
|
448
|
+
margin: 0,
|
|
449
|
+
wrap: true,
|
|
450
|
+
autoFit: false,
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
if (hasCompositeBorder) {
|
|
455
|
+
// Add border shapes after the main background
|
|
456
|
+
const borderItems = createCompositeBorderItems(
|
|
457
|
+
borderInfo.sides,
|
|
458
|
+
x,
|
|
459
|
+
y,
|
|
460
|
+
w,
|
|
461
|
+
h,
|
|
462
|
+
config.scale,
|
|
463
|
+
zIndex,
|
|
464
|
+
domOrder
|
|
465
|
+
);
|
|
466
|
+
items.push(...borderItems);
|
|
467
|
+
}
|
|
468
|
+
} else if (
|
|
469
|
+
(bgColorObj.hex && !isImageWrapper) ||
|
|
470
|
+
hasUniformBorder ||
|
|
471
|
+
hasCompositeBorder ||
|
|
472
|
+
hasShadow ||
|
|
473
|
+
textPayload
|
|
474
|
+
) {
|
|
475
|
+
const finalAlpha = elementOpacity * bgColorObj.opacity;
|
|
476
|
+
const transparency = (1 - finalAlpha) * 100;
|
|
477
|
+
const useSolidFill = bgColorObj.hex && !isImageWrapper;
|
|
478
|
+
|
|
479
|
+
if (hasPartialBorderRadius && useSolidFill && !textPayload) {
|
|
480
|
+
const shapeSvg = generateCustomShapeSVG(
|
|
481
|
+
widthPx,
|
|
482
|
+
heightPx,
|
|
483
|
+
bgColorObj.hex,
|
|
484
|
+
bgColorObj.opacity,
|
|
485
|
+
{
|
|
486
|
+
tl: parseFloat(style.borderTopLeftRadius) || 0,
|
|
487
|
+
tr: parseFloat(style.borderTopRightRadius) || 0,
|
|
488
|
+
br: parseFloat(style.borderBottomRightRadius) || 0,
|
|
489
|
+
bl: parseFloat(style.borderBottomLeftRadius) || 0,
|
|
490
|
+
}
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
items.push({
|
|
494
|
+
type: 'image',
|
|
495
|
+
zIndex,
|
|
496
|
+
domOrder,
|
|
497
|
+
options: {
|
|
498
|
+
data: shapeSvg,
|
|
499
|
+
x,
|
|
500
|
+
y,
|
|
501
|
+
w,
|
|
502
|
+
h,
|
|
503
|
+
rotate: rotation,
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
} else {
|
|
507
|
+
const shapeOpts = {
|
|
508
|
+
x,
|
|
509
|
+
y,
|
|
510
|
+
w,
|
|
511
|
+
h,
|
|
512
|
+
rotate: rotation,
|
|
513
|
+
fill: useSolidFill
|
|
514
|
+
? { color: bgColorObj.hex, transparency: transparency }
|
|
515
|
+
: { type: 'none' },
|
|
516
|
+
line: hasUniformBorder ? borderInfo.options : null,
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
if (hasShadow) {
|
|
520
|
+
shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const borderRadius = parseFloat(style.borderRadius) || 0;
|
|
524
|
+
const aspectRatio = Math.max(widthPx, heightPx) / Math.min(widthPx, heightPx);
|
|
525
|
+
const isCircle = aspectRatio < 1.1 && borderRadius >= Math.min(widthPx, heightPx) / 2 - 1;
|
|
526
|
+
|
|
527
|
+
let shapeType = pptx.ShapeType.rect;
|
|
528
|
+
if (isCircle) shapeType = pptx.ShapeType.ellipse;
|
|
529
|
+
else if (borderRadius > 0) {
|
|
530
|
+
shapeType = pptx.ShapeType.roundRect;
|
|
531
|
+
shapeOpts.rectRadius = Math.min(0.5, borderRadius / Math.min(widthPx, heightPx));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (textPayload) {
|
|
535
|
+
const textOptions = {
|
|
536
|
+
shape: shapeType,
|
|
537
|
+
...shapeOpts,
|
|
538
|
+
align: textPayload.align,
|
|
539
|
+
valign: textPayload.valign,
|
|
540
|
+
inset: textPayload.inset,
|
|
541
|
+
margin: 0,
|
|
542
|
+
wrap: true,
|
|
543
|
+
autoFit: false,
|
|
544
|
+
};
|
|
545
|
+
items.push({
|
|
546
|
+
type: 'text',
|
|
547
|
+
zIndex,
|
|
548
|
+
domOrder,
|
|
549
|
+
textParts: textPayload.text,
|
|
550
|
+
options: textOptions,
|
|
551
|
+
});
|
|
552
|
+
} else if (!hasPartialBorderRadius) {
|
|
553
|
+
items.push({
|
|
554
|
+
type: 'shape',
|
|
555
|
+
zIndex,
|
|
556
|
+
domOrder,
|
|
557
|
+
shapeType,
|
|
558
|
+
options: shapeOpts,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (hasCompositeBorder) {
|
|
564
|
+
const borderSvgData = generateCompositeBorderSVG(
|
|
565
|
+
widthPx,
|
|
566
|
+
heightPx,
|
|
567
|
+
borderRadiusValue,
|
|
568
|
+
borderInfo.sides
|
|
569
|
+
);
|
|
570
|
+
if (borderSvgData) {
|
|
571
|
+
items.push({
|
|
572
|
+
type: 'image',
|
|
573
|
+
zIndex: zIndex + 1,
|
|
574
|
+
domOrder,
|
|
575
|
+
options: { data: borderSvgData, x, y, w, h, rotate: rotation },
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return { items, stopRecursion: !!textPayload };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Helper function to create individual border shapes
|
|
586
|
+
*/
|
|
587
|
+
function createCompositeBorderItems(sides, x, y, w, h, scale, zIndex, domOrder) {
|
|
588
|
+
const items = [];
|
|
589
|
+
const pxToInch = 1 / 96;
|
|
590
|
+
|
|
591
|
+
// TOP BORDER
|
|
592
|
+
if (sides.top.width > 0) {
|
|
593
|
+
items.push({
|
|
594
|
+
type: 'shape',
|
|
595
|
+
zIndex: zIndex + 1,
|
|
596
|
+
domOrder,
|
|
597
|
+
shapeType: 'rect',
|
|
598
|
+
options: {
|
|
599
|
+
x: x,
|
|
600
|
+
y: y,
|
|
601
|
+
w: w,
|
|
602
|
+
h: sides.top.width * pxToInch * scale,
|
|
603
|
+
fill: { color: sides.top.color },
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
// RIGHT BORDER
|
|
608
|
+
if (sides.right.width > 0) {
|
|
609
|
+
items.push({
|
|
610
|
+
type: 'shape',
|
|
611
|
+
zIndex: zIndex + 1,
|
|
612
|
+
domOrder,
|
|
613
|
+
shapeType: 'rect',
|
|
614
|
+
options: {
|
|
615
|
+
x: x + w - sides.right.width * pxToInch * scale,
|
|
616
|
+
y: y,
|
|
617
|
+
w: sides.right.width * pxToInch * scale,
|
|
618
|
+
h: h,
|
|
619
|
+
fill: { color: sides.right.color },
|
|
620
|
+
},
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
// BOTTOM BORDER
|
|
624
|
+
if (sides.bottom.width > 0) {
|
|
625
|
+
items.push({
|
|
626
|
+
type: 'shape',
|
|
627
|
+
zIndex: zIndex + 1,
|
|
628
|
+
domOrder,
|
|
629
|
+
shapeType: 'rect',
|
|
630
|
+
options: {
|
|
631
|
+
x: x,
|
|
632
|
+
y: y + h - sides.bottom.width * pxToInch * scale,
|
|
633
|
+
w: w,
|
|
634
|
+
h: sides.bottom.width * pxToInch * scale,
|
|
635
|
+
fill: { color: sides.bottom.color },
|
|
636
|
+
},
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
// LEFT BORDER
|
|
640
|
+
if (sides.left.width > 0) {
|
|
641
|
+
items.push({
|
|
642
|
+
type: 'shape',
|
|
643
|
+
zIndex: zIndex + 1,
|
|
644
|
+
domOrder,
|
|
645
|
+
shapeType: 'rect',
|
|
646
|
+
options: {
|
|
647
|
+
x: x,
|
|
648
|
+
y: y,
|
|
649
|
+
w: sides.left.width * pxToInch * scale,
|
|
650
|
+
h: h,
|
|
651
|
+
fill: { color: sides.left.color },
|
|
652
|
+
},
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return items;
|
|
657
|
+
}
|