dom-to-pptx 1.0.1 → 1.0.3
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/.prettierrc +6 -0
- package/CONTRIBUTING.md +72 -0
- package/Readme.md +204 -110
- package/dist/dom-to-pptx.cjs +1038 -0
- package/dist/dom-to-pptx.min.js +1042 -0
- package/dist/dom-to-pptx.mjs +1017 -0
- package/eslint.config.js +17 -0
- package/package.json +55 -37
- package/rollup.config.js +33 -0
- package/src/image-processor.js +48 -48
- package/src/index.js +409 -160
- package/src/utils.js +477 -256
- package/code_context.txt +0 -346
package/code_context.txt
DELETED
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
// File: src/image-processor.js
|
|
2
|
-
// src/image-processor.js
|
|
3
|
-
|
|
4
|
-
export async function getProcessedImage(src, targetW, targetH, radius) {
|
|
5
|
-
return new Promise((resolve) => {
|
|
6
|
-
const img = new Image();
|
|
7
|
-
img.crossOrigin = "Anonymous"; // Critical for canvas manipulation
|
|
8
|
-
|
|
9
|
-
img.onload = () => {
|
|
10
|
-
const canvas = document.createElement('canvas');
|
|
11
|
-
// Double resolution for better quality
|
|
12
|
-
const scale = 2;
|
|
13
|
-
canvas.width = targetW * scale;
|
|
14
|
-
canvas.height = targetH * scale;
|
|
15
|
-
const ctx = canvas.getContext('2d');
|
|
16
|
-
ctx.scale(scale, scale);
|
|
17
|
-
|
|
18
|
-
// 1. Draw the Mask (Rounded Rect)
|
|
19
|
-
ctx.beginPath();
|
|
20
|
-
if (ctx.roundRect) {
|
|
21
|
-
ctx.roundRect(0, 0, targetW, targetH, radius);
|
|
22
|
-
} else {
|
|
23
|
-
// Fallback for older browsers if needed
|
|
24
|
-
ctx.rect(0, 0, targetW, targetH);
|
|
25
|
-
}
|
|
26
|
-
ctx.fillStyle = '#000';
|
|
27
|
-
ctx.fill();
|
|
28
|
-
|
|
29
|
-
// 2. Composite Source-In
|
|
30
|
-
ctx.globalCompositeOperation = 'source-in';
|
|
31
|
-
|
|
32
|
-
// 3. Draw Image (Object Cover Logic)
|
|
33
|
-
const wRatio = targetW / img.width;
|
|
34
|
-
const hRatio = targetH / img.height;
|
|
35
|
-
const maxRatio = Math.max(wRatio, hRatio);
|
|
36
|
-
const renderW = img.width * maxRatio;
|
|
37
|
-
const renderH = img.height * maxRatio;
|
|
38
|
-
const renderX = (targetW - renderW) / 2;
|
|
39
|
-
const renderY = (targetH - renderH) / 2;
|
|
40
|
-
|
|
41
|
-
ctx.drawImage(img, renderX, renderY, renderW, renderH);
|
|
42
|
-
|
|
43
|
-
resolve(canvas.toDataURL('image/png'));
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
img.onerror = () => resolve(null);
|
|
47
|
-
img.src = src;
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
// File: src/index.js
|
|
51
|
-
// src/index.js
|
|
52
|
-
import PptxGenJS from "pptxgenjs";
|
|
53
|
-
import { parseColor, getTextStyle, isTextContainer, getVisibleShadow, generateGradientSVG } from "./utils.js";
|
|
54
|
-
import { getProcessedImage } from "./image-processor.js";
|
|
55
|
-
|
|
56
|
-
const PPI = 96;
|
|
57
|
-
const PX_TO_INCH = 1 / PPI;
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Converts a DOM element to a PPTX file.
|
|
61
|
-
* @param {HTMLElement | string} elementOrSelector - The root element to convert.
|
|
62
|
-
* @param {Object} options - { fileName: string }
|
|
63
|
-
*/
|
|
64
|
-
export async function exportToPptx(elementOrSelector, options = {}) {
|
|
65
|
-
const root = typeof elementOrSelector === "string"
|
|
66
|
-
? document.querySelector(elementOrSelector)
|
|
67
|
-
: elementOrSelector;
|
|
68
|
-
|
|
69
|
-
if (!root) throw new Error("Root element not found");
|
|
70
|
-
|
|
71
|
-
const pptx = new PptxGenJS();
|
|
72
|
-
pptx.layout = "LAYOUT_16x9"; // Default
|
|
73
|
-
const slide = pptx.addSlide();
|
|
74
|
-
|
|
75
|
-
// Use the dimensions of the root element to calculate scaling
|
|
76
|
-
const rootRect = root.getBoundingClientRect();
|
|
77
|
-
|
|
78
|
-
// Standard PPTX 16:9 dimensions in inches
|
|
79
|
-
const PPTX_WIDTH_IN = 10;
|
|
80
|
-
const PPTX_HEIGHT_IN = 5.625;
|
|
81
|
-
|
|
82
|
-
const contentWidthIn = rootRect.width * PX_TO_INCH;
|
|
83
|
-
const contentHeightIn = rootRect.height * PX_TO_INCH;
|
|
84
|
-
|
|
85
|
-
// Scale content to fit within the slide
|
|
86
|
-
const scale = Math.min(
|
|
87
|
-
PPTX_WIDTH_IN / contentWidthIn,
|
|
88
|
-
PPTX_HEIGHT_IN / contentHeightIn
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
const layoutConfig = {
|
|
92
|
-
rootX: rootRect.x,
|
|
93
|
-
rootY: rootRect.y,
|
|
94
|
-
scale: scale,
|
|
95
|
-
// Center the content
|
|
96
|
-
offX: (PPTX_WIDTH_IN - contentWidthIn * scale) / 2,
|
|
97
|
-
offY: (PPTX_HEIGHT_IN - contentHeightIn * scale) / 2,
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
await processNode(root, pptx, slide, layoutConfig);
|
|
101
|
-
|
|
102
|
-
const fileName = options.fileName || "export.pptx";
|
|
103
|
-
pptx.writeFile({ fileName });
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function processNode(node, pptx, slide, config) {
|
|
107
|
-
if (node.nodeType !== 1) return; // Element nodes only
|
|
108
|
-
|
|
109
|
-
const style = window.getComputedStyle(node);
|
|
110
|
-
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return;
|
|
111
|
-
|
|
112
|
-
const rect = node.getBoundingClientRect();
|
|
113
|
-
if (rect.width === 0 || rect.height === 0) return;
|
|
114
|
-
|
|
115
|
-
const x = config.offX + (rect.x - config.rootX) * PX_TO_INCH * config.scale;
|
|
116
|
-
const y = config.offY + (rect.y - config.rootY) * PX_TO_INCH * config.scale;
|
|
117
|
-
const w = rect.width * PX_TO_INCH * config.scale;
|
|
118
|
-
const h = rect.height * PX_TO_INCH * config.scale;
|
|
119
|
-
|
|
120
|
-
// --- 1. Detect Image Wrapper ---
|
|
121
|
-
let isImageWrapper = false;
|
|
122
|
-
const imgChild = Array.from(node.children).find(c => c.tagName === 'IMG');
|
|
123
|
-
if (imgChild) {
|
|
124
|
-
const imgRect = imgChild.getBoundingClientRect();
|
|
125
|
-
if (Math.abs(imgRect.width - rect.width) < 2 && Math.abs(imgRect.height - rect.height) < 2) {
|
|
126
|
-
isImageWrapper = true;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// --- 2. Backgrounds & Borders ---
|
|
131
|
-
let bgColor = parseColor(style.backgroundColor);
|
|
132
|
-
if (isImageWrapper && bgColor) bgColor = null; // Prevent halo
|
|
133
|
-
|
|
134
|
-
const hasGradient = style.backgroundImage && style.backgroundImage.includes("linear-gradient");
|
|
135
|
-
const borderColor = parseColor(style.borderColor);
|
|
136
|
-
const borderWidth = parseFloat(style.borderWidth);
|
|
137
|
-
const hasBorder = borderWidth > 0 && borderColor;
|
|
138
|
-
const shadowStr = style.boxShadow;
|
|
139
|
-
const hasShadow = shadowStr && shadowStr !== "none";
|
|
140
|
-
const borderRadius = parseFloat(style.borderRadius) || 0;
|
|
141
|
-
|
|
142
|
-
if (hasGradient) {
|
|
143
|
-
const svgData = generateGradientSVG(rect.width, rect.height, style.backgroundImage, borderRadius, hasBorder ? { color: borderColor, width: borderWidth } : null);
|
|
144
|
-
if (svgData) slide.addImage({ data: svgData, x, y, w, h });
|
|
145
|
-
} else if (bgColor || hasBorder || hasShadow) {
|
|
146
|
-
const isCircle = borderRadius >= Math.min(rect.width, rect.height) / 2 - 1;
|
|
147
|
-
|
|
148
|
-
const shapeOpts = {
|
|
149
|
-
x, y, w, h,
|
|
150
|
-
fill: bgColor ? { color: bgColor } : null,
|
|
151
|
-
line: hasBorder ? { color: borderColor, width: borderWidth * 0.75 * config.scale } : null,
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
if (hasShadow) {
|
|
155
|
-
const shadow = getVisibleShadow(shadowStr, config.scale);
|
|
156
|
-
if (shadow) shapeOpts.shadow = shadow;
|
|
157
|
-
|
|
158
|
-
// Fix for shadow on transparent background (needed for PPTX to render shadow)
|
|
159
|
-
if (!bgColor && !hasBorder) {
|
|
160
|
-
if (style.backgroundColor && style.backgroundColor !== 'rgba(0, 0, 0, 0)') {
|
|
161
|
-
shapeOpts.fill = { color: parseColor(style.backgroundColor) };
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (isCircle) {
|
|
167
|
-
slide.addShape(pptx.ShapeType.ellipse, shapeOpts);
|
|
168
|
-
} else if (borderRadius > 0) {
|
|
169
|
-
const radiusFactor = Math.min(1, borderRadius / (Math.min(rect.width, rect.height) / 1.75));
|
|
170
|
-
shapeOpts.rectRadius = radiusFactor;
|
|
171
|
-
slide.addShape(pptx.ShapeType.roundRect, shapeOpts);
|
|
172
|
-
} else {
|
|
173
|
-
slide.addShape(pptx.ShapeType.rect, shapeOpts);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// --- 3. Process Image ---
|
|
178
|
-
if (node.tagName === "IMG") {
|
|
179
|
-
let effectiveRadius = borderRadius;
|
|
180
|
-
// Check parent clipping if current img has no radius
|
|
181
|
-
if (effectiveRadius === 0) {
|
|
182
|
-
const parentStyle = window.getComputedStyle(node.parentElement);
|
|
183
|
-
if (parentStyle.overflow !== 'visible') {
|
|
184
|
-
effectiveRadius = parseFloat(parentStyle.borderRadius) || 0;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const processedImage = await getProcessedImage(node.src, rect.width, rect.height, effectiveRadius);
|
|
189
|
-
if (processedImage) {
|
|
190
|
-
slide.addImage({ data: processedImage, x, y, w, h });
|
|
191
|
-
}
|
|
192
|
-
return; // Don't process children of IMG
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// --- 4. Process Text ---
|
|
196
|
-
if (isTextContainer(node)) {
|
|
197
|
-
const textParts = [];
|
|
198
|
-
node.childNodes.forEach((child, index) => {
|
|
199
|
-
let textVal = child.nodeType === 3 ? child.nodeValue : child.textContent;
|
|
200
|
-
let nodeStyle = child.nodeType === 1 ? window.getComputedStyle(child) : style;
|
|
201
|
-
|
|
202
|
-
textVal = textVal.replace(/[\n\r\t]+/g, " ").replace(/\s{2,}/g, " ");
|
|
203
|
-
if (index === 0) textVal = textVal.trimStart();
|
|
204
|
-
if (index === node.childNodes.length - 1) textVal = textVal.trimEnd();
|
|
205
|
-
|
|
206
|
-
if (nodeStyle.textTransform === "uppercase") textVal = textVal.toUpperCase();
|
|
207
|
-
if (nodeStyle.textTransform === "lowercase") textVal = textVal.toLowerCase();
|
|
208
|
-
|
|
209
|
-
if (textVal.length > 0) {
|
|
210
|
-
textParts.push({ text: textVal, options: getTextStyle(nodeStyle, config.scale) });
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
if (textParts.length > 0) {
|
|
215
|
-
let align = style.textAlign || "left";
|
|
216
|
-
if (align === "start") align = "left";
|
|
217
|
-
if (align === "end") align = "right";
|
|
218
|
-
|
|
219
|
-
let valign = "top";
|
|
220
|
-
if (style.alignItems === "center") valign = "middle"; // Flex approximation
|
|
221
|
-
|
|
222
|
-
slide.addText(textParts, { x, y, w, h, align, valign, margin: 0, wrap: true, autoFit: false });
|
|
223
|
-
}
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Recursive call
|
|
228
|
-
for (const child of node.children) {
|
|
229
|
-
await processNode(child, pptx, slide, config);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
// File: src/utils.js
|
|
233
|
-
// src/utils.js
|
|
234
|
-
|
|
235
|
-
export function parseColor(str) {
|
|
236
|
-
if (!str || str === "transparent" || str.startsWith("rgba(0, 0, 0, 0)")) return null;
|
|
237
|
-
const rgb = str.match(/\d+/g);
|
|
238
|
-
if (!rgb || rgb.length < 3) return null;
|
|
239
|
-
// Convert RGB to Hex
|
|
240
|
-
return ((1 << 24) + (parseInt(rgb[0]) << 16) + (parseInt(rgb[1]) << 8) + parseInt(rgb[2]))
|
|
241
|
-
.toString(16).slice(1).toUpperCase();
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
export function getTextStyle(style, scale) {
|
|
245
|
-
return {
|
|
246
|
-
color: parseColor(style.color) || "000000",
|
|
247
|
-
fontFace: style.fontFamily.split(",")[0].replace(/['"]/g, ""),
|
|
248
|
-
fontSize: parseFloat(style.fontSize) * 0.75 * scale,
|
|
249
|
-
bold: parseInt(style.fontWeight) >= 600,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export function isTextContainer(node) {
|
|
254
|
-
const hasText = node.textContent.trim().length > 0;
|
|
255
|
-
if (!hasText) return false;
|
|
256
|
-
const children = Array.from(node.children);
|
|
257
|
-
if (children.length === 0) return true;
|
|
258
|
-
// Check if children are inline elements
|
|
259
|
-
const isInline = (el) =>
|
|
260
|
-
window.getComputedStyle(el).display.includes("inline") ||
|
|
261
|
-
["SPAN", "B", "STRONG", "EM"].includes(el.tagName);
|
|
262
|
-
return children.every(isInline);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
export function getVisibleShadow(shadowStr, scale) {
|
|
266
|
-
if (!shadowStr || shadowStr === "none") return null;
|
|
267
|
-
const shadows = shadowStr.split(/,(?![^(]*\))/);
|
|
268
|
-
|
|
269
|
-
for (let s of shadows) {
|
|
270
|
-
s = s.trim();
|
|
271
|
-
if (s.startsWith("rgba(0, 0, 0, 0)")) continue;
|
|
272
|
-
|
|
273
|
-
const match = s.match(/(rgba?\([^\)]+\)|#[0-9a-fA-F]+)\s+(-?[\d\.]+)px\s+(-?[\d\.]+)px\s+([\d\.]+)px/);
|
|
274
|
-
if (match) {
|
|
275
|
-
const colorStr = match[1];
|
|
276
|
-
const x = parseFloat(match[2]);
|
|
277
|
-
const y = parseFloat(match[3]);
|
|
278
|
-
const blur = parseFloat(match[4]);
|
|
279
|
-
|
|
280
|
-
const distance = Math.sqrt(x*x + y*y);
|
|
281
|
-
let angle = Math.atan2(y, x) * (180 / Math.PI);
|
|
282
|
-
if (angle < 0) angle += 360;
|
|
283
|
-
|
|
284
|
-
let opacity = 0.4;
|
|
285
|
-
if (colorStr.includes('rgba')) {
|
|
286
|
-
const alphaMatch = colorStr.match(/, ([0-9.]+)\)/);
|
|
287
|
-
if (alphaMatch) opacity = parseFloat(alphaMatch[1]);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return {
|
|
291
|
-
type: "outer",
|
|
292
|
-
angle: angle,
|
|
293
|
-
blur: blur * 0.75 * scale,
|
|
294
|
-
offset: distance * 0.75 * scale,
|
|
295
|
-
color: parseColor(colorStr) || "000000",
|
|
296
|
-
opacity: opacity,
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
return null;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
export function generateGradientSVG(w, h, bgString, radius, border) {
|
|
304
|
-
try {
|
|
305
|
-
const match = bgString.match(/linear-gradient\((.*)\)/);
|
|
306
|
-
if (!match) return null;
|
|
307
|
-
const content = match[1];
|
|
308
|
-
// Basic gradient parsing logic (simplified from your script)
|
|
309
|
-
const parts = content.split(/,(?![^(]*\))/).map((p) => p.trim());
|
|
310
|
-
|
|
311
|
-
let x1 = "0%", y1 = "0%", x2 = "0%", y2 = "100%";
|
|
312
|
-
let stopsStartIdx = 0;
|
|
313
|
-
|
|
314
|
-
if (parts[0].includes("to right")) { x1 = "0%"; x2 = "100%"; y2 = "0%"; stopsStartIdx = 1; }
|
|
315
|
-
else if (parts[0].includes("to left")) { x1 = "100%"; x2 = "0%"; y2 = "0%"; stopsStartIdx = 1; }
|
|
316
|
-
// Add other directions as needed...
|
|
317
|
-
|
|
318
|
-
let stopsXML = "";
|
|
319
|
-
const stopParts = parts.slice(stopsStartIdx);
|
|
320
|
-
stopParts.forEach((part, idx) => {
|
|
321
|
-
let color = part;
|
|
322
|
-
let offset = Math.round((idx / (stopParts.length - 1)) * 100) + "%";
|
|
323
|
-
// Simple regex to separate color from percentage
|
|
324
|
-
const posMatch = part.match(/(.*?)\s+(\d+(\.\d+)?%?)$/);
|
|
325
|
-
if (posMatch) { color = posMatch[1]; offset = posMatch[2]; }
|
|
326
|
-
|
|
327
|
-
let opacity = 1;
|
|
328
|
-
if (color.includes("rgba")) {
|
|
329
|
-
// extract alpha
|
|
330
|
-
const rgba = color.match(/[\d\.]+/g);
|
|
331
|
-
if(rgba && rgba.length > 3) { opacity = rgba[3]; color = `rgb(${rgba[0]},${rgba[1]},${rgba[2]})`; }
|
|
332
|
-
}
|
|
333
|
-
stopsXML += `<stop offset="${offset}" stop-color="${color}" stop-opacity="${opacity}"/>`;
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
let strokeAttr = "";
|
|
337
|
-
if (border) { strokeAttr = `stroke="#${border.color}" stroke-width="${border.width}"`; }
|
|
338
|
-
|
|
339
|
-
const svg = `
|
|
340
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
341
|
-
<defs><linearGradient id="grad" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}">${stopsXML}</linearGradient></defs>
|
|
342
|
-
<rect x="0" y="0" width="${w}" height="${h}" rx="${radius}" ry="${radius}" fill="url(#grad)" ${strokeAttr} />
|
|
343
|
-
</svg>`;
|
|
344
|
-
return "data:image/svg+xml;base64," + btoa(svg);
|
|
345
|
-
} catch (e) { return null; }
|
|
346
|
-
}
|