dom-to-pptx 1.0.5 → 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 +31 -18
- package/{Readme.md → README.md} +165 -85
- package/SUPPORTED.md +50 -50
- package/dist/dom-to-pptx.bundle.js +18565 -30968
- package/dist/dom-to-pptx.cjs +358 -304
- package/dist/dom-to-pptx.min.js +356 -304
- package/dist/dom-to-pptx.mjs +352 -301
- package/package.json +73 -73
- package/rollup.config.js +62 -53
- package/src/image-processor.js +79 -76
- package/src/index.js +222 -148
- package/src/utils.js +59 -23
package/src/index.js
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
getVisibleShadow,
|
|
13
13
|
generateGradientSVG,
|
|
14
14
|
getRotation,
|
|
15
|
-
svgToPng,
|
|
16
15
|
getPadding,
|
|
17
16
|
getSoftEdges,
|
|
18
17
|
generateBlurredSVG,
|
|
@@ -87,66 +86,115 @@ async function processSlide(root, slide, pptx) {
|
|
|
87
86
|
};
|
|
88
87
|
|
|
89
88
|
const renderQueue = [];
|
|
89
|
+
const asyncTasks = []; // Queue for heavy operations (Images, Canvas)
|
|
90
90
|
let domOrderCounter = 0;
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
// Sync Traversal Function
|
|
93
|
+
function collect(node, parentZIndex) {
|
|
93
94
|
const order = domOrderCounter++;
|
|
94
|
-
|
|
95
|
+
|
|
96
|
+
let currentZ = parentZIndex;
|
|
97
|
+
let nodeStyle = null;
|
|
98
|
+
const nodeType = node.nodeType;
|
|
99
|
+
|
|
100
|
+
if (nodeType === 1) {
|
|
101
|
+
nodeStyle = window.getComputedStyle(node);
|
|
102
|
+
// Optimization: Skip completely hidden elements immediately
|
|
103
|
+
if (
|
|
104
|
+
nodeStyle.display === 'none' ||
|
|
105
|
+
nodeStyle.visibility === 'hidden' ||
|
|
106
|
+
nodeStyle.opacity === '0'
|
|
107
|
+
) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (nodeStyle.zIndex !== 'auto') {
|
|
111
|
+
currentZ = parseInt(nodeStyle.zIndex);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Prepare the item. If it needs async work, it returns a 'job'
|
|
116
|
+
const result = prepareRenderItem(
|
|
117
|
+
node,
|
|
118
|
+
{ ...layoutConfig, root },
|
|
119
|
+
order,
|
|
120
|
+
pptx,
|
|
121
|
+
currentZ,
|
|
122
|
+
nodeStyle
|
|
123
|
+
);
|
|
124
|
+
|
|
95
125
|
if (result) {
|
|
96
|
-
if (result.items)
|
|
126
|
+
if (result.items) {
|
|
127
|
+
// Push items immediately to queue (data might be missing but filled later)
|
|
128
|
+
renderQueue.push(...result.items);
|
|
129
|
+
}
|
|
130
|
+
if (result.job) {
|
|
131
|
+
// Push the promise-returning function to the task list
|
|
132
|
+
asyncTasks.push(result.job);
|
|
133
|
+
}
|
|
97
134
|
if (result.stopRecursion) return;
|
|
98
135
|
}
|
|
99
|
-
|
|
136
|
+
|
|
137
|
+
// Recurse children synchronously
|
|
138
|
+
const childNodes = node.childNodes;
|
|
139
|
+
for (let i = 0; i < childNodes.length; i++) {
|
|
140
|
+
collect(childNodes[i], currentZ);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 1. Traverse and build the structure (Fast)
|
|
145
|
+
collect(root, 0);
|
|
146
|
+
|
|
147
|
+
// 2. Execute all heavy tasks in parallel (Fast)
|
|
148
|
+
if (asyncTasks.length > 0) {
|
|
149
|
+
await Promise.all(asyncTasks.map((task) => task()));
|
|
100
150
|
}
|
|
101
151
|
|
|
102
|
-
|
|
152
|
+
// 3. Cleanup and Sort
|
|
153
|
+
// Remove items that failed to generate data (marked with skip)
|
|
154
|
+
const finalQueue = renderQueue.filter(
|
|
155
|
+
(item) => !item.skip && (item.type !== 'image' || item.options.data)
|
|
156
|
+
);
|
|
103
157
|
|
|
104
|
-
|
|
158
|
+
finalQueue.sort((a, b) => {
|
|
105
159
|
if (a.zIndex !== b.zIndex) return a.zIndex - b.zIndex;
|
|
106
160
|
return a.domOrder - b.domOrder;
|
|
107
161
|
});
|
|
108
162
|
|
|
109
|
-
|
|
163
|
+
// 4. Add to Slide
|
|
164
|
+
for (const item of finalQueue) {
|
|
110
165
|
if (item.type === 'shape') slide.addShape(item.shapeType, item.options);
|
|
111
166
|
if (item.type === 'image') slide.addImage(item.options);
|
|
112
167
|
if (item.type === 'text') slide.addText(item.textParts, item.options);
|
|
113
168
|
}
|
|
114
169
|
}
|
|
115
170
|
|
|
116
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Optimized html2canvas wrapper
|
|
173
|
+
* Now strictly captures the node itself, not the root.
|
|
174
|
+
*/
|
|
175
|
+
async function elementToCanvasImage(node, widthPx, heightPx) {
|
|
117
176
|
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
|
-
|
|
177
|
+
const width = Math.max(Math.ceil(widthPx), 1);
|
|
178
|
+
const height = Math.max(Math.ceil(heightPx), 1);
|
|
126
179
|
const style = window.getComputedStyle(node);
|
|
127
180
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
height: root.scrollHeight,
|
|
131
|
-
useCORS: true,
|
|
132
|
-
allowTaint: true,
|
|
181
|
+
// Optimized: Capture ONLY the specific node
|
|
182
|
+
html2canvas(node, {
|
|
133
183
|
backgroundColor: null,
|
|
184
|
+
logging: false,
|
|
185
|
+
scale: 2, // Slight quality boost
|
|
134
186
|
})
|
|
135
187
|
.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
188
|
const destCanvas = document.createElement('canvas');
|
|
143
189
|
destCanvas.width = width;
|
|
144
190
|
destCanvas.height = height;
|
|
145
191
|
const ctx = destCanvas.getContext('2d');
|
|
146
192
|
|
|
147
|
-
|
|
193
|
+
// Draw the captured canvas into our sized canvas
|
|
194
|
+
// html2canvas might return a larger canvas if scale > 1, so we fit it
|
|
195
|
+
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, width, height);
|
|
148
196
|
|
|
149
|
-
//
|
|
197
|
+
// Apply border radius clipping
|
|
150
198
|
let tl = parseFloat(style.borderTopLeftRadius) || 0;
|
|
151
199
|
let tr = parseFloat(style.borderTopRightRadius) || 0;
|
|
152
200
|
let br = parseFloat(style.borderBottomRightRadius) || 0;
|
|
@@ -166,38 +214,89 @@ async function elementToCanvasImage(node, widthPx, heightPx, root) {
|
|
|
166
214
|
bl *= f;
|
|
167
215
|
}
|
|
168
216
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
217
|
+
if (tl + tr + br + bl > 0) {
|
|
218
|
+
ctx.globalCompositeOperation = 'destination-in';
|
|
219
|
+
ctx.beginPath();
|
|
220
|
+
ctx.moveTo(tl, 0);
|
|
221
|
+
ctx.lineTo(width - tr, 0);
|
|
222
|
+
ctx.arcTo(width, 0, width, tr, tr);
|
|
223
|
+
ctx.lineTo(width, height - br);
|
|
224
|
+
ctx.arcTo(width, height, width - br, height, br);
|
|
225
|
+
ctx.lineTo(bl, height);
|
|
226
|
+
ctx.arcTo(0, height, 0, height - bl, bl);
|
|
227
|
+
ctx.lineTo(0, tl);
|
|
228
|
+
ctx.arcTo(0, 0, tl, 0, tl);
|
|
229
|
+
ctx.closePath();
|
|
230
|
+
ctx.fill();
|
|
231
|
+
}
|
|
182
232
|
|
|
183
233
|
resolve(destCanvas.toDataURL('image/png'));
|
|
184
234
|
})
|
|
185
|
-
.catch(() =>
|
|
235
|
+
.catch((e) => {
|
|
236
|
+
console.warn('Canvas capture failed for node', node, e);
|
|
237
|
+
resolve(null);
|
|
238
|
+
});
|
|
186
239
|
});
|
|
187
240
|
}
|
|
188
241
|
|
|
189
|
-
|
|
242
|
+
/**
|
|
243
|
+
* Replaces createRenderItem.
|
|
244
|
+
* Returns { items: [], job: () => Promise, stopRecursion: boolean }
|
|
245
|
+
*/
|
|
246
|
+
function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, computedStyle) {
|
|
247
|
+
// 1. Text Node Handling
|
|
248
|
+
if (node.nodeType === 3) {
|
|
249
|
+
const textContent = node.nodeValue.trim();
|
|
250
|
+
if (!textContent) return null;
|
|
251
|
+
|
|
252
|
+
const parent = node.parentElement;
|
|
253
|
+
if (!parent) return null;
|
|
254
|
+
|
|
255
|
+
if (isTextContainer(parent)) return null; // Parent handles it
|
|
256
|
+
|
|
257
|
+
const range = document.createRange();
|
|
258
|
+
range.selectNode(node);
|
|
259
|
+
const rect = range.getBoundingClientRect();
|
|
260
|
+
range.detach();
|
|
261
|
+
|
|
262
|
+
const style = window.getComputedStyle(parent);
|
|
263
|
+
const widthPx = rect.width;
|
|
264
|
+
const heightPx = rect.height;
|
|
265
|
+
const unrotatedW = widthPx * PX_TO_INCH * config.scale;
|
|
266
|
+
const unrotatedH = heightPx * PX_TO_INCH * config.scale;
|
|
267
|
+
|
|
268
|
+
const x = config.offX + (rect.left - config.rootX) * PX_TO_INCH * config.scale;
|
|
269
|
+
const y = config.offY + (rect.top - config.rootY) * PX_TO_INCH * config.scale;
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
items: [
|
|
273
|
+
{
|
|
274
|
+
type: 'text',
|
|
275
|
+
zIndex: effectiveZIndex,
|
|
276
|
+
domOrder,
|
|
277
|
+
textParts: [
|
|
278
|
+
{
|
|
279
|
+
text: textContent,
|
|
280
|
+
options: getTextStyle(style, config.scale),
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
options: { x, y, w: unrotatedW, h: unrotatedH, margin: 0, autoFit: false },
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
stopRecursion: false,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
190
290
|
if (node.nodeType !== 1) return null;
|
|
191
|
-
const style =
|
|
192
|
-
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')
|
|
193
|
-
return null;
|
|
291
|
+
const style = computedStyle; // Use pre-computed style
|
|
194
292
|
|
|
195
293
|
const rect = node.getBoundingClientRect();
|
|
196
294
|
if (rect.width < 0.5 || rect.height < 0.5) return null;
|
|
197
295
|
|
|
198
|
-
const zIndex =
|
|
296
|
+
const zIndex = effectiveZIndex;
|
|
199
297
|
const rotation = getRotation(style.transform);
|
|
200
298
|
const elementOpacity = parseFloat(style.opacity);
|
|
299
|
+
const safeOpacity = isNaN(elementOpacity) ? 1 : elementOpacity;
|
|
201
300
|
|
|
202
301
|
const widthPx = node.offsetWidth || rect.width;
|
|
203
302
|
const heightPx = node.offsetHeight || rect.height;
|
|
@@ -213,21 +312,31 @@ async function createRenderItem(node, config, domOrder, pptx) {
|
|
|
213
312
|
|
|
214
313
|
const items = [];
|
|
215
314
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
315
|
+
// --- ASYNC JOB: SVGs / Icons ---
|
|
316
|
+
if (
|
|
317
|
+
node.nodeName.toUpperCase() === 'SVG' ||
|
|
318
|
+
node.tagName.includes('-') ||
|
|
319
|
+
node.tagName === 'ION-ICON'
|
|
320
|
+
) {
|
|
321
|
+
const item = {
|
|
322
|
+
type: 'image',
|
|
323
|
+
zIndex,
|
|
324
|
+
domOrder,
|
|
325
|
+
options: { x, y, w, h, rotate: rotation, data: null }, // Data null initially
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Create Job
|
|
329
|
+
const job = async () => {
|
|
330
|
+
const pngData = await elementToCanvasImage(node, widthPx, heightPx);
|
|
331
|
+
if (pngData) item.options.data = pngData;
|
|
332
|
+
else item.skip = true;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
return { items: [item], job, stopRecursion: true };
|
|
226
336
|
}
|
|
227
337
|
|
|
228
|
-
// ---
|
|
338
|
+
// --- ASYNC JOB: IMG Tags ---
|
|
229
339
|
if (node.tagName === 'IMG') {
|
|
230
|
-
// Extract individual corner radii
|
|
231
340
|
let radii = {
|
|
232
341
|
tl: parseFloat(style.borderTopLeftRadius) || 0,
|
|
233
342
|
tr: parseFloat(style.borderTopRightRadius) || 0,
|
|
@@ -236,8 +345,6 @@ async function createRenderItem(node, config, domOrder, pptx) {
|
|
|
236
345
|
};
|
|
237
346
|
|
|
238
347
|
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
348
|
if (!hasAnyRadius) {
|
|
242
349
|
const parent = node.parentElement;
|
|
243
350
|
const parentStyle = window.getComputedStyle(parent);
|
|
@@ -248,10 +355,6 @@ async function createRenderItem(node, config, domOrder, pptx) {
|
|
|
248
355
|
br: parseFloat(parentStyle.borderBottomRightRadius) || 0,
|
|
249
356
|
bl: parseFloat(parentStyle.borderBottomLeftRadius) || 0,
|
|
250
357
|
};
|
|
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
358
|
const pRect = parent.getBoundingClientRect();
|
|
256
359
|
if (Math.abs(pRect.width - rect.width) < 5 && Math.abs(pRect.height - rect.height) < 5) {
|
|
257
360
|
radii = pRadii;
|
|
@@ -259,19 +362,23 @@ async function createRenderItem(node, config, domOrder, pptx) {
|
|
|
259
362
|
}
|
|
260
363
|
}
|
|
261
364
|
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
365
|
+
const item = {
|
|
366
|
+
type: 'image',
|
|
367
|
+
zIndex,
|
|
368
|
+
domOrder,
|
|
369
|
+
options: { x, y, w, h, rotate: rotation, data: null },
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const job = async () => {
|
|
373
|
+
const processed = await getProcessedImage(node.src, widthPx, heightPx, radii);
|
|
374
|
+
if (processed) item.options.data = processed;
|
|
375
|
+
else item.skip = true;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
return { items: [item], job, stopRecursion: true };
|
|
271
379
|
}
|
|
272
|
-
// --- UPDATED IMG BLOCK END ---
|
|
273
380
|
|
|
274
|
-
// Radii
|
|
381
|
+
// Radii logic
|
|
275
382
|
const borderRadiusValue = parseFloat(style.borderRadius) || 0;
|
|
276
383
|
const borderBottomLeftRadius = parseFloat(style.borderBottomLeftRadius) || 0;
|
|
277
384
|
const borderBottomRightRadius = parseFloat(style.borderBottomRightRadius) || 0;
|
|
@@ -289,25 +396,30 @@ async function createRenderItem(node, config, domOrder, pptx) {
|
|
|
289
396
|
borderTopLeftRadius ||
|
|
290
397
|
borderTopRightRadius));
|
|
291
398
|
|
|
292
|
-
//
|
|
399
|
+
// --- ASYNC JOB: Clipped Divs via Canvas ---
|
|
293
400
|
if (hasPartialBorderRadius && isClippedByParent(node)) {
|
|
294
401
|
const marginLeft = parseFloat(style.marginLeft) || 0;
|
|
295
402
|
const marginTop = parseFloat(style.marginTop) || 0;
|
|
296
403
|
x += marginLeft * PX_TO_INCH * config.scale;
|
|
297
404
|
y += marginTop * PX_TO_INCH * config.scale;
|
|
298
405
|
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
406
|
+
const item = {
|
|
407
|
+
type: 'image',
|
|
408
|
+
zIndex,
|
|
409
|
+
domOrder,
|
|
410
|
+
options: { x, y, w, h, rotate: rotation, data: null },
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const job = async () => {
|
|
414
|
+
const canvasImageData = await elementToCanvasImage(node, widthPx, heightPx);
|
|
415
|
+
if (canvasImageData) item.options.data = canvasImageData;
|
|
416
|
+
else item.skip = true;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
return { items: [item], job, stopRecursion: true };
|
|
309
420
|
}
|
|
310
421
|
|
|
422
|
+
// --- SYNC: Standard CSS Extraction ---
|
|
311
423
|
const bgColorObj = parseColor(style.backgroundColor);
|
|
312
424
|
const bgClip = style.webkitBackgroundClip || style.backgroundClip;
|
|
313
425
|
const isBgClipText = bgClip === 'text';
|
|
@@ -346,7 +458,7 @@ async function createRenderItem(node, config, domOrder, pptx) {
|
|
|
346
458
|
x -= bulletShift;
|
|
347
459
|
w += bulletShift;
|
|
348
460
|
textParts.push({
|
|
349
|
-
text: '
|
|
461
|
+
text: ' ',
|
|
350
462
|
options: {
|
|
351
463
|
color: parseColor(style.color).hex || '000000',
|
|
352
464
|
fontSize: fontSizePt,
|
|
@@ -452,7 +564,6 @@ async function createRenderItem(node, config, domOrder, pptx) {
|
|
|
452
564
|
});
|
|
453
565
|
}
|
|
454
566
|
if (hasCompositeBorder) {
|
|
455
|
-
// Add border shapes after the main background
|
|
456
567
|
const borderItems = createCompositeBorderItems(
|
|
457
568
|
borderInfo.sides,
|
|
458
569
|
x,
|
|
@@ -472,7 +583,7 @@ async function createRenderItem(node, config, domOrder, pptx) {
|
|
|
472
583
|
hasShadow ||
|
|
473
584
|
textPayload
|
|
474
585
|
) {
|
|
475
|
-
const finalAlpha =
|
|
586
|
+
const finalAlpha = safeOpacity * bgColorObj.opacity;
|
|
476
587
|
const transparency = (1 - finalAlpha) * 100;
|
|
477
588
|
const useSolidFill = bgColorObj.hex && !isImageWrapper;
|
|
478
589
|
|
|
@@ -494,14 +605,7 @@ async function createRenderItem(node, config, domOrder, pptx) {
|
|
|
494
605
|
type: 'image',
|
|
495
606
|
zIndex,
|
|
496
607
|
domOrder,
|
|
497
|
-
options: {
|
|
498
|
-
data: shapeSvg,
|
|
499
|
-
x,
|
|
500
|
-
y,
|
|
501
|
-
w,
|
|
502
|
-
h,
|
|
503
|
-
rotate: rotation,
|
|
504
|
-
},
|
|
608
|
+
options: { data: shapeSvg, x, y, w, h, rotate: rotation },
|
|
505
609
|
});
|
|
506
610
|
} else {
|
|
507
611
|
const shapeOpts = {
|
|
@@ -516,9 +620,7 @@ async function createRenderItem(node, config, domOrder, pptx) {
|
|
|
516
620
|
line: hasUniformBorder ? borderInfo.options : null,
|
|
517
621
|
};
|
|
518
622
|
|
|
519
|
-
if (hasShadow)
|
|
520
|
-
shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
|
|
521
|
-
}
|
|
623
|
+
if (hasShadow) shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
|
|
522
624
|
|
|
523
625
|
const borderRadius = parseFloat(style.borderRadius) || 0;
|
|
524
626
|
const aspectRatio = Math.max(widthPx, heightPx) / Math.min(widthPx, heightPx);
|
|
@@ -581,77 +683,49 @@ async function createRenderItem(node, config, domOrder, pptx) {
|
|
|
581
683
|
return { items, stopRecursion: !!textPayload };
|
|
582
684
|
}
|
|
583
685
|
|
|
584
|
-
/**
|
|
585
|
-
* Helper function to create individual border shapes
|
|
586
|
-
*/
|
|
587
686
|
function createCompositeBorderItems(sides, x, y, w, h, scale, zIndex, domOrder) {
|
|
588
687
|
const items = [];
|
|
589
688
|
const pxToInch = 1 / 96;
|
|
689
|
+
const common = { zIndex: zIndex + 1, domOrder, shapeType: 'rect' };
|
|
590
690
|
|
|
591
|
-
|
|
592
|
-
if (sides.top.width > 0) {
|
|
691
|
+
if (sides.top.width > 0)
|
|
593
692
|
items.push({
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
},
|
|
693
|
+
...common,
|
|
694
|
+
options: { x, y, w, h: sides.top.width * pxToInch * scale, fill: { color: sides.top.color } },
|
|
605
695
|
});
|
|
606
|
-
|
|
607
|
-
// RIGHT BORDER
|
|
608
|
-
if (sides.right.width > 0) {
|
|
696
|
+
if (sides.right.width > 0)
|
|
609
697
|
items.push({
|
|
610
|
-
|
|
611
|
-
zIndex: zIndex + 1,
|
|
612
|
-
domOrder,
|
|
613
|
-
shapeType: 'rect',
|
|
698
|
+
...common,
|
|
614
699
|
options: {
|
|
615
700
|
x: x + w - sides.right.width * pxToInch * scale,
|
|
616
|
-
y
|
|
701
|
+
y,
|
|
617
702
|
w: sides.right.width * pxToInch * scale,
|
|
618
|
-
h
|
|
703
|
+
h,
|
|
619
704
|
fill: { color: sides.right.color },
|
|
620
705
|
},
|
|
621
706
|
});
|
|
622
|
-
|
|
623
|
-
// BOTTOM BORDER
|
|
624
|
-
if (sides.bottom.width > 0) {
|
|
707
|
+
if (sides.bottom.width > 0)
|
|
625
708
|
items.push({
|
|
626
|
-
|
|
627
|
-
zIndex: zIndex + 1,
|
|
628
|
-
domOrder,
|
|
629
|
-
shapeType: 'rect',
|
|
709
|
+
...common,
|
|
630
710
|
options: {
|
|
631
|
-
x
|
|
711
|
+
x,
|
|
632
712
|
y: y + h - sides.bottom.width * pxToInch * scale,
|
|
633
|
-
w
|
|
713
|
+
w,
|
|
634
714
|
h: sides.bottom.width * pxToInch * scale,
|
|
635
715
|
fill: { color: sides.bottom.color },
|
|
636
716
|
},
|
|
637
717
|
});
|
|
638
|
-
|
|
639
|
-
// LEFT BORDER
|
|
640
|
-
if (sides.left.width > 0) {
|
|
718
|
+
if (sides.left.width > 0)
|
|
641
719
|
items.push({
|
|
642
|
-
|
|
643
|
-
zIndex: zIndex + 1,
|
|
644
|
-
domOrder,
|
|
645
|
-
shapeType: 'rect',
|
|
720
|
+
...common,
|
|
646
721
|
options: {
|
|
647
|
-
x
|
|
648
|
-
y
|
|
722
|
+
x,
|
|
723
|
+
y,
|
|
649
724
|
w: sides.left.width * pxToInch * scale,
|
|
650
|
-
h
|
|
725
|
+
h,
|
|
651
726
|
fill: { color: sides.left.color },
|
|
652
727
|
},
|
|
653
728
|
});
|
|
654
|
-
}
|
|
655
729
|
|
|
656
730
|
return items;
|
|
657
731
|
}
|
package/src/utils.js
CHANGED
|
@@ -133,17 +133,20 @@ export function generateCompositeBorderSVG(w, h, radius, sides) {
|
|
|
133
133
|
*/
|
|
134
134
|
export function generateCustomShapeSVG(w, h, color, opacity, radii) {
|
|
135
135
|
let { tl, tr, br, bl } = radii;
|
|
136
|
-
|
|
136
|
+
|
|
137
137
|
// Clamp radii using CSS spec logic (avoid overlap)
|
|
138
138
|
const factor = Math.min(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
w / (tl + tr) || Infinity,
|
|
140
|
+
h / (tr + br) || Infinity,
|
|
141
|
+
w / (br + bl) || Infinity,
|
|
142
|
+
h / (bl + tl) || Infinity
|
|
143
143
|
);
|
|
144
|
-
|
|
144
|
+
|
|
145
145
|
if (factor < 1) {
|
|
146
|
-
tl *= factor;
|
|
146
|
+
tl *= factor;
|
|
147
|
+
tr *= factor;
|
|
148
|
+
br *= factor;
|
|
149
|
+
bl *= factor;
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
const path = `
|
|
@@ -174,7 +177,10 @@ export function parseColor(str) {
|
|
|
174
177
|
if (str.startsWith('#')) {
|
|
175
178
|
let hex = str.slice(1);
|
|
176
179
|
if (hex.length === 3)
|
|
177
|
-
hex = hex
|
|
180
|
+
hex = hex
|
|
181
|
+
.split('')
|
|
182
|
+
.map((c) => c + c)
|
|
183
|
+
.join('');
|
|
178
184
|
return { hex: hex.toUpperCase(), opacity: 1 };
|
|
179
185
|
}
|
|
180
186
|
const match = str.match(/[\d.]+/g);
|
|
@@ -237,25 +243,35 @@ export function isTextContainer(node) {
|
|
|
237
243
|
|
|
238
244
|
// Check if children are purely inline text formatting or visual shapes
|
|
239
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
|
+
|
|
240
250
|
const style = window.getComputedStyle(el);
|
|
241
251
|
const display = style.display;
|
|
242
|
-
|
|
243
|
-
//
|
|
244
|
-
const isInlineTag = ['SPAN', 'B', 'STRONG', 'EM', 'I', 'A', 'SMALL'].includes(el.tagName);
|
|
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);
|
|
245
255
|
const isInlineDisplay = display.includes('inline');
|
|
246
256
|
|
|
247
257
|
if (!isInlineTag && !isInlineDisplay) return false;
|
|
248
258
|
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
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.
|
|
253
263
|
const bgColor = parseColor(style.backgroundColor);
|
|
254
264
|
const hasVisibleBg = bgColor.hex && bgColor.opacity > 0;
|
|
255
265
|
const hasBorder = parseFloat(style.borderWidth) > 0 && parseColor(style.borderColor).opacity > 0;
|
|
256
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;
|
|
257
273
|
if (!hasContent && (hasVisibleBg || hasBorder)) {
|
|
258
|
-
return false;
|
|
274
|
+
return false;
|
|
259
275
|
}
|
|
260
276
|
|
|
261
277
|
return true;
|
|
@@ -283,8 +299,15 @@ export function svgToPng(node) {
|
|
|
283
299
|
function inlineStyles(source, target) {
|
|
284
300
|
const computed = window.getComputedStyle(source);
|
|
285
301
|
const properties = [
|
|
286
|
-
'fill',
|
|
287
|
-
'stroke
|
|
302
|
+
'fill',
|
|
303
|
+
'stroke',
|
|
304
|
+
'stroke-width',
|
|
305
|
+
'stroke-linecap',
|
|
306
|
+
'stroke-linejoin',
|
|
307
|
+
'opacity',
|
|
308
|
+
'font-family',
|
|
309
|
+
'font-size',
|
|
310
|
+
'font-weight',
|
|
288
311
|
];
|
|
289
312
|
|
|
290
313
|
if (computed.fill === 'none') target.setAttribute('fill', 'none');
|
|
@@ -367,16 +390,29 @@ export function generateGradientSVG(w, h, bgString, radius, border) {
|
|
|
367
390
|
const content = match[1];
|
|
368
391
|
const parts = content.split(/,(?![^()]*\))/).map((p) => p.trim());
|
|
369
392
|
|
|
370
|
-
let x1 = '0%',
|
|
393
|
+
let x1 = '0%',
|
|
394
|
+
y1 = '0%',
|
|
395
|
+
x2 = '0%',
|
|
396
|
+
y2 = '100%';
|
|
371
397
|
let stopsStartIdx = 0;
|
|
372
398
|
if (parts[0].includes('to right')) {
|
|
373
|
-
x1 = '0%';
|
|
399
|
+
x1 = '0%';
|
|
400
|
+
x2 = '100%';
|
|
401
|
+
y2 = '0%';
|
|
402
|
+
stopsStartIdx = 1;
|
|
374
403
|
} else if (parts[0].includes('to left')) {
|
|
375
|
-
x1 = '100%';
|
|
404
|
+
x1 = '100%';
|
|
405
|
+
x2 = '0%';
|
|
406
|
+
y2 = '0%';
|
|
407
|
+
stopsStartIdx = 1;
|
|
376
408
|
} else if (parts[0].includes('to top')) {
|
|
377
|
-
y1 = '100%';
|
|
409
|
+
y1 = '100%';
|
|
410
|
+
y2 = '0%';
|
|
411
|
+
stopsStartIdx = 1;
|
|
378
412
|
} else if (parts[0].includes('to bottom')) {
|
|
379
|
-
y1 = '0%';
|
|
413
|
+
y1 = '0%';
|
|
414
|
+
y2 = '100%';
|
|
415
|
+
stopsStartIdx = 1;
|
|
380
416
|
}
|
|
381
417
|
|
|
382
418
|
let stopsXML = '';
|