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