dom-to-pptx 1.0.7 → 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 +14 -0
- package/README.md +1 -1
- package/dist/dom-to-pptx.bundle.js +306 -55
- package/dist/dom-to-pptx.cjs +306 -55
- package/dist/dom-to-pptx.min.js +306 -55
- package/dist/dom-to-pptx.mjs +306 -55
- package/package.json +1 -1
- package/src/index.js +117 -15
- package/src/utils.js +128 -41
package/dist/dom-to-pptx.mjs
CHANGED
|
@@ -8055,6 +8055,7 @@ function getTextStyle(style, scale) {
|
|
|
8055
8055
|
|
|
8056
8056
|
/**
|
|
8057
8057
|
* Determines if a given DOM node is primarily a text container.
|
|
8058
|
+
* Updated to correctly reject Icon elements so they are rendered as images.
|
|
8058
8059
|
*/
|
|
8059
8060
|
function isTextContainer(node) {
|
|
8060
8061
|
const hasText = node.textContent.trim().length > 0;
|
|
@@ -8063,28 +8064,46 @@ function isTextContainer(node) {
|
|
|
8063
8064
|
const children = Array.from(node.children);
|
|
8064
8065
|
if (children.length === 0) return true;
|
|
8065
8066
|
|
|
8066
|
-
// Check if children are purely inline text formatting or visual shapes
|
|
8067
8067
|
const isSafeInline = (el) => {
|
|
8068
|
-
// 1. Reject Web Components /
|
|
8068
|
+
// 1. Reject Web Components / Custom Elements
|
|
8069
8069
|
if (el.tagName.includes('-')) return false;
|
|
8070
|
+
// 2. Reject Explicit Images/SVGs
|
|
8070
8071
|
if (el.tagName === 'IMG' || el.tagName === 'SVG') return false;
|
|
8071
8072
|
|
|
8073
|
+
// 3. Reject Class-based Icons (FontAwesome, Material, Bootstrap, etc.)
|
|
8074
|
+
// If an <i> or <span> has icon classes, it is a visual object, not text.
|
|
8075
|
+
if (el.tagName === 'I' || el.tagName === 'SPAN') {
|
|
8076
|
+
const cls = el.getAttribute('class') || '';
|
|
8077
|
+
if (
|
|
8078
|
+
cls.includes('fa-') ||
|
|
8079
|
+
cls.includes('fas') ||
|
|
8080
|
+
cls.includes('far') ||
|
|
8081
|
+
cls.includes('fab') ||
|
|
8082
|
+
cls.includes('material-icons') ||
|
|
8083
|
+
cls.includes('bi-') ||
|
|
8084
|
+
cls.includes('icon')
|
|
8085
|
+
) {
|
|
8086
|
+
return false;
|
|
8087
|
+
}
|
|
8088
|
+
}
|
|
8089
|
+
|
|
8072
8090
|
const style = window.getComputedStyle(el);
|
|
8073
8091
|
const display = style.display;
|
|
8074
8092
|
|
|
8075
|
-
//
|
|
8076
|
-
const isInlineTag = ['SPAN', 'B', 'STRONG', 'EM', 'I', 'A', 'SMALL', 'MARK'].includes(
|
|
8093
|
+
// 4. Standard Inline Tag Check
|
|
8094
|
+
const isInlineTag = ['SPAN', 'B', 'STRONG', 'EM', 'I', 'A', 'SMALL', 'MARK'].includes(
|
|
8095
|
+
el.tagName
|
|
8096
|
+
);
|
|
8077
8097
|
const isInlineDisplay = display.includes('inline');
|
|
8078
8098
|
|
|
8079
8099
|
if (!isInlineTag && !isInlineDisplay) return false;
|
|
8080
8100
|
|
|
8081
|
-
//
|
|
8082
|
-
//
|
|
8083
|
-
// If a child element has these, the parent is NOT a simple text container;
|
|
8084
|
-
// it is a layout container composed of styled blocks.
|
|
8101
|
+
// 5. Structural Styling Check
|
|
8102
|
+
// If a child has a background or border, it's a layout block, not a simple text span.
|
|
8085
8103
|
const bgColor = parseColor(style.backgroundColor);
|
|
8086
8104
|
const hasVisibleBg = bgColor.hex && bgColor.opacity > 0;
|
|
8087
|
-
const hasBorder =
|
|
8105
|
+
const hasBorder =
|
|
8106
|
+
parseFloat(style.borderWidth) > 0 && parseColor(style.borderColor).opacity > 0;
|
|
8088
8107
|
|
|
8089
8108
|
if (hasVisibleBg || hasBorder) {
|
|
8090
8109
|
return false;
|
|
@@ -8111,6 +8130,69 @@ function getRotation(transformStr) {
|
|
|
8111
8130
|
return Math.round(Math.atan2(b, a) * (180 / Math.PI));
|
|
8112
8131
|
}
|
|
8113
8132
|
|
|
8133
|
+
function svgToPng(node) {
|
|
8134
|
+
return new Promise((resolve) => {
|
|
8135
|
+
const clone = node.cloneNode(true);
|
|
8136
|
+
const rect = node.getBoundingClientRect();
|
|
8137
|
+
const width = rect.width || 300;
|
|
8138
|
+
const height = rect.height || 150;
|
|
8139
|
+
|
|
8140
|
+
function inlineStyles(source, target) {
|
|
8141
|
+
const computed = window.getComputedStyle(source);
|
|
8142
|
+
const properties = [
|
|
8143
|
+
'fill',
|
|
8144
|
+
'stroke',
|
|
8145
|
+
'stroke-width',
|
|
8146
|
+
'stroke-linecap',
|
|
8147
|
+
'stroke-linejoin',
|
|
8148
|
+
'opacity',
|
|
8149
|
+
'font-family',
|
|
8150
|
+
'font-size',
|
|
8151
|
+
'font-weight',
|
|
8152
|
+
];
|
|
8153
|
+
|
|
8154
|
+
if (computed.fill === 'none') target.setAttribute('fill', 'none');
|
|
8155
|
+
else if (computed.fill) target.style.fill = computed.fill;
|
|
8156
|
+
|
|
8157
|
+
if (computed.stroke === 'none') target.setAttribute('stroke', 'none');
|
|
8158
|
+
else if (computed.stroke) target.style.stroke = computed.stroke;
|
|
8159
|
+
|
|
8160
|
+
properties.forEach((prop) => {
|
|
8161
|
+
if (prop !== 'fill' && prop !== 'stroke') {
|
|
8162
|
+
const val = computed[prop];
|
|
8163
|
+
if (val && val !== 'auto') target.style[prop] = val;
|
|
8164
|
+
}
|
|
8165
|
+
});
|
|
8166
|
+
|
|
8167
|
+
for (let i = 0; i < source.children.length; i++) {
|
|
8168
|
+
if (target.children[i]) inlineStyles(source.children[i], target.children[i]);
|
|
8169
|
+
}
|
|
8170
|
+
}
|
|
8171
|
+
|
|
8172
|
+
inlineStyles(node, clone);
|
|
8173
|
+
clone.setAttribute('width', width);
|
|
8174
|
+
clone.setAttribute('height', height);
|
|
8175
|
+
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
8176
|
+
|
|
8177
|
+
const xml = new XMLSerializer().serializeToString(clone);
|
|
8178
|
+
const svgUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(xml)}`;
|
|
8179
|
+
const img = new Image();
|
|
8180
|
+
img.crossOrigin = 'Anonymous';
|
|
8181
|
+
img.onload = () => {
|
|
8182
|
+
const canvas = document.createElement('canvas');
|
|
8183
|
+
const scale = 3;
|
|
8184
|
+
canvas.width = width * scale;
|
|
8185
|
+
canvas.height = height * scale;
|
|
8186
|
+
const ctx = canvas.getContext('2d');
|
|
8187
|
+
ctx.scale(scale, scale);
|
|
8188
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
8189
|
+
resolve(canvas.toDataURL('image/png'));
|
|
8190
|
+
};
|
|
8191
|
+
img.onerror = () => resolve(null);
|
|
8192
|
+
img.src = svgUrl;
|
|
8193
|
+
});
|
|
8194
|
+
}
|
|
8195
|
+
|
|
8114
8196
|
function getVisibleShadow(shadowStr, scale) {
|
|
8115
8197
|
if (!shadowStr || shadowStr === 'none') return null;
|
|
8116
8198
|
const shadows = shadowStr.split(/,(?![^()]*\))/);
|
|
@@ -8142,57 +8224,119 @@ function getVisibleShadow(shadowStr, scale) {
|
|
|
8142
8224
|
return null;
|
|
8143
8225
|
}
|
|
8144
8226
|
|
|
8227
|
+
/**
|
|
8228
|
+
* Generates an SVG image for gradients, supporting degrees and keywords.
|
|
8229
|
+
*/
|
|
8145
8230
|
function generateGradientSVG(w, h, bgString, radius, border) {
|
|
8146
8231
|
try {
|
|
8147
8232
|
const match = bgString.match(/linear-gradient\((.*)\)/);
|
|
8148
8233
|
if (!match) return null;
|
|
8149
8234
|
const content = match[1];
|
|
8235
|
+
|
|
8236
|
+
// Split by comma, ignoring commas inside parentheses (e.g. rgba())
|
|
8150
8237
|
const parts = content.split(/,(?![^()]*\))/).map((p) => p.trim());
|
|
8238
|
+
if (parts.length < 2) return null;
|
|
8151
8239
|
|
|
8152
8240
|
let x1 = '0%',
|
|
8153
8241
|
y1 = '0%',
|
|
8154
8242
|
x2 = '0%',
|
|
8155
8243
|
y2 = '100%';
|
|
8156
|
-
let
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8244
|
+
let stopsStartIndex = 0;
|
|
8245
|
+
const firstPart = parts[0].toLowerCase();
|
|
8246
|
+
|
|
8247
|
+
// 1. Check for Keywords (to right, etc.)
|
|
8248
|
+
if (firstPart.startsWith('to ')) {
|
|
8249
|
+
stopsStartIndex = 1;
|
|
8250
|
+
const direction = firstPart.replace('to ', '').trim();
|
|
8251
|
+
switch (direction) {
|
|
8252
|
+
case 'top':
|
|
8253
|
+
y1 = '100%';
|
|
8254
|
+
y2 = '0%';
|
|
8255
|
+
break;
|
|
8256
|
+
case 'bottom':
|
|
8257
|
+
y1 = '0%';
|
|
8258
|
+
y2 = '100%';
|
|
8259
|
+
break;
|
|
8260
|
+
case 'left':
|
|
8261
|
+
x1 = '100%';
|
|
8262
|
+
x2 = '0%';
|
|
8263
|
+
break;
|
|
8264
|
+
case 'right':
|
|
8265
|
+
x2 = '100%';
|
|
8266
|
+
break;
|
|
8267
|
+
case 'top right':
|
|
8268
|
+
x1 = '0%';
|
|
8269
|
+
y1 = '100%';
|
|
8270
|
+
x2 = '100%';
|
|
8271
|
+
y2 = '0%';
|
|
8272
|
+
break;
|
|
8273
|
+
case 'top left':
|
|
8274
|
+
x1 = '100%';
|
|
8275
|
+
y1 = '100%';
|
|
8276
|
+
x2 = '0%';
|
|
8277
|
+
y2 = '0%';
|
|
8278
|
+
break;
|
|
8279
|
+
case 'bottom right':
|
|
8280
|
+
x2 = '100%';
|
|
8281
|
+
y2 = '100%';
|
|
8282
|
+
break;
|
|
8283
|
+
case 'bottom left':
|
|
8284
|
+
x1 = '100%';
|
|
8285
|
+
y2 = '100%';
|
|
8286
|
+
break;
|
|
8287
|
+
}
|
|
8288
|
+
}
|
|
8289
|
+
// 2. Check for Degrees (45deg, 90deg, etc.)
|
|
8290
|
+
else if (firstPart.match(/^-?[\d.]+(deg|rad|turn|grad)$/)) {
|
|
8291
|
+
stopsStartIndex = 1;
|
|
8292
|
+
const val = parseFloat(firstPart);
|
|
8293
|
+
// CSS 0deg is Top (North), 90deg is Right (East), 180deg is Bottom (South)
|
|
8294
|
+
// We convert this to SVG coordinates on a unit square (0-100%).
|
|
8295
|
+
// Formula: Map angle to perimeter coordinates.
|
|
8296
|
+
if (!isNaN(val)) {
|
|
8297
|
+
const deg = firstPart.includes('rad') ? val * (180 / Math.PI) : val;
|
|
8298
|
+
const cssRad = ((deg - 90) * Math.PI) / 180; // Correct CSS angle offset
|
|
8299
|
+
|
|
8300
|
+
// Calculate standard vector for rectangle center (50, 50)
|
|
8301
|
+
const scale = 50; // Distance from center to edge (approx)
|
|
8302
|
+
const cos = Math.cos(cssRad); // Y component (reversed in SVG)
|
|
8303
|
+
const sin = Math.sin(cssRad); // X component
|
|
8304
|
+
|
|
8305
|
+
// Invert Y for SVG coordinate system
|
|
8306
|
+
x1 = (50 - sin * scale).toFixed(1) + '%';
|
|
8307
|
+
y1 = (50 + cos * scale).toFixed(1) + '%';
|
|
8308
|
+
x2 = (50 + sin * scale).toFixed(1) + '%';
|
|
8309
|
+
y2 = (50 - cos * scale).toFixed(1) + '%';
|
|
8310
|
+
}
|
|
8175
8311
|
}
|
|
8176
8312
|
|
|
8313
|
+
// 3. Process Color Stops
|
|
8177
8314
|
let stopsXML = '';
|
|
8178
|
-
const stopParts = parts.slice(
|
|
8315
|
+
const stopParts = parts.slice(stopsStartIndex);
|
|
8316
|
+
|
|
8179
8317
|
stopParts.forEach((part, idx) => {
|
|
8318
|
+
// Parse "Color Position" (e.g., "red 50%")
|
|
8319
|
+
// Regex looks for optional space + number + unit at the end of the string
|
|
8180
8320
|
let color = part;
|
|
8181
8321
|
let offset = Math.round((idx / (stopParts.length - 1)) * 100) + '%';
|
|
8182
|
-
|
|
8322
|
+
|
|
8323
|
+
const posMatch = part.match(/^(.*?)\s+(-?[\d.]+(?:%|px)?)$/);
|
|
8183
8324
|
if (posMatch) {
|
|
8184
8325
|
color = posMatch[1];
|
|
8185
8326
|
offset = posMatch[2];
|
|
8186
8327
|
}
|
|
8328
|
+
|
|
8329
|
+
// Handle RGBA/RGB for SVG compatibility
|
|
8187
8330
|
let opacity = 1;
|
|
8188
8331
|
if (color.includes('rgba')) {
|
|
8189
|
-
const
|
|
8190
|
-
if (
|
|
8191
|
-
opacity =
|
|
8192
|
-
color = `rgb(${
|
|
8332
|
+
const rgbaMatch = color.match(/[\d.]+/g);
|
|
8333
|
+
if (rgbaMatch && rgbaMatch.length >= 4) {
|
|
8334
|
+
opacity = rgbaMatch[3];
|
|
8335
|
+
color = `rgb(${rgbaMatch[0]},${rgbaMatch[1]},${rgbaMatch[2]})`;
|
|
8193
8336
|
}
|
|
8194
8337
|
}
|
|
8195
|
-
|
|
8338
|
+
|
|
8339
|
+
stopsXML += `<stop offset="${offset}" stop-color="${color.trim()}" stop-opacity="${opacity}"/>`;
|
|
8196
8340
|
});
|
|
8197
8341
|
|
|
8198
8342
|
let strokeAttr = '';
|
|
@@ -8201,12 +8345,18 @@ function generateGradientSVG(w, h, bgString, radius, border) {
|
|
|
8201
8345
|
}
|
|
8202
8346
|
|
|
8203
8347
|
const svg = `
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8348
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
8349
|
+
<defs>
|
|
8350
|
+
<linearGradient id="grad" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}">
|
|
8351
|
+
${stopsXML}
|
|
8352
|
+
</linearGradient>
|
|
8353
|
+
</defs>
|
|
8354
|
+
<rect x="0" y="0" width="${w}" height="${h}" rx="${radius}" ry="${radius}" fill="url(#grad)" ${strokeAttr} />
|
|
8355
|
+
</svg>`;
|
|
8356
|
+
|
|
8208
8357
|
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
8209
|
-
} catch {
|
|
8358
|
+
} catch (e) {
|
|
8359
|
+
console.warn('Gradient generation failed:', e);
|
|
8210
8360
|
return null;
|
|
8211
8361
|
}
|
|
8212
8362
|
}
|
|
@@ -8481,29 +8631,69 @@ async function processSlide(root, slide, pptx) {
|
|
|
8481
8631
|
* Optimized html2canvas wrapper
|
|
8482
8632
|
* Now strictly captures the node itself, not the root.
|
|
8483
8633
|
*/
|
|
8634
|
+
/**
|
|
8635
|
+
* Optimized html2canvas wrapper
|
|
8636
|
+
* Includes fix for cropped icons by adjusting styles in the cloned document.
|
|
8637
|
+
*/
|
|
8484
8638
|
async function elementToCanvasImage(node, widthPx, heightPx) {
|
|
8485
8639
|
return new Promise((resolve) => {
|
|
8640
|
+
// 1. Assign a temp ID to locate the node inside the cloned document
|
|
8641
|
+
const originalId = node.id;
|
|
8642
|
+
const tempId = 'pptx-capture-' + Math.random().toString(36).substr(2, 9);
|
|
8643
|
+
node.id = tempId;
|
|
8644
|
+
|
|
8486
8645
|
const width = Math.max(Math.ceil(widthPx), 1);
|
|
8487
8646
|
const height = Math.max(Math.ceil(heightPx), 1);
|
|
8488
8647
|
const style = window.getComputedStyle(node);
|
|
8489
8648
|
|
|
8490
|
-
// Optimized: Capture ONLY the specific node
|
|
8491
8649
|
html2canvas(node, {
|
|
8492
8650
|
backgroundColor: null,
|
|
8493
8651
|
logging: false,
|
|
8494
|
-
scale:
|
|
8652
|
+
scale: 3, // Higher scale for sharper icons
|
|
8653
|
+
useCORS: true, // critical for external fonts/images
|
|
8654
|
+
onclone: (clonedDoc) => {
|
|
8655
|
+
const clonedNode = clonedDoc.getElementById(tempId);
|
|
8656
|
+
if (clonedNode) {
|
|
8657
|
+
// --- FIX: PREVENT ICON CLIPPING ---
|
|
8658
|
+
// 1. Force overflow visible so glyphs bleeding out aren't cut
|
|
8659
|
+
clonedNode.style.overflow = 'visible';
|
|
8660
|
+
|
|
8661
|
+
// 2. Adjust alignment for Icons to prevent baseline clipping
|
|
8662
|
+
// (Applies to <i>, <span>, or standard icon classes)
|
|
8663
|
+
const tag = clonedNode.tagName;
|
|
8664
|
+
if (tag === 'I' || tag === 'SPAN' || clonedNode.className.includes('fa-')) {
|
|
8665
|
+
// Flex center helps align the glyph exactly in the middle of the box
|
|
8666
|
+
// preventing top/bottom cropping due to line-height mismatches.
|
|
8667
|
+
clonedNode.style.display = 'inline-flex';
|
|
8668
|
+
clonedNode.style.justifyContent = 'center';
|
|
8669
|
+
clonedNode.style.alignItems = 'center';
|
|
8670
|
+
|
|
8671
|
+
// Remove margins that might offset the capture
|
|
8672
|
+
clonedNode.style.margin = '0';
|
|
8673
|
+
|
|
8674
|
+
// Ensure the font fits
|
|
8675
|
+
clonedNode.style.lineHeight = '1';
|
|
8676
|
+
clonedNode.style.verticalAlign = 'middle';
|
|
8677
|
+
}
|
|
8678
|
+
}
|
|
8679
|
+
},
|
|
8495
8680
|
})
|
|
8496
8681
|
.then((canvas) => {
|
|
8682
|
+
// Restore the original ID
|
|
8683
|
+
if (originalId) node.id = originalId;
|
|
8684
|
+
else node.removeAttribute('id');
|
|
8685
|
+
|
|
8497
8686
|
const destCanvas = document.createElement('canvas');
|
|
8498
8687
|
destCanvas.width = width;
|
|
8499
8688
|
destCanvas.height = height;
|
|
8500
8689
|
const ctx = destCanvas.getContext('2d');
|
|
8501
8690
|
|
|
8502
|
-
// Draw
|
|
8503
|
-
//
|
|
8691
|
+
// Draw captured canvas.
|
|
8692
|
+
// We simply draw it to fill the box. Since we centered it in 'onclone',
|
|
8693
|
+
// the glyph should now be visible within the bounds.
|
|
8504
8694
|
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, width, height);
|
|
8505
8695
|
|
|
8506
|
-
//
|
|
8696
|
+
// --- Border Radius Clipping (Existing Logic) ---
|
|
8507
8697
|
let tl = parseFloat(style.borderTopLeftRadius) || 0;
|
|
8508
8698
|
let tr = parseFloat(style.borderTopRightRadius) || 0;
|
|
8509
8699
|
let br = parseFloat(style.borderBottomRightRadius) || 0;
|
|
@@ -8542,12 +8732,62 @@ async function elementToCanvasImage(node, widthPx, heightPx) {
|
|
|
8542
8732
|
resolve(destCanvas.toDataURL('image/png'));
|
|
8543
8733
|
})
|
|
8544
8734
|
.catch((e) => {
|
|
8735
|
+
if (originalId) node.id = originalId;
|
|
8736
|
+
else node.removeAttribute('id');
|
|
8545
8737
|
console.warn('Canvas capture failed for node', node, e);
|
|
8546
8738
|
resolve(null);
|
|
8547
8739
|
});
|
|
8548
8740
|
});
|
|
8549
8741
|
}
|
|
8550
8742
|
|
|
8743
|
+
/**
|
|
8744
|
+
* Helper to identify elements that should be rendered as icons (Images).
|
|
8745
|
+
* Detects Custom Elements AND generic tags (<i>, <span>) with icon classes/pseudo-elements.
|
|
8746
|
+
*/
|
|
8747
|
+
function isIconElement(node) {
|
|
8748
|
+
// 1. Custom Elements (hyphenated tags) or Explicit Library Tags
|
|
8749
|
+
const tag = node.tagName.toUpperCase();
|
|
8750
|
+
if (
|
|
8751
|
+
tag.includes('-') ||
|
|
8752
|
+
[
|
|
8753
|
+
'MATERIAL-ICON',
|
|
8754
|
+
'ICONIFY-ICON',
|
|
8755
|
+
'REMIX-ICON',
|
|
8756
|
+
'ION-ICON',
|
|
8757
|
+
'EVA-ICON',
|
|
8758
|
+
'BOX-ICON',
|
|
8759
|
+
'FA-ICON',
|
|
8760
|
+
].includes(tag)
|
|
8761
|
+
) {
|
|
8762
|
+
return true;
|
|
8763
|
+
}
|
|
8764
|
+
|
|
8765
|
+
// 2. Class-based Icons (FontAwesome, Bootstrap, Material symbols) on <i> or <span>
|
|
8766
|
+
if (tag === 'I' || tag === 'SPAN') {
|
|
8767
|
+
const cls = node.getAttribute('class') || '';
|
|
8768
|
+
if (
|
|
8769
|
+
typeof cls === 'string' &&
|
|
8770
|
+
(cls.includes('fa-') ||
|
|
8771
|
+
cls.includes('fas') ||
|
|
8772
|
+
cls.includes('far') ||
|
|
8773
|
+
cls.includes('fab') ||
|
|
8774
|
+
cls.includes('bi-') ||
|
|
8775
|
+
cls.includes('material-icons') ||
|
|
8776
|
+
cls.includes('icon'))
|
|
8777
|
+
) {
|
|
8778
|
+
// Double-check: Must have pseudo-element content to be a CSS icon
|
|
8779
|
+
const before = window.getComputedStyle(node, '::before').content;
|
|
8780
|
+
const after = window.getComputedStyle(node, '::after').content;
|
|
8781
|
+
const hasContent = (c) => c && c !== 'none' && c !== 'normal' && c !== '""';
|
|
8782
|
+
|
|
8783
|
+
if (hasContent(before) || hasContent(after)) return true;
|
|
8784
|
+
}
|
|
8785
|
+
console.log('Icon element:', node, cls);
|
|
8786
|
+
}
|
|
8787
|
+
|
|
8788
|
+
return false;
|
|
8789
|
+
}
|
|
8790
|
+
|
|
8551
8791
|
/**
|
|
8552
8792
|
* Replaces createRenderItem.
|
|
8553
8793
|
* Returns { items: [], job: () => Promise, stopRecursion: boolean }
|
|
@@ -8621,23 +8861,18 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
|
|
|
8621
8861
|
|
|
8622
8862
|
const items = [];
|
|
8623
8863
|
|
|
8624
|
-
// --- ASYNC JOB:
|
|
8625
|
-
if (
|
|
8626
|
-
node.nodeName.toUpperCase() === 'SVG' ||
|
|
8627
|
-
node.tagName.includes('-') ||
|
|
8628
|
-
node.tagName === 'ION-ICON'
|
|
8629
|
-
) {
|
|
8864
|
+
// --- ASYNC JOB: SVG Tags ---
|
|
8865
|
+
if (node.nodeName.toUpperCase() === 'SVG') {
|
|
8630
8866
|
const item = {
|
|
8631
8867
|
type: 'image',
|
|
8632
8868
|
zIndex,
|
|
8633
8869
|
domOrder,
|
|
8634
|
-
options: { x, y, w, h, rotate: rotation
|
|
8870
|
+
options: { data: null, x, y, w, h, rotate: rotation },
|
|
8635
8871
|
};
|
|
8636
8872
|
|
|
8637
|
-
// Create Job
|
|
8638
8873
|
const job = async () => {
|
|
8639
|
-
const
|
|
8640
|
-
if (
|
|
8874
|
+
const processed = await svgToPng(node);
|
|
8875
|
+
if (processed) item.options.data = processed;
|
|
8641
8876
|
else item.skip = true;
|
|
8642
8877
|
};
|
|
8643
8878
|
|
|
@@ -8687,6 +8922,22 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
|
|
|
8687
8922
|
return { items: [item], job, stopRecursion: true };
|
|
8688
8923
|
}
|
|
8689
8924
|
|
|
8925
|
+
// --- ASYNC JOB: Icons and Other Elements ---
|
|
8926
|
+
if (isIconElement(node)) {
|
|
8927
|
+
const item = {
|
|
8928
|
+
type: 'image',
|
|
8929
|
+
zIndex,
|
|
8930
|
+
domOrder,
|
|
8931
|
+
options: { x, y, w, h, rotate: rotation, data: null },
|
|
8932
|
+
};
|
|
8933
|
+
const job = async () => {
|
|
8934
|
+
const pngData = await elementToCanvasImage(node, widthPx, heightPx);
|
|
8935
|
+
if (pngData) item.options.data = pngData;
|
|
8936
|
+
else item.skip = true;
|
|
8937
|
+
};
|
|
8938
|
+
return { items: [item], job, stopRecursion: true };
|
|
8939
|
+
}
|
|
8940
|
+
|
|
8690
8941
|
// Radii logic
|
|
8691
8942
|
const borderRadiusValue = parseFloat(style.borderRadius) || 0;
|
|
8692
8943
|
const borderBottomLeftRadius = parseFloat(style.borderBottomLeftRadius) || 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dom-to-pptx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "A client-side library that converts any HTML element into a fully editable PowerPoint slide. **dom-to-pptx** transforms DOM structures into pixel-accurate `.pptx` content, preserving gradients, shadows, rounded images, and responsive layouts. It translates CSS Flexbox/Grid, linear-gradients, box-shadows, and typography into native PowerPoint shapes, enabling precise, design-faithful slide generation directly from the browser.",
|
|
5
5
|
"main": "dist/dom-to-pptx.cjs",
|
|
6
6
|
"module": "dist/dom-to-pptx.mjs",
|