docgen-utils 1.0.9 → 1.0.11
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/dist/bundle.js +1282 -124
- package/dist/bundle.min.js +82 -80
- package/dist/cli.js +322 -29
- package/dist/packages/docs/convert.d.ts.map +1 -1
- package/dist/packages/docs/convert.js +18 -2
- package/dist/packages/docs/convert.js.map +1 -1
- package/dist/packages/slides/common.d.ts +17 -0
- package/dist/packages/slides/common.d.ts.map +1 -1
- package/dist/packages/slides/convert.d.ts +5 -2
- package/dist/packages/slides/convert.d.ts.map +1 -1
- package/dist/packages/slides/convert.js +122 -28
- package/dist/packages/slides/convert.js.map +1 -1
- package/dist/packages/slides/createPresentation.d.ts.map +1 -1
- package/dist/packages/slides/createPresentation.js +18 -1
- package/dist/packages/slides/createPresentation.js.map +1 -1
- package/dist/packages/slides/import-pptx.d.ts.map +1 -1
- package/dist/packages/slides/import-pptx.js +388 -25
- package/dist/packages/slides/import-pptx.js.map +1 -1
- package/dist/packages/slides/parse.d.ts.map +1 -1
- package/dist/packages/slides/parse.js +1103 -89
- package/dist/packages/slides/parse.js.map +1 -1
- package/dist/packages/slides/transform.d.ts.map +1 -1
- package/dist/packages/slides/transform.js +60 -10
- package/dist/packages/slides/transform.js.map +1 -1
- package/package.json +9 -4
|
@@ -13,9 +13,121 @@ const TARGET_WIDTH = 1280;
|
|
|
13
13
|
const TARGET_HEIGHT = 720;
|
|
14
14
|
// EMU (English Metric Units) to pixels: 1 inch = 914400 EMU, 96 DPI
|
|
15
15
|
const EMU_PER_PX = 914400 / 96;
|
|
16
|
+
// Font fallback mapping: maps fonts that may not be installed to the closest
|
|
17
|
+
// available alternatives. This improves rendering fidelity when exact fonts
|
|
18
|
+
// are not present on the system. Each value is a comma-separated CSS font stack.
|
|
19
|
+
const FONT_FALLBACK_MAP = {
|
|
20
|
+
// Microsoft Office fonts → metrically compatible open-source alternatives
|
|
21
|
+
"Calibri": "'Calibri','Carlito','Helvetica Neue',Helvetica,Arial,sans-serif",
|
|
22
|
+
"Calibri Light": "'Calibri Light','Carlito','Helvetica Neue Light','Helvetica Neue',Arial,sans-serif",
|
|
23
|
+
"Cambria": "'Cambria','Caladea','Times New Roman',Georgia,serif",
|
|
24
|
+
"Cambria Math": "'Cambria Math','Caladea','Times New Roman',serif",
|
|
25
|
+
"Consolas": "'Consolas','Courier New',monospace",
|
|
26
|
+
"Aptos": "'Aptos','Carlito','Helvetica Neue',Arial,sans-serif",
|
|
27
|
+
"Times New Roman": "'Times New Roman','Liberation Serif',Georgia,serif",
|
|
28
|
+
// Handwriting / decorative fonts
|
|
29
|
+
"MV Boli": "'MV Boli','Comic Sans MS','Marker Felt',cursive",
|
|
30
|
+
"Kristen ITC": "'Kristen ITC','Comic Sans MS','Marker Felt',cursive",
|
|
31
|
+
"Stylus BT": "'Stylus BT','Brush Script MT','Snell Roundhand',cursive",
|
|
32
|
+
// Japanese sans-serif fonts → Noto Sans CJK JP (commonly installed)
|
|
33
|
+
"MS Gothic": "'MS Gothic','Noto Sans CJK JP','Hiragino Kaku Gothic ProN','Yu Gothic',sans-serif",
|
|
34
|
+
"MS PGothic": "'MS PGothic','Noto Sans CJK JP','Hiragino Kaku Gothic ProN','Yu Gothic',sans-serif",
|
|
35
|
+
"MS Pゴシック": "'MS Pゴシック','Noto Sans CJK JP','Hiragino Kaku Gothic ProN','Yu Gothic',sans-serif",
|
|
36
|
+
"MS ゴシック": "'MS ゴシック','Noto Sans CJK JP','Hiragino Kaku Gothic ProN','Yu Gothic',sans-serif",
|
|
37
|
+
"Kozuka Gothic Pro R": "'Kozuka Gothic Pro R','Noto Sans CJK JP','Hiragino Kaku Gothic ProN',sans-serif",
|
|
38
|
+
"Kozuka Gothic Pro B": "'Kozuka Gothic Pro B','Noto Sans CJK JP Bold','Hiragino Kaku Gothic ProN',sans-serif",
|
|
39
|
+
"Kozuka Gothic Pro M": "'Kozuka Gothic Pro M','Noto Sans CJK JP Medium','Hiragino Kaku Gothic ProN',sans-serif",
|
|
40
|
+
"Kozuka Gothic Pro EL": "'Kozuka Gothic Pro EL','Noto Sans CJK JP Light','Hiragino Kaku Gothic ProN',sans-serif",
|
|
41
|
+
"HGSKyokashotai": "'HGSKyokashotai','Noto Sans CJK JP','Hiragino Kaku Gothic ProN',sans-serif",
|
|
42
|
+
// Japanese serif fonts → Noto Serif CJK JP
|
|
43
|
+
"MS Mincho": "'MS Mincho','Noto Serif CJK JP','Hiragino Mincho ProN','Yu Mincho',serif",
|
|
44
|
+
"MS PMincho": "'MS PMincho','Noto Serif CJK JP','Hiragino Mincho ProN','Yu Mincho',serif",
|
|
45
|
+
// CJK fonts
|
|
46
|
+
"宋体": "'宋体','Noto Serif CJK SC','Songti SC','STSong',serif",
|
|
47
|
+
"新細明體": "'新細明體','Noto Serif CJK TC','Songti TC',serif",
|
|
48
|
+
"黑体": "'黑体','Noto Sans CJK SC','Heiti SC','STHeiti',sans-serif",
|
|
49
|
+
"微軟正黑體": "'微軟正黑體','Noto Sans CJK TC','Heiti TC',sans-serif",
|
|
50
|
+
"맑은 고딕": "'맑은 고딕','Apple SD Gothic Neo',sans-serif",
|
|
51
|
+
// UI / system fonts
|
|
52
|
+
"Lucida Sans Unicode": "'Lucida Sans Unicode','Lucida Grande','Lucida Sans',sans-serif",
|
|
53
|
+
"Segoe UI": "'Segoe UI','-apple-system','Helvetica Neue',sans-serif",
|
|
54
|
+
"Segoe Sans Display": "'Segoe Sans Display','-apple-system','Helvetica Neue',Arial,sans-serif",
|
|
55
|
+
"Segoe Sans Display Semibold": "'Segoe Sans Display Semibold','-apple-system','Helvetica Neue',Arial,sans-serif",
|
|
56
|
+
// Google Fonts / web fonts commonly used in presentations
|
|
57
|
+
"Inter": "'Inter','Helvetica Neue',Helvetica,Arial,sans-serif",
|
|
58
|
+
"Inter Light": "'Inter Light','Inter','Helvetica Neue',Arial,sans-serif",
|
|
59
|
+
"Inter ExtraBold": "'Inter ExtraBold','Inter','Helvetica Neue',Arial,sans-serif",
|
|
60
|
+
"Space Grotesk": "'Space Grotesk','Helvetica Neue',Helvetica,Arial,sans-serif",
|
|
61
|
+
"Montserrat": "'Montserrat','Helvetica Neue',Helvetica,Arial,sans-serif",
|
|
62
|
+
"Open Sans": "'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif",
|
|
63
|
+
"Lato": "'Lato','Helvetica Neue',Helvetica,Arial,sans-serif",
|
|
64
|
+
"Oswald": "'Oswald','Helvetica Neue',Helvetica,Arial,sans-serif",
|
|
65
|
+
"Archivo": "'Archivo','Helvetica Neue',Helvetica,Arial,sans-serif",
|
|
66
|
+
"Economica": "'Economica','Helvetica Neue',Arial,sans-serif",
|
|
67
|
+
"Libre Baskerville": "'Libre Baskerville','Georgia','Times New Roman',serif",
|
|
68
|
+
// GitHub Copilot fonts
|
|
69
|
+
"Ginto Copilot": "'Ginto Copilot','Helvetica Neue',Helvetica,Arial,sans-serif",
|
|
70
|
+
"Ginto Copilot Light": "'Ginto Copilot Light','Helvetica Neue Light','Helvetica Neue',Arial,sans-serif",
|
|
71
|
+
"Ginto Copilot 400": "'Ginto Copilot 400','Helvetica Neue',Helvetica,Arial,sans-serif",
|
|
72
|
+
// Google Fonts / decorative (serif & display)
|
|
73
|
+
"Playfair Display": "'Playfair Display','Georgia','Times New Roman',serif",
|
|
74
|
+
"Playfair Display SemiBold": "'Playfair Display SemiBold','Playfair Display','Georgia',serif",
|
|
75
|
+
"Caveat": "'Caveat','Comic Sans MS','Marker Felt',cursive",
|
|
76
|
+
"Syncopate": "'Syncopate','Helvetica Neue',Helvetica,Arial,sans-serif",
|
|
77
|
+
// Montserrat weight variants
|
|
78
|
+
"Montserrat SemiBold": "'Montserrat SemiBold','Montserrat','Helvetica Neue',Arial,sans-serif",
|
|
79
|
+
"Montserrat ExtraBold": "'Montserrat ExtraBold','Montserrat','Helvetica Neue',Arial,sans-serif",
|
|
80
|
+
// GitHub Copilot font variants
|
|
81
|
+
"Ginto Copilot Medium": "'Ginto Copilot Medium','Ginto Copilot','Helvetica Neue',Arial,sans-serif",
|
|
82
|
+
"Ginto Copilot Black": "'Ginto Copilot Black','Ginto Copilot','Helvetica Neue',Arial,sans-serif",
|
|
83
|
+
"Ginto Copilot Thin": "'Ginto Copilot Thin','Ginto Copilot Light','Helvetica Neue',Arial,sans-serif",
|
|
84
|
+
// Microsoft UI fonts
|
|
85
|
+
"Segoe Sans Small Regular": "'Segoe Sans Small Regular','Segoe UI','-apple-system','Helvetica Neue',sans-serif",
|
|
86
|
+
"Segoe Sans Text Regular": "'Segoe Sans Text Regular','Segoe UI','-apple-system','Helvetica Neue',sans-serif",
|
|
87
|
+
"Grandview": "'Grandview','Helvetica Neue',Arial,sans-serif",
|
|
88
|
+
"Nirmala UI": "'Nirmala UI','Helvetica Neue',Arial,sans-serif",
|
|
89
|
+
"Ebrima": "'Ebrima','Helvetica Neue',Arial,sans-serif",
|
|
90
|
+
// Common system fonts with fallbacks
|
|
91
|
+
"Arial": "Arial,'Helvetica Neue',Helvetica,sans-serif",
|
|
92
|
+
"Arial Black": "'Arial Black','Helvetica Neue',Helvetica,Arial,sans-serif",
|
|
93
|
+
"Georgia": "Georgia,'Times New Roman',serif",
|
|
94
|
+
"Georgia Regular": "'Georgia Regular',Georgia,'Times New Roman',serif",
|
|
95
|
+
"Courier New": "'Courier New',Courier,monospace",
|
|
96
|
+
"Times": "Times,'Times New Roman',serif",
|
|
97
|
+
// Symbol fonts (preserved as-is with system fallback)
|
|
98
|
+
"Wingdings": "'Wingdings','Zapf Dingbats',sans-serif",
|
|
99
|
+
"Wingdings 2": "'Wingdings 2','Zapf Dingbats',sans-serif",
|
|
100
|
+
"Wingdings 3": "'Wingdings 3','Zapf Dingbats',sans-serif",
|
|
101
|
+
// Common fallbacks
|
|
102
|
+
"Tahoma": "'Tahoma','Verdana','Geneva',sans-serif",
|
|
103
|
+
"Verdana": "'Verdana','Geneva',sans-serif",
|
|
104
|
+
"Helvetica Neue Light": "'Helvetica Neue Light','Helvetica Neue',Helvetica,Arial,sans-serif",
|
|
105
|
+
};
|
|
16
106
|
// ============================================================================
|
|
17
107
|
// Utility Functions
|
|
18
108
|
// ============================================================================
|
|
109
|
+
/**
|
|
110
|
+
* Returns a CSS font-family value for the given font name.
|
|
111
|
+
* Uses FONT_FALLBACK_MAP to provide platform-appropriate fallbacks
|
|
112
|
+
* for fonts that may not be installed on the system.
|
|
113
|
+
*/
|
|
114
|
+
function cssFontFamily(fontName) {
|
|
115
|
+
const mapped = FONT_FALLBACK_MAP[fontName];
|
|
116
|
+
if (mapped)
|
|
117
|
+
return mapped;
|
|
118
|
+
// Handle malformed typeface attributes that already contain commas (e.g. "Arial,Sans-Serif")
|
|
119
|
+
if (fontName.includes(',')) {
|
|
120
|
+
return fontName.split(',').map(f => {
|
|
121
|
+
const trimmed = f.trim();
|
|
122
|
+
const lower = trimmed.toLowerCase();
|
|
123
|
+
if (lower === 'sans-serif' || lower === 'serif' || lower === 'monospace' || lower === 'cursive' || lower === 'fantasy') {
|
|
124
|
+
return lower;
|
|
125
|
+
}
|
|
126
|
+
return `'${trimmed}'`;
|
|
127
|
+
}).join(',');
|
|
128
|
+
}
|
|
129
|
+
return `'${fontName}',sans-serif`;
|
|
130
|
+
}
|
|
19
131
|
function emuToPx(emu) {
|
|
20
132
|
return emu / EMU_PER_PX;
|
|
21
133
|
}
|
|
@@ -95,6 +207,25 @@ function extractGradientFill(parent, themeColors) {
|
|
|
95
207
|
}
|
|
96
208
|
if (stops.length === 0)
|
|
97
209
|
return undefined;
|
|
210
|
+
// Check for radial gradient (path element with path="circle")
|
|
211
|
+
const pathEl = findChild(gradFill, "path");
|
|
212
|
+
if (pathEl && pathEl.getAttribute("path") === "circle") {
|
|
213
|
+
// Extract center position from fillToRect
|
|
214
|
+
const fillToRect = findChild(pathEl, "fillToRect");
|
|
215
|
+
let centerX = 50;
|
|
216
|
+
let centerY = 50;
|
|
217
|
+
if (fillToRect) {
|
|
218
|
+
// OOXML uses l/t/r/b as percentages * 1000 from edges
|
|
219
|
+
// l=50000, t=50000 means center is at 50%, 50%
|
|
220
|
+
const l = parseInt(fillToRect.getAttribute("l") ?? "50000", 10) / 1000;
|
|
221
|
+
const t = parseInt(fillToRect.getAttribute("t") ?? "50000", 10) / 1000;
|
|
222
|
+
centerX = l;
|
|
223
|
+
centerY = t;
|
|
224
|
+
}
|
|
225
|
+
const stopStr = stops.map((s) => `${s.color} ${s.pos}%`).join(",");
|
|
226
|
+
return `radial-gradient(ellipse at ${centerX}% ${centerY}%,${stopStr})`;
|
|
227
|
+
}
|
|
228
|
+
// Linear gradient
|
|
98
229
|
const lin = findChild(gradFill, "lin");
|
|
99
230
|
const angAttr = lin?.getAttribute("ang");
|
|
100
231
|
const cssDeg = angAttr ? ooxmlAngleToCss(parseInt(angAttr, 10)) : 180;
|
|
@@ -126,12 +257,88 @@ function extractRunProps(rPr, scale, themeColors) {
|
|
|
126
257
|
if (color)
|
|
127
258
|
result.color = color;
|
|
128
259
|
}
|
|
260
|
+
// Handle gradient fill for text - use CSS background-clip:text technique
|
|
261
|
+
if (!result.color) {
|
|
262
|
+
const gradFill = findChild(rPr, "gradFill");
|
|
263
|
+
if (gradFill) {
|
|
264
|
+
const gsLst = findChild(gradFill, "gsLst");
|
|
265
|
+
if (gsLst) {
|
|
266
|
+
const gsEls = findChildren(gsLst, "gs");
|
|
267
|
+
if (gsEls.length > 0) {
|
|
268
|
+
// Extract gradient stops
|
|
269
|
+
const stops = [];
|
|
270
|
+
for (const gs of gsEls) {
|
|
271
|
+
const pos = parseInt(gs.getAttribute("pos") ?? "0", 10) / 1000;
|
|
272
|
+
const color = resolveColor(gs, themeColors);
|
|
273
|
+
if (color)
|
|
274
|
+
stops.push({ pos, color });
|
|
275
|
+
}
|
|
276
|
+
if (stops.length >= 2) {
|
|
277
|
+
// Extract angle from lin element
|
|
278
|
+
const lin = findChild(gradFill, "lin");
|
|
279
|
+
const angAttr = lin?.getAttribute("ang");
|
|
280
|
+
const cssDeg = angAttr ? ooxmlAngleToCss(parseInt(angAttr, 10)) : 135;
|
|
281
|
+
const stopStr = stops.map((s) => `${s.color} ${s.pos}%`).join(",");
|
|
282
|
+
result.gradientFill = `linear-gradient(${cssDeg}deg,${stopStr})`;
|
|
283
|
+
}
|
|
284
|
+
// Also set color as fallback (first gradient stop)
|
|
285
|
+
const firstColor = resolveColor(gsEls[0], themeColors);
|
|
286
|
+
if (firstColor)
|
|
287
|
+
result.color = firstColor;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
129
292
|
const latin = findChild(rPr, "latin");
|
|
130
293
|
if (latin) {
|
|
131
294
|
const typeface = latin.getAttribute("typeface");
|
|
132
295
|
if (typeface)
|
|
133
296
|
result.fontFamily = typeface;
|
|
134
297
|
}
|
|
298
|
+
// Extract glow effect from effectLst
|
|
299
|
+
const effectLst = findChild(rPr, "effectLst");
|
|
300
|
+
if (effectLst) {
|
|
301
|
+
const glow = findChild(effectLst, "glow");
|
|
302
|
+
if (glow) {
|
|
303
|
+
// rad is in EMUs (English Metric Units)
|
|
304
|
+
const radAttr = glow.getAttribute("rad");
|
|
305
|
+
const radiusEmu = radAttr ? parseInt(radAttr, 10) : 0;
|
|
306
|
+
// Convert EMU to pixels: 914400 EMU = 1 inch = 96 pixels
|
|
307
|
+
const radiusPx = Math.round(emuToPx(radiusEmu) * scale);
|
|
308
|
+
// Get glow color
|
|
309
|
+
const glowColor = resolveColor(glow, themeColors);
|
|
310
|
+
if (glowColor && radiusPx > 0) {
|
|
311
|
+
// During export, CSS text-shadow is scaled for OOXML visual equivalence:
|
|
312
|
+
// - Size scaled by 2.5x (CSS blur spreads wider than OOXML glow)
|
|
313
|
+
// - Opacity scaled by 0.3x (OOXML glow appears more intense)
|
|
314
|
+
//
|
|
315
|
+
// During import, apply inverse scaling to restore CSS-like values:
|
|
316
|
+
const INVERSE_GLOW_SIZE_SCALE = 0.4; // 1/2.5
|
|
317
|
+
const INVERSE_GLOW_OPACITY_SCALE = 3.33; // 1/0.3
|
|
318
|
+
const cssRadius = Math.round(radiusPx * INVERSE_GLOW_SIZE_SCALE);
|
|
319
|
+
// Extract and scale opacity from rgba color
|
|
320
|
+
const rgbaMatch = glowColor.match(/rgba?\((\d+),(\d+),(\d+)(?:,([0-9.]+))?\)/);
|
|
321
|
+
if (rgbaMatch) {
|
|
322
|
+
const r = rgbaMatch[1];
|
|
323
|
+
const g = rgbaMatch[2];
|
|
324
|
+
const b = rgbaMatch[3];
|
|
325
|
+
const originalAlpha = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1;
|
|
326
|
+
const scaledAlpha = Math.min(originalAlpha * INVERSE_GLOW_OPACITY_SCALE, 1.0);
|
|
327
|
+
result.textShadow = `0 0 ${cssRadius}px rgba(${r},${g},${b},${scaledAlpha.toFixed(2)})`;
|
|
328
|
+
}
|
|
329
|
+
else if (glowColor.startsWith('#')) {
|
|
330
|
+
// Hex color - assume full opacity, apply scaling
|
|
331
|
+
const r = parseInt(glowColor.slice(1, 3), 16);
|
|
332
|
+
const g = parseInt(glowColor.slice(3, 5), 16);
|
|
333
|
+
const b = parseInt(glowColor.slice(5, 7), 16);
|
|
334
|
+
result.textShadow = `0 0 ${cssRadius}px rgba(${r},${g},${b},1)`;
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
result.textShadow = `0 0 ${cssRadius}px ${glowColor}`;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
135
342
|
return result;
|
|
136
343
|
}
|
|
137
344
|
// ============================================================================
|
|
@@ -158,9 +365,10 @@ function parseShape(sp, scale, themeColors) {
|
|
|
158
365
|
fill: extractFill(spPr, themeColors),
|
|
159
366
|
paragraphs: [],
|
|
160
367
|
};
|
|
161
|
-
// Border radius from prstGeom roundRect
|
|
368
|
+
// Border radius from prstGeom roundRect or ellipse
|
|
162
369
|
const prstGeom = findChild(spPr, "prstGeom");
|
|
163
|
-
|
|
370
|
+
const prstType = prstGeom?.getAttribute("prst");
|
|
371
|
+
if (prstType === "roundRect" && prstGeom) {
|
|
164
372
|
const avLst = findChild(prstGeom, "avLst");
|
|
165
373
|
const gd = avLst ? findChild(avLst, "gd") : null;
|
|
166
374
|
const adjVal = gd
|
|
@@ -170,6 +378,10 @@ function parseShape(sp, scale, themeColors) {
|
|
|
170
378
|
const radiusEmu = (minDim * Math.min(adjVal, 50000)) / 100000;
|
|
171
379
|
shape.borderRadius = Math.round(emuToPx(radiusEmu) * scale);
|
|
172
380
|
}
|
|
381
|
+
else if (prstType === "ellipse") {
|
|
382
|
+
// Ellipse shapes should have border-radius: 50% to render as circles
|
|
383
|
+
shape.isEllipse = true;
|
|
384
|
+
}
|
|
173
385
|
// Border from a:ln
|
|
174
386
|
const ln = findChild(spPr, "ln");
|
|
175
387
|
if (ln) {
|
|
@@ -180,6 +392,14 @@ function parseShape(sp, scale, themeColors) {
|
|
|
180
392
|
if (lnFill) {
|
|
181
393
|
shape.borderColor = resolveColor(lnFill, themeColors);
|
|
182
394
|
}
|
|
395
|
+
// Extract dash type from prstDash
|
|
396
|
+
const prstDash = findChild(ln, "prstDash");
|
|
397
|
+
if (prstDash) {
|
|
398
|
+
const dashVal = prstDash.getAttribute("val");
|
|
399
|
+
if (dashVal && dashVal !== "solid") {
|
|
400
|
+
shape.borderDashType = dashVal;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
183
403
|
}
|
|
184
404
|
}
|
|
185
405
|
// Body properties: vertical alignment and padding
|
|
@@ -261,21 +481,42 @@ function parseShape(sp, scale, themeColors) {
|
|
|
261
481
|
para.bulletChar = buChar.getAttribute("char") ?? undefined;
|
|
262
482
|
}
|
|
263
483
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
484
|
+
// Iterate over all children to handle both text runs (<a:r>) and line breaks (<a:br>)
|
|
485
|
+
// This ensures proper spacing when text spans multiple lines
|
|
486
|
+
for (const child of Array.from(p.childNodes)) {
|
|
487
|
+
if (child.nodeType !== 1)
|
|
488
|
+
continue; // ELEMENT_NODE = 1
|
|
489
|
+
const el = child;
|
|
490
|
+
const localName = el.localName || el.nodeName.split(':').pop();
|
|
491
|
+
if (localName === 'r') {
|
|
492
|
+
// Text run
|
|
493
|
+
const rPr = findChild(el, "rPr");
|
|
494
|
+
const props = extractRunProps(rPr, scale, themeColors);
|
|
495
|
+
const tEls = findChildren(el, "t");
|
|
496
|
+
const text = tEls.map((t) => t.textContent ?? "").join("");
|
|
497
|
+
if (text) {
|
|
498
|
+
para.runs.push({
|
|
499
|
+
text,
|
|
500
|
+
bold: props.bold ?? defaults?.bold,
|
|
501
|
+
italic: props.italic ?? defaults?.italic,
|
|
502
|
+
fontSize: props.fontSize ?? defaults?.fontSize,
|
|
503
|
+
color: props.color ?? defaults?.color,
|
|
504
|
+
fontFamily: props.fontFamily ?? defaults?.fontFamily,
|
|
505
|
+
textShadow: props.textShadow ?? defaults?.textShadow,
|
|
506
|
+
gradientFill: props.gradientFill ?? defaults?.gradientFill,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
else if (localName === 'br') {
|
|
511
|
+
// Line break - add a newline to the previous run or create a new run with just newline
|
|
512
|
+
if (para.runs.length > 0) {
|
|
513
|
+
// Append newline to the last run's text
|
|
514
|
+
para.runs[para.runs.length - 1].text += '\n';
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
// No previous run, create a new one with just newline
|
|
518
|
+
para.runs.push({ text: '\n' });
|
|
519
|
+
}
|
|
279
520
|
}
|
|
280
521
|
}
|
|
281
522
|
if (para.runs.length > 0) {
|
|
@@ -302,8 +543,34 @@ function parsePicture(pic, scale, imageMap) {
|
|
|
302
543
|
const blip = findChild(blipFill, "blip");
|
|
303
544
|
if (!blip)
|
|
304
545
|
return null;
|
|
305
|
-
|
|
306
|
-
|
|
546
|
+
// First, try to get SVG from asvg:svgBlip (higher quality)
|
|
547
|
+
let rEmbed = null;
|
|
548
|
+
const extLst = findChild(blip, "extLst");
|
|
549
|
+
if (extLst) {
|
|
550
|
+
for (let i = 0; i < extLst.children.length; i++) {
|
|
551
|
+
const ext = extLst.children[i];
|
|
552
|
+
if (ext.localName === "ext") {
|
|
553
|
+
// Look for svgBlip in extension
|
|
554
|
+
for (let j = 0; j < ext.children.length; j++) {
|
|
555
|
+
const child = ext.children[j];
|
|
556
|
+
if (child.localName === "svgBlip") {
|
|
557
|
+
rEmbed =
|
|
558
|
+
child.getAttribute("r:embed") ??
|
|
559
|
+
child.getAttributeNS("http://schemas.openxmlformats.org/officeDocument/2006/relationships", "embed");
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (rEmbed)
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// Fallback to regular blip if no SVG found
|
|
569
|
+
if (!rEmbed) {
|
|
570
|
+
rEmbed =
|
|
571
|
+
blip.getAttribute("r:embed") ??
|
|
572
|
+
blip.getAttributeNS("http://schemas.openxmlformats.org/officeDocument/2006/relationships", "embed");
|
|
573
|
+
}
|
|
307
574
|
if (!rEmbed)
|
|
308
575
|
return null;
|
|
309
576
|
const dataUri = imageMap.get(rEmbed);
|
|
@@ -312,6 +579,34 @@ function parsePicture(pic, scale, imageMap) {
|
|
|
312
579
|
const nvPicPr = findChild(pic, "nvPicPr");
|
|
313
580
|
const cNvPr = nvPicPr ? findChild(nvPicPr, "cNvPr") : null;
|
|
314
581
|
const alt = cNvPr?.getAttribute("descr") ?? undefined;
|
|
582
|
+
// Check for srcRect cropping - indicates the image should use object-fit:cover
|
|
583
|
+
const srcRect = findChild(blipFill, "srcRect");
|
|
584
|
+
let hasCrop = false;
|
|
585
|
+
if (srcRect) {
|
|
586
|
+
const l = parseInt(srcRect.getAttribute("l") ?? "0", 10);
|
|
587
|
+
const r = parseInt(srcRect.getAttribute("r") ?? "0", 10);
|
|
588
|
+
const t = parseInt(srcRect.getAttribute("t") ?? "0", 10);
|
|
589
|
+
const b = parseInt(srcRect.getAttribute("b") ?? "0", 10);
|
|
590
|
+
// If any cropping is applied, the original image used object-fit:cover
|
|
591
|
+
hasCrop = l > 0 || r > 0 || t > 0 || b > 0;
|
|
592
|
+
}
|
|
593
|
+
// Extract border-radius from roundRect preset geometry
|
|
594
|
+
let borderRadius;
|
|
595
|
+
const prstGeom = findChild(spPr, "prstGeom");
|
|
596
|
+
if (prstGeom?.getAttribute("prst") === "roundRect") {
|
|
597
|
+
const avLst = findChild(prstGeom, "avLst");
|
|
598
|
+
const gd = avLst ? findChild(avLst, "gd") : null;
|
|
599
|
+
// Default adj value for roundRect is 16667 (1/6 of 100000)
|
|
600
|
+
const adjVal = gd
|
|
601
|
+
? parseInt(gd.getAttribute("fmla")?.replace("val ", "") ?? "16667", 10)
|
|
602
|
+
: 16667;
|
|
603
|
+
const wEmu = parseInt(ext.getAttribute("cx") ?? "0", 10);
|
|
604
|
+
const hEmu = parseInt(ext.getAttribute("cy") ?? "0", 10);
|
|
605
|
+
const minDim = Math.min(wEmu, hEmu);
|
|
606
|
+
// The adjustment value is a percentage where 50000 = 50% = max possible radius
|
|
607
|
+
const radiusEmu = (minDim * Math.min(adjVal, 50000)) / 100000;
|
|
608
|
+
borderRadius = Math.round(emuToPx(radiusEmu) * scale);
|
|
609
|
+
}
|
|
315
610
|
return {
|
|
316
611
|
x: Math.round(emuToPx(parseInt(off.getAttribute("x") ?? "0", 10)) * scale),
|
|
317
612
|
y: Math.round(emuToPx(parseInt(off.getAttribute("y") ?? "0", 10)) * scale),
|
|
@@ -319,6 +614,8 @@ function parsePicture(pic, scale, imageMap) {
|
|
|
319
614
|
h: Math.round(emuToPx(parseInt(ext.getAttribute("cy") ?? "0", 10)) * scale),
|
|
320
615
|
dataUri,
|
|
321
616
|
alt,
|
|
617
|
+
borderRadius,
|
|
618
|
+
hasCrop,
|
|
322
619
|
};
|
|
323
620
|
}
|
|
324
621
|
// ============================================================================
|
|
@@ -331,14 +628,25 @@ function renderSlideHtml(elements, bgColor) {
|
|
|
331
628
|
const elId = `el-${elementIndex++}`;
|
|
332
629
|
if (el.kind === "image") {
|
|
333
630
|
const img = el.data;
|
|
334
|
-
|
|
631
|
+
// Detect images that should use object-fit:cover:
|
|
632
|
+
// 1. Fullbleed images (covering entire slide)
|
|
633
|
+
// 2. Images with srcRect cropping applied (hasCrop)
|
|
634
|
+
const isFullbleed = img.x <= 10 && img.y <= 10 &&
|
|
635
|
+
img.w >= TARGET_WIDTH - 20 && img.h >= TARGET_HEIGHT - 20;
|
|
636
|
+
const objectFit = isFullbleed || img.hasCrop ? "cover" : "contain";
|
|
637
|
+
const stylesList = [
|
|
335
638
|
"position:absolute",
|
|
336
639
|
`left:${img.x}px`,
|
|
337
640
|
`top:${img.y}px`,
|
|
338
641
|
`width:${img.w}px`,
|
|
339
642
|
`height:${img.h}px`,
|
|
340
|
-
|
|
341
|
-
]
|
|
643
|
+
`object-fit:${objectFit}`,
|
|
644
|
+
];
|
|
645
|
+
// Add border-radius if present
|
|
646
|
+
if (img.borderRadius && img.borderRadius > 0) {
|
|
647
|
+
stylesList.push(`border-radius:${img.borderRadius}px`);
|
|
648
|
+
}
|
|
649
|
+
const styles = stylesList.join(";");
|
|
342
650
|
const altAttr = img.alt
|
|
343
651
|
? ` alt="${img.alt.replace(/"/g, """)}"`
|
|
344
652
|
: "";
|
|
@@ -360,8 +668,27 @@ function renderSlideHtml(elements, bgColor) {
|
|
|
360
668
|
if (shape.borderRadius) {
|
|
361
669
|
styles.push(`border-radius:${shape.borderRadius}px`);
|
|
362
670
|
}
|
|
671
|
+
else if (shape.isEllipse) {
|
|
672
|
+
// Ellipse shapes render as circles/ovals with 50% border-radius
|
|
673
|
+
styles.push(`border-radius:50%`);
|
|
674
|
+
}
|
|
363
675
|
if (shape.borderWidth && shape.borderColor) {
|
|
364
|
-
|
|
676
|
+
// Convert PPTX dashType to CSS border-style
|
|
677
|
+
let borderStyle = "solid";
|
|
678
|
+
if (shape.borderDashType) {
|
|
679
|
+
switch (shape.borderDashType) {
|
|
680
|
+
case "dash":
|
|
681
|
+
case "lgDash":
|
|
682
|
+
case "sysDash":
|
|
683
|
+
borderStyle = "dashed";
|
|
684
|
+
break;
|
|
685
|
+
case "sysDot":
|
|
686
|
+
case "dot":
|
|
687
|
+
borderStyle = "dotted";
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
styles.push(`border:${shape.borderWidth}px ${borderStyle} ${shape.borderColor}`);
|
|
365
692
|
styles.push("box-sizing:border-box");
|
|
366
693
|
}
|
|
367
694
|
if (shape.padding) {
|
|
@@ -402,14 +729,24 @@ function renderSlideHtml(elements, bgColor) {
|
|
|
402
729
|
const rStyles = [];
|
|
403
730
|
if (run.fontSize)
|
|
404
731
|
rStyles.push(`font-size:${run.fontSize}px`);
|
|
405
|
-
if (run.
|
|
732
|
+
if (run.gradientFill) {
|
|
733
|
+
// CSS gradient text using background-clip technique
|
|
734
|
+
rStyles.push(`background:${run.gradientFill}`);
|
|
735
|
+
rStyles.push("-webkit-background-clip:text");
|
|
736
|
+
rStyles.push("-webkit-text-fill-color:transparent");
|
|
737
|
+
rStyles.push("background-clip:text");
|
|
738
|
+
}
|
|
739
|
+
else if (run.color) {
|
|
406
740
|
rStyles.push(`color:${run.color}`);
|
|
741
|
+
}
|
|
407
742
|
if (run.bold)
|
|
408
743
|
rStyles.push("font-weight:bold");
|
|
409
744
|
if (run.italic)
|
|
410
745
|
rStyles.push("font-style:italic");
|
|
411
746
|
if (run.fontFamily)
|
|
412
|
-
rStyles.push(`font-family
|
|
747
|
+
rStyles.push(`font-family:${cssFontFamily(run.fontFamily)}`);
|
|
748
|
+
if (run.textShadow)
|
|
749
|
+
rStyles.push(`text-shadow:${run.textShadow}`);
|
|
413
750
|
const escapedText = run.text
|
|
414
751
|
.replace(/&/g, "&")
|
|
415
752
|
.replace(/</g, "<")
|
|
@@ -544,6 +881,28 @@ export default async function importPptx(arrayBuffer) {
|
|
|
544
881
|
}
|
|
545
882
|
}
|
|
546
883
|
}
|
|
884
|
+
// Parse embedded fonts from presentation.xml
|
|
885
|
+
// PPTX files can embed TrueType fonts in ppt/fonts/ as .fntdata (EOT format)
|
|
886
|
+
const embeddedFontNames = new Set();
|
|
887
|
+
const embeddedFontEls = presDoc.getElementsByTagName("p:embeddedFont");
|
|
888
|
+
for (let i = 0; i < embeddedFontEls.length; i++) {
|
|
889
|
+
const fontEl = embeddedFontEls[i].getElementsByTagName("p:font")[0];
|
|
890
|
+
if (fontEl) {
|
|
891
|
+
const typeface = fontEl.getAttribute("typeface");
|
|
892
|
+
if (typeface)
|
|
893
|
+
embeddedFontNames.add(typeface);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
// Build Google Fonts link for embedded font families
|
|
897
|
+
// EOT files in PPTX are compressed and not usable in modern browsers,
|
|
898
|
+
// so we load them via Google Fonts CDN if available.
|
|
899
|
+
let fontStyleBlock = "";
|
|
900
|
+
if (embeddedFontNames.size > 0) {
|
|
901
|
+
const fontFamilies = Array.from(embeddedFontNames)
|
|
902
|
+
.map((name) => name.replace(/ /g, "+") + ":ital,wght@0,400;0,700;1,400;1,700")
|
|
903
|
+
.join("&family=");
|
|
904
|
+
fontStyleBlock = `<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=${fontFamilies}&display=swap">`;
|
|
905
|
+
}
|
|
547
906
|
const slides = [];
|
|
548
907
|
for (const slideFile of slideFiles) {
|
|
549
908
|
const slideXml = await zip.file(slideFile)?.async("text");
|
|
@@ -614,6 +973,10 @@ export default async function importPptx(arrayBuffer) {
|
|
|
614
973
|
}
|
|
615
974
|
slides.push(renderSlideHtml(elements, slideBg));
|
|
616
975
|
}
|
|
976
|
+
// Prepend font style block to first slide so embedded fonts load via Google Fonts
|
|
977
|
+
if (fontStyleBlock && slides.length > 0) {
|
|
978
|
+
slides[0] = fontStyleBlock + slides[0];
|
|
979
|
+
}
|
|
617
980
|
return slides;
|
|
618
981
|
}
|
|
619
982
|
//# sourceMappingURL=import-pptx.js.map
|