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/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
- async function collect(node) {
92
+ // Sync Traversal Function
93
+ function collect(node, parentZIndex) {
93
94
  const order = domOrderCounter++;
94
- const result = await createRenderItem(node, { ...layoutConfig, root }, order, pptx);
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) renderQueue.push(...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
- for (const child of node.children) await collect(child);
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
- await collect(root);
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
- renderQueue.sort((a, b) => {
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
- for (const item of renderQueue) {
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
- async function elementToCanvasImage(node, widthPx, heightPx, root) {
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
- html2canvas(root, {
129
- width: root.scrollWidth,
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
- ctx.drawImage(rootCanvas, sourceX, sourceY, width, height, 0, 0, width, height);
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
- // Parse radii
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
- 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();
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(() => resolve(null));
235
+ .catch((e) => {
236
+ console.warn('Canvas capture failed for node', node, e);
237
+ resolve(null);
238
+ });
186
239
  });
187
240
  }
188
241
 
189
- async function createRenderItem(node, config, domOrder, pptx) {
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 = window.getComputedStyle(node);
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 = style.zIndex !== 'auto' ? parseInt(style.zIndex) : 0;
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
- 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 };
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
- // --- UPDATED IMG BLOCK START ---
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 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 };
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 processing for Divs/Shapes
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
- // Allow clipped elements to be rendered via canvas
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 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
- }
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 = elementOpacity * bgColorObj.opacity;
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
- // TOP BORDER
592
- if (sides.top.width > 0) {
691
+ if (sides.top.width > 0)
593
692
  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
- },
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
- type: 'shape',
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: y,
701
+ y,
617
702
  w: sides.right.width * pxToInch * scale,
618
- h: 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
- type: 'shape',
627
- zIndex: zIndex + 1,
628
- domOrder,
629
- shapeType: 'rect',
709
+ ...common,
630
710
  options: {
631
- x: x,
711
+ x,
632
712
  y: y + h - sides.bottom.width * pxToInch * scale,
633
- w: 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
- type: 'shape',
643
- zIndex: zIndex + 1,
644
- domOrder,
645
- shapeType: 'rect',
720
+ ...common,
646
721
  options: {
647
- x: x,
648
- y: y,
722
+ x,
723
+ y,
649
724
  w: sides.left.width * pxToInch * scale,
650
- h: 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
- (w / (tl + tr)) || Infinity,
140
- (h / (tr + br)) || Infinity,
141
- (w / (br + bl)) || Infinity,
142
- (h / (bl + tl)) || Infinity
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; tr *= factor; br *= factor; bl *= 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.split('').map((c) => c + c).join('');
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
- // If it's a standard inline element
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
- // Check if element is a shape (visual object without text)
250
- // If an element is empty but has a visible background/border, it's a shape (like a dot).
251
- // We must return false so the parent isn't treated as a text-only container.
252
- const hasContent = el.textContent.trim().length > 0;
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', 'stroke', 'stroke-width', 'stroke-linecap',
287
- 'stroke-linejoin', 'opacity', 'font-family', 'font-size', 'font-weight',
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%', y1 = '0%', x2 = '0%', y2 = '100%';
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%'; x2 = '100%'; y2 = '0%'; stopsStartIdx = 1;
399
+ x1 = '0%';
400
+ x2 = '100%';
401
+ y2 = '0%';
402
+ stopsStartIdx = 1;
374
403
  } else if (parts[0].includes('to left')) {
375
- x1 = '100%'; x2 = '0%'; y2 = '0%'; stopsStartIdx = 1;
404
+ x1 = '100%';
405
+ x2 = '0%';
406
+ y2 = '0%';
407
+ stopsStartIdx = 1;
376
408
  } else if (parts[0].includes('to top')) {
377
- y1 = '100%'; y2 = '0%'; stopsStartIdx = 1;
409
+ y1 = '100%';
410
+ y2 = '0%';
411
+ stopsStartIdx = 1;
378
412
  } else if (parts[0].includes('to bottom')) {
379
- y1 = '0%'; y2 = '100%'; stopsStartIdx = 1;
413
+ y1 = '0%';
414
+ y2 = '100%';
415
+ stopsStartIdx = 1;
380
416
  }
381
417
 
382
418
  let stopsXML = '';