dom-to-pptx 1.0.8 → 1.1.0
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 +17 -0
- package/README.md +56 -62
- package/dist/dom-to-pptx.bundle.js +45947 -724
- package/dist/dom-to-pptx.cjs +606 -7890
- package/dist/dom-to-pptx.cjs.map +1 -0
- package/dist/dom-to-pptx.min.js +55536 -406
- package/dist/dom-to-pptx.mjs +585 -7875
- package/dist/dom-to-pptx.mjs.map +1 -0
- package/package.json +83 -73
- package/rollup.config.js +63 -24
- package/src/font-embedder.js +159 -0
- package/src/font-utils.js +35 -0
- package/src/index.js +173 -23
- package/src/utils.js +226 -40
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// src/font-embedder.js
|
|
2
|
+
import opentype from 'opentype.js';
|
|
3
|
+
import { fontToEot } from './font-utils.js';
|
|
4
|
+
|
|
5
|
+
const START_RID = 201314;
|
|
6
|
+
|
|
7
|
+
export class PPTXEmbedFonts {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.zip = null;
|
|
10
|
+
this.rId = START_RID;
|
|
11
|
+
this.fonts = []; // { name, data, rid }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async loadZip(zip) {
|
|
15
|
+
this.zip = zip;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Reads the font name from the buffer using opentype.js
|
|
20
|
+
*/
|
|
21
|
+
getFontInfo(fontBuffer) {
|
|
22
|
+
try {
|
|
23
|
+
const font = opentype.parse(fontBuffer);
|
|
24
|
+
const names = font.names;
|
|
25
|
+
// Prefer English name, fallback to others
|
|
26
|
+
const fontFamily = names.fontFamily.en || Object.values(names.fontFamily)[0];
|
|
27
|
+
return { name: fontFamily };
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.warn('Could not parse font info', e);
|
|
30
|
+
return { name: 'Unknown' };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async addFont(fontFace, fontBuffer, type) {
|
|
35
|
+
// Convert to EOT/fntdata for PPTX compatibility
|
|
36
|
+
const eotData = await fontToEot(type, fontBuffer);
|
|
37
|
+
const rid = this.rId++;
|
|
38
|
+
this.fonts.push({ name: fontFace, data: eotData, rid });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async updateFiles() {
|
|
42
|
+
await this.updateContentTypesXML();
|
|
43
|
+
await this.updatePresentationXML();
|
|
44
|
+
await this.updateRelsPresentationXML();
|
|
45
|
+
this.updateFontFiles();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async generateBlob() {
|
|
49
|
+
if (!this.zip) throw new Error('Zip not loaded');
|
|
50
|
+
return this.zip.generateAsync({
|
|
51
|
+
type: 'blob',
|
|
52
|
+
compression: 'DEFLATE',
|
|
53
|
+
compressionOptions: { level: 6 },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- XML Manipulation Methods ---
|
|
58
|
+
|
|
59
|
+
async updateContentTypesXML() {
|
|
60
|
+
const file = this.zip.file('[Content_Types].xml');
|
|
61
|
+
if (!file) throw new Error('[Content_Types].xml not found');
|
|
62
|
+
|
|
63
|
+
const xmlStr = await file.async('string');
|
|
64
|
+
const parser = new DOMParser();
|
|
65
|
+
const doc = parser.parseFromString(xmlStr, 'text/xml');
|
|
66
|
+
|
|
67
|
+
const types = doc.getElementsByTagName('Types')[0];
|
|
68
|
+
const defaults = Array.from(doc.getElementsByTagName('Default'));
|
|
69
|
+
|
|
70
|
+
const hasFntData = defaults.some(el => el.getAttribute('Extension') === 'fntdata');
|
|
71
|
+
|
|
72
|
+
if (!hasFntData) {
|
|
73
|
+
const el = doc.createElement('Default');
|
|
74
|
+
el.setAttribute('Extension', 'fntdata');
|
|
75
|
+
el.setAttribute('ContentType', 'application/x-fontdata');
|
|
76
|
+
types.insertBefore(el, types.firstChild);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.zip.file('[Content_Types].xml', new XMLSerializer().serializeToString(doc));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async updatePresentationXML() {
|
|
83
|
+
const file = this.zip.file('ppt/presentation.xml');
|
|
84
|
+
if (!file) throw new Error('ppt/presentation.xml not found');
|
|
85
|
+
|
|
86
|
+
const xmlStr = await file.async('string');
|
|
87
|
+
const parser = new DOMParser();
|
|
88
|
+
const doc = parser.parseFromString(xmlStr, 'text/xml');
|
|
89
|
+
const presentation = doc.getElementsByTagName('p:presentation')[0];
|
|
90
|
+
|
|
91
|
+
// Enable embedding flags
|
|
92
|
+
presentation.setAttribute('saveSubsetFonts', 'true');
|
|
93
|
+
presentation.setAttribute('embedTrueTypeFonts', 'true');
|
|
94
|
+
|
|
95
|
+
// Find or create embeddedFontLst
|
|
96
|
+
let embeddedFontLst = presentation.getElementsByTagName('p:embeddedFontLst')[0];
|
|
97
|
+
|
|
98
|
+
if (!embeddedFontLst) {
|
|
99
|
+
embeddedFontLst = doc.createElement('p:embeddedFontLst');
|
|
100
|
+
|
|
101
|
+
// Insert before defaultTextStyle or at end
|
|
102
|
+
const defaultTextStyle = presentation.getElementsByTagName('p:defaultTextStyle')[0];
|
|
103
|
+
if (defaultTextStyle) {
|
|
104
|
+
presentation.insertBefore(embeddedFontLst, defaultTextStyle);
|
|
105
|
+
} else {
|
|
106
|
+
presentation.appendChild(embeddedFontLst);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Add font references
|
|
111
|
+
this.fonts.forEach(font => {
|
|
112
|
+
// Check if already exists
|
|
113
|
+
const existing = Array.from(embeddedFontLst.getElementsByTagName('p:font'))
|
|
114
|
+
.find(node => node.getAttribute('typeface') === font.name);
|
|
115
|
+
|
|
116
|
+
if (!existing) {
|
|
117
|
+
const embedFont = doc.createElement('p:embeddedFont');
|
|
118
|
+
|
|
119
|
+
const fontNode = doc.createElement('p:font');
|
|
120
|
+
fontNode.setAttribute('typeface', font.name);
|
|
121
|
+
embedFont.appendChild(fontNode);
|
|
122
|
+
|
|
123
|
+
const regular = doc.createElement('p:regular');
|
|
124
|
+
regular.setAttribute('r:id', `rId${font.rid}`);
|
|
125
|
+
embedFont.appendChild(regular);
|
|
126
|
+
|
|
127
|
+
embeddedFontLst.appendChild(embedFont);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
this.zip.file('ppt/presentation.xml', new XMLSerializer().serializeToString(doc));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async updateRelsPresentationXML() {
|
|
135
|
+
const file = this.zip.file('ppt/_rels/presentation.xml.rels');
|
|
136
|
+
if (!file) throw new Error('presentation.xml.rels not found');
|
|
137
|
+
|
|
138
|
+
const xmlStr = await file.async('string');
|
|
139
|
+
const parser = new DOMParser();
|
|
140
|
+
const doc = parser.parseFromString(xmlStr, 'text/xml');
|
|
141
|
+
const relationships = doc.getElementsByTagName('Relationships')[0];
|
|
142
|
+
|
|
143
|
+
this.fonts.forEach(font => {
|
|
144
|
+
const rel = doc.createElement('Relationship');
|
|
145
|
+
rel.setAttribute('Id', `rId${font.rid}`);
|
|
146
|
+
rel.setAttribute('Target', `fonts/${font.rid}.fntdata`);
|
|
147
|
+
rel.setAttribute('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/font');
|
|
148
|
+
relationships.appendChild(rel);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
this.zip.file('ppt/_rels/presentation.xml.rels', new XMLSerializer().serializeToString(doc));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
updateFontFiles() {
|
|
155
|
+
this.fonts.forEach(font => {
|
|
156
|
+
this.zip.file(`ppt/fonts/${font.rid}.fntdata`, font.data);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/font-utils.js
|
|
2
|
+
import { Font } from 'fonteditor-core';
|
|
3
|
+
import pako from 'pako';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Converts various font formats to EOT (Embedded OpenType),
|
|
7
|
+
* which is highly compatible with PowerPoint embedding.
|
|
8
|
+
* @param {string} type - 'ttf', 'woff', or 'otf'
|
|
9
|
+
* @param {ArrayBuffer} fontBuffer - The raw font data
|
|
10
|
+
*/
|
|
11
|
+
export async function fontToEot(type, fontBuffer) {
|
|
12
|
+
const options = {
|
|
13
|
+
type,
|
|
14
|
+
hinting: true,
|
|
15
|
+
// inflate is required for WOFF decoding
|
|
16
|
+
inflate: type === 'woff' ? pako.inflate : undefined,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const font = Font.create(fontBuffer, options);
|
|
20
|
+
|
|
21
|
+
const eotBuffer = font.write({
|
|
22
|
+
type: 'eot',
|
|
23
|
+
toBuffer: true,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (eotBuffer instanceof ArrayBuffer) {
|
|
27
|
+
return eotBuffer;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Ensure we return an ArrayBuffer
|
|
31
|
+
return eotBuffer.buffer.slice(
|
|
32
|
+
eotBuffer.byteOffset,
|
|
33
|
+
eotBuffer.byteOffset + eotBuffer.byteLength
|
|
34
|
+
);
|
|
35
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// src/index.js
|
|
2
2
|
import * as PptxGenJSImport from 'pptxgenjs';
|
|
3
3
|
import html2canvas from 'html2canvas';
|
|
4
|
+
import { PPTXEmbedFonts } from './font-embedder.js';
|
|
5
|
+
import JSZip from 'jszip';
|
|
4
6
|
|
|
5
7
|
// Normalize import
|
|
6
8
|
const PptxGenJS = PptxGenJSImport?.default ?? PptxGenJSImport;
|
|
@@ -20,6 +22,8 @@ import {
|
|
|
20
22
|
generateCompositeBorderSVG,
|
|
21
23
|
isClippedByParent,
|
|
22
24
|
generateCustomShapeSVG,
|
|
25
|
+
getUsedFontFamilies,
|
|
26
|
+
getAutoDetectedFonts,
|
|
23
27
|
} from './utils.js';
|
|
24
28
|
import { getProcessedImage } from './image-processor.js';
|
|
25
29
|
|
|
@@ -27,9 +31,12 @@ const PPI = 96;
|
|
|
27
31
|
const PX_TO_INCH = 1 / PPI;
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
|
-
* Main export function.
|
|
31
|
-
* @param {HTMLElement | string | Array<HTMLElement | string>} target
|
|
32
|
-
* @param {Object} options
|
|
34
|
+
* Main export function.
|
|
35
|
+
* @param {HTMLElement | string | Array<HTMLElement | string>} target
|
|
36
|
+
* @param {Object} options
|
|
37
|
+
* @param {string} [options.fileName]
|
|
38
|
+
* @param {Array<{name: string, url: string}>} [options.fonts] - Explicit fonts
|
|
39
|
+
* @param {boolean} [options.autoEmbedFonts=true] - Attempt to auto-detect and embed used fonts
|
|
33
40
|
*/
|
|
34
41
|
export async function exportToPptx(target, options = {}) {
|
|
35
42
|
const resolvePptxConstructor = (pkg) => {
|
|
@@ -59,8 +66,74 @@ export async function exportToPptx(target, options = {}) {
|
|
|
59
66
|
await processSlide(root, slide, pptx);
|
|
60
67
|
}
|
|
61
68
|
|
|
69
|
+
// 3. Font Embedding Logic
|
|
70
|
+
let finalBlob;
|
|
71
|
+
let fontsToEmbed = options.fonts || [];
|
|
72
|
+
|
|
73
|
+
if (options.autoEmbedFonts) {
|
|
74
|
+
// A. Scan DOM for used font families
|
|
75
|
+
const usedFamilies = getUsedFontFamilies(elements);
|
|
76
|
+
|
|
77
|
+
// B. Scan CSS for URLs matches
|
|
78
|
+
const detectedFonts = await getAutoDetectedFonts(usedFamilies);
|
|
79
|
+
|
|
80
|
+
// C. Merge (Avoid duplicates)
|
|
81
|
+
const explicitNames = new Set(fontsToEmbed.map(f => f.name));
|
|
82
|
+
for (const autoFont of detectedFonts) {
|
|
83
|
+
if (!explicitNames.has(autoFont.name)) {
|
|
84
|
+
fontsToEmbed.push(autoFont);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (detectedFonts.length > 0) {
|
|
89
|
+
console.log('Auto-detected fonts:', detectedFonts.map(f => f.name));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (fontsToEmbed.length > 0) {
|
|
94
|
+
// Generate initial PPTX
|
|
95
|
+
const initialBlob = await pptx.write({ outputType: 'blob' });
|
|
96
|
+
|
|
97
|
+
// Load into Embedder
|
|
98
|
+
const zip = await JSZip.loadAsync(initialBlob);
|
|
99
|
+
const embedder = new PPTXEmbedFonts();
|
|
100
|
+
await embedder.loadZip(zip);
|
|
101
|
+
|
|
102
|
+
// Fetch and Embed
|
|
103
|
+
for (const fontCfg of fontsToEmbed) {
|
|
104
|
+
try {
|
|
105
|
+
const response = await fetch(fontCfg.url);
|
|
106
|
+
if (!response.ok) throw new Error(`Failed to fetch ${fontCfg.url}`);
|
|
107
|
+
const buffer = await response.arrayBuffer();
|
|
108
|
+
|
|
109
|
+
// Infer type
|
|
110
|
+
const ext = fontCfg.url.split('.').pop().split(/[?#]/)[0].toLowerCase();
|
|
111
|
+
let type = 'ttf';
|
|
112
|
+
if (['woff', 'otf'].includes(ext)) type = ext;
|
|
113
|
+
|
|
114
|
+
await embedder.addFont(fontCfg.name, buffer, type);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.warn(`Failed to embed font: ${fontCfg.name} (${fontCfg.url})`, e);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await embedder.updateFiles();
|
|
121
|
+
finalBlob = await embedder.generateBlob();
|
|
122
|
+
} else {
|
|
123
|
+
// No fonts to embed
|
|
124
|
+
finalBlob = await pptx.write({ outputType: 'blob' });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 4. Download
|
|
62
128
|
const fileName = options.fileName || 'export.pptx';
|
|
63
|
-
|
|
129
|
+
const url = URL.createObjectURL(finalBlob);
|
|
130
|
+
const a = document.createElement('a');
|
|
131
|
+
a.href = url;
|
|
132
|
+
a.download = fileName;
|
|
133
|
+
document.body.appendChild(a);
|
|
134
|
+
a.click();
|
|
135
|
+
document.body.removeChild(a);
|
|
136
|
+
URL.revokeObjectURL(url);
|
|
64
137
|
}
|
|
65
138
|
|
|
66
139
|
/**
|
|
@@ -173,29 +246,69 @@ async function processSlide(root, slide, pptx) {
|
|
|
173
246
|
* Optimized html2canvas wrapper
|
|
174
247
|
* Now strictly captures the node itself, not the root.
|
|
175
248
|
*/
|
|
249
|
+
/**
|
|
250
|
+
* Optimized html2canvas wrapper
|
|
251
|
+
* Includes fix for cropped icons by adjusting styles in the cloned document.
|
|
252
|
+
*/
|
|
176
253
|
async function elementToCanvasImage(node, widthPx, heightPx) {
|
|
177
254
|
return new Promise((resolve) => {
|
|
255
|
+
// 1. Assign a temp ID to locate the node inside the cloned document
|
|
256
|
+
const originalId = node.id;
|
|
257
|
+
const tempId = 'pptx-capture-' + Math.random().toString(36).substr(2, 9);
|
|
258
|
+
node.id = tempId;
|
|
259
|
+
|
|
178
260
|
const width = Math.max(Math.ceil(widthPx), 1);
|
|
179
261
|
const height = Math.max(Math.ceil(heightPx), 1);
|
|
180
262
|
const style = window.getComputedStyle(node);
|
|
181
263
|
|
|
182
|
-
// Optimized: Capture ONLY the specific node
|
|
183
264
|
html2canvas(node, {
|
|
184
265
|
backgroundColor: null,
|
|
185
266
|
logging: false,
|
|
186
|
-
scale:
|
|
267
|
+
scale: 3, // Higher scale for sharper icons
|
|
268
|
+
useCORS: true, // critical for external fonts/images
|
|
269
|
+
onclone: (clonedDoc) => {
|
|
270
|
+
const clonedNode = clonedDoc.getElementById(tempId);
|
|
271
|
+
if (clonedNode) {
|
|
272
|
+
// --- FIX: PREVENT ICON CLIPPING ---
|
|
273
|
+
// 1. Force overflow visible so glyphs bleeding out aren't cut
|
|
274
|
+
clonedNode.style.overflow = 'visible';
|
|
275
|
+
|
|
276
|
+
// 2. Adjust alignment for Icons to prevent baseline clipping
|
|
277
|
+
// (Applies to <i>, <span>, or standard icon classes)
|
|
278
|
+
const tag = clonedNode.tagName;
|
|
279
|
+
if (tag === 'I' || tag === 'SPAN' || clonedNode.className.includes('fa-')) {
|
|
280
|
+
// Flex center helps align the glyph exactly in the middle of the box
|
|
281
|
+
// preventing top/bottom cropping due to line-height mismatches.
|
|
282
|
+
clonedNode.style.display = 'inline-flex';
|
|
283
|
+
clonedNode.style.justifyContent = 'center';
|
|
284
|
+
clonedNode.style.alignItems = 'center';
|
|
285
|
+
|
|
286
|
+
// Remove margins that might offset the capture
|
|
287
|
+
clonedNode.style.margin = '0';
|
|
288
|
+
|
|
289
|
+
// Ensure the font fits
|
|
290
|
+
clonedNode.style.lineHeight = '1';
|
|
291
|
+
clonedNode.style.verticalAlign = 'middle';
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
},
|
|
187
295
|
})
|
|
188
296
|
.then((canvas) => {
|
|
297
|
+
// Restore the original ID
|
|
298
|
+
if (originalId) node.id = originalId;
|
|
299
|
+
else node.removeAttribute('id');
|
|
300
|
+
|
|
189
301
|
const destCanvas = document.createElement('canvas');
|
|
190
302
|
destCanvas.width = width;
|
|
191
303
|
destCanvas.height = height;
|
|
192
304
|
const ctx = destCanvas.getContext('2d');
|
|
193
305
|
|
|
194
|
-
// Draw
|
|
195
|
-
//
|
|
306
|
+
// Draw captured canvas.
|
|
307
|
+
// We simply draw it to fill the box. Since we centered it in 'onclone',
|
|
308
|
+
// the glyph should now be visible within the bounds.
|
|
196
309
|
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, width, height);
|
|
197
310
|
|
|
198
|
-
//
|
|
311
|
+
// --- Border Radius Clipping (Existing Logic) ---
|
|
199
312
|
let tl = parseFloat(style.borderTopLeftRadius) || 0;
|
|
200
313
|
let tr = parseFloat(style.borderTopRightRadius) || 0;
|
|
201
314
|
let br = parseFloat(style.borderBottomRightRadius) || 0;
|
|
@@ -234,12 +347,61 @@ async function elementToCanvasImage(node, widthPx, heightPx) {
|
|
|
234
347
|
resolve(destCanvas.toDataURL('image/png'));
|
|
235
348
|
})
|
|
236
349
|
.catch((e) => {
|
|
350
|
+
if (originalId) node.id = originalId;
|
|
351
|
+
else node.removeAttribute('id');
|
|
237
352
|
console.warn('Canvas capture failed for node', node, e);
|
|
238
353
|
resolve(null);
|
|
239
354
|
});
|
|
240
355
|
});
|
|
241
356
|
}
|
|
242
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Helper to identify elements that should be rendered as icons (Images).
|
|
360
|
+
* Detects Custom Elements AND generic tags (<i>, <span>) with icon classes/pseudo-elements.
|
|
361
|
+
*/
|
|
362
|
+
function isIconElement(node) {
|
|
363
|
+
// 1. Custom Elements (hyphenated tags) or Explicit Library Tags
|
|
364
|
+
const tag = node.tagName.toUpperCase();
|
|
365
|
+
if (
|
|
366
|
+
tag.includes('-') ||
|
|
367
|
+
[
|
|
368
|
+
'MATERIAL-ICON',
|
|
369
|
+
'ICONIFY-ICON',
|
|
370
|
+
'REMIX-ICON',
|
|
371
|
+
'ION-ICON',
|
|
372
|
+
'EVA-ICON',
|
|
373
|
+
'BOX-ICON',
|
|
374
|
+
'FA-ICON',
|
|
375
|
+
].includes(tag)
|
|
376
|
+
) {
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// 2. Class-based Icons (FontAwesome, Bootstrap, Material symbols) on <i> or <span>
|
|
381
|
+
if (tag === 'I' || tag === 'SPAN') {
|
|
382
|
+
const cls = node.getAttribute('class') || '';
|
|
383
|
+
if (
|
|
384
|
+
typeof cls === 'string' &&
|
|
385
|
+
(cls.includes('fa-') ||
|
|
386
|
+
cls.includes('fas') ||
|
|
387
|
+
cls.includes('far') ||
|
|
388
|
+
cls.includes('fab') ||
|
|
389
|
+
cls.includes('bi-') ||
|
|
390
|
+
cls.includes('material-icons') ||
|
|
391
|
+
cls.includes('icon'))
|
|
392
|
+
) {
|
|
393
|
+
// Double-check: Must have pseudo-element content to be a CSS icon
|
|
394
|
+
const before = window.getComputedStyle(node, '::before').content;
|
|
395
|
+
const after = window.getComputedStyle(node, '::after').content;
|
|
396
|
+
const hasContent = (c) => c && c !== 'none' && c !== 'normal' && c !== '""';
|
|
397
|
+
|
|
398
|
+
if (hasContent(before) || hasContent(after)) return true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
|
|
243
405
|
/**
|
|
244
406
|
* Replaces createRenderItem.
|
|
245
407
|
* Returns { items: [], job: () => Promise, stopRecursion: boolean }
|
|
@@ -375,30 +537,18 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
|
|
|
375
537
|
}
|
|
376
538
|
|
|
377
539
|
// --- ASYNC JOB: Icons and Other Elements ---
|
|
378
|
-
if (
|
|
379
|
-
node.tagName.toUpperCase() === 'MATERIAL-ICON' ||
|
|
380
|
-
node.tagName.toUpperCase() === 'ICONIFY-ICON' ||
|
|
381
|
-
node.tagName.toUpperCase() === 'REMIX-ICON' ||
|
|
382
|
-
node.tagName.toUpperCase() === 'ION-ICON' ||
|
|
383
|
-
node.tagName.toUpperCase() === 'EVA-ICON' ||
|
|
384
|
-
node.tagName.toUpperCase() === 'BOX-ICON' ||
|
|
385
|
-
node.tagName.toUpperCase() === 'FA-ICON' ||
|
|
386
|
-
node.tagName.includes('-')
|
|
387
|
-
) {
|
|
540
|
+
if (isIconElement(node)) {
|
|
388
541
|
const item = {
|
|
389
542
|
type: 'image',
|
|
390
543
|
zIndex,
|
|
391
544
|
domOrder,
|
|
392
|
-
options: { x, y, w, h, rotate: rotation, data: null },
|
|
545
|
+
options: { x, y, w, h, rotate: rotation, data: null },
|
|
393
546
|
};
|
|
394
|
-
|
|
395
|
-
// Create Job
|
|
396
547
|
const job = async () => {
|
|
397
548
|
const pngData = await elementToCanvasImage(node, widthPx, heightPx);
|
|
398
549
|
if (pngData) item.options.data = pngData;
|
|
399
550
|
else item.skip = true;
|
|
400
551
|
};
|
|
401
|
-
|
|
402
552
|
return { items: [item], job, stopRecursion: true };
|
|
403
553
|
}
|
|
404
554
|
|