dom-to-pptx 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  // src/index.js
2
2
  import * as PptxGenJSImport from 'pptxgenjs';
3
- // Normalize import so consumers get the constructor whether `pptxgenjs`
4
- // was published as a default export or CommonJS module with a `default` property.
3
+ import html2canvas from 'html2canvas';
4
+
5
+ // Normalize import
5
6
  const PptxGenJS = PptxGenJSImport?.default ?? PptxGenJSImport;
7
+
6
8
  import {
7
9
  parseColor,
8
10
  getTextStyle,
@@ -16,6 +18,8 @@ import {
16
18
  generateBlurredSVG,
17
19
  getBorderInfo,
18
20
  generateCompositeBorderSVG,
21
+ isClippedByParent,
22
+ generateCustomShapeSVG,
19
23
  } from './utils.js';
20
24
  import { getProcessedImage } from './image-processor.js';
21
25
 
@@ -28,23 +32,21 @@ const PX_TO_INCH = 1 / PPI;
28
32
  * @param {Object} options - { fileName: string }
29
33
  */
30
34
  export async function exportToPptx(target, options = {}) {
31
- // Resolve the actual constructor in case `pptxgenjs` was imported/required
32
- // with different shapes (function, { default: fn }, or { PptxGenJS: fn }).
33
35
  const resolvePptxConstructor = (pkg) => {
34
36
  if (!pkg) return null;
35
37
  if (typeof pkg === 'function') return pkg;
36
38
  if (pkg && typeof pkg.default === 'function') return pkg.default;
37
39
  if (pkg && typeof pkg.PptxGenJS === 'function') return pkg.PptxGenJS;
38
- if (pkg && pkg.PptxGenJS && typeof pkg.PptxGenJS.default === 'function') return pkg.PptxGenJS.default;
40
+ if (pkg && pkg.PptxGenJS && typeof pkg.PptxGenJS.default === 'function')
41
+ return pkg.PptxGenJS.default;
39
42
  return null;
40
43
  };
41
44
 
42
45
  const PptxConstructor = resolvePptxConstructor(PptxGenJS);
43
- if (!PptxConstructor) throw new Error('PptxGenJS constructor not found. Ensure `pptxgenjs` is installed or included as a script.');
46
+ if (!PptxConstructor) throw new Error('PptxGenJS constructor not found.');
44
47
  const pptx = new PptxConstructor();
45
48
  pptx.layout = 'LAYOUT_16x9';
46
49
 
47
- // Standardize input to an array, ensuring single or multiple elements are handled consistently
48
50
  const elements = Array.isArray(target) ? target : [target];
49
51
 
50
52
  for (const el of elements) {
@@ -53,7 +55,6 @@ export async function exportToPptx(target, options = {}) {
53
55
  console.warn('Element not found, skipping slide:', el);
54
56
  continue;
55
57
  }
56
-
57
58
  const slide = pptx.addSlide();
58
59
  await processSlide(root, slide, pptx);
59
60
  }
@@ -89,11 +90,11 @@ async function processSlide(root, slide, pptx) {
89
90
  let domOrderCounter = 0;
90
91
 
91
92
  async function collect(node) {
92
- const order = domOrderCounter++; // Assign a DOM order for z-index tie-breaking
93
- const result = await createRenderItem(node, layoutConfig, order, pptx);
93
+ const order = domOrderCounter++;
94
+ const result = await createRenderItem(node, { ...layoutConfig, root }, order, pptx);
94
95
  if (result) {
95
- if (result.items) renderQueue.push(...result.items); // Add any generated render items to the queue
96
- if (result.stopRecursion) return; // Stop processing children if the item fully consumed the node
96
+ if (result.items) renderQueue.push(...result.items);
97
+ if (result.stopRecursion) return;
97
98
  }
98
99
  for (const child of node.children) await collect(child);
99
100
  }
@@ -112,6 +113,79 @@ async function processSlide(root, slide, pptx) {
112
113
  }
113
114
  }
114
115
 
116
+ async function elementToCanvasImage(node, widthPx, heightPx, root) {
117
+ return new Promise((resolve) => {
118
+ const width = Math.ceil(widthPx);
119
+ const height = Math.ceil(heightPx);
120
+
121
+ if (width <= 0 || height <= 0) {
122
+ resolve(null);
123
+ return;
124
+ }
125
+
126
+ const style = window.getComputedStyle(node);
127
+
128
+ html2canvas(root, {
129
+ width: root.scrollWidth,
130
+ height: root.scrollHeight,
131
+ useCORS: true,
132
+ allowTaint: true,
133
+ backgroundColor: null,
134
+ })
135
+ .then((canvas) => {
136
+ const rootCanvas = canvas;
137
+ const nodeRect = node.getBoundingClientRect();
138
+ const rootRect = root.getBoundingClientRect();
139
+ const sourceX = nodeRect.left - rootRect.left;
140
+ const sourceY = nodeRect.top - rootRect.top;
141
+
142
+ const destCanvas = document.createElement('canvas');
143
+ destCanvas.width = width;
144
+ destCanvas.height = height;
145
+ const ctx = destCanvas.getContext('2d');
146
+
147
+ ctx.drawImage(rootCanvas, sourceX, sourceY, width, height, 0, 0, width, height);
148
+
149
+ // Parse radii
150
+ let tl = parseFloat(style.borderTopLeftRadius) || 0;
151
+ let tr = parseFloat(style.borderTopRightRadius) || 0;
152
+ let br = parseFloat(style.borderBottomRightRadius) || 0;
153
+ let bl = parseFloat(style.borderBottomLeftRadius) || 0;
154
+
155
+ const f = Math.min(
156
+ width / (tl + tr) || Infinity,
157
+ height / (tr + br) || Infinity,
158
+ width / (br + bl) || Infinity,
159
+ height / (bl + tl) || Infinity
160
+ );
161
+
162
+ if (f < 1) {
163
+ tl *= f;
164
+ tr *= f;
165
+ br *= f;
166
+ bl *= f;
167
+ }
168
+
169
+ ctx.globalCompositeOperation = 'destination-in';
170
+ ctx.beginPath();
171
+ ctx.moveTo(tl, 0);
172
+ ctx.lineTo(width - tr, 0);
173
+ ctx.arcTo(width, 0, width, tr, tr);
174
+ ctx.lineTo(width, height - br);
175
+ ctx.arcTo(width, height, width - br, height, br);
176
+ ctx.lineTo(bl, height);
177
+ ctx.arcTo(0, height, 0, height - bl, bl);
178
+ ctx.lineTo(0, tl);
179
+ ctx.arcTo(0, 0, tl, 0, tl);
180
+ ctx.closePath();
181
+ ctx.fill();
182
+
183
+ resolve(destCanvas.toDataURL('image/png'));
184
+ })
185
+ .catch(() => resolve(null));
186
+ });
187
+ }
188
+
115
189
  async function createRenderItem(node, config, domOrder, pptx) {
116
190
  if (node.nodeType !== 1) return null;
117
191
  const style = window.getComputedStyle(node);
@@ -119,7 +193,7 @@ async function createRenderItem(node, config, domOrder, pptx) {
119
193
  return null;
120
194
 
121
195
  const rect = node.getBoundingClientRect();
122
- if (rect.width === 0 || rect.height === 0) return null;
196
+ if (rect.width < 0.5 || rect.height < 0.5) return null;
123
197
 
124
198
  const zIndex = style.zIndex !== 'auto' ? parseInt(style.zIndex) : 0;
125
199
  const rotation = getRotation(style.transform);
@@ -139,7 +213,6 @@ async function createRenderItem(node, config, domOrder, pptx) {
139
213
 
140
214
  const items = [];
141
215
 
142
- // Image handling for SVG nodes directly
143
216
  if (node.nodeName.toUpperCase() === 'SVG') {
144
217
  const pngData = await svgToPng(node);
145
218
  if (pngData)
@@ -151,15 +224,42 @@ async function createRenderItem(node, config, domOrder, pptx) {
151
224
  });
152
225
  return { items, stopRecursion: true };
153
226
  }
154
- // Image handling for <img> tags, including rounded corners
227
+
228
+ // --- UPDATED IMG BLOCK START ---
155
229
  if (node.tagName === 'IMG') {
156
- let borderRadius = parseFloat(style.borderRadius) || 0;
157
- if (borderRadius === 0) {
158
- const parentStyle = window.getComputedStyle(node.parentElement);
159
- if (parentStyle.overflow !== 'visible')
160
- borderRadius = parseFloat(parentStyle.borderRadius) || 0;
230
+ // Extract individual corner radii
231
+ let radii = {
232
+ tl: parseFloat(style.borderTopLeftRadius) || 0,
233
+ tr: parseFloat(style.borderTopRightRadius) || 0,
234
+ br: parseFloat(style.borderBottomRightRadius) || 0,
235
+ bl: parseFloat(style.borderBottomLeftRadius) || 0,
236
+ };
237
+
238
+ const hasAnyRadius = radii.tl > 0 || radii.tr > 0 || radii.br > 0 || radii.bl > 0;
239
+
240
+ // Fallback: Check parent if image has no specific radius but parent clips it
241
+ if (!hasAnyRadius) {
242
+ const parent = node.parentElement;
243
+ const parentStyle = window.getComputedStyle(parent);
244
+ if (parentStyle.overflow !== 'visible') {
245
+ const pRadii = {
246
+ tl: parseFloat(parentStyle.borderTopLeftRadius) || 0,
247
+ tr: parseFloat(parentStyle.borderTopRightRadius) || 0,
248
+ br: parseFloat(parentStyle.borderBottomRightRadius) || 0,
249
+ bl: parseFloat(parentStyle.borderBottomLeftRadius) || 0,
250
+ };
251
+ // Simple heuristic: If image takes up full size of parent, inherit radii.
252
+ // For complex grids (like slide-1), this blindly applies parent radius.
253
+ // In a perfect world, we'd calculate intersection, but for now we apply parent radius
254
+ // if the image is close to the parent's size, effectively masking it.
255
+ const pRect = parent.getBoundingClientRect();
256
+ if (Math.abs(pRect.width - rect.width) < 5 && Math.abs(pRect.height - rect.height) < 5) {
257
+ radii = pRadii;
258
+ }
259
+ }
161
260
  }
162
- const processed = await getProcessedImage(node.src, widthPx, heightPx, borderRadius);
261
+
262
+ const processed = await getProcessedImage(node.src, widthPx, heightPx, radii);
163
263
  if (processed)
164
264
  items.push({
165
265
  type: 'image',
@@ -169,6 +269,44 @@ async function createRenderItem(node, config, domOrder, pptx) {
169
269
  });
170
270
  return { items, stopRecursion: true };
171
271
  }
272
+ // --- UPDATED IMG BLOCK END ---
273
+
274
+ // Radii processing for Divs/Shapes
275
+ const borderRadiusValue = parseFloat(style.borderRadius) || 0;
276
+ const borderBottomLeftRadius = parseFloat(style.borderBottomLeftRadius) || 0;
277
+ const borderBottomRightRadius = parseFloat(style.borderBottomRightRadius) || 0;
278
+ const borderTopLeftRadius = parseFloat(style.borderTopLeftRadius) || 0;
279
+ const borderTopRightRadius = parseFloat(style.borderTopRightRadius) || 0;
280
+
281
+ const hasPartialBorderRadius =
282
+ (borderBottomLeftRadius > 0 && borderBottomLeftRadius !== borderRadiusValue) ||
283
+ (borderBottomRightRadius > 0 && borderBottomRightRadius !== borderRadiusValue) ||
284
+ (borderTopLeftRadius > 0 && borderTopLeftRadius !== borderRadiusValue) ||
285
+ (borderTopRightRadius > 0 && borderTopRightRadius !== borderRadiusValue) ||
286
+ (borderRadiusValue === 0 &&
287
+ (borderBottomLeftRadius ||
288
+ borderBottomRightRadius ||
289
+ borderTopLeftRadius ||
290
+ borderTopRightRadius));
291
+
292
+ // Allow clipped elements to be rendered via canvas
293
+ if (hasPartialBorderRadius && isClippedByParent(node)) {
294
+ const marginLeft = parseFloat(style.marginLeft) || 0;
295
+ const marginTop = parseFloat(style.marginTop) || 0;
296
+ x += marginLeft * PX_TO_INCH * config.scale;
297
+ y += marginTop * PX_TO_INCH * config.scale;
298
+
299
+ const canvasImageData = await elementToCanvasImage(node, widthPx, heightPx, config.root);
300
+ if (canvasImageData) {
301
+ items.push({
302
+ type: 'image',
303
+ zIndex,
304
+ domOrder,
305
+ options: { data: canvasImageData, x, y, w, h, rotate: rotation },
306
+ });
307
+ return { items, stopRecursion: true };
308
+ }
309
+ }
172
310
 
173
311
  const bgColorObj = parseColor(style.backgroundColor);
174
312
  const bgClip = style.webkitBackgroundClip || style.backgroundClip;
@@ -186,7 +324,6 @@ async function createRenderItem(node, config, domOrder, pptx) {
186
324
 
187
325
  const shadowStr = style.boxShadow;
188
326
  const hasShadow = shadowStr && shadowStr !== 'none';
189
- const borderRadius = parseFloat(style.borderRadius) || 0;
190
327
  const softEdge = getSoftEdges(style.filter, config.scale);
191
328
 
192
329
  let isImageWrapper = false;
@@ -211,7 +348,6 @@ async function createRenderItem(node, config, domOrder, pptx) {
211
348
  textParts.push({
212
349
  text: '• ',
213
350
  options: {
214
- // Default bullet point styling
215
351
  color: parseColor(style.color).hex || '000000',
216
352
  fontSize: fontSizePt,
217
353
  },
@@ -219,7 +355,6 @@ async function createRenderItem(node, config, domOrder, pptx) {
219
355
  }
220
356
 
221
357
  node.childNodes.forEach((child, index) => {
222
- // Process text content, sanitizing whitespace and applying text transformations
223
358
  let textVal = child.nodeType === 3 ? child.nodeValue : child.textContent;
224
359
  let nodeStyle = child.nodeType === 1 ? window.getComputedStyle(child) : style;
225
360
  textVal = textVal.replace(/[\n\r\t]+/g, ' ').replace(/\s{2,}/g, ' ');
@@ -260,7 +395,13 @@ async function createRenderItem(node, config, domOrder, pptx) {
260
395
  let bgData = null;
261
396
  let padIn = 0;
262
397
  if (softEdge) {
263
- const svgInfo = generateBlurredSVG(widthPx, heightPx, bgColorObj.hex, borderRadius, softEdge);
398
+ const svgInfo = generateBlurredSVG(
399
+ widthPx,
400
+ heightPx,
401
+ bgColorObj.hex,
402
+ borderRadiusValue,
403
+ softEdge
404
+ );
264
405
  bgData = svgInfo.data;
265
406
  padIn = svgInfo.padding * PX_TO_INCH * config.scale;
266
407
  } else {
@@ -268,7 +409,7 @@ async function createRenderItem(node, config, domOrder, pptx) {
268
409
  widthPx,
269
410
  heightPx,
270
411
  style.backgroundImage,
271
- borderRadius,
412
+ borderRadiusValue,
272
413
  hasBorder ? { color: borderColorObj.hex, width: borderWidth } : null
273
414
  );
274
415
  }
@@ -333,91 +474,105 @@ async function createRenderItem(node, config, domOrder, pptx) {
333
474
  ) {
334
475
  const finalAlpha = elementOpacity * bgColorObj.opacity;
335
476
  const transparency = (1 - finalAlpha) * 100;
477
+ const useSolidFill = bgColorObj.hex && !isImageWrapper;
336
478
 
337
- const shapeOpts = {
338
- x,
339
- y,
340
- w,
341
- h,
342
- rotate: rotation,
343
- fill:
344
- bgColorObj.hex && !isImageWrapper
345
- ? { color: bgColorObj.hex, transparency: transparency }
346
- : { type: 'none' },
347
- // Only apply line if the border is uniform
348
- line: hasUniformBorder ? borderInfo.options : null,
349
- };
350
-
351
- if (hasShadow) {
352
- shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
353
- }
354
-
355
- const borderRadius = parseFloat(style.borderRadius) || 0;
356
- const widthPx = node.offsetWidth || rect.width;
357
- const heightPx = node.offsetHeight || rect.height;
358
- const isCircle =
359
- borderRadius >= Math.min(widthPx, heightPx) / 2 - 1 && Math.abs(widthPx - heightPx) < 2;
360
-
361
- let shapeType = pptx.ShapeType.rect;
362
- if (isCircle) shapeType = pptx.ShapeType.ellipse;
363
- else if (borderRadius > 0) {
364
- shapeType = pptx.ShapeType.roundRect;
365
- shapeOpts.rectRadius = Math.min(1, borderRadius / (Math.min(widthPx, heightPx) / 2));
366
- }
479
+ if (hasPartialBorderRadius && useSolidFill && !textPayload) {
480
+ const shapeSvg = generateCustomShapeSVG(
481
+ widthPx,
482
+ heightPx,
483
+ bgColorObj.hex,
484
+ bgColorObj.opacity,
485
+ {
486
+ tl: parseFloat(style.borderTopLeftRadius) || 0,
487
+ tr: parseFloat(style.borderTopRightRadius) || 0,
488
+ br: parseFloat(style.borderBottomRightRadius) || 0,
489
+ bl: parseFloat(style.borderBottomLeftRadius) || 0,
490
+ }
491
+ );
367
492
 
368
- // MERGE TEXT INTO SHAPE (if text exists)
369
- if (textPayload) {
370
- const textOptions = {
371
- shape: shapeType,
372
- ...shapeOpts,
373
- align: textPayload.align,
374
- valign: textPayload.valign,
375
- inset: textPayload.inset,
376
- margin: 0,
377
- wrap: true,
378
- autoFit: false,
379
- };
380
493
  items.push({
381
- type: 'text',
494
+ type: 'image',
382
495
  zIndex,
383
496
  domOrder,
384
- textParts: textPayload.text,
385
- options: textOptions,
497
+ options: {
498
+ data: shapeSvg,
499
+ x,
500
+ y,
501
+ w,
502
+ h,
503
+ rotate: rotation,
504
+ },
386
505
  });
387
- // If no text, just draw the shape
388
506
  } else {
389
- items.push({
390
- type: 'shape',
391
- zIndex,
392
- domOrder,
393
- shapeType,
394
- options: shapeOpts,
395
- });
507
+ const shapeOpts = {
508
+ x,
509
+ y,
510
+ w,
511
+ h,
512
+ rotate: rotation,
513
+ fill: useSolidFill
514
+ ? { color: bgColorObj.hex, transparency: transparency }
515
+ : { type: 'none' },
516
+ line: hasUniformBorder ? borderInfo.options : null,
517
+ };
518
+
519
+ if (hasShadow) {
520
+ shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
521
+ }
522
+
523
+ const borderRadius = parseFloat(style.borderRadius) || 0;
524
+ const aspectRatio = Math.max(widthPx, heightPx) / Math.min(widthPx, heightPx);
525
+ const isCircle = aspectRatio < 1.1 && borderRadius >= Math.min(widthPx, heightPx) / 2 - 1;
526
+
527
+ let shapeType = pptx.ShapeType.rect;
528
+ if (isCircle) shapeType = pptx.ShapeType.ellipse;
529
+ else if (borderRadius > 0) {
530
+ shapeType = pptx.ShapeType.roundRect;
531
+ shapeOpts.rectRadius = Math.min(0.5, borderRadius / Math.min(widthPx, heightPx));
532
+ }
533
+
534
+ if (textPayload) {
535
+ const textOptions = {
536
+ shape: shapeType,
537
+ ...shapeOpts,
538
+ align: textPayload.align,
539
+ valign: textPayload.valign,
540
+ inset: textPayload.inset,
541
+ margin: 0,
542
+ wrap: true,
543
+ autoFit: false,
544
+ };
545
+ items.push({
546
+ type: 'text',
547
+ zIndex,
548
+ domOrder,
549
+ textParts: textPayload.text,
550
+ options: textOptions,
551
+ });
552
+ } else if (!hasPartialBorderRadius) {
553
+ items.push({
554
+ type: 'shape',
555
+ zIndex,
556
+ domOrder,
557
+ shapeType,
558
+ options: shapeOpts,
559
+ });
560
+ }
396
561
  }
397
562
 
398
- // ADD COMPOSITE BORDERS (if they exist)
399
563
  if (hasCompositeBorder) {
400
- // Generate a single SVG image that contains all the rounded border sides
401
564
  const borderSvgData = generateCompositeBorderSVG(
402
565
  widthPx,
403
566
  heightPx,
404
- borderRadius,
567
+ borderRadiusValue,
405
568
  borderInfo.sides
406
569
  );
407
-
408
570
  if (borderSvgData) {
409
571
  items.push({
410
572
  type: 'image',
411
573
  zIndex: zIndex + 1,
412
574
  domOrder,
413
- options: {
414
- data: borderSvgData,
415
- x: x,
416
- y: y,
417
- w: w,
418
- h: h,
419
- rotate: rotation,
420
- },
575
+ options: { data: borderSvgData, x, y, w, h, rotate: rotation },
421
576
  });
422
577
  }
423
578
  }