dom-to-pptx 1.0.6 → 1.0.7
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 +9 -0
- package/{Readme.md → README.md} +329 -288
- package/dist/dom-to-pptx.bundle.js +16411 -27916
- package/dist/dom-to-pptx.cjs +1135 -1114
- package/dist/dom-to-pptx.min.js +1135 -1114
- package/dist/dom-to-pptx.mjs +1135 -1114
- package/package.json +1 -1
- package/rollup.config.js +13 -7
- package/src/index.js +731 -657
- package/src/utils.js +488 -479
package/src/utils.js
CHANGED
|
@@ -1,479 +1,488 @@
|
|
|
1
|
-
// src/utils.js
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Checks if any parent element has overflow: hidden which would clip this element
|
|
5
|
-
* @param {HTMLElement} node - The DOM node to check
|
|
6
|
-
* @returns {boolean} - True if a parent has overflow-hidden or overflow-clip
|
|
7
|
-
*/
|
|
8
|
-
export function isClippedByParent(node) {
|
|
9
|
-
let parent = node.parentElement;
|
|
10
|
-
while (parent && parent !== document.body) {
|
|
11
|
-
const style = window.getComputedStyle(parent);
|
|
12
|
-
const overflow = style.overflow;
|
|
13
|
-
if (overflow === 'hidden' || overflow === 'clip') {
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
16
|
-
parent = parent.parentElement;
|
|
17
|
-
}
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Helper to save gradient text
|
|
22
|
-
export function getGradientFallbackColor(bgImage) {
|
|
23
|
-
if (!bgImage) return null;
|
|
24
|
-
const hexMatch = bgImage.match(/#(?:[0-9a-fA-F]{3}){1,2}/);
|
|
25
|
-
if (hexMatch) return hexMatch[0];
|
|
26
|
-
const rgbMatch = bgImage.match(/rgba?\(.*?\)/);
|
|
27
|
-
if (rgbMatch) return rgbMatch[0];
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function mapDashType(style) {
|
|
32
|
-
if (style === 'dashed') return 'dash';
|
|
33
|
-
if (style === 'dotted') return 'dot';
|
|
34
|
-
return 'solid';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Analyzes computed border styles and determines the rendering strategy.
|
|
39
|
-
*/
|
|
40
|
-
export function getBorderInfo(style, scale) {
|
|
41
|
-
const top = {
|
|
42
|
-
width: parseFloat(style.borderTopWidth) || 0,
|
|
43
|
-
style: style.borderTopStyle,
|
|
44
|
-
color: parseColor(style.borderTopColor).hex,
|
|
45
|
-
};
|
|
46
|
-
const right = {
|
|
47
|
-
width: parseFloat(style.borderRightWidth) || 0,
|
|
48
|
-
style: style.borderRightStyle,
|
|
49
|
-
color: parseColor(style.borderRightColor).hex,
|
|
50
|
-
};
|
|
51
|
-
const bottom = {
|
|
52
|
-
width: parseFloat(style.borderBottomWidth) || 0,
|
|
53
|
-
style: style.borderBottomStyle,
|
|
54
|
-
color: parseColor(style.borderBottomColor).hex,
|
|
55
|
-
};
|
|
56
|
-
const left = {
|
|
57
|
-
width: parseFloat(style.borderLeftWidth) || 0,
|
|
58
|
-
style: style.borderLeftStyle,
|
|
59
|
-
color: parseColor(style.borderLeftColor).hex,
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const hasAnyBorder = top.width > 0 || right.width > 0 || bottom.width > 0 || left.width > 0;
|
|
63
|
-
if (!hasAnyBorder) return { type: 'none' };
|
|
64
|
-
|
|
65
|
-
// Check if all sides are uniform
|
|
66
|
-
const isUniform =
|
|
67
|
-
top.width === right.width &&
|
|
68
|
-
top.width === bottom.width &&
|
|
69
|
-
top.width === left.width &&
|
|
70
|
-
top.style === right.style &&
|
|
71
|
-
top.style === bottom.style &&
|
|
72
|
-
top.style === left.style &&
|
|
73
|
-
top.color === right.color &&
|
|
74
|
-
top.color === bottom.color &&
|
|
75
|
-
top.color === left.color;
|
|
76
|
-
|
|
77
|
-
if (isUniform) {
|
|
78
|
-
return {
|
|
79
|
-
type: 'uniform',
|
|
80
|
-
options: {
|
|
81
|
-
width: top.width * 0.75 * scale,
|
|
82
|
-
color: top.color,
|
|
83
|
-
transparency: (1 - parseColor(style.borderTopColor).opacity) * 100,
|
|
84
|
-
dashType: mapDashType(top.style),
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
} else {
|
|
88
|
-
return {
|
|
89
|
-
type: 'composite',
|
|
90
|
-
sides: { top, right, bottom, left },
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Generates an SVG image for composite borders that respects border-radius.
|
|
97
|
-
*/
|
|
98
|
-
export function generateCompositeBorderSVG(w, h, radius, sides) {
|
|
99
|
-
radius = radius / 2; // Adjust for SVG rendering
|
|
100
|
-
const clipId = 'clip_' + Math.random().toString(36).substr(2, 9);
|
|
101
|
-
let borderRects = '';
|
|
102
|
-
|
|
103
|
-
if (sides.top.width > 0 && sides.top.color) {
|
|
104
|
-
borderRects += `<rect x="0" y="0" width="${w}" height="${sides.top.width}" fill="#${sides.top.color}" />`;
|
|
105
|
-
}
|
|
106
|
-
if (sides.right.width > 0 && sides.right.color) {
|
|
107
|
-
borderRects += `<rect x="${w - sides.right.width}" y="0" width="${sides.right.width}" height="${h}" fill="#${sides.right.color}" />`;
|
|
108
|
-
}
|
|
109
|
-
if (sides.bottom.width > 0 && sides.bottom.color) {
|
|
110
|
-
borderRects += `<rect x="0" y="${h - sides.bottom.width}" width="${w}" height="${sides.bottom.width}" fill="#${sides.bottom.color}" />`;
|
|
111
|
-
}
|
|
112
|
-
if (sides.left.width > 0 && sides.left.color) {
|
|
113
|
-
borderRects += `<rect x="0" y="0" width="${sides.left.width}" height="${h}" fill="#${sides.left.color}" />`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const svg = `
|
|
117
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
118
|
-
<defs>
|
|
119
|
-
<clipPath id="${clipId}">
|
|
120
|
-
<rect x="0" y="0" width="${w}" height="${h}" rx="${radius}" ry="${radius}" />
|
|
121
|
-
</clipPath>
|
|
122
|
-
</defs>
|
|
123
|
-
<g clip-path="url(#${clipId})">
|
|
124
|
-
${borderRects}
|
|
125
|
-
</g>
|
|
126
|
-
</svg>`;
|
|
127
|
-
|
|
128
|
-
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Generates an SVG data URL for a solid shape with non-uniform corner radii.
|
|
133
|
-
*/
|
|
134
|
-
export function generateCustomShapeSVG(w, h, color, opacity, radii) {
|
|
135
|
-
let { tl, tr, br, bl } = radii;
|
|
136
|
-
|
|
137
|
-
// Clamp radii using CSS spec logic (avoid overlap)
|
|
138
|
-
const factor = Math.min(
|
|
139
|
-
w / (tl + tr) || Infinity,
|
|
140
|
-
h / (tr + br) || Infinity,
|
|
141
|
-
w / (br + bl) || Infinity,
|
|
142
|
-
h / (bl + tl) || Infinity
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
if (factor < 1) {
|
|
146
|
-
tl *= factor;
|
|
147
|
-
tr *= factor;
|
|
148
|
-
br *= factor;
|
|
149
|
-
bl *= factor;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const path = `
|
|
153
|
-
M ${tl} 0
|
|
154
|
-
L ${w - tr} 0
|
|
155
|
-
A ${tr} ${tr} 0 0 1 ${w} ${tr}
|
|
156
|
-
L ${w} ${h - br}
|
|
157
|
-
A ${br} ${br} 0 0 1 ${w - br} ${h}
|
|
158
|
-
L ${bl} ${h}
|
|
159
|
-
A ${bl} ${bl} 0 0 1 0 ${h - bl}
|
|
160
|
-
L 0 ${tl}
|
|
161
|
-
A ${tl} ${tl} 0 0 1 ${tl} 0
|
|
162
|
-
Z
|
|
163
|
-
`;
|
|
164
|
-
|
|
165
|
-
const svg = `
|
|
166
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
167
|
-
<path d="${path}" fill="#${color}" fill-opacity="${opacity}" />
|
|
168
|
-
</svg>`;
|
|
169
|
-
|
|
170
|
-
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function parseColor(str) {
|
|
174
|
-
if (!str || str === 'transparent' || str.startsWith('rgba(0, 0, 0, 0)')) {
|
|
175
|
-
return { hex: null, opacity: 0 };
|
|
176
|
-
}
|
|
177
|
-
if (str.startsWith('#')) {
|
|
178
|
-
let hex = str.slice(1);
|
|
179
|
-
if (hex.length === 3)
|
|
180
|
-
hex = hex
|
|
181
|
-
.split('')
|
|
182
|
-
.map((c) => c + c)
|
|
183
|
-
.join('');
|
|
184
|
-
return { hex: hex.toUpperCase(), opacity: 1 };
|
|
185
|
-
}
|
|
186
|
-
const match = str.match(/[\d.]+/g);
|
|
187
|
-
if (match && match.length >= 3) {
|
|
188
|
-
const r = parseInt(match[0]);
|
|
189
|
-
const g = parseInt(match[1]);
|
|
190
|
-
const b = parseInt(match[2]);
|
|
191
|
-
const a = match.length > 3 ? parseFloat(match[3]) : 1;
|
|
192
|
-
const hex = ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
|
|
193
|
-
return { hex, opacity: a };
|
|
194
|
-
}
|
|
195
|
-
return { hex: null, opacity: 0 };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export function getPadding(style, scale) {
|
|
199
|
-
const pxToInch = 1 / 96;
|
|
200
|
-
return [
|
|
201
|
-
(parseFloat(style.paddingTop) || 0) * pxToInch * scale,
|
|
202
|
-
(parseFloat(style.paddingRight) || 0) * pxToInch * scale,
|
|
203
|
-
(parseFloat(style.paddingBottom) || 0) * pxToInch * scale,
|
|
204
|
-
(parseFloat(style.paddingLeft) || 0) * pxToInch * scale,
|
|
205
|
-
];
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export function getSoftEdges(filterStr, scale) {
|
|
209
|
-
if (!filterStr || filterStr === 'none') return null;
|
|
210
|
-
const match = filterStr.match(/blur\(([\d.]+)px\)/);
|
|
211
|
-
if (match) return parseFloat(match[1]) * 0.75 * scale;
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export function getTextStyle(style, scale) {
|
|
216
|
-
let colorObj = parseColor(style.color);
|
|
217
|
-
|
|
218
|
-
const bgClip = style.webkitBackgroundClip || style.backgroundClip;
|
|
219
|
-
if (colorObj.opacity === 0 && bgClip === 'text') {
|
|
220
|
-
const fallback = getGradientFallbackColor(style.backgroundImage);
|
|
221
|
-
if (fallback) colorObj = parseColor(fallback);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
color: colorObj.hex || '000000',
|
|
226
|
-
fontFace: style.fontFamily.split(',')[0].replace(/['"]/g, ''),
|
|
227
|
-
fontSize: parseFloat(style.fontSize) * 0.75 * scale,
|
|
228
|
-
bold: parseInt(style.fontWeight) >= 600,
|
|
229
|
-
italic: style.fontStyle === 'italic',
|
|
230
|
-
underline: style.textDecoration.includes('underline'),
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Determines if a given DOM node is primarily a text container.
|
|
236
|
-
*/
|
|
237
|
-
export function isTextContainer(node) {
|
|
238
|
-
const hasText = node.textContent.trim().length > 0;
|
|
239
|
-
if (!hasText) return false;
|
|
240
|
-
|
|
241
|
-
const children = Array.from(node.children);
|
|
242
|
-
if (children.length === 0) return true;
|
|
243
|
-
|
|
244
|
-
// Check if children are purely inline text formatting or visual shapes
|
|
245
|
-
const isSafeInline = (el) => {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export function
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
y2 = '0%';
|
|
402
|
-
stopsStartIdx = 1;
|
|
403
|
-
} else if (parts[0].includes('to
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
1
|
+
// src/utils.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if any parent element has overflow: hidden which would clip this element
|
|
5
|
+
* @param {HTMLElement} node - The DOM node to check
|
|
6
|
+
* @returns {boolean} - True if a parent has overflow-hidden or overflow-clip
|
|
7
|
+
*/
|
|
8
|
+
export function isClippedByParent(node) {
|
|
9
|
+
let parent = node.parentElement;
|
|
10
|
+
while (parent && parent !== document.body) {
|
|
11
|
+
const style = window.getComputedStyle(parent);
|
|
12
|
+
const overflow = style.overflow;
|
|
13
|
+
if (overflow === 'hidden' || overflow === 'clip') {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
parent = parent.parentElement;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Helper to save gradient text
|
|
22
|
+
export function getGradientFallbackColor(bgImage) {
|
|
23
|
+
if (!bgImage) return null;
|
|
24
|
+
const hexMatch = bgImage.match(/#(?:[0-9a-fA-F]{3}){1,2}/);
|
|
25
|
+
if (hexMatch) return hexMatch[0];
|
|
26
|
+
const rgbMatch = bgImage.match(/rgba?\(.*?\)/);
|
|
27
|
+
if (rgbMatch) return rgbMatch[0];
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function mapDashType(style) {
|
|
32
|
+
if (style === 'dashed') return 'dash';
|
|
33
|
+
if (style === 'dotted') return 'dot';
|
|
34
|
+
return 'solid';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Analyzes computed border styles and determines the rendering strategy.
|
|
39
|
+
*/
|
|
40
|
+
export function getBorderInfo(style, scale) {
|
|
41
|
+
const top = {
|
|
42
|
+
width: parseFloat(style.borderTopWidth) || 0,
|
|
43
|
+
style: style.borderTopStyle,
|
|
44
|
+
color: parseColor(style.borderTopColor).hex,
|
|
45
|
+
};
|
|
46
|
+
const right = {
|
|
47
|
+
width: parseFloat(style.borderRightWidth) || 0,
|
|
48
|
+
style: style.borderRightStyle,
|
|
49
|
+
color: parseColor(style.borderRightColor).hex,
|
|
50
|
+
};
|
|
51
|
+
const bottom = {
|
|
52
|
+
width: parseFloat(style.borderBottomWidth) || 0,
|
|
53
|
+
style: style.borderBottomStyle,
|
|
54
|
+
color: parseColor(style.borderBottomColor).hex,
|
|
55
|
+
};
|
|
56
|
+
const left = {
|
|
57
|
+
width: parseFloat(style.borderLeftWidth) || 0,
|
|
58
|
+
style: style.borderLeftStyle,
|
|
59
|
+
color: parseColor(style.borderLeftColor).hex,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const hasAnyBorder = top.width > 0 || right.width > 0 || bottom.width > 0 || left.width > 0;
|
|
63
|
+
if (!hasAnyBorder) return { type: 'none' };
|
|
64
|
+
|
|
65
|
+
// Check if all sides are uniform
|
|
66
|
+
const isUniform =
|
|
67
|
+
top.width === right.width &&
|
|
68
|
+
top.width === bottom.width &&
|
|
69
|
+
top.width === left.width &&
|
|
70
|
+
top.style === right.style &&
|
|
71
|
+
top.style === bottom.style &&
|
|
72
|
+
top.style === left.style &&
|
|
73
|
+
top.color === right.color &&
|
|
74
|
+
top.color === bottom.color &&
|
|
75
|
+
top.color === left.color;
|
|
76
|
+
|
|
77
|
+
if (isUniform) {
|
|
78
|
+
return {
|
|
79
|
+
type: 'uniform',
|
|
80
|
+
options: {
|
|
81
|
+
width: top.width * 0.75 * scale,
|
|
82
|
+
color: top.color,
|
|
83
|
+
transparency: (1 - parseColor(style.borderTopColor).opacity) * 100,
|
|
84
|
+
dashType: mapDashType(top.style),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
} else {
|
|
88
|
+
return {
|
|
89
|
+
type: 'composite',
|
|
90
|
+
sides: { top, right, bottom, left },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Generates an SVG image for composite borders that respects border-radius.
|
|
97
|
+
*/
|
|
98
|
+
export function generateCompositeBorderSVG(w, h, radius, sides) {
|
|
99
|
+
radius = radius / 2; // Adjust for SVG rendering
|
|
100
|
+
const clipId = 'clip_' + Math.random().toString(36).substr(2, 9);
|
|
101
|
+
let borderRects = '';
|
|
102
|
+
|
|
103
|
+
if (sides.top.width > 0 && sides.top.color) {
|
|
104
|
+
borderRects += `<rect x="0" y="0" width="${w}" height="${sides.top.width}" fill="#${sides.top.color}" />`;
|
|
105
|
+
}
|
|
106
|
+
if (sides.right.width > 0 && sides.right.color) {
|
|
107
|
+
borderRects += `<rect x="${w - sides.right.width}" y="0" width="${sides.right.width}" height="${h}" fill="#${sides.right.color}" />`;
|
|
108
|
+
}
|
|
109
|
+
if (sides.bottom.width > 0 && sides.bottom.color) {
|
|
110
|
+
borderRects += `<rect x="0" y="${h - sides.bottom.width}" width="${w}" height="${sides.bottom.width}" fill="#${sides.bottom.color}" />`;
|
|
111
|
+
}
|
|
112
|
+
if (sides.left.width > 0 && sides.left.color) {
|
|
113
|
+
borderRects += `<rect x="0" y="0" width="${sides.left.width}" height="${h}" fill="#${sides.left.color}" />`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const svg = `
|
|
117
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
118
|
+
<defs>
|
|
119
|
+
<clipPath id="${clipId}">
|
|
120
|
+
<rect x="0" y="0" width="${w}" height="${h}" rx="${radius}" ry="${radius}" />
|
|
121
|
+
</clipPath>
|
|
122
|
+
</defs>
|
|
123
|
+
<g clip-path="url(#${clipId})">
|
|
124
|
+
${borderRects}
|
|
125
|
+
</g>
|
|
126
|
+
</svg>`;
|
|
127
|
+
|
|
128
|
+
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Generates an SVG data URL for a solid shape with non-uniform corner radii.
|
|
133
|
+
*/
|
|
134
|
+
export function generateCustomShapeSVG(w, h, color, opacity, radii) {
|
|
135
|
+
let { tl, tr, br, bl } = radii;
|
|
136
|
+
|
|
137
|
+
// Clamp radii using CSS spec logic (avoid overlap)
|
|
138
|
+
const factor = Math.min(
|
|
139
|
+
w / (tl + tr) || Infinity,
|
|
140
|
+
h / (tr + br) || Infinity,
|
|
141
|
+
w / (br + bl) || Infinity,
|
|
142
|
+
h / (bl + tl) || Infinity
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
if (factor < 1) {
|
|
146
|
+
tl *= factor;
|
|
147
|
+
tr *= factor;
|
|
148
|
+
br *= factor;
|
|
149
|
+
bl *= factor;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const path = `
|
|
153
|
+
M ${tl} 0
|
|
154
|
+
L ${w - tr} 0
|
|
155
|
+
A ${tr} ${tr} 0 0 1 ${w} ${tr}
|
|
156
|
+
L ${w} ${h - br}
|
|
157
|
+
A ${br} ${br} 0 0 1 ${w - br} ${h}
|
|
158
|
+
L ${bl} ${h}
|
|
159
|
+
A ${bl} ${bl} 0 0 1 0 ${h - bl}
|
|
160
|
+
L 0 ${tl}
|
|
161
|
+
A ${tl} ${tl} 0 0 1 ${tl} 0
|
|
162
|
+
Z
|
|
163
|
+
`;
|
|
164
|
+
|
|
165
|
+
const svg = `
|
|
166
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
167
|
+
<path d="${path}" fill="#${color}" fill-opacity="${opacity}" />
|
|
168
|
+
</svg>`;
|
|
169
|
+
|
|
170
|
+
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function parseColor(str) {
|
|
174
|
+
if (!str || str === 'transparent' || str.startsWith('rgba(0, 0, 0, 0)')) {
|
|
175
|
+
return { hex: null, opacity: 0 };
|
|
176
|
+
}
|
|
177
|
+
if (str.startsWith('#')) {
|
|
178
|
+
let hex = str.slice(1);
|
|
179
|
+
if (hex.length === 3)
|
|
180
|
+
hex = hex
|
|
181
|
+
.split('')
|
|
182
|
+
.map((c) => c + c)
|
|
183
|
+
.join('');
|
|
184
|
+
return { hex: hex.toUpperCase(), opacity: 1 };
|
|
185
|
+
}
|
|
186
|
+
const match = str.match(/[\d.]+/g);
|
|
187
|
+
if (match && match.length >= 3) {
|
|
188
|
+
const r = parseInt(match[0]);
|
|
189
|
+
const g = parseInt(match[1]);
|
|
190
|
+
const b = parseInt(match[2]);
|
|
191
|
+
const a = match.length > 3 ? parseFloat(match[3]) : 1;
|
|
192
|
+
const hex = ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
|
|
193
|
+
return { hex, opacity: a };
|
|
194
|
+
}
|
|
195
|
+
return { hex: null, opacity: 0 };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function getPadding(style, scale) {
|
|
199
|
+
const pxToInch = 1 / 96;
|
|
200
|
+
return [
|
|
201
|
+
(parseFloat(style.paddingTop) || 0) * pxToInch * scale,
|
|
202
|
+
(parseFloat(style.paddingRight) || 0) * pxToInch * scale,
|
|
203
|
+
(parseFloat(style.paddingBottom) || 0) * pxToInch * scale,
|
|
204
|
+
(parseFloat(style.paddingLeft) || 0) * pxToInch * scale,
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function getSoftEdges(filterStr, scale) {
|
|
209
|
+
if (!filterStr || filterStr === 'none') return null;
|
|
210
|
+
const match = filterStr.match(/blur\(([\d.]+)px\)/);
|
|
211
|
+
if (match) return parseFloat(match[1]) * 0.75 * scale;
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function getTextStyle(style, scale) {
|
|
216
|
+
let colorObj = parseColor(style.color);
|
|
217
|
+
|
|
218
|
+
const bgClip = style.webkitBackgroundClip || style.backgroundClip;
|
|
219
|
+
if (colorObj.opacity === 0 && bgClip === 'text') {
|
|
220
|
+
const fallback = getGradientFallbackColor(style.backgroundImage);
|
|
221
|
+
if (fallback) colorObj = parseColor(fallback);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
color: colorObj.hex || '000000',
|
|
226
|
+
fontFace: style.fontFamily.split(',')[0].replace(/['"]/g, ''),
|
|
227
|
+
fontSize: parseFloat(style.fontSize) * 0.75 * scale,
|
|
228
|
+
bold: parseInt(style.fontWeight) >= 600,
|
|
229
|
+
italic: style.fontStyle === 'italic',
|
|
230
|
+
underline: style.textDecoration.includes('underline'),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Determines if a given DOM node is primarily a text container.
|
|
236
|
+
*/
|
|
237
|
+
export function isTextContainer(node) {
|
|
238
|
+
const hasText = node.textContent.trim().length > 0;
|
|
239
|
+
if (!hasText) return false;
|
|
240
|
+
|
|
241
|
+
const children = Array.from(node.children);
|
|
242
|
+
if (children.length === 0) return true;
|
|
243
|
+
|
|
244
|
+
// Check if children are purely inline text formatting or visual shapes
|
|
245
|
+
const isSafeInline = (el) => {
|
|
246
|
+
// 1. Reject Web Components / Icons / Images
|
|
247
|
+
if (el.tagName.includes('-')) return false;
|
|
248
|
+
if (el.tagName === 'IMG' || el.tagName === 'SVG') return false;
|
|
249
|
+
|
|
250
|
+
const style = window.getComputedStyle(el);
|
|
251
|
+
const display = style.display;
|
|
252
|
+
|
|
253
|
+
// 2. Initial check: Must be a standard inline tag OR display:inline
|
|
254
|
+
const isInlineTag = ['SPAN', 'B', 'STRONG', 'EM', 'I', 'A', 'SMALL', 'MARK'].includes(el.tagName);
|
|
255
|
+
const isInlineDisplay = display.includes('inline');
|
|
256
|
+
|
|
257
|
+
if (!isInlineTag && !isInlineDisplay) return false;
|
|
258
|
+
|
|
259
|
+
// 3. CRITICAL FIX: Check for Structural Styling
|
|
260
|
+
// PPTX Text Runs (parts of a text line) CANNOT have backgrounds, borders, or padding.
|
|
261
|
+
// If a child element has these, the parent is NOT a simple text container;
|
|
262
|
+
// it is a layout container composed of styled blocks.
|
|
263
|
+
const bgColor = parseColor(style.backgroundColor);
|
|
264
|
+
const hasVisibleBg = bgColor.hex && bgColor.opacity > 0;
|
|
265
|
+
const hasBorder = parseFloat(style.borderWidth) > 0 && parseColor(style.borderColor).opacity > 0;
|
|
266
|
+
|
|
267
|
+
if (hasVisibleBg || hasBorder) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 4. Check for empty shapes (visual objects without text, like dots)
|
|
272
|
+
const hasContent = el.textContent.trim().length > 0;
|
|
273
|
+
if (!hasContent && (hasVisibleBg || hasBorder)) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return true;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
return children.every(isSafeInline);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function getRotation(transformStr) {
|
|
284
|
+
if (!transformStr || transformStr === 'none') return 0;
|
|
285
|
+
const values = transformStr.split('(')[1].split(')')[0].split(',');
|
|
286
|
+
if (values.length < 4) return 0;
|
|
287
|
+
const a = parseFloat(values[0]);
|
|
288
|
+
const b = parseFloat(values[1]);
|
|
289
|
+
return Math.round(Math.atan2(b, a) * (180 / Math.PI));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function svgToPng(node) {
|
|
293
|
+
return new Promise((resolve) => {
|
|
294
|
+
const clone = node.cloneNode(true);
|
|
295
|
+
const rect = node.getBoundingClientRect();
|
|
296
|
+
const width = rect.width || 300;
|
|
297
|
+
const height = rect.height || 150;
|
|
298
|
+
|
|
299
|
+
function inlineStyles(source, target) {
|
|
300
|
+
const computed = window.getComputedStyle(source);
|
|
301
|
+
const properties = [
|
|
302
|
+
'fill',
|
|
303
|
+
'stroke',
|
|
304
|
+
'stroke-width',
|
|
305
|
+
'stroke-linecap',
|
|
306
|
+
'stroke-linejoin',
|
|
307
|
+
'opacity',
|
|
308
|
+
'font-family',
|
|
309
|
+
'font-size',
|
|
310
|
+
'font-weight',
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
if (computed.fill === 'none') target.setAttribute('fill', 'none');
|
|
314
|
+
else if (computed.fill) target.style.fill = computed.fill;
|
|
315
|
+
|
|
316
|
+
if (computed.stroke === 'none') target.setAttribute('stroke', 'none');
|
|
317
|
+
else if (computed.stroke) target.style.stroke = computed.stroke;
|
|
318
|
+
|
|
319
|
+
properties.forEach((prop) => {
|
|
320
|
+
if (prop !== 'fill' && prop !== 'stroke') {
|
|
321
|
+
const val = computed[prop];
|
|
322
|
+
if (val && val !== 'auto') target.style[prop] = val;
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
for (let i = 0; i < source.children.length; i++) {
|
|
327
|
+
if (target.children[i]) inlineStyles(source.children[i], target.children[i]);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
inlineStyles(node, clone);
|
|
332
|
+
clone.setAttribute('width', width);
|
|
333
|
+
clone.setAttribute('height', height);
|
|
334
|
+
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
335
|
+
|
|
336
|
+
const xml = new XMLSerializer().serializeToString(clone);
|
|
337
|
+
const svgUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(xml)}`;
|
|
338
|
+
const img = new Image();
|
|
339
|
+
img.crossOrigin = 'Anonymous';
|
|
340
|
+
img.onload = () => {
|
|
341
|
+
const canvas = document.createElement('canvas');
|
|
342
|
+
const scale = 3;
|
|
343
|
+
canvas.width = width * scale;
|
|
344
|
+
canvas.height = height * scale;
|
|
345
|
+
const ctx = canvas.getContext('2d');
|
|
346
|
+
ctx.scale(scale, scale);
|
|
347
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
348
|
+
resolve(canvas.toDataURL('image/png'));
|
|
349
|
+
};
|
|
350
|
+
img.onerror = () => resolve(null);
|
|
351
|
+
img.src = svgUrl;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function getVisibleShadow(shadowStr, scale) {
|
|
356
|
+
if (!shadowStr || shadowStr === 'none') return null;
|
|
357
|
+
const shadows = shadowStr.split(/,(?![^()]*\))/);
|
|
358
|
+
for (let s of shadows) {
|
|
359
|
+
s = s.trim();
|
|
360
|
+
if (s.startsWith('rgba(0, 0, 0, 0)')) continue;
|
|
361
|
+
const match = s.match(
|
|
362
|
+
/(rgba?\([^)]+\)|#[0-9a-fA-F]+)\s+(-?[\d.]+)px\s+(-?[\d.]+)px\s+([\d.]+)px/
|
|
363
|
+
);
|
|
364
|
+
if (match) {
|
|
365
|
+
const colorStr = match[1];
|
|
366
|
+
const x = parseFloat(match[2]);
|
|
367
|
+
const y = parseFloat(match[3]);
|
|
368
|
+
const blur = parseFloat(match[4]);
|
|
369
|
+
const distance = Math.sqrt(x * x + y * y);
|
|
370
|
+
let angle = Math.atan2(y, x) * (180 / Math.PI);
|
|
371
|
+
if (angle < 0) angle += 360;
|
|
372
|
+
const colorObj = parseColor(colorStr);
|
|
373
|
+
return {
|
|
374
|
+
type: 'outer',
|
|
375
|
+
angle: angle,
|
|
376
|
+
blur: blur * 0.75 * scale,
|
|
377
|
+
offset: distance * 0.75 * scale,
|
|
378
|
+
color: colorObj.hex || '000000',
|
|
379
|
+
opacity: colorObj.opacity,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function generateGradientSVG(w, h, bgString, radius, border) {
|
|
387
|
+
try {
|
|
388
|
+
const match = bgString.match(/linear-gradient\((.*)\)/);
|
|
389
|
+
if (!match) return null;
|
|
390
|
+
const content = match[1];
|
|
391
|
+
const parts = content.split(/,(?![^()]*\))/).map((p) => p.trim());
|
|
392
|
+
|
|
393
|
+
let x1 = '0%',
|
|
394
|
+
y1 = '0%',
|
|
395
|
+
x2 = '0%',
|
|
396
|
+
y2 = '100%';
|
|
397
|
+
let stopsStartIdx = 0;
|
|
398
|
+
if (parts[0].includes('to right')) {
|
|
399
|
+
x1 = '0%';
|
|
400
|
+
x2 = '100%';
|
|
401
|
+
y2 = '0%';
|
|
402
|
+
stopsStartIdx = 1;
|
|
403
|
+
} else if (parts[0].includes('to left')) {
|
|
404
|
+
x1 = '100%';
|
|
405
|
+
x2 = '0%';
|
|
406
|
+
y2 = '0%';
|
|
407
|
+
stopsStartIdx = 1;
|
|
408
|
+
} else if (parts[0].includes('to top')) {
|
|
409
|
+
y1 = '100%';
|
|
410
|
+
y2 = '0%';
|
|
411
|
+
stopsStartIdx = 1;
|
|
412
|
+
} else if (parts[0].includes('to bottom')) {
|
|
413
|
+
y1 = '0%';
|
|
414
|
+
y2 = '100%';
|
|
415
|
+
stopsStartIdx = 1;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
let stopsXML = '';
|
|
419
|
+
const stopParts = parts.slice(stopsStartIdx);
|
|
420
|
+
stopParts.forEach((part, idx) => {
|
|
421
|
+
let color = part;
|
|
422
|
+
let offset = Math.round((idx / (stopParts.length - 1)) * 100) + '%';
|
|
423
|
+
const posMatch = part.match(/(.*?)\s+(\d+(\.\d+)?%?)$/);
|
|
424
|
+
if (posMatch) {
|
|
425
|
+
color = posMatch[1];
|
|
426
|
+
offset = posMatch[2];
|
|
427
|
+
}
|
|
428
|
+
let opacity = 1;
|
|
429
|
+
if (color.includes('rgba')) {
|
|
430
|
+
const rgba = color.match(/[\d.]+/g);
|
|
431
|
+
if (rgba && rgba.length > 3) {
|
|
432
|
+
opacity = rgba[3];
|
|
433
|
+
color = `rgb(${rgba[0]},${rgba[1]},${rgba[2]})`;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
stopsXML += `<stop offset="${offset}" stop-color="${color}" stop-opacity="${opacity}"/>`;
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
let strokeAttr = '';
|
|
440
|
+
if (border) {
|
|
441
|
+
strokeAttr = `stroke="#${border.color}" stroke-width="${border.width}"`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const svg = `
|
|
445
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
446
|
+
<defs><linearGradient id="grad" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}">${stopsXML}</linearGradient></defs>
|
|
447
|
+
<rect x="0" y="0" width="${w}" height="${h}" rx="${radius}" ry="${radius}" fill="url(#grad)" ${strokeAttr} />
|
|
448
|
+
</svg>`;
|
|
449
|
+
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
450
|
+
} catch {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export function generateBlurredSVG(w, h, color, radius, blurPx) {
|
|
456
|
+
const padding = blurPx * 3;
|
|
457
|
+
const fullW = w + padding * 2;
|
|
458
|
+
const fullH = h + padding * 2;
|
|
459
|
+
const x = padding;
|
|
460
|
+
const y = padding;
|
|
461
|
+
let shapeTag = '';
|
|
462
|
+
const isCircle = radius >= Math.min(w, h) / 2 - 1 && Math.abs(w - h) < 2;
|
|
463
|
+
|
|
464
|
+
if (isCircle) {
|
|
465
|
+
const cx = x + w / 2;
|
|
466
|
+
const cy = y + h / 2;
|
|
467
|
+
const rx = w / 2;
|
|
468
|
+
const ry = h / 2;
|
|
469
|
+
shapeTag = `<ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}" fill="#${color}" filter="url(#f1)" />`;
|
|
470
|
+
} else {
|
|
471
|
+
shapeTag = `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${radius}" ry="${radius}" fill="#${color}" filter="url(#f1)" />`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const svg = `
|
|
475
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${fullW}" height="${fullH}" viewBox="0 0 ${fullW} ${fullH}">
|
|
476
|
+
<defs>
|
|
477
|
+
<filter id="f1" x="-50%" y="-50%" width="200%" height="200%">
|
|
478
|
+
<feGaussianBlur in="SourceGraphic" stdDeviation="${blurPx}" />
|
|
479
|
+
</filter>
|
|
480
|
+
</defs>
|
|
481
|
+
${shapeTag}
|
|
482
|
+
</svg>`;
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
data: 'data:image/svg+xml;base64,' + btoa(svg),
|
|
486
|
+
padding: padding,
|
|
487
|
+
};
|
|
488
|
+
}
|