dom-to-pptx 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +1200 -1167
- package/dist/dom-to-pptx.min.js +1198 -1167
- package/dist/dom-to-pptx.mjs +1194 -1164
- package/package.json +73 -73
- package/rollup.config.js +56 -53
- package/src/image-processor.js +79 -76
- package/src/index.js +657 -657
- package/src/utils.js +479 -452
package/src/utils.js
CHANGED
|
@@ -1,452 +1,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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
if (factor < 1) {
|
|
146
|
-
tl *= factor;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (str.startsWith('
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
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
|
+
const style = window.getComputedStyle(el);
|
|
247
|
+
const display = style.display;
|
|
248
|
+
|
|
249
|
+
// If it's a standard inline element
|
|
250
|
+
const isInlineTag = ['SPAN', 'B', 'STRONG', 'EM', 'I', 'A', 'SMALL'].includes(el.tagName);
|
|
251
|
+
const isInlineDisplay = display.includes('inline');
|
|
252
|
+
|
|
253
|
+
if (!isInlineTag && !isInlineDisplay) return false;
|
|
254
|
+
|
|
255
|
+
// Check if element is a shape (visual object without text)
|
|
256
|
+
// If an element is empty but has a visible background/border, it's a shape (like a dot).
|
|
257
|
+
// We must return false so the parent isn't treated as a text-only container.
|
|
258
|
+
const hasContent = el.textContent.trim().length > 0;
|
|
259
|
+
const bgColor = parseColor(style.backgroundColor);
|
|
260
|
+
const hasVisibleBg = bgColor.hex && bgColor.opacity > 0;
|
|
261
|
+
const hasBorder =
|
|
262
|
+
parseFloat(style.borderWidth) > 0 && parseColor(style.borderColor).opacity > 0;
|
|
263
|
+
|
|
264
|
+
if (!hasContent && (hasVisibleBg || hasBorder)) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return true;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
return children.every(isSafeInline);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function getRotation(transformStr) {
|
|
275
|
+
if (!transformStr || transformStr === 'none') return 0;
|
|
276
|
+
const values = transformStr.split('(')[1].split(')')[0].split(',');
|
|
277
|
+
if (values.length < 4) return 0;
|
|
278
|
+
const a = parseFloat(values[0]);
|
|
279
|
+
const b = parseFloat(values[1]);
|
|
280
|
+
return Math.round(Math.atan2(b, a) * (180 / Math.PI));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function svgToPng(node) {
|
|
284
|
+
return new Promise((resolve) => {
|
|
285
|
+
const clone = node.cloneNode(true);
|
|
286
|
+
const rect = node.getBoundingClientRect();
|
|
287
|
+
const width = rect.width || 300;
|
|
288
|
+
const height = rect.height || 150;
|
|
289
|
+
|
|
290
|
+
function inlineStyles(source, target) {
|
|
291
|
+
const computed = window.getComputedStyle(source);
|
|
292
|
+
const properties = [
|
|
293
|
+
'fill',
|
|
294
|
+
'stroke',
|
|
295
|
+
'stroke-width',
|
|
296
|
+
'stroke-linecap',
|
|
297
|
+
'stroke-linejoin',
|
|
298
|
+
'opacity',
|
|
299
|
+
'font-family',
|
|
300
|
+
'font-size',
|
|
301
|
+
'font-weight',
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
if (computed.fill === 'none') target.setAttribute('fill', 'none');
|
|
305
|
+
else if (computed.fill) target.style.fill = computed.fill;
|
|
306
|
+
|
|
307
|
+
if (computed.stroke === 'none') target.setAttribute('stroke', 'none');
|
|
308
|
+
else if (computed.stroke) target.style.stroke = computed.stroke;
|
|
309
|
+
|
|
310
|
+
properties.forEach((prop) => {
|
|
311
|
+
if (prop !== 'fill' && prop !== 'stroke') {
|
|
312
|
+
const val = computed[prop];
|
|
313
|
+
if (val && val !== 'auto') target.style[prop] = val;
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
for (let i = 0; i < source.children.length; i++) {
|
|
318
|
+
if (target.children[i]) inlineStyles(source.children[i], target.children[i]);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
inlineStyles(node, clone);
|
|
323
|
+
clone.setAttribute('width', width);
|
|
324
|
+
clone.setAttribute('height', height);
|
|
325
|
+
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
326
|
+
|
|
327
|
+
const xml = new XMLSerializer().serializeToString(clone);
|
|
328
|
+
const svgUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(xml)}`;
|
|
329
|
+
const img = new Image();
|
|
330
|
+
img.crossOrigin = 'Anonymous';
|
|
331
|
+
img.onload = () => {
|
|
332
|
+
const canvas = document.createElement('canvas');
|
|
333
|
+
const scale = 3;
|
|
334
|
+
canvas.width = width * scale;
|
|
335
|
+
canvas.height = height * scale;
|
|
336
|
+
const ctx = canvas.getContext('2d');
|
|
337
|
+
ctx.scale(scale, scale);
|
|
338
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
339
|
+
resolve(canvas.toDataURL('image/png'));
|
|
340
|
+
};
|
|
341
|
+
img.onerror = () => resolve(null);
|
|
342
|
+
img.src = svgUrl;
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function getVisibleShadow(shadowStr, scale) {
|
|
347
|
+
if (!shadowStr || shadowStr === 'none') return null;
|
|
348
|
+
const shadows = shadowStr.split(/,(?![^()]*\))/);
|
|
349
|
+
for (let s of shadows) {
|
|
350
|
+
s = s.trim();
|
|
351
|
+
if (s.startsWith('rgba(0, 0, 0, 0)')) continue;
|
|
352
|
+
const match = s.match(
|
|
353
|
+
/(rgba?\([^)]+\)|#[0-9a-fA-F]+)\s+(-?[\d.]+)px\s+(-?[\d.]+)px\s+([\d.]+)px/
|
|
354
|
+
);
|
|
355
|
+
if (match) {
|
|
356
|
+
const colorStr = match[1];
|
|
357
|
+
const x = parseFloat(match[2]);
|
|
358
|
+
const y = parseFloat(match[3]);
|
|
359
|
+
const blur = parseFloat(match[4]);
|
|
360
|
+
const distance = Math.sqrt(x * x + y * y);
|
|
361
|
+
let angle = Math.atan2(y, x) * (180 / Math.PI);
|
|
362
|
+
if (angle < 0) angle += 360;
|
|
363
|
+
const colorObj = parseColor(colorStr);
|
|
364
|
+
return {
|
|
365
|
+
type: 'outer',
|
|
366
|
+
angle: angle,
|
|
367
|
+
blur: blur * 0.75 * scale,
|
|
368
|
+
offset: distance * 0.75 * scale,
|
|
369
|
+
color: colorObj.hex || '000000',
|
|
370
|
+
opacity: colorObj.opacity,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function generateGradientSVG(w, h, bgString, radius, border) {
|
|
378
|
+
try {
|
|
379
|
+
const match = bgString.match(/linear-gradient\((.*)\)/);
|
|
380
|
+
if (!match) return null;
|
|
381
|
+
const content = match[1];
|
|
382
|
+
const parts = content.split(/,(?![^()]*\))/).map((p) => p.trim());
|
|
383
|
+
|
|
384
|
+
let x1 = '0%',
|
|
385
|
+
y1 = '0%',
|
|
386
|
+
x2 = '0%',
|
|
387
|
+
y2 = '100%';
|
|
388
|
+
let stopsStartIdx = 0;
|
|
389
|
+
if (parts[0].includes('to right')) {
|
|
390
|
+
x1 = '0%';
|
|
391
|
+
x2 = '100%';
|
|
392
|
+
y2 = '0%';
|
|
393
|
+
stopsStartIdx = 1;
|
|
394
|
+
} else if (parts[0].includes('to left')) {
|
|
395
|
+
x1 = '100%';
|
|
396
|
+
x2 = '0%';
|
|
397
|
+
y2 = '0%';
|
|
398
|
+
stopsStartIdx = 1;
|
|
399
|
+
} else if (parts[0].includes('to top')) {
|
|
400
|
+
y1 = '100%';
|
|
401
|
+
y2 = '0%';
|
|
402
|
+
stopsStartIdx = 1;
|
|
403
|
+
} else if (parts[0].includes('to bottom')) {
|
|
404
|
+
y1 = '0%';
|
|
405
|
+
y2 = '100%';
|
|
406
|
+
stopsStartIdx = 1;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let stopsXML = '';
|
|
410
|
+
const stopParts = parts.slice(stopsStartIdx);
|
|
411
|
+
stopParts.forEach((part, idx) => {
|
|
412
|
+
let color = part;
|
|
413
|
+
let offset = Math.round((idx / (stopParts.length - 1)) * 100) + '%';
|
|
414
|
+
const posMatch = part.match(/(.*?)\s+(\d+(\.\d+)?%?)$/);
|
|
415
|
+
if (posMatch) {
|
|
416
|
+
color = posMatch[1];
|
|
417
|
+
offset = posMatch[2];
|
|
418
|
+
}
|
|
419
|
+
let opacity = 1;
|
|
420
|
+
if (color.includes('rgba')) {
|
|
421
|
+
const rgba = color.match(/[\d.]+/g);
|
|
422
|
+
if (rgba && rgba.length > 3) {
|
|
423
|
+
opacity = rgba[3];
|
|
424
|
+
color = `rgb(${rgba[0]},${rgba[1]},${rgba[2]})`;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
stopsXML += `<stop offset="${offset}" stop-color="${color}" stop-opacity="${opacity}"/>`;
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
let strokeAttr = '';
|
|
431
|
+
if (border) {
|
|
432
|
+
strokeAttr = `stroke="#${border.color}" stroke-width="${border.width}"`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const svg = `
|
|
436
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
437
|
+
<defs><linearGradient id="grad" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}">${stopsXML}</linearGradient></defs>
|
|
438
|
+
<rect x="0" y="0" width="${w}" height="${h}" rx="${radius}" ry="${radius}" fill="url(#grad)" ${strokeAttr} />
|
|
439
|
+
</svg>`;
|
|
440
|
+
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
441
|
+
} catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function generateBlurredSVG(w, h, color, radius, blurPx) {
|
|
447
|
+
const padding = blurPx * 3;
|
|
448
|
+
const fullW = w + padding * 2;
|
|
449
|
+
const fullH = h + padding * 2;
|
|
450
|
+
const x = padding;
|
|
451
|
+
const y = padding;
|
|
452
|
+
let shapeTag = '';
|
|
453
|
+
const isCircle = radius >= Math.min(w, h) / 2 - 1 && Math.abs(w - h) < 2;
|
|
454
|
+
|
|
455
|
+
if (isCircle) {
|
|
456
|
+
const cx = x + w / 2;
|
|
457
|
+
const cy = y + h / 2;
|
|
458
|
+
const rx = w / 2;
|
|
459
|
+
const ry = h / 2;
|
|
460
|
+
shapeTag = `<ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}" fill="#${color}" filter="url(#f1)" />`;
|
|
461
|
+
} else {
|
|
462
|
+
shapeTag = `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${radius}" ry="${radius}" fill="#${color}" filter="url(#f1)" />`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const svg = `
|
|
466
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${fullW}" height="${fullH}" viewBox="0 0 ${fullW} ${fullH}">
|
|
467
|
+
<defs>
|
|
468
|
+
<filter id="f1" x="-50%" y="-50%" width="200%" height="200%">
|
|
469
|
+
<feGaussianBlur in="SourceGraphic" stdDeviation="${blurPx}" />
|
|
470
|
+
</filter>
|
|
471
|
+
</defs>
|
|
472
|
+
${shapeTag}
|
|
473
|
+
</svg>`;
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
data: 'data:image/svg+xml;base64,' + btoa(svg),
|
|
477
|
+
padding: padding,
|
|
478
|
+
};
|
|
479
|
+
}
|