dom-to-pptx 1.0.8 → 1.0.9
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/CHANGELOG.md +7 -0
- package/dist/dom-to-pptx.bundle.js +224 -59
- package/dist/dom-to-pptx.cjs +224 -59
- package/dist/dom-to-pptx.min.js +224 -59
- package/dist/dom-to-pptx.mjs +224 -59
- package/package.json +1 -1
- package/src/index.js +97 -19
- package/src/utils.js +128 -41
package/dist/dom-to-pptx.cjs
CHANGED
|
@@ -8079,6 +8079,7 @@ function getTextStyle(style, scale) {
|
|
|
8079
8079
|
|
|
8080
8080
|
/**
|
|
8081
8081
|
* Determines if a given DOM node is primarily a text container.
|
|
8082
|
+
* Updated to correctly reject Icon elements so they are rendered as images.
|
|
8082
8083
|
*/
|
|
8083
8084
|
function isTextContainer(node) {
|
|
8084
8085
|
const hasText = node.textContent.trim().length > 0;
|
|
@@ -8087,28 +8088,46 @@ function isTextContainer(node) {
|
|
|
8087
8088
|
const children = Array.from(node.children);
|
|
8088
8089
|
if (children.length === 0) return true;
|
|
8089
8090
|
|
|
8090
|
-
// Check if children are purely inline text formatting or visual shapes
|
|
8091
8091
|
const isSafeInline = (el) => {
|
|
8092
|
-
// 1. Reject Web Components /
|
|
8092
|
+
// 1. Reject Web Components / Custom Elements
|
|
8093
8093
|
if (el.tagName.includes('-')) return false;
|
|
8094
|
+
// 2. Reject Explicit Images/SVGs
|
|
8094
8095
|
if (el.tagName === 'IMG' || el.tagName === 'SVG') return false;
|
|
8095
8096
|
|
|
8097
|
+
// 3. Reject Class-based Icons (FontAwesome, Material, Bootstrap, etc.)
|
|
8098
|
+
// If an <i> or <span> has icon classes, it is a visual object, not text.
|
|
8099
|
+
if (el.tagName === 'I' || el.tagName === 'SPAN') {
|
|
8100
|
+
const cls = el.getAttribute('class') || '';
|
|
8101
|
+
if (
|
|
8102
|
+
cls.includes('fa-') ||
|
|
8103
|
+
cls.includes('fas') ||
|
|
8104
|
+
cls.includes('far') ||
|
|
8105
|
+
cls.includes('fab') ||
|
|
8106
|
+
cls.includes('material-icons') ||
|
|
8107
|
+
cls.includes('bi-') ||
|
|
8108
|
+
cls.includes('icon')
|
|
8109
|
+
) {
|
|
8110
|
+
return false;
|
|
8111
|
+
}
|
|
8112
|
+
}
|
|
8113
|
+
|
|
8096
8114
|
const style = window.getComputedStyle(el);
|
|
8097
8115
|
const display = style.display;
|
|
8098
8116
|
|
|
8099
|
-
//
|
|
8100
|
-
const isInlineTag = ['SPAN', 'B', 'STRONG', 'EM', 'I', 'A', 'SMALL', 'MARK'].includes(
|
|
8117
|
+
// 4. Standard Inline Tag Check
|
|
8118
|
+
const isInlineTag = ['SPAN', 'B', 'STRONG', 'EM', 'I', 'A', 'SMALL', 'MARK'].includes(
|
|
8119
|
+
el.tagName
|
|
8120
|
+
);
|
|
8101
8121
|
const isInlineDisplay = display.includes('inline');
|
|
8102
8122
|
|
|
8103
8123
|
if (!isInlineTag && !isInlineDisplay) return false;
|
|
8104
8124
|
|
|
8105
|
-
//
|
|
8106
|
-
//
|
|
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.
|
|
8125
|
+
// 5. Structural Styling Check
|
|
8126
|
+
// If a child has a background or border, it's a layout block, not a simple text span.
|
|
8109
8127
|
const bgColor = parseColor(style.backgroundColor);
|
|
8110
8128
|
const hasVisibleBg = bgColor.hex && bgColor.opacity > 0;
|
|
8111
|
-
const hasBorder =
|
|
8129
|
+
const hasBorder =
|
|
8130
|
+
parseFloat(style.borderWidth) > 0 && parseColor(style.borderColor).opacity > 0;
|
|
8112
8131
|
|
|
8113
8132
|
if (hasVisibleBg || hasBorder) {
|
|
8114
8133
|
return false;
|
|
@@ -8229,57 +8248,119 @@ function getVisibleShadow(shadowStr, scale) {
|
|
|
8229
8248
|
return null;
|
|
8230
8249
|
}
|
|
8231
8250
|
|
|
8251
|
+
/**
|
|
8252
|
+
* Generates an SVG image for gradients, supporting degrees and keywords.
|
|
8253
|
+
*/
|
|
8232
8254
|
function generateGradientSVG(w, h, bgString, radius, border) {
|
|
8233
8255
|
try {
|
|
8234
8256
|
const match = bgString.match(/linear-gradient\((.*)\)/);
|
|
8235
8257
|
if (!match) return null;
|
|
8236
8258
|
const content = match[1];
|
|
8259
|
+
|
|
8260
|
+
// Split by comma, ignoring commas inside parentheses (e.g. rgba())
|
|
8237
8261
|
const parts = content.split(/,(?![^()]*\))/).map((p) => p.trim());
|
|
8262
|
+
if (parts.length < 2) return null;
|
|
8238
8263
|
|
|
8239
8264
|
let x1 = '0%',
|
|
8240
8265
|
y1 = '0%',
|
|
8241
8266
|
x2 = '0%',
|
|
8242
8267
|
y2 = '100%';
|
|
8243
|
-
let
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8268
|
+
let stopsStartIndex = 0;
|
|
8269
|
+
const firstPart = parts[0].toLowerCase();
|
|
8270
|
+
|
|
8271
|
+
// 1. Check for Keywords (to right, etc.)
|
|
8272
|
+
if (firstPart.startsWith('to ')) {
|
|
8273
|
+
stopsStartIndex = 1;
|
|
8274
|
+
const direction = firstPart.replace('to ', '').trim();
|
|
8275
|
+
switch (direction) {
|
|
8276
|
+
case 'top':
|
|
8277
|
+
y1 = '100%';
|
|
8278
|
+
y2 = '0%';
|
|
8279
|
+
break;
|
|
8280
|
+
case 'bottom':
|
|
8281
|
+
y1 = '0%';
|
|
8282
|
+
y2 = '100%';
|
|
8283
|
+
break;
|
|
8284
|
+
case 'left':
|
|
8285
|
+
x1 = '100%';
|
|
8286
|
+
x2 = '0%';
|
|
8287
|
+
break;
|
|
8288
|
+
case 'right':
|
|
8289
|
+
x2 = '100%';
|
|
8290
|
+
break;
|
|
8291
|
+
case 'top right':
|
|
8292
|
+
x1 = '0%';
|
|
8293
|
+
y1 = '100%';
|
|
8294
|
+
x2 = '100%';
|
|
8295
|
+
y2 = '0%';
|
|
8296
|
+
break;
|
|
8297
|
+
case 'top left':
|
|
8298
|
+
x1 = '100%';
|
|
8299
|
+
y1 = '100%';
|
|
8300
|
+
x2 = '0%';
|
|
8301
|
+
y2 = '0%';
|
|
8302
|
+
break;
|
|
8303
|
+
case 'bottom right':
|
|
8304
|
+
x2 = '100%';
|
|
8305
|
+
y2 = '100%';
|
|
8306
|
+
break;
|
|
8307
|
+
case 'bottom left':
|
|
8308
|
+
x1 = '100%';
|
|
8309
|
+
y2 = '100%';
|
|
8310
|
+
break;
|
|
8311
|
+
}
|
|
8312
|
+
}
|
|
8313
|
+
// 2. Check for Degrees (45deg, 90deg, etc.)
|
|
8314
|
+
else if (firstPart.match(/^-?[\d.]+(deg|rad|turn|grad)$/)) {
|
|
8315
|
+
stopsStartIndex = 1;
|
|
8316
|
+
const val = parseFloat(firstPart);
|
|
8317
|
+
// CSS 0deg is Top (North), 90deg is Right (East), 180deg is Bottom (South)
|
|
8318
|
+
// We convert this to SVG coordinates on a unit square (0-100%).
|
|
8319
|
+
// Formula: Map angle to perimeter coordinates.
|
|
8320
|
+
if (!isNaN(val)) {
|
|
8321
|
+
const deg = firstPart.includes('rad') ? val * (180 / Math.PI) : val;
|
|
8322
|
+
const cssRad = ((deg - 90) * Math.PI) / 180; // Correct CSS angle offset
|
|
8323
|
+
|
|
8324
|
+
// Calculate standard vector for rectangle center (50, 50)
|
|
8325
|
+
const scale = 50; // Distance from center to edge (approx)
|
|
8326
|
+
const cos = Math.cos(cssRad); // Y component (reversed in SVG)
|
|
8327
|
+
const sin = Math.sin(cssRad); // X component
|
|
8328
|
+
|
|
8329
|
+
// Invert Y for SVG coordinate system
|
|
8330
|
+
x1 = (50 - sin * scale).toFixed(1) + '%';
|
|
8331
|
+
y1 = (50 + cos * scale).toFixed(1) + '%';
|
|
8332
|
+
x2 = (50 + sin * scale).toFixed(1) + '%';
|
|
8333
|
+
y2 = (50 - cos * scale).toFixed(1) + '%';
|
|
8334
|
+
}
|
|
8262
8335
|
}
|
|
8263
8336
|
|
|
8337
|
+
// 3. Process Color Stops
|
|
8264
8338
|
let stopsXML = '';
|
|
8265
|
-
const stopParts = parts.slice(
|
|
8339
|
+
const stopParts = parts.slice(stopsStartIndex);
|
|
8340
|
+
|
|
8266
8341
|
stopParts.forEach((part, idx) => {
|
|
8342
|
+
// Parse "Color Position" (e.g., "red 50%")
|
|
8343
|
+
// Regex looks for optional space + number + unit at the end of the string
|
|
8267
8344
|
let color = part;
|
|
8268
8345
|
let offset = Math.round((idx / (stopParts.length - 1)) * 100) + '%';
|
|
8269
|
-
|
|
8346
|
+
|
|
8347
|
+
const posMatch = part.match(/^(.*?)\s+(-?[\d.]+(?:%|px)?)$/);
|
|
8270
8348
|
if (posMatch) {
|
|
8271
8349
|
color = posMatch[1];
|
|
8272
8350
|
offset = posMatch[2];
|
|
8273
8351
|
}
|
|
8352
|
+
|
|
8353
|
+
// Handle RGBA/RGB for SVG compatibility
|
|
8274
8354
|
let opacity = 1;
|
|
8275
8355
|
if (color.includes('rgba')) {
|
|
8276
|
-
const
|
|
8277
|
-
if (
|
|
8278
|
-
opacity =
|
|
8279
|
-
color = `rgb(${
|
|
8356
|
+
const rgbaMatch = color.match(/[\d.]+/g);
|
|
8357
|
+
if (rgbaMatch && rgbaMatch.length >= 4) {
|
|
8358
|
+
opacity = rgbaMatch[3];
|
|
8359
|
+
color = `rgb(${rgbaMatch[0]},${rgbaMatch[1]},${rgbaMatch[2]})`;
|
|
8280
8360
|
}
|
|
8281
8361
|
}
|
|
8282
|
-
|
|
8362
|
+
|
|
8363
|
+
stopsXML += `<stop offset="${offset}" stop-color="${color.trim()}" stop-opacity="${opacity}"/>`;
|
|
8283
8364
|
});
|
|
8284
8365
|
|
|
8285
8366
|
let strokeAttr = '';
|
|
@@ -8288,12 +8369,18 @@ function generateGradientSVG(w, h, bgString, radius, border) {
|
|
|
8288
8369
|
}
|
|
8289
8370
|
|
|
8290
8371
|
const svg = `
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8372
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
8373
|
+
<defs>
|
|
8374
|
+
<linearGradient id="grad" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}">
|
|
8375
|
+
${stopsXML}
|
|
8376
|
+
</linearGradient>
|
|
8377
|
+
</defs>
|
|
8378
|
+
<rect x="0" y="0" width="${w}" height="${h}" rx="${radius}" ry="${radius}" fill="url(#grad)" ${strokeAttr} />
|
|
8379
|
+
</svg>`;
|
|
8380
|
+
|
|
8295
8381
|
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
8296
|
-
} catch {
|
|
8382
|
+
} catch (e) {
|
|
8383
|
+
console.warn('Gradient generation failed:', e);
|
|
8297
8384
|
return null;
|
|
8298
8385
|
}
|
|
8299
8386
|
}
|
|
@@ -8568,29 +8655,69 @@ async function processSlide(root, slide, pptx) {
|
|
|
8568
8655
|
* Optimized html2canvas wrapper
|
|
8569
8656
|
* Now strictly captures the node itself, not the root.
|
|
8570
8657
|
*/
|
|
8658
|
+
/**
|
|
8659
|
+
* Optimized html2canvas wrapper
|
|
8660
|
+
* Includes fix for cropped icons by adjusting styles in the cloned document.
|
|
8661
|
+
*/
|
|
8571
8662
|
async function elementToCanvasImage(node, widthPx, heightPx) {
|
|
8572
8663
|
return new Promise((resolve) => {
|
|
8664
|
+
// 1. Assign a temp ID to locate the node inside the cloned document
|
|
8665
|
+
const originalId = node.id;
|
|
8666
|
+
const tempId = 'pptx-capture-' + Math.random().toString(36).substr(2, 9);
|
|
8667
|
+
node.id = tempId;
|
|
8668
|
+
|
|
8573
8669
|
const width = Math.max(Math.ceil(widthPx), 1);
|
|
8574
8670
|
const height = Math.max(Math.ceil(heightPx), 1);
|
|
8575
8671
|
const style = window.getComputedStyle(node);
|
|
8576
8672
|
|
|
8577
|
-
// Optimized: Capture ONLY the specific node
|
|
8578
8673
|
html2canvas(node, {
|
|
8579
8674
|
backgroundColor: null,
|
|
8580
8675
|
logging: false,
|
|
8581
|
-
scale:
|
|
8676
|
+
scale: 3, // Higher scale for sharper icons
|
|
8677
|
+
useCORS: true, // critical for external fonts/images
|
|
8678
|
+
onclone: (clonedDoc) => {
|
|
8679
|
+
const clonedNode = clonedDoc.getElementById(tempId);
|
|
8680
|
+
if (clonedNode) {
|
|
8681
|
+
// --- FIX: PREVENT ICON CLIPPING ---
|
|
8682
|
+
// 1. Force overflow visible so glyphs bleeding out aren't cut
|
|
8683
|
+
clonedNode.style.overflow = 'visible';
|
|
8684
|
+
|
|
8685
|
+
// 2. Adjust alignment for Icons to prevent baseline clipping
|
|
8686
|
+
// (Applies to <i>, <span>, or standard icon classes)
|
|
8687
|
+
const tag = clonedNode.tagName;
|
|
8688
|
+
if (tag === 'I' || tag === 'SPAN' || clonedNode.className.includes('fa-')) {
|
|
8689
|
+
// Flex center helps align the glyph exactly in the middle of the box
|
|
8690
|
+
// preventing top/bottom cropping due to line-height mismatches.
|
|
8691
|
+
clonedNode.style.display = 'inline-flex';
|
|
8692
|
+
clonedNode.style.justifyContent = 'center';
|
|
8693
|
+
clonedNode.style.alignItems = 'center';
|
|
8694
|
+
|
|
8695
|
+
// Remove margins that might offset the capture
|
|
8696
|
+
clonedNode.style.margin = '0';
|
|
8697
|
+
|
|
8698
|
+
// Ensure the font fits
|
|
8699
|
+
clonedNode.style.lineHeight = '1';
|
|
8700
|
+
clonedNode.style.verticalAlign = 'middle';
|
|
8701
|
+
}
|
|
8702
|
+
}
|
|
8703
|
+
},
|
|
8582
8704
|
})
|
|
8583
8705
|
.then((canvas) => {
|
|
8706
|
+
// Restore the original ID
|
|
8707
|
+
if (originalId) node.id = originalId;
|
|
8708
|
+
else node.removeAttribute('id');
|
|
8709
|
+
|
|
8584
8710
|
const destCanvas = document.createElement('canvas');
|
|
8585
8711
|
destCanvas.width = width;
|
|
8586
8712
|
destCanvas.height = height;
|
|
8587
8713
|
const ctx = destCanvas.getContext('2d');
|
|
8588
8714
|
|
|
8589
|
-
// Draw
|
|
8590
|
-
//
|
|
8715
|
+
// Draw captured canvas.
|
|
8716
|
+
// We simply draw it to fill the box. Since we centered it in 'onclone',
|
|
8717
|
+
// the glyph should now be visible within the bounds.
|
|
8591
8718
|
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, width, height);
|
|
8592
8719
|
|
|
8593
|
-
//
|
|
8720
|
+
// --- Border Radius Clipping (Existing Logic) ---
|
|
8594
8721
|
let tl = parseFloat(style.borderTopLeftRadius) || 0;
|
|
8595
8722
|
let tr = parseFloat(style.borderTopRightRadius) || 0;
|
|
8596
8723
|
let br = parseFloat(style.borderBottomRightRadius) || 0;
|
|
@@ -8629,12 +8756,62 @@ async function elementToCanvasImage(node, widthPx, heightPx) {
|
|
|
8629
8756
|
resolve(destCanvas.toDataURL('image/png'));
|
|
8630
8757
|
})
|
|
8631
8758
|
.catch((e) => {
|
|
8759
|
+
if (originalId) node.id = originalId;
|
|
8760
|
+
else node.removeAttribute('id');
|
|
8632
8761
|
console.warn('Canvas capture failed for node', node, e);
|
|
8633
8762
|
resolve(null);
|
|
8634
8763
|
});
|
|
8635
8764
|
});
|
|
8636
8765
|
}
|
|
8637
8766
|
|
|
8767
|
+
/**
|
|
8768
|
+
* Helper to identify elements that should be rendered as icons (Images).
|
|
8769
|
+
* Detects Custom Elements AND generic tags (<i>, <span>) with icon classes/pseudo-elements.
|
|
8770
|
+
*/
|
|
8771
|
+
function isIconElement(node) {
|
|
8772
|
+
// 1. Custom Elements (hyphenated tags) or Explicit Library Tags
|
|
8773
|
+
const tag = node.tagName.toUpperCase();
|
|
8774
|
+
if (
|
|
8775
|
+
tag.includes('-') ||
|
|
8776
|
+
[
|
|
8777
|
+
'MATERIAL-ICON',
|
|
8778
|
+
'ICONIFY-ICON',
|
|
8779
|
+
'REMIX-ICON',
|
|
8780
|
+
'ION-ICON',
|
|
8781
|
+
'EVA-ICON',
|
|
8782
|
+
'BOX-ICON',
|
|
8783
|
+
'FA-ICON',
|
|
8784
|
+
].includes(tag)
|
|
8785
|
+
) {
|
|
8786
|
+
return true;
|
|
8787
|
+
}
|
|
8788
|
+
|
|
8789
|
+
// 2. Class-based Icons (FontAwesome, Bootstrap, Material symbols) on <i> or <span>
|
|
8790
|
+
if (tag === 'I' || tag === 'SPAN') {
|
|
8791
|
+
const cls = node.getAttribute('class') || '';
|
|
8792
|
+
if (
|
|
8793
|
+
typeof cls === 'string' &&
|
|
8794
|
+
(cls.includes('fa-') ||
|
|
8795
|
+
cls.includes('fas') ||
|
|
8796
|
+
cls.includes('far') ||
|
|
8797
|
+
cls.includes('fab') ||
|
|
8798
|
+
cls.includes('bi-') ||
|
|
8799
|
+
cls.includes('material-icons') ||
|
|
8800
|
+
cls.includes('icon'))
|
|
8801
|
+
) {
|
|
8802
|
+
// Double-check: Must have pseudo-element content to be a CSS icon
|
|
8803
|
+
const before = window.getComputedStyle(node, '::before').content;
|
|
8804
|
+
const after = window.getComputedStyle(node, '::after').content;
|
|
8805
|
+
const hasContent = (c) => c && c !== 'none' && c !== 'normal' && c !== '""';
|
|
8806
|
+
|
|
8807
|
+
if (hasContent(before) || hasContent(after)) return true;
|
|
8808
|
+
}
|
|
8809
|
+
console.log('Icon element:', node, cls);
|
|
8810
|
+
}
|
|
8811
|
+
|
|
8812
|
+
return false;
|
|
8813
|
+
}
|
|
8814
|
+
|
|
8638
8815
|
/**
|
|
8639
8816
|
* Replaces createRenderItem.
|
|
8640
8817
|
* Returns { items: [], job: () => Promise, stopRecursion: boolean }
|
|
@@ -8770,30 +8947,18 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
|
|
|
8770
8947
|
}
|
|
8771
8948
|
|
|
8772
8949
|
// --- ASYNC JOB: Icons and Other Elements ---
|
|
8773
|
-
if (
|
|
8774
|
-
node.tagName.toUpperCase() === 'MATERIAL-ICON' ||
|
|
8775
|
-
node.tagName.toUpperCase() === 'ICONIFY-ICON' ||
|
|
8776
|
-
node.tagName.toUpperCase() === 'REMIX-ICON' ||
|
|
8777
|
-
node.tagName.toUpperCase() === 'ION-ICON' ||
|
|
8778
|
-
node.tagName.toUpperCase() === 'EVA-ICON' ||
|
|
8779
|
-
node.tagName.toUpperCase() === 'BOX-ICON' ||
|
|
8780
|
-
node.tagName.toUpperCase() === 'FA-ICON' ||
|
|
8781
|
-
node.tagName.includes('-')
|
|
8782
|
-
) {
|
|
8950
|
+
if (isIconElement(node)) {
|
|
8783
8951
|
const item = {
|
|
8784
8952
|
type: 'image',
|
|
8785
8953
|
zIndex,
|
|
8786
8954
|
domOrder,
|
|
8787
|
-
options: { x, y, w, h, rotate: rotation, data: null },
|
|
8955
|
+
options: { x, y, w, h, rotate: rotation, data: null },
|
|
8788
8956
|
};
|
|
8789
|
-
|
|
8790
|
-
// Create Job
|
|
8791
8957
|
const job = async () => {
|
|
8792
8958
|
const pngData = await elementToCanvasImage(node, widthPx, heightPx);
|
|
8793
8959
|
if (pngData) item.options.data = pngData;
|
|
8794
8960
|
else item.skip = true;
|
|
8795
8961
|
};
|
|
8796
|
-
|
|
8797
8962
|
return { items: [item], job, stopRecursion: true };
|
|
8798
8963
|
}
|
|
8799
8964
|
|