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.
@@ -1,8 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
3
5
  var PptxGenJSImport = require('pptxgenjs');
4
6
 
5
- function _interopNamespaceDefault(e) {
7
+ function _interopNamespace(e) {
8
+ if (e && e.__esModule) return e;
6
9
  var n = Object.create(null);
7
10
  if (e) {
8
11
  Object.keys(e).forEach(function (k) {
@@ -15,11 +18,11 @@ function _interopNamespaceDefault(e) {
15
18
  }
16
19
  });
17
20
  }
18
- n.default = e;
21
+ n["default"] = e;
19
22
  return Object.freeze(n);
20
23
  }
21
24
 
22
- var PptxGenJSImport__namespace = /*#__PURE__*/_interopNamespaceDefault(PptxGenJSImport);
25
+ var PptxGenJSImport__namespace = /*#__PURE__*/_interopNamespace(PptxGenJSImport);
23
26
 
24
27
  /*!
25
28
  * html2canvas 1.4.1 <https://html2canvas.hertzen.com>
@@ -74,7 +77,7 @@ function __awaiter(thisArg, _arguments, P, generator) {
74
77
  function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
75
78
  function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
76
79
  function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
77
- step((generator = generator.apply(thisArg, [])).next());
80
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
78
81
  });
79
82
  }
80
83
 
@@ -107,7 +110,7 @@ function __generator(thisArg, body) {
107
110
  }
108
111
 
109
112
  function __spreadArray(to, from, pack) {
110
- if (arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
113
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
111
114
  if (ar || !(i in from)) {
112
115
  if (!ar) ar = Array.prototype.slice.call(from, 0, i);
113
116
  ar[i] = from[i];
@@ -7976,17 +7979,20 @@ function generateCompositeBorderSVG(w, h, radius, sides) {
7976
7979
  */
7977
7980
  function generateCustomShapeSVG(w, h, color, opacity, radii) {
7978
7981
  let { tl, tr, br, bl } = radii;
7979
-
7982
+
7980
7983
  // Clamp radii using CSS spec logic (avoid overlap)
7981
7984
  const factor = Math.min(
7982
- (w / (tl + tr)) || Infinity,
7983
- (h / (tr + br)) || Infinity,
7984
- (w / (br + bl)) || Infinity,
7985
- (h / (bl + tl)) || Infinity
7985
+ w / (tl + tr) || Infinity,
7986
+ h / (tr + br) || Infinity,
7987
+ w / (br + bl) || Infinity,
7988
+ h / (bl + tl) || Infinity
7986
7989
  );
7987
-
7990
+
7988
7991
  if (factor < 1) {
7989
- tl *= factor; tr *= factor; br *= factor; bl *= factor;
7992
+ tl *= factor;
7993
+ tr *= factor;
7994
+ br *= factor;
7995
+ bl *= factor;
7990
7996
  }
7991
7997
 
7992
7998
  const path = `
@@ -8017,7 +8023,10 @@ function parseColor(str) {
8017
8023
  if (str.startsWith('#')) {
8018
8024
  let hex = str.slice(1);
8019
8025
  if (hex.length === 3)
8020
- hex = hex.split('').map((c) => c + c).join('');
8026
+ hex = hex
8027
+ .split('')
8028
+ .map((c) => c + c)
8029
+ .join('');
8021
8030
  return { hex: hex.toUpperCase(), opacity: 1 };
8022
8031
  }
8023
8032
  const match = str.match(/[\d.]+/g);
@@ -8080,25 +8089,35 @@ function isTextContainer(node) {
8080
8089
 
8081
8090
  // Check if children are purely inline text formatting or visual shapes
8082
8091
  const isSafeInline = (el) => {
8092
+ // 1. Reject Web Components / Icons / Images
8093
+ if (el.tagName.includes('-')) return false;
8094
+ if (el.tagName === 'IMG' || el.tagName === 'SVG') return false;
8095
+
8083
8096
  const style = window.getComputedStyle(el);
8084
8097
  const display = style.display;
8085
-
8086
- // If it's a standard inline element
8087
- const isInlineTag = ['SPAN', 'B', 'STRONG', 'EM', 'I', 'A', 'SMALL'].includes(el.tagName);
8098
+
8099
+ // 2. Initial check: Must be a standard inline tag OR display:inline
8100
+ const isInlineTag = ['SPAN', 'B', 'STRONG', 'EM', 'I', 'A', 'SMALL', 'MARK'].includes(el.tagName);
8088
8101
  const isInlineDisplay = display.includes('inline');
8089
8102
 
8090
8103
  if (!isInlineTag && !isInlineDisplay) return false;
8091
8104
 
8092
- // Check if element is a shape (visual object without text)
8093
- // If an element is empty but has a visible background/border, it's a shape (like a dot).
8094
- // We must return false so the parent isn't treated as a text-only container.
8095
- const hasContent = el.textContent.trim().length > 0;
8105
+ // 3. CRITICAL FIX: Check for Structural Styling
8106
+ // PPTX Text Runs (parts of a text line) CANNOT have backgrounds, borders, or padding.
8107
+ // If a child element has these, the parent is NOT a simple text container;
8108
+ // it is a layout container composed of styled blocks.
8096
8109
  const bgColor = parseColor(style.backgroundColor);
8097
8110
  const hasVisibleBg = bgColor.hex && bgColor.opacity > 0;
8098
8111
  const hasBorder = parseFloat(style.borderWidth) > 0 && parseColor(style.borderColor).opacity > 0;
8099
8112
 
8113
+ if (hasVisibleBg || hasBorder) {
8114
+ return false;
8115
+ }
8116
+
8117
+ // 4. Check for empty shapes (visual objects without text, like dots)
8118
+ const hasContent = el.textContent.trim().length > 0;
8100
8119
  if (!hasContent && (hasVisibleBg || hasBorder)) {
8101
- return false;
8120
+ return false;
8102
8121
  }
8103
8122
 
8104
8123
  return true;
@@ -8116,62 +8135,6 @@ function getRotation(transformStr) {
8116
8135
  return Math.round(Math.atan2(b, a) * (180 / Math.PI));
8117
8136
  }
8118
8137
 
8119
- function svgToPng(node) {
8120
- return new Promise((resolve) => {
8121
- const clone = node.cloneNode(true);
8122
- const rect = node.getBoundingClientRect();
8123
- const width = rect.width || 300;
8124
- const height = rect.height || 150;
8125
-
8126
- function inlineStyles(source, target) {
8127
- const computed = window.getComputedStyle(source);
8128
- const properties = [
8129
- 'fill', 'stroke', 'stroke-width', 'stroke-linecap',
8130
- 'stroke-linejoin', 'opacity', 'font-family', 'font-size', 'font-weight',
8131
- ];
8132
-
8133
- if (computed.fill === 'none') target.setAttribute('fill', 'none');
8134
- else if (computed.fill) target.style.fill = computed.fill;
8135
-
8136
- if (computed.stroke === 'none') target.setAttribute('stroke', 'none');
8137
- else if (computed.stroke) target.style.stroke = computed.stroke;
8138
-
8139
- properties.forEach((prop) => {
8140
- if (prop !== 'fill' && prop !== 'stroke') {
8141
- const val = computed[prop];
8142
- if (val && val !== 'auto') target.style[prop] = val;
8143
- }
8144
- });
8145
-
8146
- for (let i = 0; i < source.children.length; i++) {
8147
- if (target.children[i]) inlineStyles(source.children[i], target.children[i]);
8148
- }
8149
- }
8150
-
8151
- inlineStyles(node, clone);
8152
- clone.setAttribute('width', width);
8153
- clone.setAttribute('height', height);
8154
- clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
8155
-
8156
- const xml = new XMLSerializer().serializeToString(clone);
8157
- const svgUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(xml)}`;
8158
- const img = new Image();
8159
- img.crossOrigin = 'Anonymous';
8160
- img.onload = () => {
8161
- const canvas = document.createElement('canvas');
8162
- const scale = 3;
8163
- canvas.width = width * scale;
8164
- canvas.height = height * scale;
8165
- const ctx = canvas.getContext('2d');
8166
- ctx.scale(scale, scale);
8167
- ctx.drawImage(img, 0, 0, width, height);
8168
- resolve(canvas.toDataURL('image/png'));
8169
- };
8170
- img.onerror = () => resolve(null);
8171
- img.src = svgUrl;
8172
- });
8173
- }
8174
-
8175
8138
  function getVisibleShadow(shadowStr, scale) {
8176
8139
  if (!shadowStr || shadowStr === 'none') return null;
8177
8140
  const shadows = shadowStr.split(/,(?![^()]*\))/);
