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.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;
|
|
@@ -8135,6 +8154,69 @@ function getRotation(transformStr) {
|
|
|
8135
8154
|
return Math.round(Math.atan2(b, a) * (180 / Math.PI));
|
|
8136
8155
|
}
|
|
8137
8156
|
|
|
8157
|
+
function svgToPng(node) {
|
|
8158
|
+
return new Promise((resolve) => {
|
|
8159
|
+
const clone = node.cloneNode(true);
|
|
8160
|
+
const rect = node.getBoundingClientRect();
|
|
8161
|
+
const width = rect.width || 300;
|
|
8162
|
+
const height = rect.height || 150;
|
|
8163
|
+
|
|
8164
|
+
function inlineStyles(source, target) {
|
|
8165
|
+
const computed = window.getComputedStyle(source);
|
|
8166
|
+
const properties = [
|
|
8167
|
+
'fill',
|
|
8168
|
+
'stroke',
|
|
8169
|
+
'stroke-width',
|
|
8170
|
+
'stroke-linecap',
|
|
8171
|
+
'stroke-linejoin',
|
|
8172
|
+
'opacity',
|
|
8173
|
+
'font-family',
|
|
8174
|
+
'font-size',
|
|
8175
|
+
'font-weight',
|
|
8176
|
+
];
|
|
8177
|
+
|
|
8178
|
+
if (computed.fill === 'none') target.setAttribute('fill', 'none');
|
|
8179
|
+
else if (computed.fill) target.style.fill = computed.fill;
|
|
8180
|
+
|
|
8181
|
+
if (computed.stroke === 'none') target.setAttribute('stroke', 'none');
|
|
8182
|
+
else if (computed.stroke) target.style.stroke = computed.stroke;
|
|
8183
|
+
|
|
8184
|
+
properties.forEach((prop) => {
|
|
8185
|
+
if (prop !== 'fill' && prop !== 'stroke') {
|
|
8186
|
+
const val = computed[prop];
|
|
8187
|
+
if (val && val !== 'auto') target.style[prop] = val;
|
|
8188
|
+
}
|
|
8189
|
+
});
|
|
8190
|
+
|
|
8191
|
+
for (let i = 0; i < source.children.length; i++) {
|
|
8192
|
+
if (target.children[i]) inlineStyles(source.children[i], target.children[i]);
|
|
8193
|
+
}
|
|
8194
|
+
}
|
|
8195
|
+
|
|
8196
|
+
inlineStyles(node, clone);
|
|
8197
|
+
clone.setAttribute('width', width);
|
|
8198
|
+
clone.setAttribute('height', height);
|
|
8199
|
+
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
8200
|
+
|
|
8201
|
+
const xml = new XMLSerializer().serializeToString(clone);
|
|
8202
|
+
const svgUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(xml)}`;
|
|
8203
|
+
const img = new Image();
|
|
8204
|
+
img.crossOrigin = 'Anonymous';
|
|
8205
|
+
img.onload = () => {
|
|
8206
|
+
const canvas = document.createElement('canvas');
|
|
8207
|
+
const scale = 3;
|
|
8208
|
+
canvas.width = width * scale;
|
|
8209
|
+
canvas.height = height * scale;
|
|
8210
|
+
const ctx = canvas.getContext('2d');
|
|
8211
|
+
ctx.scale(scale, scale);
|
|
8212
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
8213
|
+
resolve(canvas.toDataURL('image/png'));
|
|
8214
|
+
};
|
|
8215
|
+
img.onerror = () => resolve(null);
|
|
8216
|
+
img.src = svgUrl;
|
|
8217
|
+
});
|
|
8218
|
+
}
|
|
8219
|
+
|
|
8138
8220
|
function getVisibleShadow(shadowStr, scale) {
|
|
8139
8221
|
if (!shadowStr || shadowStr === 'none') return null;
|
|
8140
8222
|
const shadows = shadowStr.split(/,(?![^()]*\))/);
|
|
@@ -8166,57 +8248,119 @@ function getVisibleShadow(shadowStr, scale) {
|
|
|
8166
8248
|
return null;
|
|
8167
8249
|
}
|
|
8168
8250
|
|
|
8251
|
+
/**
|
|
8252
|
+
* Generates an SVG image for gradients, supporting degrees and keywords.
|
|
8253
|
+
*/
|
|
8169
8254
|
function generateGradientSVG(w, h, bgString, radius, border) {
|
|
8170
8255
|
try {
|
|
8171
8256
|
const match = bgString.match(/linear-gradient\((.*)\)/);
|
|
8172
8257
|
if (!match) return null;
|
|
8173
8258
|
const content = match[1];
|
|
8259
|
+
|
|
8260
|
+
// Split by comma, ignoring commas inside parentheses (e.g. rgba())
|
|
8174
8261
|
const parts = content.split(/,(?![^()]*\))/).map((p) => p.trim());
|
|
8262
|
+
if (parts.length < 2) return null;
|
|
8175
8263
|
|
|
8176
8264
|
let x1 = '0%',
|
|
8177
8265
|
y1 = '0%',
|
|
8178
8266
|
x2 = '0%',
|
|
8179
8267
|
y2 = '100%';
|
|
8180
|
-
let
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
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
|
+
}
|
|
8199
8335
|
}
|
|
8200
8336
|
|
|
8337
|
+
// 3. Process Color Stops
|
|
8201
8338
|
let stopsXML = '';
|
|
8202
|
-
const stopParts = parts.slice(
|
|
8339
|
+
const stopParts = parts.slice(stopsStartIndex);
|
|
8340
|
+
|
|
8203
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
|
|
8204
8344
|
let color = part;
|
|
8205
8345
|
let offset = Math.round((idx / (stopParts.length - 1)) * 100) + '%';
|
|
8206
|
-
|
|
8346
|
+
|
|
8347
|
+
const posMatch = part.match(/^(.*?)\s+(-?[\d.]+(?:%|px)?)$/);
|
|
8207
8348
|
if (posMatch) {
|
|
8208
8349
|
color = posMatch[1];
|
|
8209
8350
|
offset = posMatch[2];
|
|
8210
8351
|
}
|
|
8352
|
+
|
|
8353
|
+
// Handle RGBA/RGB for SVG compatibility
|
|
8211
8354
|
let opacity = 1;
|
|
8212
8355
|
if (color.includes('rgba')) {
|
|
8213
|
-
const
|
|
8214
|
-
if (
|
|
8215
|
-
opacity =
|
|
8216
|
-
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]})`;
|
|
8217
8360
|
}
|
|
8218
8361
|
}
|
|
8219
|
-
|
|
8362
|
+
|
|
8363
|
+
stopsXML += `<stop offset="${offset}" stop-color="${color.trim()}" stop-opacity="${opacity}"/>`;
|
|
8220
8364
|
});
|
|
8221
8365
|
|
|
8222
8366
|
let strokeAttr = '';
|
|
@@ -8225,12 +8369,18 @@ function generateGradientSVG(w, h, bgString, radius, border) {
|
|
|
8225
8369
|
}
|
|
8226
8370
|
|
|
8227
8371
|
const svg = `
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
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
|
+
|
|
8232
8381
|
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
8233
|
-
} catch {
|
|
8382
|
+
} catch (e) {
|
|
8383
|
+
console.warn('Gradient generation failed:', e);
|
|
8234
8384
|
return null;
|
|
8235
8385
|
}
|
|
8236
8386
|
}
|
|
@@ -8505,29 +8655,69 @@ async function processSlide(root, slide, pptx) {
|
|
|
8505
8655
|
* Optimized html2canvas wrapper
|
|
8506
8656
|
* Now strictly captures the node itself, not the root.
|
|
8507
8657
|
*/
|
|
8658
|
+
/**
|
|
8659
|
+
* Optimized html2canvas wrapper
|
|
8660
|
+
* Includes fix for cropped icons by adjusting styles in the cloned document.
|
|
8661
|
+
*/
|
|
8508
8662
|
async function elementToCanvasImage(node, widthPx, heightPx) {
|
|
8509
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
|
+
|
|
8510
8669
|
const width = Math.max(Math.ceil(widthPx), 1);
|
|
8511
8670
|
const height = Math.max(Math.ceil(heightPx), 1);
|
|
8512
8671
|
const style = window.getComputedStyle(node);
|
|
8513
8672
|
|
|
8514
|
-
// Optimized: Capture ONLY the specific node
|
|
8515
8673
|
html2canvas(node, {
|
|
8516
8674
|
backgroundColor: null,
|
|
8517
8675
|
logging: false,
|
|
8518
|
-
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
|
+
},
|
|
8519
8704
|
})
|
|
8520
8705
|
.then((canvas) => {
|
|
8706
|
+
// Restore the original ID
|
|
8707
|
+
if (originalId) node.id = originalId;
|
|
8708
|
+
else node.removeAttribute('id');
|
|
8709
|
+
|
|
8521
8710
|
const destCanvas = document.createElement('canvas');
|
|
8522
8711
|
destCanvas.width = width;
|
|
8523
8712
|
destCanvas.height = height;
|
|
8524
8713
|
const ctx = destCanvas.getContext('2d');
|
|
8525
8714
|
|
|
8526
|
-
// Draw
|
|
8527
|
-
//
|
|
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.
|
|
8528
8718
|
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, width, height);
|
|
8529
8719
|
|
|
8530
|
-
//
|
|
8720
|
+
// --- Border Radius Clipping (Existing Logic) ---
|
|
8531
8721
|
let tl = parseFloat(style.borderTopLeftRadius) || 0;
|
|
8532
8722
|
let tr = parseFloat(style.borderTopRightRadius) || 0;
|
|
8533
8723
|
let br = parseFloat(style.borderBottomRightRadius) || 0;
|
|
@@ -8566,12 +8756,62 @@ async function elementToCanvasImage(node, widthPx, heightPx) {
|
|
|
8566
8756
|
resolve(destCanvas.toDataURL('image/png'));
|
|
8567
8757
|
})
|
|
8568
8758
|
.catch((e) => {
|
|
8759
|
+
if (originalId) node.id = originalId;
|
|
8760
|
+
else node.removeAttribute('id');
|
|
8569
8761
|
console.warn('Canvas capture failed for node', node, e);
|
|
8570
8762
|
resolve(null);
|
|
8571
8763
|
});
|
|
8572
8764
|
});
|
|
8573
8765
|
}
|
|
8574
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
|
+
|
|
8575
8815
|
/**
|
|
8576
8816
|
* Replaces createRenderItem.
|
|
8577
8817
|
* Returns { items: [], job: () => Promise, stopRecursion: boolean }
|
|
@@ -8645,23 +8885,18 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
|
|
|
8645
8885
|
|
|
8646
8886
|
const items = [];
|
|
8647
8887
|
|
|
8648
|
-
// --- ASYNC JOB:
|
|
8649
|
-
if (
|
|
8650
|
-
node.nodeName.toUpperCase() === 'SVG' ||
|
|
8651
|
-
node.tagName.includes('-') ||
|
|
8652
|
-
node.tagName === 'ION-ICON'
|
|
8653
|
-
) {
|
|
8888
|
+
// --- ASYNC JOB: SVG Tags ---
|
|
8889
|
+
if (node.nodeName.toUpperCase() === 'SVG') {
|
|
8654
8890
|
const item = {
|
|
8655
8891
|
type: 'image',
|
|
8656
8892
|
zIndex,
|
|
8657
8893
|
domOrder,
|
|
8658
|
-
options: { x, y, w, h, rotate: rotation
|
|
8894
|
+
options: { data: null, x, y, w, h, rotate: rotation },
|
|
8659
8895
|
};
|
|
8660
8896
|
|
|
8661
|
-
// Create Job
|
|
8662
8897
|
const job = async () => {
|
|
8663
|
-
const
|
|
8664
|
-
if (
|
|
8898
|
+
const processed = await svgToPng(node);
|
|
8899
|
+
if (processed) item.options.data = processed;
|
|
8665
8900
|
else item.skip = true;
|
|
8666
8901
|
};
|
|
8667
8902
|
|
|
@@ -8711,6 +8946,22 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
|
|
|
8711
8946
|
return { items: [item], job, stopRecursion: true };
|
|
8712
8947
|
}
|
|
8713
8948
|
|
|
8949
|
+
// --- ASYNC JOB: Icons and Other Elements ---
|
|
8950
|
+
if (isIconElement(node)) {
|
|
8951
|
+
const item = {
|
|
8952
|
+
type: 'image',
|
|
8953
|
+
zIndex,
|
|
8954
|
+
domOrder,
|
|
8955
|
+
options: { x, y, w, h, rotate: rotation, data: null },
|
|
8956
|
+
};
|
|
8957
|
+
const job = async () => {
|
|
8958
|
+
const pngData = await elementToCanvasImage(node, widthPx, heightPx);
|
|
8959
|
+
if (pngData) item.options.data = pngData;
|
|
8960
|
+
else item.skip = true;
|
|
8961
|
+
};
|
|
8962
|
+
return { items: [item], job, stopRecursion: true };
|
|
8963
|
+
}
|
|
8964
|
+
|
|
8714
8965
|
// Radii logic
|
|
8715
8966
|
const borderRadiusValue = parseFloat(style.borderRadius) || 0;
|
|
8716
8967
|
const borderBottomLeftRadius = parseFloat(style.borderBottomLeftRadius) || 0;
|