dom-to-pptx 1.0.1 → 1.0.2

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/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
- }