@@ -8210,16 +8173,29 @@ function generateGradientSVG(w, h, bgString, radius, border) {
8210
8173
  const content = match[1];
8211
8174
  const parts = content.split(/,(?![^()]*\))/).map((p) => p.trim());
8212
8175
 
8213
- let x1 = '0%', y1 = '0%', x2 = '0%', y2 = '100%';
8176
+ let x1 = '0%',
8177
+ y1 = '0%',
8178
+ x2 = '0%',
8179
+ y2 = '100%';
8214
8180
  let stopsStartIdx = 0;
8215
8181
  if (parts[0].includes('to right')) {
8216
- x1 = '0%'; x2 = '100%'; y2 = '0%'; stopsStartIdx = 1;
8182
+ x1 = '0%';
8183
+ x2 = '100%';
8184
+ y2 = '0%';
8185
+ stopsStartIdx = 1;
8217
8186
  } else if (parts[0].includes('to left')) {
8218
- x1 = '100%'; x2 = '0%'; y2 = '0%'; stopsStartIdx = 1;
8187
+ x1 = '100%';
8188
+ x2 = '0%';
8189
+ y2 = '0%';
8190
+ stopsStartIdx = 1;
8219
8191
  } else if (parts[0].includes('to top')) {
8220
- y1 = '100%'; y2 = '0%'; stopsStartIdx = 1;
8192
+ y1 = '100%';
8193
+ y2 = '0%';
8194
+ stopsStartIdx = 1;
8221
8195
  } else if (parts[0].includes('to bottom')) {
8222
- y1 = '0%'; y2 = '100%'; stopsStartIdx = 1;
8196
+ y1 = '0%';
8197
+ y2 = '100%';
8198
+ stopsStartIdx = 1;
8223
8199
  }
