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.
@@ -53,7 +53,7 @@ function __awaiter(thisArg, _arguments, P, generator) {
53
53
  function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
54
54
  function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
55
55
  function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
56
- step((generator = generator.apply(thisArg, [])).next());
56
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
57
57
  });
58
58
  }
59
59
 
@@ -86,7 +86,7 @@ function __generator(thisArg, body) {
86
86
  }
87
87
 
88
88
  function __spreadArray(to, from, pack) {
89
- if (arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
89
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
90
90
  if (ar || !(i in from)) {
91
91
  if (!ar) ar = Array.prototype.slice.call(from, 0, i);
92
92
  ar[i] = from[i];
@@ -7955,17 +7955,20 @@ function generateCompositeBorderSVG(w, h, radius, sides) {
7955
7955
  */
7956
7956
  function generateCustomShapeSVG(w, h, color, opacity, radii) {
7957
7957
  let { tl, tr, br, bl } = radii;
7958
-
7958
+
7959
7959
  // Clamp radii using CSS spec logic (avoid overlap)
7960
7960
  const factor = Math.min(
7961
- (w / (tl + tr)) || Infinity,
7962
- (h / (tr + br)) || Infinity,
7963
- (w / (br + bl)) || Infinity,
7964
- (h / (bl + tl)) || Infinity
7961
+ w / (tl + tr) || Infinity,
7962
+ h / (tr + br) || Infinity,
7963
+ w / (br + bl) || Infinity,
7964
+ h / (bl + tl) || Infinity
7965
7965
  );
7966
-
7966
+
7967
7967
  if (factor < 1) {
7968
- tl *= factor; tr *= factor; br *= factor; bl *= factor;
7968
+ tl *= factor;
7969
+ tr *= factor;
7970
+ br *= factor;
7971
+ bl *= factor;
7969
7972
  }
7970
7973
 
7971
7974
  const path = `
@@ -7996,7 +7999,10 @@ function parseColor(str) {
7996
7999
  if (str.startsWith('#')) {
7997
8000
  let hex = str.slice(1);
7998
8001
  if (hex.length === 3)
7999
- hex = hex.split('').map((c) => c + c).join('');
8002
+ hex = hex
8003
+ .split('')
8004
+ .map((c) => c + c)
8005
+ .join('');
8000
8006
  return { hex: hex.toUpperCase(), opacity: 1 };
8001
8007
  }
8002
8008
  const match = str.match(/[\d.]+/g);
@@ -8059,25 +8065,35 @@ function isTextContainer(node) {
8059
8065
 
8060
8066
  // Check if children are purely inline text formatting or visual shapes
8061
8067
  const isSafeInline = (el) => {
8068
+ // 1. Reject Web Components / Icons / Images
8069
+ if (el.tagName.includes('-')) return false;
8070
+ if (el.tagName === 'IMG' || el.tagName === 'SVG') return false;
8071
+
8062
8072
  const style = window.getComputedStyle(el);
8063
8073
  const display = style.display;
8064
-
8065
- // If it's a standard inline element
8066
- const isInlineTag = ['SPAN', 'B', 'STRONG', 'EM', 'I', 'A', 'SMALL'].includes(el.tagName);
8074
+
8075
+ // 2. Initial check: Must be a standard inline tag OR display:inline
8076
+ const isInlineTag = ['SPAN', 'B', 'STRONG', 'EM', 'I', 'A', 'SMALL', 'MARK'].includes(el.tagName);
8067
8077
  const isInlineDisplay = display.includes('inline');
8068
8078
 
8069
8079
  if (!isInlineTag && !isInlineDisplay) return false;
8070
8080
 
8071
- // Check if element is a shape (visual object without text)
8072
- // If an element is empty but has a visible background/border, it's a shape (like a dot).
8073
- // We must return false so the parent isn't treated as a text-only container.
8074
- const hasContent = el.textContent.trim().length > 0;
8081
+ // 3. CRITICAL FIX: Check for Structural Styling
8082
+ // PPTX Text Runs (parts of a text line) CANNOT have backgrounds, borders, or padding.
8083
+ // If a child element has these, the parent is NOT a simple text container;
8084
+ // it is a layout container composed of styled blocks.
8075
8085
  const bgColor = parseColor(style.backgroundColor);
8076
8086
  const hasVisibleBg = bgColor.hex && bgColor.opacity > 0;
8077
8087
  const hasBorder = parseFloat(style.borderWidth) > 0 && parseColor(style.borderColor).opacity > 0;
8078
8088
 
8089
+ if (hasVisibleBg || hasBorder) {
8090
+ return false;
8091
+ }
8092
+
8093
+ // 4. Check for empty shapes (visual objects without text, like dots)
8094
+ const hasContent = el.textContent.trim().length > 0;
8079
8095
  if (!hasContent && (hasVisibleBg || hasBorder)) {
8080
- return false;
8096
+ return false;
8081
8097
  }
8082
8098
 
8083
8099
  return true;
@@ -8095,62 +8111,6 @@ function getRotation(transformStr) {
8095
8111
  return Math.round(Math.atan2(b, a) * (180 / Math.PI));
8096
8112
  }
8097
8113
 
8098
- function svgToPng(node) {
8099
- return new Promise((resolve) => {
8100
- const clone = node.cloneNode(true);
8101
- const rect = node.getBoundingClientRect();
8102
- const width = rect.width || 300;
8103
- const height = rect.height || 150;
8104
-
8105
- function inlineStyles(source, target) {
8106
- const computed = window.getComputedStyle(source);
8107
- const properties = [
8108
- 'fill', 'stroke', 'stroke-width', 'stroke-linecap',
8109
- 'stroke-linejoin', 'opacity', 'font-family', 'font-size', 'font-weight',
8110
- ];
8111
-
8112
- if (computed.fill === 'none') target.setAttribute('fill', 'none');
8113
- else if (computed.fill) target.style.fill = computed.fill;
8114
-
8115
- if (computed.stroke === 'none') target.setAttribute('stroke', 'none');
8116
- else if (computed.stroke) target.style.stroke = computed.stroke;
8117
-
8118
- properties.forEach((prop) => {
8119
- if (prop !== 'fill' && prop !== 'stroke') {
8120
- const val = computed[prop];
8121
- if (val && val !== 'auto') target.style[prop] = val;
8122
- }
8123
- });
8124
-
8125
- for (let i = 0; i < source.children.length; i++) {
8126
- if (target.children[i]) inlineStyles(source.children[i], target.children[i]);
8127
- }
8128
- }
8129
-
8130
- inlineStyles(node, clone);
8131
- clone.setAttribute('width', width);
8132
- clone.setAttribute('height', height);
8133
- clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
8134
-
8135
- const xml = new XMLSerializer().serializeToString(clone);
8136
- const svgUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(xml)}`;
8137
- const img = new Image();
8138
- img.crossOrigin = 'Anonymous';
8139
- img.onload = () => {
8140
- const canvas = document.createElement('canvas');
8141
- const scale = 3;
8142
- canvas.width = width * scale;
8143
- canvas.height = height * scale;
8144
- const ctx = canvas.getContext('2d');
8145
- ctx.scale(scale, scale);
8146
- ctx.drawImage(img, 0, 0, width, height);
8147
- resolve(canvas.toDataURL('image/png'));
8148
- };
8149
- img.onerror = () => resolve(null);
8150
- img.src = svgUrl;
8151
- });
8152
- }
8153
-
8154
8114
  function getVisibleShadow(shadowStr, scale) {
8155
8115
  if (!shadowStr || shadowStr === 'none') return null;
8156
8116
  const shadows = shadowStr.split(/,(?![^()]*\))/);
@@ -8189,16 +8149,29 @@ function generateGradientSVG(w, h, bgString, radius, border) {
8189
8149
  const content = match[1];
8190
8150
  const parts = content.split(/,(?![^()]*\))/).map((p) => p.trim());
8191
8151
 
8192
- let x1 = '0%', y1 = '0%', x2 = '0%', y2 = '100%';
8152
+ let x1 = '0%',
8153
+ y1 = '0%',
8154
+ x2 = '0%',
8155
+ y2 = '100%';
8193
8156
  let stopsStartIdx = 0;
8194
8157
  if (parts[0].includes('to right')) {
8195
- x1 = '0%'; x2 = '100%'; y2 = '0%'; stopsStartIdx = 1;
8158
+ x1 = '0%';
8159
+ x2 = '100%';
8160
+ y2 = '0%';
8161
+ stopsStartIdx = 1;
8196
8162
  } else if (parts[0].includes('to left')) {
8197
- x1 = '100%'; x2 = '0%'; y2 = '0%'; stopsStartIdx = 1;
8163
+ x1 = '100%';
8164
+ x2 = '0%';
8165
+ y2 = '0%';
8166
+ stopsStartIdx = 1;
8198
8167
  } else if (parts[0].includes('to top')) {
8199
- y1 = '100%'; y2 = '0%'; stopsStartIdx = 1;
8168
+ y1 = '100%';
8169
+ y2 = '0%';
8170
+ stopsStartIdx = 1;
8200
8171
  } else if (parts[0].includes('to bottom')) {
8201
- y1 = '0%'; y2 = '100%'; stopsStartIdx = 1;
8172
+ y1 = '0%';
8173
+ y2 = '100%';
8174
+ stopsStartIdx = 1;
8202
8175
  }
8203
8176
 
8204
8177
  let stopsXML = '';
@@ -8273,81 +8246,84 @@ function generateBlurredSVG(w, h, color, radius, blurPx) {
8273
8246
  };
8274
8247
  }
8275
8248
 
8276
- // src/image-processor.js
8277
-
8278
- async function getProcessedImage(src, targetW, targetH, radius) {
8279
- return new Promise((resolve) => {
8280
- const img = new Image();
8281
- img.crossOrigin = 'Anonymous'; // Critical for canvas manipulation
8282
-
8283
- img.onload = () => {
8284
- const canvas = document.createElement('canvas');
8285
- // Double resolution for better quality
8286
- const scale = 2;
8287
- canvas.width = targetW * scale;
8288
- canvas.height = targetH * scale;
8289
- const ctx = canvas.getContext('2d');
8290
- ctx.scale(scale, scale);
8291
-
8292
- // Normalize radius input to an object { tl, tr, br, bl }
8293
- let r = { tl: 0, tr: 0, br: 0, bl: 0 };
8294
- if (typeof radius === 'number') {
8295
- r = { tl: radius, tr: radius, br: radius, bl: radius };
8296
- } else if (typeof radius === 'object' && radius !== null) {
8297
- r = { ...r, ...radius }; // Merge with defaults
8298
- }
8299
-
8300
- // 1. Draw the Mask (Custom Shape with specific corners)
8301
- ctx.beginPath();
8302
-
8303
- // Border Radius Clamping Logic (CSS Spec)
8304
- // Prevents corners from overlapping if radii are too large for the container
8305
- const factor = Math.min(
8306
- (targetW / (r.tl + r.tr)) || Infinity,
8307
- (targetH / (r.tr + r.br)) || Infinity,
8308
- (targetW / (r.br + r.bl)) || Infinity,
8309
- (targetH / (r.bl + r.tl)) || Infinity
8310
- );
8311
-
8312
- if (factor < 1) {
8313
- r.tl *= factor; r.tr *= factor; r.br *= factor; r.bl *= factor;
8314
- }
8315
-
8316
- // Draw path: Top-Left -> Top-Right -> Bottom-Right -> Bottom-Left
8317
- ctx.moveTo(r.tl, 0);
8318
- ctx.lineTo(targetW - r.tr, 0);
8319
- ctx.arcTo(targetW, 0, targetW, r.tr, r.tr);
8320
- ctx.lineTo(targetW, targetH - r.br);
8321
- ctx.arcTo(targetW, targetH, targetW - r.br, targetH, r.br);
8322
- ctx.lineTo(r.bl, targetH);
8323
- ctx.arcTo(0, targetH, 0, targetH - r.bl, r.bl);
8324
- ctx.lineTo(0, r.tl);
8325
- ctx.arcTo(0, 0, r.tl, 0, r.tl);
8326
-
8327
- ctx.closePath();
8328
- ctx.fillStyle = '#000';
8329
- ctx.fill();
8330
-
8331
- // 2. Composite Source-In (Crops the next image draw to the mask)
8332
- ctx.globalCompositeOperation = 'source-in';
8333
-
8334
- // 3. Draw Image (Object Cover Logic)
8335
- const wRatio = targetW / img.width;
8336
- const hRatio = targetH / img.height;
8337
- const maxRatio = Math.max(wRatio, hRatio);
8338
- const renderW = img.width * maxRatio;
8339
- const renderH = img.height * maxRatio;
8340
- const renderX = (targetW - renderW) / 2;
8341
- const renderY = (targetH - renderH) / 2;
8342
-
8343
- ctx.drawImage(img, renderX, renderY, renderW, renderH);
8344
-
8345
- resolve(canvas.toDataURL('image/png'));
8346
- };
8347
-
8348
- img.onerror = () => resolve(null);
8349
- img.src = src;
8350
- });
8249
+ // src/image-processor.js
8250
+
8251
+ async function getProcessedImage(src, targetW, targetH, radius) {
8252
+ return new Promise((resolve) => {
8253
+ const img = new Image();
8254
+ img.crossOrigin = 'Anonymous'; // Critical for canvas manipulation
8255
+
8256
+ img.onload = () => {
8257
+ const canvas = document.createElement('canvas');
8258
+ // Double resolution for better quality
8259
+ const scale = 2;
8260
+ canvas.width = targetW * scale;
8261
+ canvas.height = targetH * scale;
8262
+ const ctx = canvas.getContext('2d');
8263
+ ctx.scale(scale, scale);
8264
+
8265
+ // Normalize radius input to an object { tl, tr, br, bl }
8266
+ let r = { tl: 0, tr: 0, br: 0, bl: 0 };
8267
+ if (typeof radius === 'number') {
8268
+ r = { tl: radius, tr: radius, br: radius, bl: radius };
8269
+ } else if (typeof radius === 'object' && radius !== null) {
8270
+ r = { ...r, ...radius }; // Merge with defaults
8271
+ }
8272
+
8273
+ // 1. Draw the Mask (Custom Shape with specific corners)
8274
+ ctx.beginPath();
8275
+
8276
+ // Border Radius Clamping Logic (CSS Spec)
8277
+ // Prevents corners from overlapping if radii are too large for the container
8278
+ const factor = Math.min(
8279
+ targetW / (r.tl + r.tr) || Infinity,
8280
+ targetH / (r.tr + r.br) || Infinity,
8281
+ targetW / (r.br + r.bl) || Infinity,
8282
+ targetH / (r.bl + r.tl) || Infinity
8283
+ );
8284
+
8285
+ if (factor < 1) {
8286
+ r.tl *= factor;
8287
+ r.tr *= factor;
8288
+ r.br *= factor;
8289
+ r.bl *= factor;
8290
+ }
8291
+
8292
+ // Draw path: Top-Left -> Top-Right -> Bottom-Right -> Bottom-Left
8293
+ ctx.moveTo(r.tl, 0);
8294
+ ctx.lineTo(targetW - r.tr, 0);
8295
+ ctx.arcTo(targetW, 0, targetW, r.tr, r.tr);
8296
+ ctx.lineTo(targetW, targetH - r.br);
8297
+ ctx.arcTo(targetW, targetH, targetW - r.br, targetH, r.br);
8298
+ ctx.lineTo(r.bl, targetH);
8299
+ ctx.arcTo(0, targetH, 0, targetH - r.bl, r.bl);
8300
+ ctx.lineTo(0, r.tl);
8301
+ ctx.arcTo(0, 0, r.tl, 0, r.tl);
8302
+
8303
+ ctx.closePath();
8304
+ ctx.fillStyle = '#000';
8305
+ ctx.fill();
8306
+
8307
+ // 2. Composite Source-In (Crops the next image draw to the mask)
8308
+ ctx.globalCompositeOperation = 'source-in';
8309
+
8310
+ // 3. Draw Image (Object Cover Logic)
8311
+ const wRatio = targetW / img.width;
8312
+ const hRatio = targetH / img.height;
8313
+ const maxRatio = Math.max(wRatio, hRatio);
8314
+ const renderW = img.width * maxRatio;
8315
+ const renderH = img.height * maxRatio;
8316
+ const renderX = (targetW - renderW) / 2;
8317
+ const renderY = (targetH - renderH) / 2;
8318
+
8319
+ ctx.drawImage(img, renderX, renderY, renderW, renderH);
8320
+
8321
+ resolve(canvas.toDataURL('image/png'));
8322
+ };
8323
+
8324
+ img.onerror = () => resolve(null);
8325
+ img.src = src;
8326
+ });
8351
8327
  }
8352
8328
 
8353
8329
  // src/index.js
@@ -8419,66 +8395,115 @@ async function processSlide(root, slide, pptx) {
8419
8395
  };
8420
8396
 
8421
8397
  const renderQueue = [];
8398
+ const asyncTasks = []; // Queue for heavy operations (Images, Canvas)
8422
8399
  let domOrderCounter = 0;
8423
8400
 
8424
- async function collect(node) {
8401
+ // Sync Traversal Function
8402
+ function collect(node, parentZIndex) {
8425
8403
  const order = domOrderCounter++;
8426
- const result = await createRenderItem(node, { ...layoutConfig, root }, order, pptx);
8404
+
8405
+ let currentZ = parentZIndex;
8406
+ let nodeStyle = null;
8407
+ const nodeType = node.nodeType;
8408
+
8409
+ if (nodeType === 1) {
8410
+ nodeStyle = window.getComputedStyle(node);
8411
+ // Optimization: Skip completely hidden elements immediately
8412
+ if (
8413
+ nodeStyle.display === 'none' ||
8414
+ nodeStyle.visibility === 'hidden' ||
8415
+ nodeStyle.opacity === '0'
8416
+ ) {
8417
+ return;
8418
+ }
8419
+ if (nodeStyle.zIndex !== 'auto') {
8420
+ currentZ = parseInt(nodeStyle.zIndex);
8421
+ }
8422
+ }
8423
+
8424
+ // Prepare the item. If it needs async work, it returns a 'job'
8425
+ const result = prepareRenderItem(
8426
+ node,
8427
+ { ...layoutConfig, root },
8428
+ order,
8429
+ pptx,
8430
+ currentZ,
8431
+ nodeStyle
8432
+ );
8433
+
8427
8434
  if (result) {
8428
- if (result.items) renderQueue.push(...result.items);
8435
+ if (result.items) {
8436
+ // Push items immediately to queue (data might be missing but filled later)
8437
+ renderQueue.push(...result.items);
8438
+ }
8439
+ if (result.job) {
8440
+ // Push the promise-returning function to the task list
8441
+ asyncTasks.push(result.job);
8442
+ }
8429
8443
  if (result.stopRecursion) return;
8430
8444
  }
8431
- for (const child of node.children) await collect(child);
8445
+
8446
+ // Recurse children synchronously
8447
+ const childNodes = node.childNodes;
8448
+ for (let i = 0; i < childNodes.length; i++) {
8449
+ collect(childNodes[i], currentZ);
8450
+ }
8432
8451
  }
8433
8452
 
8434
- await collect(root);
8453
+ // 1. Traverse and build the structure (Fast)
8454
+ collect(root, 0);
8435
8455
 
8436
- renderQueue.sort((a, b) => {
8456
+ // 2. Execute all heavy tasks in parallel (Fast)
8457
+ if (asyncTasks.length > 0) {
8458
+ await Promise.all(asyncTasks.map((task) => task()));
8459
+ }
8460
+
8461
+ // 3. Cleanup and Sort
8462
+ // Remove items that failed to generate data (marked with skip)
8463
+ const finalQueue = renderQueue.filter(
8464
+ (item) => !item.skip && (item.type !== 'image' || item.options.data)
8465
+ );
8466
+
8467
+ finalQueue.sort((a, b) => {
8437
8468
  if (a.zIndex !== b.zIndex) return a.zIndex - b.zIndex;
8438
8469
  return a.domOrder - b.domOrder;
8439
8470
  });
8440
8471
 
8441
- for (const item of renderQueue) {
8472
+ // 4. Add to Slide
8473
+ for (const item of finalQueue) {
8442
8474
  if (item.type === 'shape') slide.addShape(item.shapeType, item.options);
8443
8475
  if (item.type === 'image') slide.addImage(item.options);
8444
8476
  if (item.type === 'text') slide.addText(item.textParts, item.options);
8445
8477
  }
8446
8478
  }
8447
8479
 
8448
- async function elementToCanvasImage(node, widthPx, heightPx, root) {
8480
+ /**
8481
+ * Optimized html2canvas wrapper
8482
+ * Now strictly captures the node itself, not the root.
8483
+ */
8484
+ async function elementToCanvasImage(node, widthPx, heightPx) {
8449
8485
  return new Promise((resolve) => {
8450
- const width = Math.ceil(widthPx);
8451
- const height = Math.ceil(heightPx);
8452
-
8453
- if (width <= 0 || height <= 0) {
8454
- resolve(null);
8455
- return;
8456
- }
8457
-
8486
+ const width = Math.max(Math.ceil(widthPx), 1);
8487
+ const height = Math.max(Math.ceil(heightPx), 1);
8458
8488
  const style = window.getComputedStyle(node);
8459
8489
 
8460
- html2canvas(root, {
8461
- width: root.scrollWidth,
8462
- height: root.scrollHeight,
8463
- useCORS: true,
8464
- allowTaint: true,
8490
+ // Optimized: Capture ONLY the specific node
8491
+ html2canvas(node, {
8465
8492
  backgroundColor: null,
8493
+ logging: false,
8494
+ scale: 2, // Slight quality boost
8466
8495
  })
8467
8496
  .then((canvas) => {
8468
- const rootCanvas = canvas;
8469
- const nodeRect = node.getBoundingClientRect();
8470
- const rootRect = root.getBoundingClientRect();
8471
- const sourceX = nodeRect.left - rootRect.left;
8472
- const sourceY = nodeRect.top - rootRect.top;
8473
-
8474
8497
  const destCanvas = document.createElement('canvas');
8475
8498
  destCanvas.width = width;
8476
8499
  destCanvas.height = height;
8477
8500
  const ctx = destCanvas.getContext('2d');
8478
8501
 
8479
- ctx.drawImage(rootCanvas, sourceX, sourceY, width, height, 0, 0, width, height);
8502
+ // Draw the captured canvas into our sized canvas
8503
+ // html2canvas might return a larger canvas if scale > 1, so we fit it
8504
+ ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, width, height);
8480
8505
 
8481
- // Parse radii
8506
+ // Apply border radius clipping
8482
8507
  let tl = parseFloat(style.borderTopLeftRadius) || 0;
8483
8508
  let tr = parseFloat(style.borderTopRightRadius) || 0;
8484
8509
  let br = parseFloat(style.borderBottomRightRadius) || 0;
@@ -8498,38 +8523,89 @@ async function elementToCanvasImage(node, widthPx, heightPx, root) {
8498
8523
  bl *= f;
8499
8524
  }
8500
8525
 
8501
- ctx.globalCompositeOperation = 'destination-in';
8502
- ctx.beginPath();
8503
- ctx.moveTo(tl, 0);
8504
- ctx.lineTo(width - tr, 0);
8505
- ctx.arcTo(width, 0, width, tr, tr);
8506
- ctx.lineTo(width, height - br);
8507
- ctx.arcTo(width, height, width - br, height, br);
8508
- ctx.lineTo(bl, height);
8509
- ctx.arcTo(0, height, 0, height - bl, bl);
8510
- ctx.lineTo(0, tl);
8511
- ctx.arcTo(0, 0, tl, 0, tl);
8512
- ctx.closePath();
8513
- ctx.fill();
8526
+ if (tl + tr + br + bl > 0) {
8527
+ ctx.globalCompositeOperation = 'destination-in';
8528
+ ctx.beginPath();
8529
+ ctx.moveTo(tl, 0);
8530
+ ctx.lineTo(width - tr, 0);
8531
+ ctx.arcTo(width, 0, width, tr, tr);
8532
+ ctx.lineTo(width, height - br);
8533
+ ctx.arcTo(width, height, width - br, height, br);
8534
+ ctx.lineTo(bl, height);
8535
+ ctx.arcTo(0, height, 0, height - bl, bl);
8536
+ ctx.lineTo(0, tl);
8537
+ ctx.arcTo(0, 0, tl, 0, tl);
8538
+ ctx.closePath();
8539
+ ctx.fill();
8540
+ }
8514
8541
 
8515
8542
  resolve(destCanvas.toDataURL('image/png'));
8516
8543
  })
8517
- .catch(() => resolve(null));
8544
+ .catch((e) => {
8545
+ console.warn('Canvas capture failed for node', node, e);
8546
+ resolve(null);
8547
+ });
8518
8548
  });
8519
8549
  }
8520
8550
 
8521
- async function createRenderItem(node, config, domOrder, pptx) {
8551
+ /**
8552
+ * Replaces createRenderItem.
8553
+ * Returns { items: [], job: () => Promise, stopRecursion: boolean }
8554
+ */
8555
+ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, computedStyle) {
8556
+ // 1. Text Node Handling
8557
+ if (node.nodeType === 3) {
8558
+ const textContent = node.nodeValue.trim();
8559
+ if (!textContent) return null;
8560
+
8561
+ const parent = node.parentElement;
8562
+ if (!parent) return null;
8563
+
8564
+ if (isTextContainer(parent)) return null; // Parent handles it
8565
+
8566
+ const range = document.createRange();
8567
+ range.selectNode(node);
8568
+ const rect = range.getBoundingClientRect();
8569
+ range.detach();
8570
+
8571
+ const style = window.getComputedStyle(parent);
8572
+ const widthPx = rect.width;
8573
+ const heightPx = rect.height;
8574
+ const unrotatedW = widthPx * PX_TO_INCH * config.scale;
8575
+ const unrotatedH = heightPx * PX_TO_INCH * config.scale;
8576
+
8577
+ const x = config.offX + (rect.left - config.rootX) * PX_TO_INCH * config.scale;
8578
+ const y = config.offY + (rect.top - config.rootY) * PX_TO_INCH * config.scale;
8579
+
8580
+ return {
8581
+ items: [
8582
+ {
8583
+ type: 'text',
8584
+ zIndex: effectiveZIndex,
8585
+ domOrder,
8586
+ textParts: [
8587
+ {
8588
+ text: textContent,
8589
+ options: getTextStyle(style, config.scale),
8590
+ },
8591
+ ],
8592
+ options: { x, y, w: unrotatedW, h: unrotatedH, margin: 0, autoFit: false },
8593
+ },
8594
+ ],
8595
+ stopRecursion: false,
8596
+ };
8597
+ }
8598
+
8522
8599
  if (node.nodeType !== 1) return null;
8523
- const style = window.getComputedStyle(node);
8524
- if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')
8525
- return null;
8600
+ const style = computedStyle; // Use pre-computed style
8526
8601
 
8527
8602
  const rect = node.getBoundingClientRect();
8528
8603
  if (rect.width < 0.5 || rect.height < 0.5) return null;
8529
8604
 
8530
- const zIndex = style.zIndex !== 'auto' ? parseInt(style.zIndex) : 0;
8605
+ const zIndex = effectiveZIndex;
8531
8606
  const rotation = getRotation(style.transform);
8532
8607
  const elementOpacity = parseFloat(style.opacity);
8608
+ const safeOpacity = isNaN(elementOpacity) ? 1 : elementOpacity;
8533
8609
 
8534
8610
  const widthPx = node.offsetWidth || rect.width;
8535
8611
  const heightPx = node.offsetHeight || rect.height;
@@ -8545,21 +8621,31 @@ async function createRenderItem(node, config, domOrder, pptx) {
8545
8621
 
8546
8622
  const items = [];
8547
8623
 
8548
- if (node.nodeName.toUpperCase() === 'SVG') {
8549
- const pngData = await svgToPng(node);
8550
- if (pngData)
8551
- items.push({
8552
- type: 'image',
8553
- zIndex,
8554
- domOrder,
8555
- options: { data: pngData, x, y, w, h, rotate: rotation },
8556
- });
8557
- return { items, stopRecursion: true };
8624
+ // --- ASYNC JOB: SVGs / Icons ---
8625
+ if (
8626
+ node.nodeName.toUpperCase() === 'SVG' ||
8627
+ node.tagName.includes('-') ||
8628
+ node.tagName === 'ION-ICON'
8629
+ ) {
8630
+ const item = {
8631
+ type: 'image',
8632
+ zIndex,
8633
+ domOrder,
8634
+ options: { x, y, w, h, rotate: rotation, data: null }, // Data null initially
8635
+ };
8636
+
8637
+ // Create Job
8638
+ const job = async () => {
8639
+ const pngData = await elementToCanvasImage(node, widthPx, heightPx);
8640
+ if (pngData) item.options.data = pngData;
8641
+ else item.skip = true;
8642
+ };
8643
+
8644
+ return { items: [item], job, stopRecursion: true };
8558
8645
  }
8559
8646
 
8560
- // --- UPDATED IMG BLOCK START ---
8647
+ // --- ASYNC JOB: IMG Tags ---
8561
8648
  if (node.tagName === 'IMG') {
8562
- // Extract individual corner radii
8563
8649
  let radii = {
8564
8650
  tl: parseFloat(style.borderTopLeftRadius) || 0,
8565
8651
  tr: parseFloat(style.borderTopRightRadius) || 0,
@@ -8568,8 +8654,6 @@ async function createRenderItem(node, config, domOrder, pptx) {
8568
8654
  };
8569
8655
 
8570
8656
  const hasAnyRadius = radii.tl > 0 || radii.tr > 0 || radii.br > 0 || radii.bl > 0;
8571
-
8572
- // Fallback: Check parent if image has no specific radius but parent clips it
8573
8657
  if (!hasAnyRadius) {
8574
8658
  const parent = node.parentElement;
8575
8659
  const parentStyle = window.getComputedStyle(parent);
@@ -8580,10 +8664,6 @@ async function createRenderItem(node, config, domOrder, pptx) {
8580
8664
  br: parseFloat(parentStyle.borderBottomRightRadius) || 0,
8581
8665
  bl: parseFloat(parentStyle.borderBottomLeftRadius) || 0,
8582
8666
  };
8583
- // Simple heuristic: If image takes up full size of parent, inherit radii.
8584
- // For complex grids (like slide-1), this blindly applies parent radius.
8585
- // In a perfect world, we'd calculate intersection, but for now we apply parent radius
8586
- // if the image is close to the parent's size, effectively masking it.
8587
8667
  const pRect = parent.getBoundingClientRect();
8588
8668
  if (Math.abs(pRect.width - rect.width) < 5 && Math.abs(pRect.height - rect.height) < 5) {
8589
8669
  radii = pRadii;
@@ -8591,19 +8671,23 @@ async function createRenderItem(node, config, domOrder, pptx) {
8591
8671
  }
8592
8672
  }
8593
8673
 
8594
- const processed = await getProcessedImage(node.src, widthPx, heightPx, radii);
8595
- if (processed)
8596
- items.push({
8597
- type: 'image',
8598
- zIndex,
8599
- domOrder,
8600
- options: { data: processed, x, y, w, h, rotate: rotation },
8601
- });
8602
- return { items, stopRecursion: true };
8674
+ const item = {
8675
+ type: 'image',
8676
+ zIndex,
8677
+ domOrder,
8678
+ options: { x, y, w, h, rotate: rotation, data: null },
8679
+ };
8680
+
8681
+ const job = async () => {
8682
+ const processed = await getProcessedImage(node.src, widthPx, heightPx, radii);
8683
+ if (processed) item.options.data = processed;
8684
+ else item.skip = true;
8685
+ };
8686
+
8687
+ return { items: [item], job, stopRecursion: true };
8603
8688
  }
8604
- // --- UPDATED IMG BLOCK END ---
8605
8689
 
8606
- // Radii processing for Divs/Shapes
8690
+ // Radii logic
8607
8691
  const borderRadiusValue = parseFloat(style.borderRadius) || 0;
8608
8692
  const borderBottomLeftRadius = parseFloat(style.borderBottomLeftRadius) || 0;
8609
8693
  const borderBottomRightRadius = parseFloat(style.borderBottomRightRadius) || 0;
@@ -8621,25 +8705,30 @@ async function createRenderItem(node, config, domOrder, pptx) {
8621
8705
  borderTopLeftRadius ||
8622
8706
  borderTopRightRadius));
8623
8707
 
8624
- // Allow clipped elements to be rendered via canvas
8708
+ // --- ASYNC JOB: Clipped Divs via Canvas ---
8625
8709
  if (hasPartialBorderRadius && isClippedByParent(node)) {
8626
8710
  const marginLeft = parseFloat(style.marginLeft) || 0;
8627
8711
  const marginTop = parseFloat(style.marginTop) || 0;
8628
8712
  x += marginLeft * PX_TO_INCH * config.scale;
8629
8713
  y += marginTop * PX_TO_INCH * config.scale;
8630
8714
 
8631
- const canvasImageData = await elementToCanvasImage(node, widthPx, heightPx, config.root);
8632
- if (canvasImageData) {
8633
- items.push({
8634
- type: 'image',
8635
- zIndex,
8636
- domOrder,
8637
- options: { data: canvasImageData, x, y, w, h, rotate: rotation },
8638
- });
8639
- return { items, stopRecursion: true };
8640
- }
8715
+ const item = {
8716
+ type: 'image',
8717
+ zIndex,
8718
+ domOrder,
8719
+ options: { x, y, w, h, rotate: rotation, data: null },
8720
+ };
8721
+
8722
+ const job = async () => {
8723
+ const canvasImageData = await elementToCanvasImage(node, widthPx, heightPx);
8724
+ if (canvasImageData) item.options.data = canvasImageData;
8725
+ else item.skip = true;
8726
+ };
8727
+
8728
+ return { items: [item], job, stopRecursion: true };
8641
8729
  }
8642
8730
 
8731
+ // --- SYNC: Standard CSS Extraction ---
8643
8732
  const bgColorObj = parseColor(style.backgroundColor);
8644
8733
  const bgClip = style.webkitBackgroundClip || style.backgroundClip;
8645
8734
  const isBgClipText = bgClip === 'text';
@@ -8678,7 +8767,7 @@ async function createRenderItem(node, config, domOrder, pptx) {
8678
8767
  x -= bulletShift;
8679
8768
  w += bulletShift;
8680
8769
  textParts.push({
8681
- text: '',
8770
+ text: ' ',
8682
8771
  options: {
8683
8772
  color: parseColor(style.color).hex || '000000',
8684
8773
  fontSize: fontSizePt,
@@ -8784,7 +8873,6 @@ async function createRenderItem(node, config, domOrder, pptx) {
8784
8873
  });
8785
8874
  }
8786
8875
  if (hasCompositeBorder) {
8787
- // Add border shapes after the main background
8788
8876
  const borderItems = createCompositeBorderItems(
8789
8877
  borderInfo.sides,
8790
8878
  x,
@@ -8804,7 +8892,7 @@ async function createRenderItem(node, config, domOrder, pptx) {
8804
8892
  hasShadow ||
8805
8893
  textPayload
8806
8894
  ) {
8807
- const finalAlpha = elementOpacity * bgColorObj.opacity;
8895
+ const finalAlpha = safeOpacity * bgColorObj.opacity;
8808
8896
  const transparency = (1 - finalAlpha) * 100;
8809
8897
  const useSolidFill = bgColorObj.hex && !isImageWrapper;
8810
8898
 
@@ -8826,14 +8914,7 @@ async function createRenderItem(node, config, domOrder, pptx) {
8826
8914
  type: 'image',
8827
8915
  zIndex,
8828
8916
  domOrder,
8829
- options: {
8830
- data: shapeSvg,
8831
- x,
8832
- y,
8833
- w,
8834
- h,
8835
- rotate: rotation,
8836
- },
8917
+ options: { data: shapeSvg, x, y, w, h, rotate: rotation },
8837
8918
  });
8838
8919
  } else {
8839
8920
  const shapeOpts = {
@@ -8848,9 +8929,7 @@ async function createRenderItem(node, config, domOrder, pptx) {
8848
8929
  line: hasUniformBorder ? borderInfo.options : null,
8849
8930
  };
8850
8931
 
8851
- if (hasShadow) {
8852
- shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
8853
- }
8932
+ if (hasShadow) shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
8854
8933
 
8855
8934
  const borderRadius = parseFloat(style.borderRadius) || 0;
8856
8935
  const aspectRatio = Math.max(widthPx, heightPx) / Math.min(widthPx, heightPx);
@@ -8913,77 +8992,49 @@ async function createRenderItem(node, config, domOrder, pptx) {
8913
8992
  return { items, stopRecursion: !!textPayload };
8914
8993
  }
8915
8994
 
8916
- /**
8917
- * Helper function to create individual border shapes
8918
- */
8919
8995
  function createCompositeBorderItems(sides, x, y, w, h, scale, zIndex, domOrder) {
8920
8996
  const items = [];
8921
8997
  const pxToInch = 1 / 96;
8998
+ const common = { zIndex: zIndex + 1, domOrder, shapeType: 'rect' };
8922
8999
 
8923
- // TOP BORDER
8924
- if (sides.top.width > 0) {
9000
+ if (sides.top.width > 0)
8925
9001
  items.push({
8926
- type: 'shape',
8927
- zIndex: zIndex + 1,
8928
- domOrder,
8929
- shapeType: 'rect',
8930
- options: {
8931
- x: x,
8932
- y: y,
8933
- w: w,
8934
- h: sides.top.width * pxToInch * scale,
8935
- fill: { color: sides.top.color },
8936
- },
9002
+ ...common,
9003
+ options: { x, y, w, h: sides.top.width * pxToInch * scale, fill: { color: sides.top.color } },
8937
9004
  });
8938
- }
8939
- // RIGHT BORDER
8940
- if (sides.right.width > 0) {
9005
+ if (sides.right.width > 0)
8941
9006
  items.push({
8942
- type: 'shape',
8943
- zIndex: zIndex + 1,
8944
- domOrder,
8945
- shapeType: 'rect',
9007
+ ...common,
8946
9008
  options: {
8947
9009
  x: x + w - sides.right.width * pxToInch * scale,
8948
- y: y,
9010
+ y,
8949
9011
  w: sides.right.width * pxToInch * scale,
8950
- h: h,
9012
+ h,
8951
9013
  fill: { color: sides.right.color },
8952
9014
  },
8953
9015
  });
8954
- }
8955
- // BOTTOM BORDER
8956
- if (sides.bottom.width > 0) {
9016
+ if (sides.bottom.width > 0)
8957
9017
  items.push({
8958
- type: 'shape',
8959
- zIndex: zIndex + 1,
8960
- domOrder,
8961
- shapeType: 'rect',
9018
+ ...common,
8962
9019
  options: {
8963
- x: x,
9020
+ x,
8964
9021
  y: y + h - sides.bottom.width * pxToInch * scale,
8965
- w: w,
9022
+ w,
8966
9023
  h: sides.bottom.width * pxToInch * scale,
8967
9024
  fill: { color: sides.bottom.color },
8968
9025
  },
8969
9026
  });
8970
- }
8971
- // LEFT BORDER
8972
- if (sides.left.width > 0) {
9027
+ if (sides.left.width > 0)
8973
9028
  items.push({
8974
- type: 'shape',
8975
- zIndex: zIndex + 1,
8976
- domOrder,
8977
- shapeType: 'rect',
9029
+ ...common,
8978
9030
  options: {
8979
- x: x,
8980
- y: y,
9031
+ x,
9032
+ y,
8981
9033
  w: sides.left.width * pxToInch * scale,
8982
- h: h,
9034
+ h,
8983
9035
  fill: { color: sides.left.color },
8984
9036
  },
8985
9037
  });
8986
- }
8987
9038
 
8988
9039
  return items;
8989
9040
  }