8224
8200
 
8225
8201
  let stopsXML = '';
@@ -8294,81 +8270,84 @@ function generateBlurredSVG(w, h, color, radius, blurPx) {
8294
8270
  };
8295
8271
  }
8296
8272
 
8297
- // src/image-processor.js
8298
-
8299
- async function getProcessedImage(src, targetW, targetH, radius) {
8300
- return new Promise((resolve) => {
8301
- const img = new Image();
8302
- img.crossOrigin = 'Anonymous'; // Critical for canvas manipulation
8303
-
8304
- img.onload = () => {
8305
- const canvas = document.createElement('canvas');
8306
- // Double resolution for better quality
8307
- const scale = 2;
8308
- canvas.width = targetW * scale;
8309
- canvas.height = targetH * scale;
8310
- const ctx = canvas.getContext('2d');
8311
- ctx.scale(scale, scale);
8312
-
8313
- // Normalize radius input to an object { tl, tr, br, bl }
8314
- let r = { tl: 0, tr: 0, br: 0, bl: 0 };
8315
- if (typeof radius === 'number') {
8316
- r = { tl: radius, tr: radius, br: radius, bl: radius };
8317
- } else if (typeof radius === 'object' && radius !== null) {
8318
- r = { ...r, ...radius }; // Merge with defaults
8319
- }
8320
-
8321
- // 1. Draw the Mask (Custom Shape with specific corners)
8322
- ctx.beginPath();
8323
-
8324
- // Border Radius Clamping Logic (CSS Spec)
8325
- // Prevents corners from overlapping if radii are too large for the container
8326
- const factor = Math.min(
8327
- (targetW / (r.tl + r.tr)) || Infinity,
8328
- (targetH / (r.tr + r.br)) || Infinity,
8329
- (targetW / (r.br + r.bl)) || Infinity,
8330
- (targetH / (r.bl + r.tl)) || Infinity
8331
- );
8332
-
8333
- if (factor < 1) {
8334
- r.tl *= factor; r.tr *= factor; r.br *= factor; r.bl *= factor;
8335
- }
8336
-
8337
- // Draw path: Top-Left -> Top-Right -> Bottom-Right -> Bottom-Left
8338
- ctx.moveTo(r.tl, 0);
8339
- ctx.lineTo(targetW - r.tr, 0);
8340
- ctx.arcTo(targetW, 0, targetW, r.tr, r.tr);
8341
- ctx.lineTo(targetW, targetH - r.br);
8342
- ctx.arcTo(targetW, targetH, targetW - r.br, targetH, r.br);
8343
- ctx.lineTo(r.bl, targetH);
8344
- ctx.arcTo(0, targetH, 0, targetH - r.bl, r.bl);
8345
- ctx.lineTo(0, r.tl);
8346
- ctx.arcTo(0, 0, r.tl, 0, r.tl);
8347
-
8348
- ctx.closePath();
8349
- ctx.fillStyle = '#000';
8350
- ctx.fill();
8351
-
8352
- // 2. Composite Source-In (Crops the next image draw to the mask)
8353
- ctx.globalCompositeOperation = 'source-in';
8354
-
8355
- // 3. Draw Image (Object Cover Logic)
8356
- const wRatio = targetW / img.width;
8357
- const hRatio = targetH / img.height;
8358
- const maxRatio = Math.max(wRatio, hRatio);
8359
- const renderW = img.width * maxRatio;
8360
- const renderH = img.height * maxRatio;
8361
- const renderX = (targetW - renderW) / 2;
8362
- const renderY = (targetH - renderH) / 2;
8363
-
8364
- ctx.drawImage(img, renderX, renderY, renderW, renderH);
8365
-
8366
- resolve(canvas.toDataURL('image/png'));
8367
- };
8368
-
8369
- img.onerror = () => resolve(null);
8370
- img.src = src;
8371
- });
8273
+ // src/image-processor.js
8274
+
8275
+ async function getProcessedImage(src, targetW, targetH, radius) {
8276
+ return new Promise((resolve) => {
8277
+ const img = new Image();
8278
+ img.crossOrigin = 'Anonymous'; // Critical for canvas manipulation
8279
+
8280
+ img.onload = () => {
8281
+ const canvas = document.createElement('canvas');
8282
+ // Double resolution for better quality
8283
+ const scale = 2;
8284
+ canvas.width = targetW * scale;
8285
+ canvas.height = targetH * scale;
8286
+ const ctx = canvas.getContext('2d');
8287
+ ctx.scale(scale, scale);
8288
+
8289
+ // Normalize radius input to an object { tl, tr, br, bl }
8290
+ let r = { tl: 0, tr: 0, br: 0, bl: 0 };
8291
+ if (typeof radius === 'number') {
8292
+ r = { tl: radius, tr: radius, br: radius, bl: radius };
8293
+ } else if (typeof radius === 'object' && radius !== null) {
8294
+ r = { ...r, ...radius }; // Merge with defaults
8295
+ }
8296
+
8297
+ // 1. Draw the Mask (Custom Shape with specific corners)
8298
+ ctx.beginPath();
8299
+
8300
+ // Border Radius Clamping Logic (CSS Spec)
8301
+ // Prevents corners from overlapping if radii are too large for the container
8302
+ const factor = Math.min(
8303
+ targetW / (r.tl + r.tr) || Infinity,
8304
+ targetH / (r.tr + r.br) || Infinity,
8305
+ targetW / (r.br + r.bl) || Infinity,
8306
+ targetH / (r.bl + r.tl) || Infinity
8307
+ );
8308
+
8309
+ if (factor < 1) {
8310
+ r.tl *= factor;
8311
+ r.tr *= factor;
8312
+ r.br *= factor;
8313
+ 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
+ });
8372
8351
  }
8373
8352
 
8374
8353
  // src/index.js
@@ -8440,66 +8419,115 @@ async function processSlide(root, slide, pptx) {
8440
8419
  };
8441
8420
 
8442
8421
  const renderQueue = [];
8422
+ const asyncTasks = []; // Queue for heavy operations (Images, Canvas)
8443
8423
  let domOrderCounter = 0;
8444
8424
 
8445
- async function collect(node) {
8425
+ // Sync Traversal Function
8426
+ function collect(node, parentZIndex) {
8446
8427
  const order = domOrderCounter++;
8447
- const result = await createRenderItem(node, { ...layoutConfig, root }, order, pptx);
8428
+
8429
+ let currentZ = parentZIndex;
8430
+ let nodeStyle = null;
8431
+ const nodeType = node.nodeType;
8432
+
8433
+ if (nodeType === 1) {
8434
+ nodeStyle = window.getComputedStyle(node);
8435
+ // Optimization: Skip completely hidden elements immediately
8436
+ if (
8437
+ nodeStyle.display === 'none' ||
8438
+ nodeStyle.visibility === 'hidden' ||
8439
+ nodeStyle.opacity === '0'
8440
+ ) {
8441
+ return;
8442
+ }
8443
+ if (nodeStyle.zIndex !== 'auto') {
8444
+ currentZ = parseInt(nodeStyle.zIndex);
8445
+ }
8446
+ }
8447
+
8448
+ // Prepare the item. If it needs async work, it returns a 'job'
8449
+ const result = prepareRenderItem(
8450
+ node,
8451
+ { ...layoutConfig, root },
8452
+ order,
8453
+ pptx,
8454
+ currentZ,
8455
+ nodeStyle
8456
+ );
8457
+
8448
8458
  if (result) {
8449
- if (result.items) renderQueue.push(...result.items);
8459
+ if (result.items) {
8460
+ // Push items immediately to queue (data might be missing but filled later)
8461
+ renderQueue.push(...result.items);
8462
+ }
8463
+ if (result.job) {
8464
+ // Push the promise-returning function to the task list
8465
+ asyncTasks.push(result.job);
8466
+ }
8450
8467
  if (result.stopRecursion) return;
8451
8468
  }
8452
- for (const child of node.children) await collect(child);
8469
+
8470
+ // Recurse children synchronously
8471
+ const childNodes = node.childNodes;
8472
+ for (let i = 0; i < childNodes.length; i++) {
8473
+ collect(childNodes[i], currentZ);
8474
+ }
8453
8475
  }
8454
8476
 
8455
- await collect(root);
8477
+ // 1. Traverse and build the structure (Fast)
8478
+ collect(root, 0);
8479
+
8480
+ // 2. Execute all heavy tasks in parallel (Fast)
8481
+ if (asyncTasks.length > 0) {
8482
+ await Promise.all(asyncTasks.map((task) => task()));
8483
+ }
8484
+
8485
+ // 3. Cleanup and Sort
8486
+ // Remove items that failed to generate data (marked with skip)
8487
+ const finalQueue = renderQueue.filter(
8488
+ (item) => !item.skip && (item.type !== 'image' || item.options.data)
8489
+ );
8456
8490
 
8457
- renderQueue.sort((a, b) => {
8491
+ finalQueue.sort((a, b) => {
8458
8492
  if (a.zIndex !== b.zIndex) return a.zIndex - b.zIndex;
8459
8493
  return a.domOrder - b.domOrder;
8460
8494
  });
8461
8495
 
8462
- for (const item of renderQueue) {
8496
+ // 4. Add to Slide
8497
+ for (const item of finalQueue) {
8463
8498
  if (item.type === 'shape') slide.addShape(item.shapeType, item.options);
8464
8499
  if (item.type === 'image') slide.addImage(item.options);
8465
8500
  if (item.type === 'text') slide.addText(item.textParts, item.options);
8466
8501
  }
8467
8502
  }
8468
8503
 
8469
- async function elementToCanvasImage(node, widthPx, heightPx, root) {
8504
+ /**
8505
+ * Optimized html2canvas wrapper
8506
+ * Now strictly captures the node itself, not the root.
8507
+ */
8508
+ async function elementToCanvasImage(node, widthPx, heightPx) {
8470
8509
  return new Promise((resolve) => {
8471
- const width = Math.ceil(widthPx);
8472
- const height = Math.ceil(heightPx);
8473
-
8474
- if (width <= 0 || height <= 0) {
8475
- resolve(null);
8476
- return;
8477
- }
8478
-
8510
+ const width = Math.max(Math.ceil(widthPx), 1);
8511
+ const height = Math.max(Math.ceil(heightPx), 1);
8479
8512
  const style = window.getComputedStyle(node);
8480
8513
 
8481
- html2canvas(root, {
8482
- width: root.scrollWidth,
8483
- height: root.scrollHeight,
8484
- useCORS: true,
8485
- allowTaint: true,
8514
+ // Optimized: Capture ONLY the specific node
8515
+ html2canvas(node, {
8486
8516
  backgroundColor: null,
8517
+ logging: false,
8518
+ scale: 2, // Slight quality boost
8487
8519
  })
8488
8520
  .then((canvas) => {
8489
- const rootCanvas = canvas;
8490
- const nodeRect = node.getBoundingClientRect();
8491
- const rootRect = root.getBoundingClientRect();
8492
- const sourceX = nodeRect.left - rootRect.left;
8493
- const sourceY = nodeRect.top - rootRect.top;
8494
-
8495
8521
  const destCanvas = document.createElement('canvas');
8496
8522
  destCanvas.width = width;
8497
8523
  destCanvas.height = height;
8498
8524
  const ctx = destCanvas.getContext('2d');
8499
8525
 
8500
- ctx.drawImage(rootCanvas, sourceX, sourceY, width, height, 0, 0, width, height);
8526
+ // Draw the captured canvas into our sized canvas
8527
+ // html2canvas might return a larger canvas if scale > 1, so we fit it
8528
+ ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, width, height);
8501
8529
 
8502
- // Parse radii
8530
+ // Apply border radius clipping
8503
8531
  let tl = parseFloat(style.borderTopLeftRadius) || 0;
8504
8532
  let tr = parseFloat(style.borderTopRightRadius) || 0;
8505
8533
  let br = parseFloat(style.borderBottomRightRadius) || 0;
@@ -8519,38 +8547,89 @@ async function elementToCanvasImage(node, widthPx, heightPx, root) {
8519
8547
  bl *= f;
8520
8548
  }
8521
8549
 
8522
- ctx.globalCompositeOperation = 'destination-in';
8523
- ctx.beginPath();
8524
- ctx.moveTo(tl, 0);
8525
- ctx.lineTo(width - tr, 0);
8526
- ctx.arcTo(width, 0, width, tr, tr);
8527
- ctx.lineTo(width, height - br);
8528
- ctx.arcTo(width, height, width - br, height, br);
8529
- ctx.lineTo(bl, height);
8530
- ctx.arcTo(0, height, 0, height - bl, bl);
8531
- ctx.lineTo(0, tl);
8532
- ctx.arcTo(0, 0, tl, 0, tl);
8533
- ctx.closePath();
8534
- ctx.fill();
8550
+ if (tl + tr + br + bl > 0) {
8551
+ ctx.globalCompositeOperation = 'destination-in';
8552
+ ctx.beginPath();
8553
+ ctx.moveTo(tl, 0);
8554
+ ctx.lineTo(width - tr, 0);
8555
+ ctx.arcTo(width, 0, width, tr, tr);
8556
+ ctx.lineTo(width, height - br);
8557
+ ctx.arcTo(width, height, width - br, height, br);
8558
+ ctx.lineTo(bl, height);
8559
+ ctx.arcTo(0, height, 0, height - bl, bl);
8560
+ ctx.lineTo(0, tl);
8561
+ ctx.arcTo(0, 0, tl, 0, tl);
8562
+ ctx.closePath();
8563
+ ctx.fill();
8564
+ }
8535
8565
 
8536
8566
  resolve(destCanvas.toDataURL('image/png'));
8537
8567
  })
8538
- .catch(() => resolve(null));
8568
+ .catch((e) => {
8569
+ console.warn('Canvas capture failed for node', node, e);
8570
+ resolve(null);
8571
+ });
8539
8572
  });
8540
8573
  }
8541
8574
 
8542
- async function createRenderItem(node, config, domOrder, pptx) {
8575
+ /**
8576
+ * Replaces createRenderItem.
8577
+ * Returns { items: [], job: () => Promise, stopRecursion: boolean }
8578
+ */
8579
+ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, computedStyle) {
8580
+ // 1. Text Node Handling
8581
+ if (node.nodeType === 3) {
8582
+ const textContent = node.nodeValue.trim();
8583
+ if (!textContent) return null;
8584
+
8585
+ const parent = node.parentElement;
8586
+ if (!parent) return null;
8587
+
8588
+ if (isTextContainer(parent)) return null; // Parent handles it
8589
+
8590
+ const range = document.createRange();
8591
+ range.selectNode(node);
8592
+ const rect = range.getBoundingClientRect();
8593
+ range.detach();
8594
+
8595
+ const style = window.getComputedStyle(parent);
8596
+ const widthPx = rect.width;
8597
+ const heightPx = rect.height;
8598
+ const unrotatedW = widthPx * PX_TO_INCH * config.scale;
8599
+ const unrotatedH = heightPx * PX_TO_INCH * config.scale;
8600
+
8601
+ const x = config.offX + (rect.left - config.rootX) * PX_TO_INCH * config.scale;
8602
+ const y = config.offY + (rect.top - config.rootY) * PX_TO_INCH * config.scale;
8603
+
8604
+ return {
8605
+ items: [
8606
+ {
8607
+ type: 'text',
8608
+ zIndex: effectiveZIndex,
8609
+ domOrder,
8610
+ textParts: [
8611
+ {
8612
+ text: textContent,
8613
+ options: getTextStyle(style, config.scale),
8614
+ },
8615
+ ],
8616
+ options: { x, y, w: unrotatedW, h: unrotatedH, margin: 0, autoFit: false },
8617
+ },
8618
+ ],
8619
+ stopRecursion: false,
8620
+ };
8621
+ }
8622
+
8543
8623
  if (node.nodeType !== 1) return null;
8544
- const style = window.getComputedStyle(node);
8545
- if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')
8546
- return null;
8624
+ const style = computedStyle; // Use pre-computed style
8547
8625
 
8548
8626
  const rect = node.getBoundingClientRect();
8549
8627
  if (rect.width < 0.5 || rect.height < 0.5) return null;
8550
8628
 
8551
- const zIndex = style.zIndex !== 'auto' ? parseInt(style.zIndex) : 0;
8629
+ const zIndex = effectiveZIndex;
8552
8630
  const rotation = getRotation(style.transform);
8553
8631
  const elementOpacity = parseFloat(style.opacity);
8632
+ const safeOpacity = isNaN(elementOpacity) ? 1 : elementOpacity;
8554
8633
 
8555
8634
  const widthPx = node.offsetWidth || rect.width;
8556
8635
  const heightPx = node.offsetHeight || rect.height;
@@ -8566,21 +8645,31 @@ async function createRenderItem(node, config, domOrder, pptx) {
8566
8645
 
8567
8646
  const items = [];
8568
8647
 
8569
- if (node.nodeName.toUpperCase() === 'SVG') {
8570
- const pngData = await svgToPng(node);
8571
- if (pngData)
8572
- items.push({
8573
- type: 'image',
8574
- zIndex,
8575
- domOrder,
8576
- options: { data: pngData, x, y, w, h, rotate: rotation },
8577
- });
8578
- return { items, stopRecursion: true };
8648
+ // --- ASYNC JOB: SVGs / Icons ---
8649
+ if (
8650
+ node.nodeName.toUpperCase() === 'SVG' ||
8651
+ node.tagName.includes('-') ||
8652
+ node.tagName === 'ION-ICON'
8653
+ ) {
8654
+ const item = {
8655
+ type: 'image',
8656
+ zIndex,
8657
+ domOrder,
8658
+ options: { x, y, w, h, rotate: rotation, data: null }, // Data null initially
8659
+ };
8660
+
8661
+ // Create Job
8662
+ const job = async () => {
8663
+ const pngData = await elementToCanvasImage(node, widthPx, heightPx);
8664
+ if (pngData) item.options.data = pngData;
8665
+ else item.skip = true;
8666
+ };
8667
+
8668
+ return { items: [item], job, stopRecursion: true };
8579
8669
  }
8580
8670
 
8581
- // --- UPDATED IMG BLOCK START ---
8671
+ // --- ASYNC JOB: IMG Tags ---
8582
8672
  if (node.tagName === 'IMG') {
8583
- // Extract individual corner radii
8584
8673
  let radii = {
8585
8674
  tl: parseFloat(style.borderTopLeftRadius) || 0,
8586
8675
  tr: parseFloat(style.borderTopRightRadius) || 0,
@@ -8589,8 +8678,6 @@ async function createRenderItem(node, config, domOrder, pptx) {
8589
8678
  };
8590
8679
 
8591
8680
  const hasAnyRadius = radii.tl > 0 || radii.tr > 0 || radii.br > 0 || radii.bl > 0;
8592
-
8593
- // Fallback: Check parent if image has no specific radius but parent clips it
8594
8681
  if (!hasAnyRadius) {
8595
8682
  const parent = node.parentElement;
8596
8683
  const parentStyle = window.getComputedStyle(parent);
@@ -8601,10 +8688,6 @@ async function createRenderItem(node, config, domOrder, pptx) {
8601
8688
  br: parseFloat(parentStyle.borderBottomRightRadius) || 0,
8602
8689
  bl: parseFloat(parentStyle.borderBottomLeftRadius) || 0,
8603
8690
  };
8604
- // Simple heuristic: If image takes up full size of parent, inherit radii.
8605
- // For complex grids (like slide-1), this blindly applies parent radius.
8606
- // In a perfect world, we'd calculate intersection, but for now we apply parent radius
8607
- // if the image is close to the parent's size, effectively masking it.
8608
8691
  const pRect = parent.getBoundingClientRect();
8609
8692
  if (Math.abs(pRect.width - rect.width) < 5 && Math.abs(pRect.height - rect.height) < 5) {
8610
8693
  radii = pRadii;
@@ -8612,19 +8695,23 @@ async function createRenderItem(node, config, domOrder, pptx) {
8612
8695
  }
8613
8696
  }
8614
8697
 
8615
- const processed = await getProcessedImage(node.src, widthPx, heightPx, radii);
8616
- if (processed)
8617
- items.push({
8618
- type: 'image',
8619
- zIndex,
8620
- domOrder,
8621
- options: { data: processed, x, y, w, h, rotate: rotation },
8622
- });
8623
- return { items, stopRecursion: true };
8698
+ const item = {
8699
+ type: 'image',
8700
+ zIndex,
8701
+ domOrder,
8702
+ options: { x, y, w, h, rotate: rotation, data: null },
8703
+ };
8704
+
8705
+ const job = async () => {
8706
+ const processed = await getProcessedImage(node.src, widthPx, heightPx, radii);
8707
+ if (processed) item.options.data = processed;
8708
+ else item.skip = true;
8709
+ };
8710
+
8711
+ return { items: [item], job, stopRecursion: true };
8624
8712
  }
8625
- // --- UPDATED IMG BLOCK END ---
8626
8713
 
8627
- // Radii processing for Divs/Shapes
8714
+ // Radii logic
8628
8715
  const borderRadiusValue = parseFloat(style.borderRadius) || 0;
8629
8716
  const borderBottomLeftRadius = parseFloat(style.borderBottomLeftRadius) || 0;
8630
8717
  const borderBottomRightRadius = parseFloat(style.borderBottomRightRadius) || 0;
@@ -8642,25 +8729,30 @@ async function createRenderItem(node, config, domOrder, pptx) {
8642
8729
  borderTopLeftRadius ||
8643
8730
  borderTopRightRadius));
8644
8731
 
8645
- // Allow clipped elements to be rendered via canvas
8732
+ // --- ASYNC JOB: Clipped Divs via Canvas ---
8646
8733
  if (hasPartialBorderRadius && isClippedByParent(node)) {
8647
8734
  const marginLeft = parseFloat(style.marginLeft) || 0;
8648
8735
  const marginTop = parseFloat(style.marginTop) || 0;
8649
8736
  x += marginLeft * PX_TO_INCH * config.scale;
8650
8737
  y += marginTop * PX_TO_INCH * config.scale;
8651
8738
 
8652
- const canvasImageData = await elementToCanvasImage(node, widthPx, heightPx, config.root);
8653
- if (canvasImageData) {
8654
- items.push({
8655
- type: 'image',
8656
- zIndex,
8657
- domOrder,
8658
- options: { data: canvasImageData, x, y, w, h, rotate: rotation },
8659
- });
8660
- return { items, stopRecursion: true };
8661
- }
8739
+ const item = {
8740
+ type: 'image',
8741
+ zIndex,
8742
+ domOrder,
8743
+ options: { x, y, w, h, rotate: rotation, data: null },
8744
+ };
8745
+
8746
+ const job = async () => {
8747
+ const canvasImageData = await elementToCanvasImage(node, widthPx, heightPx);
8748
+ if (canvasImageData) item.options.data = canvasImageData;
8749
+ else item.skip = true;
8750
+ };
8751
+
8752
+ return { items: [item], job, stopRecursion: true };
8662
8753
  }
8663
8754
 
8755
+ // --- SYNC: Standard CSS Extraction ---
8664
8756
  const bgColorObj = parseColor(style.backgroundColor);
8665
8757
  const bgClip = style.webkitBackgroundClip || style.backgroundClip;
8666
8758
  const isBgClipText = bgClip === 'text';
@@ -8699,7 +8791,7 @@ async function createRenderItem(node, config, domOrder, pptx) {
8699
8791
  x -= bulletShift;
8700
8792
  w += bulletShift;
8701
8793
  textParts.push({
8702
- text: '',
8794
+ text: ' ',
8703
8795
  options: {
8704
8796
  color: parseColor(style.color).hex || '000000',
8705
8797
  fontSize: fontSizePt,
@@ -8805,7 +8897,6 @@ async function createRenderItem(node, config, domOrder, pptx) {
8805
8897
  });
8806
8898
  }
8807
8899
  if (hasCompositeBorder) {
8808
- // Add border shapes after the main background
8809
8900
  const borderItems = createCompositeBorderItems(
8810
8901
  borderInfo.sides,
8811
8902
  x,
@@ -8825,7 +8916,7 @@ async function createRenderItem(node, config, domOrder, pptx) {
8825
8916
  hasShadow ||
8826
8917
  textPayload
8827
8918
  ) {
8828
- const finalAlpha = elementOpacity * bgColorObj.opacity;
8919
+ const finalAlpha = safeOpacity * bgColorObj.opacity;
8829
8920
  const transparency = (1 - finalAlpha) * 100;
8830
8921
  const useSolidFill = bgColorObj.hex && !isImageWrapper;
8831
8922
 
@@ -8847,14 +8938,7 @@ async function createRenderItem(node, config, domOrder, pptx) {
8847
8938
  type: 'image',
8848
8939
  zIndex,
8849
8940
  domOrder,
8850
- options: {
8851
- data: shapeSvg,
8852
- x,
8853
- y,
8854
- w,
8855
- h,
8856
- rotate: rotation,
8857
- },
8941
+ options: { data: shapeSvg, x, y, w, h, rotate: rotation },
8858
8942
  });
8859
8943
  } else {
8860
8944
  const shapeOpts = {
@@ -8869,9 +8953,7 @@ async function createRenderItem(node, config, domOrder, pptx) {
8869
8953
  line: hasUniformBorder ? borderInfo.options : null,
8870
8954
  };
8871
8955
 
8872
- if (hasShadow) {
8873
- shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
8874
- }
8956
+ if (hasShadow) shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
8875
8957
 
8876
8958
  const borderRadius = parseFloat(style.borderRadius) || 0;
8877
8959
  const aspectRatio = Math.max(widthPx, heightPx) / Math.min(widthPx, heightPx);
@@ -8934,77 +9016,49 @@ async function createRenderItem(node, config, domOrder, pptx) {
8934
9016
  return { items, stopRecursion: !!textPayload };
8935
9017
  }
8936
9018
 
8937
- /**
8938
- * Helper function to create individual border shapes
8939
- */
8940
9019
  function createCompositeBorderItems(sides, x, y, w, h, scale, zIndex, domOrder) {
8941
9020
  const items = [];
8942
9021
  const pxToInch = 1 / 96;
9022
+ const common = { zIndex: zIndex + 1, domOrder, shapeType: 'rect' };
8943
9023
 
8944
- // TOP BORDER
8945
- if (sides.top.width > 0) {
9024
+ if (sides.top.width > 0)
8946
9025
  items.push({
8947
- type: 'shape',
8948
- zIndex: zIndex + 1,
8949
- domOrder,
8950
- shapeType: 'rect',
8951
- options: {
8952
- x: x,
8953
- y: y,
8954
- w: w,
8955
- h: sides.top.width * pxToInch * scale,
8956
- fill: { color: sides.top.color },
8957
- },
9026
+ ...common,
9027
+ options: { x, y, w, h: sides.top.width * pxToInch * scale, fill: { color: sides.top.color } },
8958
9028
  });
8959
- }
8960
- // RIGHT BORDER
8961
- if (sides.right.width > 0) {
9029
+ if (sides.right.width > 0)
8962
9030
  items.push({
8963
- type: 'shape',
8964
- zIndex: zIndex + 1,
8965
- domOrder,
8966
- shapeType: 'rect',
9031
+ ...common,
8967
9032
  options: {
8968
9033
  x: x + w - sides.right.width * pxToInch * scale,
8969
- y: y,
9034
+ y,
8970
9035
  w: sides.right.width * pxToInch * scale,
8971
- h: h,
9036
+ h,
8972
9037
  fill: { color: sides.right.color },
8973
9038
  },
8974
9039
  });
8975
- }
8976
- // BOTTOM BORDER
8977
- if (sides.bottom.width > 0) {
9040
+ if (sides.bottom.width > 0)
8978
9041
  items.push({
8979
- type: 'shape',
8980
- zIndex: zIndex + 1,
8981
- domOrder,
8982
- shapeType: 'rect',
9042
+ ...common,
8983
9043
  options: {
8984
- x: x,
9044
+ x,
8985
9045
  y: y + h - sides.bottom.width * pxToInch * scale,
8986
- w: w,
9046
+ w,
8987
9047
  h: sides.bottom.width * pxToInch * scale,
8988
9048
  fill: { color: sides.bottom.color },
8989
9049
  },
8990
9050
  });
8991
- }
8992
- // LEFT BORDER
8993
- if (sides.left.width > 0) {
9051
+ if (sides.left.width > 0)
8994
9052
  items.push({
8995
- type: 'shape',
8996
- zIndex: zIndex + 1,
8997
- domOrder,
8998
- shapeType: 'rect',
9053
+ ...common,
8999
9054
  options: {
9000
- x: x,
9001
- y: y,
9055
+ x,
9056
+ y,
9002
9057
  w: sides.left.width * pxToInch * scale,
9003
- h: h,
9058
+ h,
9004
9059
  fill: { color: sides.left.color },
9005
9060
  },
9006
9061
  });
9007
- }
9008
9062
 
9009
9063
  return items;
9010
9064
  }