docgen-utils 1.0.11 → 1.0.12
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 +40084 -5825
- package/dist/bundle.min.js +288 -110
- package/dist/cli.js +23758 -110
- package/dist/packages/docs/import-docx.d.ts.map +1 -1
- package/dist/packages/docs/import-docx.js +397 -7
- package/dist/packages/docs/import-docx.js.map +1 -1
- package/dist/packages/slides/fonts.d.ts +41 -0
- package/dist/packages/slides/fonts.d.ts.map +1 -0
- package/dist/packages/slides/fonts.js +209 -0
- package/dist/packages/slides/fonts.js.map +1 -0
- package/dist/packages/slides/import-pptx.d.ts.map +1 -1
- package/dist/packages/slides/import-pptx.js +583 -120
- 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 +1 -51
- package/dist/packages/slides/parse.js.map +1 -1
- package/dist/packages/slides/transform.d.ts +6 -6
- package/dist/packages/slides/transform.d.ts.map +1 -1
- package/dist/packages/slides/transform.js +13 -44
- package/dist/packages/slides/transform.js.map +1 -1
- package/package.json +3 -2
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Usage: const slides = await importPptx(arrayBuffer);
|
|
6
6
|
*/
|
|
7
7
|
import JSZip from "jszip";
|
|
8
|
+
import { cssFontFamily } from "./fonts";
|
|
8
9
|
// ============================================================================
|
|
9
10
|
// Constants
|
|
10
11
|
// ============================================================================
|
|
@@ -13,121 +14,9 @@ const TARGET_WIDTH = 1280;
|
|
|
13
14
|
const TARGET_HEIGHT = 720;
|
|
14
15
|
// EMU (English Metric Units) to pixels: 1 inch = 914400 EMU, 96 DPI
|
|
15
16
|
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
|
-
};
|
|
106
17
|
// ============================================================================
|
|
107
18
|
// Utility Functions
|
|
108
19
|
// ============================================================================
|
|
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
|
-
}
|
|
131
20
|
function emuToPx(emu) {
|
|
132
21
|
return emu / EMU_PER_PX;
|
|
133
22
|
}
|
|
@@ -238,7 +127,7 @@ function extractFill(spPr, themeColors) {
|
|
|
238
127
|
return resolveColor(solidFill, themeColors);
|
|
239
128
|
return extractGradientFill(spPr, themeColors);
|
|
240
129
|
}
|
|
241
|
-
function extractRunProps(rPr, scale, themeColors) {
|
|
130
|
+
function extractRunProps(rPr, scale, themeColors, themeFonts) {
|
|
242
131
|
if (!rPr)
|
|
243
132
|
return {};
|
|
244
133
|
const result = {};
|
|
@@ -291,10 +180,32 @@ function extractRunProps(rPr, scale, themeColors) {
|
|
|
291
180
|
}
|
|
292
181
|
const latin = findChild(rPr, "latin");
|
|
293
182
|
if (latin) {
|
|
294
|
-
|
|
295
|
-
if (typeface)
|
|
183
|
+
let typeface = latin.getAttribute("typeface");
|
|
184
|
+
if (typeface && themeFonts) {
|
|
185
|
+
// Resolve theme font references
|
|
186
|
+
if (typeface === "+mn-lt")
|
|
187
|
+
typeface = themeFonts.minorLatin;
|
|
188
|
+
else if (typeface === "+mj-lt")
|
|
189
|
+
typeface = themeFonts.majorLatin;
|
|
190
|
+
}
|
|
191
|
+
if (typeface && !typeface.startsWith("+"))
|
|
296
192
|
result.fontFamily = typeface;
|
|
297
193
|
}
|
|
194
|
+
// Fallback: check East Asian font
|
|
195
|
+
if (!result.fontFamily) {
|
|
196
|
+
const ea = findChild(rPr, "ea");
|
|
197
|
+
if (ea) {
|
|
198
|
+
let typeface = ea.getAttribute("typeface");
|
|
199
|
+
if (typeface && themeFonts) {
|
|
200
|
+
if (typeface === "+mn-ea")
|
|
201
|
+
typeface = themeFonts.minorEastAsian ?? themeFonts.minorLatin;
|
|
202
|
+
else if (typeface === "+mj-ea")
|
|
203
|
+
typeface = themeFonts.majorEastAsian ?? themeFonts.majorLatin;
|
|
204
|
+
}
|
|
205
|
+
if (typeface && !typeface.startsWith("+"))
|
|
206
|
+
result.fontFamily = typeface;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
298
209
|
// Extract glow effect from effectLst
|
|
299
210
|
const effectLst = findChild(rPr, "effectLst");
|
|
300
211
|
if (effectLst) {
|
|
@@ -344,7 +255,7 @@ function extractRunProps(rPr, scale, themeColors) {
|
|
|
344
255
|
// ============================================================================
|
|
345
256
|
// Parsing Functions
|
|
346
257
|
// ============================================================================
|
|
347
|
-
function parseShape(sp, scale, themeColors) {
|
|
258
|
+
function parseShape(sp, scale, themeColors, themeFonts) {
|
|
348
259
|
const spPr = findChild(sp, "spPr");
|
|
349
260
|
if (!spPr)
|
|
350
261
|
return null;
|
|
@@ -450,10 +361,23 @@ function parseShape(sp, scale, themeColors) {
|
|
|
450
361
|
para.indentPx = Math.round(emuToPx(parseInt(indent, 10)) * scale);
|
|
451
362
|
const defRPr = findChild(pPr, "defRPr");
|
|
452
363
|
if (defRPr) {
|
|
453
|
-
defaults = extractRunProps(defRPr, scale, themeColors);
|
|
364
|
+
defaults = extractRunProps(defRPr, scale, themeColors, themeFonts);
|
|
454
365
|
}
|
|
455
366
|
const lnSpc = findChild(pPr, "lnSpc");
|
|
456
367
|
if (lnSpc) {
|
|
368
|
+
const spcPct = findChild(lnSpc, "spcPct");
|
|
369
|
+
if (spcPct) {
|
|
370
|
+
const pctVal = parseInt(spcPct.getAttribute("val") ?? "100000", 10);
|
|
371
|
+
// PPTX spcPct 100000 = 100% = "single" spacing.
|
|
372
|
+
// In CSS, line-height:100% is actually tighter than normal (~120%),
|
|
373
|
+
// so we only emit lineSpacingPercent when it differs from the default.
|
|
374
|
+
// This avoids both the tighter-than-intended 100% and the text-overflow
|
|
375
|
+
// issues caused by large values like 160% inside fixed-height boxes.
|
|
376
|
+
const pct = pctVal / 1000;
|
|
377
|
+
if (pct !== 100) {
|
|
378
|
+
para.lineSpacingPercent = pct;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
457
381
|
const spcPts = findChild(lnSpc, "spcPts");
|
|
458
382
|
if (spcPts) {
|
|
459
383
|
para.lineSpacingPt =
|
|
@@ -480,6 +404,11 @@ function parseShape(sp, scale, themeColors) {
|
|
|
480
404
|
if (buChar) {
|
|
481
405
|
para.bulletChar = buChar.getAttribute("char") ?? undefined;
|
|
482
406
|
}
|
|
407
|
+
// Bullet color
|
|
408
|
+
const buClr = findChild(pPr, "buClr");
|
|
409
|
+
if (buClr) {
|
|
410
|
+
para.bulletColor = resolveColor(buClr, themeColors);
|
|
411
|
+
}
|
|
483
412
|
}
|
|
484
413
|
// Iterate over all children to handle both text runs (<a:r>) and line breaks (<a:br>)
|
|
485
414
|
// This ensures proper spacing when text spans multiple lines
|
|
@@ -491,7 +420,7 @@ function parseShape(sp, scale, themeColors) {
|
|
|
491
420
|
if (localName === 'r') {
|
|
492
421
|
// Text run
|
|
493
422
|
const rPr = findChild(el, "rPr");
|
|
494
|
-
const props = extractRunProps(rPr, scale, themeColors);
|
|
423
|
+
const props = extractRunProps(rPr, scale, themeColors, themeFonts);
|
|
495
424
|
const tEls = findChildren(el, "t");
|
|
496
425
|
const text = tEls.map((t) => t.textContent ?? "").join("");
|
|
497
426
|
if (text) {
|
|
@@ -619,6 +548,379 @@ function parsePicture(pic, scale, imageMap) {
|
|
|
619
548
|
};
|
|
620
549
|
}
|
|
621
550
|
// ============================================================================
|
|
551
|
+
// Table Style Parsing
|
|
552
|
+
// ============================================================================
|
|
553
|
+
/**
|
|
554
|
+
* Parse ppt/tableStyles.xml and return a map from style GUID to TableStyleInfo.
|
|
555
|
+
* Table styles define default fills for wholeTbl, banded rows, header row, etc.
|
|
556
|
+
* Cells without explicit fills inherit colors from their table style.
|
|
557
|
+
*/
|
|
558
|
+
async function parseTableStyles(zip, parser, themeColors) {
|
|
559
|
+
const map = new Map();
|
|
560
|
+
const xml = await zip.file("ppt/tableStyles.xml")?.async("text");
|
|
561
|
+
if (!xml)
|
|
562
|
+
return map;
|
|
563
|
+
const doc = parser.parseFromString(xml, "application/xml");
|
|
564
|
+
// Each <a:tblStyle styleId="{GUID}"> defines one table style
|
|
565
|
+
const styleEls = doc.getElementsByTagName("a:tblStyle");
|
|
566
|
+
for (let i = 0; i < styleEls.length; i++) {
|
|
567
|
+
const styleEl = styleEls[i];
|
|
568
|
+
const styleId = styleEl.getAttribute("styleId");
|
|
569
|
+
if (!styleId)
|
|
570
|
+
continue;
|
|
571
|
+
const info = {};
|
|
572
|
+
// Helper: extract solid fill color from a tcStyle element
|
|
573
|
+
const extractTcStyleFill = (sectionEl) => {
|
|
574
|
+
const tcStyle = findChild(sectionEl, "tcStyle");
|
|
575
|
+
if (!tcStyle)
|
|
576
|
+
return undefined;
|
|
577
|
+
const fill = findChild(tcStyle, "fill");
|
|
578
|
+
if (!fill)
|
|
579
|
+
return undefined;
|
|
580
|
+
const solidFill = findChild(fill, "solidFill");
|
|
581
|
+
if (!solidFill)
|
|
582
|
+
return undefined;
|
|
583
|
+
return resolveColor(solidFill, themeColors);
|
|
584
|
+
};
|
|
585
|
+
// wholeTbl — default fill for all cells
|
|
586
|
+
const wholeTbl = findChild(styleEl, "wholeTbl");
|
|
587
|
+
if (wholeTbl) {
|
|
588
|
+
info.wholeTblFill = extractTcStyleFill(wholeTbl);
|
|
589
|
+
}
|
|
590
|
+
// band1H — odd-row banding fill
|
|
591
|
+
const band1H = findChild(styleEl, "band1H");
|
|
592
|
+
if (band1H) {
|
|
593
|
+
info.band1Fill = extractTcStyleFill(band1H);
|
|
594
|
+
}
|
|
595
|
+
// band2H — even-row banding fill (if absent, inherits wholeTblFill)
|
|
596
|
+
const band2H = findChild(styleEl, "band2H");
|
|
597
|
+
if (band2H) {
|
|
598
|
+
info.band2Fill = extractTcStyleFill(band2H);
|
|
599
|
+
}
|
|
600
|
+
// firstRow — header-row fill
|
|
601
|
+
const firstRow = findChild(styleEl, "firstRow");
|
|
602
|
+
if (firstRow) {
|
|
603
|
+
info.firstRowFill = extractTcStyleFill(firstRow);
|
|
604
|
+
}
|
|
605
|
+
map.set(styleId, info);
|
|
606
|
+
}
|
|
607
|
+
return map;
|
|
608
|
+
}
|
|
609
|
+
// ============================================================================
|
|
610
|
+
// Table Parsing Functions
|
|
611
|
+
// ============================================================================
|
|
612
|
+
function parseCellBorder(tcPr, borderName, scale, themeColors) {
|
|
613
|
+
const borderEl = findChild(tcPr, borderName);
|
|
614
|
+
if (!borderEl)
|
|
615
|
+
return undefined;
|
|
616
|
+
// Check for noFill (no border)
|
|
617
|
+
const noFill = findChild(borderEl, "noFill");
|
|
618
|
+
if (noFill)
|
|
619
|
+
return undefined;
|
|
620
|
+
const wAttr = borderEl.getAttribute("w");
|
|
621
|
+
const width = wAttr
|
|
622
|
+
? Math.max(1, Math.round(emuToPx(parseInt(wAttr, 10)) * scale))
|
|
623
|
+
: 1;
|
|
624
|
+
const solidFill = findChild(borderEl, "solidFill");
|
|
625
|
+
const color = solidFill ? resolveColor(solidFill, themeColors) ?? "#000000" : "#000000";
|
|
626
|
+
// Borders with fully-transparent color (alpha=0) are effectively invisible;
|
|
627
|
+
// treat them as no border to avoid layout artifacts in the HTML output.
|
|
628
|
+
if (color.startsWith("rgba(") && color.endsWith(",0)"))
|
|
629
|
+
return undefined;
|
|
630
|
+
// Parse dash type
|
|
631
|
+
let style = "solid";
|
|
632
|
+
const prstDash = findChild(borderEl, "prstDash");
|
|
633
|
+
if (prstDash) {
|
|
634
|
+
const dashVal = prstDash.getAttribute("val");
|
|
635
|
+
if (dashVal === "dash" || dashVal === "lgDash" || dashVal === "sysDash") {
|
|
636
|
+
style = "dashed";
|
|
637
|
+
}
|
|
638
|
+
else if (dashVal === "dot" || dashVal === "sysDot") {
|
|
639
|
+
style = "dotted";
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return { width, color, style };
|
|
643
|
+
}
|
|
644
|
+
function parseParagraphsFromTxBody(txBody, scale, themeColors, themeFonts) {
|
|
645
|
+
const paragraphs = [];
|
|
646
|
+
const pEls = findChildren(txBody, "p");
|
|
647
|
+
for (const p of pEls) {
|
|
648
|
+
const para = { runs: [] };
|
|
649
|
+
const pPr = findChild(p, "pPr");
|
|
650
|
+
let defaults = undefined;
|
|
651
|
+
if (pPr) {
|
|
652
|
+
const algn = pPr.getAttribute("algn");
|
|
653
|
+
if (algn) {
|
|
654
|
+
para.align =
|
|
655
|
+
algn === "ctr"
|
|
656
|
+
? "center"
|
|
657
|
+
: algn === "r"
|
|
658
|
+
? "right"
|
|
659
|
+
: algn === "just"
|
|
660
|
+
? "justify"
|
|
661
|
+
: "left";
|
|
662
|
+
}
|
|
663
|
+
const marL = pPr.getAttribute("marL");
|
|
664
|
+
if (marL)
|
|
665
|
+
para.marginLeftPx = Math.round(emuToPx(parseInt(marL, 10)) * scale);
|
|
666
|
+
const indent = pPr.getAttribute("indent");
|
|
667
|
+
if (indent)
|
|
668
|
+
para.indentPx = Math.round(emuToPx(parseInt(indent, 10)) * scale);
|
|
669
|
+
const defRPr = findChild(pPr, "defRPr");
|
|
670
|
+
if (defRPr) {
|
|
671
|
+
defaults = extractRunProps(defRPr, scale, themeColors, themeFonts);
|
|
672
|
+
}
|
|
673
|
+
const lnSpc = findChild(pPr, "lnSpc");
|
|
674
|
+
if (lnSpc) {
|
|
675
|
+
const spcPct = findChild(lnSpc, "spcPct");
|
|
676
|
+
if (spcPct) {
|
|
677
|
+
const pctVal = parseInt(spcPct.getAttribute("val") ?? "100000", 10);
|
|
678
|
+
// spcPct val is percentage * 1000, e.g. 150000 = 150%
|
|
679
|
+
// Skip 100% (default single spacing) — CSS line-height:100% is tighter than intended
|
|
680
|
+
const pct = pctVal / 1000;
|
|
681
|
+
if (pct !== 100) {
|
|
682
|
+
para.lineSpacingPercent = pct;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const spcPts = findChild(lnSpc, "spcPts");
|
|
686
|
+
if (spcPts) {
|
|
687
|
+
para.lineSpacingPt =
|
|
688
|
+
parseInt(spcPts.getAttribute("val") ?? "0", 10) / 100;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const spcAft = findChild(pPr, "spcAft");
|
|
692
|
+
if (spcAft) {
|
|
693
|
+
const spcPts = findChild(spcAft, "spcPts");
|
|
694
|
+
if (spcPts) {
|
|
695
|
+
para.spacingAfterPt =
|
|
696
|
+
parseInt(spcPts.getAttribute("val") ?? "0", 10) / 100;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
const spcBef = findChild(pPr, "spcBef");
|
|
700
|
+
if (spcBef) {
|
|
701
|
+
const spcPts = findChild(spcBef, "spcPts");
|
|
702
|
+
if (spcPts) {
|
|
703
|
+
para.spacingBeforePt =
|
|
704
|
+
parseInt(spcPts.getAttribute("val") ?? "0", 10) / 100;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
const buChar = findChild(pPr, "buChar");
|
|
708
|
+
if (buChar) {
|
|
709
|
+
para.bulletChar = buChar.getAttribute("char") ?? undefined;
|
|
710
|
+
}
|
|
711
|
+
// Bullet color
|
|
712
|
+
const buClr = findChild(pPr, "buClr");
|
|
713
|
+
if (buClr) {
|
|
714
|
+
para.bulletColor = resolveColor(buClr, themeColors);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
for (const child of Array.from(p.childNodes)) {
|
|
718
|
+
if (child.nodeType !== 1)
|
|
719
|
+
continue;
|
|
720
|
+
const el = child;
|
|
721
|
+
const localName = el.localName || el.nodeName.split(':').pop();
|
|
722
|
+
if (localName === 'r') {
|
|
723
|
+
const rPr = findChild(el, "rPr");
|
|
724
|
+
const props = extractRunProps(rPr, scale, themeColors, themeFonts);
|
|
725
|
+
const tEls = findChildren(el, "t");
|
|
726
|
+
const text = tEls.map((t) => t.textContent ?? "").join("");
|
|
727
|
+
if (text) {
|
|
728
|
+
para.runs.push({
|
|
729
|
+
text,
|
|
730
|
+
bold: props.bold ?? defaults?.bold,
|
|
731
|
+
italic: props.italic ?? defaults?.italic,
|
|
732
|
+
fontSize: props.fontSize ?? defaults?.fontSize,
|
|
733
|
+
color: props.color ?? defaults?.color,
|
|
734
|
+
fontFamily: props.fontFamily ?? defaults?.fontFamily,
|
|
735
|
+
textShadow: props.textShadow ?? defaults?.textShadow,
|
|
736
|
+
gradientFill: props.gradientFill ?? defaults?.gradientFill,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
else if (localName === 'br') {
|
|
741
|
+
if (para.runs.length > 0) {
|
|
742
|
+
para.runs[para.runs.length - 1].text += '\n';
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
para.runs.push({ text: '\n' });
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (para.runs.length > 0) {
|
|
750
|
+
paragraphs.push(para);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return paragraphs;
|
|
754
|
+
}
|
|
755
|
+
function parseTable(graphicFrame, scale, themeColors, themeFonts, tableStyleMap) {
|
|
756
|
+
// Extract position from xfrm
|
|
757
|
+
const xfrm = findChild(graphicFrame, "xfrm");
|
|
758
|
+
if (!xfrm)
|
|
759
|
+
return null;
|
|
760
|
+
const off = findChild(xfrm, "off");
|
|
761
|
+
const ext = findChild(xfrm, "ext");
|
|
762
|
+
if (!off || !ext)
|
|
763
|
+
return null;
|
|
764
|
+
// Find the table element: graphicFrame > graphic > graphicData > tbl
|
|
765
|
+
const graphic = findChild(graphicFrame, "graphic");
|
|
766
|
+
if (!graphic)
|
|
767
|
+
return null;
|
|
768
|
+
const graphicData = findChild(graphic, "graphicData");
|
|
769
|
+
if (!graphicData)
|
|
770
|
+
return null;
|
|
771
|
+
// Check if it's a table (URI should contain "table")
|
|
772
|
+
const uri = graphicData.getAttribute("uri") ?? "";
|
|
773
|
+
if (!uri.includes("table"))
|
|
774
|
+
return null;
|
|
775
|
+
const tbl = findChild(graphicData, "tbl");
|
|
776
|
+
if (!tbl)
|
|
777
|
+
return null;
|
|
778
|
+
// Read table properties for style flags
|
|
779
|
+
const tblPr = findChild(tbl, "tblPr");
|
|
780
|
+
const hasFirstRow = tblPr?.getAttribute("firstRow") === "1";
|
|
781
|
+
const hasBandRow = tblPr?.getAttribute("bandRow") === "1";
|
|
782
|
+
// Look up table style for fill defaults
|
|
783
|
+
let styleInfo;
|
|
784
|
+
if (tblPr && tableStyleMap) {
|
|
785
|
+
const styleIdEl = findChild(tblPr, "tableStyleId");
|
|
786
|
+
if (styleIdEl) {
|
|
787
|
+
const styleId = styleIdEl.textContent?.trim();
|
|
788
|
+
if (styleId)
|
|
789
|
+
styleInfo = tableStyleMap.get(styleId);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
const table = {
|
|
793
|
+
x: Math.round(emuToPx(parseInt(off.getAttribute("x") ?? "0", 10)) * scale),
|
|
794
|
+
y: Math.round(emuToPx(parseInt(off.getAttribute("y") ?? "0", 10)) * scale),
|
|
795
|
+
w: Math.round(emuToPx(parseInt(ext.getAttribute("cx") ?? "0", 10)) * scale),
|
|
796
|
+
h: Math.round(emuToPx(parseInt(ext.getAttribute("cy") ?? "0", 10)) * scale),
|
|
797
|
+
rows: [],
|
|
798
|
+
colWidths: [],
|
|
799
|
+
};
|
|
800
|
+
// Parse column widths from tblGrid
|
|
801
|
+
const tblGrid = findChild(tbl, "tblGrid");
|
|
802
|
+
if (tblGrid) {
|
|
803
|
+
const gridCols = findChildren(tblGrid, "gridCol");
|
|
804
|
+
for (const col of gridCols) {
|
|
805
|
+
const w = parseInt(col.getAttribute("w") ?? "0", 10);
|
|
806
|
+
table.colWidths.push(Math.round(emuToPx(w) * scale));
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
// Parse rows
|
|
810
|
+
const trEls = findChildren(tbl, "tr");
|
|
811
|
+
let rowIndex = 0;
|
|
812
|
+
for (const tr of trEls) {
|
|
813
|
+
const rowHeight = tr.getAttribute("h");
|
|
814
|
+
const row = {
|
|
815
|
+
cells: [],
|
|
816
|
+
height: rowHeight
|
|
817
|
+
? Math.round(emuToPx(parseInt(rowHeight, 10)) * scale)
|
|
818
|
+
: undefined,
|
|
819
|
+
};
|
|
820
|
+
const tcEls = findChildren(tr, "tc");
|
|
821
|
+
for (const tc of tcEls) {
|
|
822
|
+
const cell = {
|
|
823
|
+
paragraphs: [],
|
|
824
|
+
};
|
|
825
|
+
// Parse cell text content
|
|
826
|
+
const txBody = findChild(tc, "txBody");
|
|
827
|
+
if (txBody) {
|
|
828
|
+
cell.paragraphs = parseParagraphsFromTxBody(txBody, scale, themeColors, themeFonts);
|
|
829
|
+
}
|
|
830
|
+
// Parse cell properties
|
|
831
|
+
const tcPr = findChild(tc, "tcPr");
|
|
832
|
+
if (tcPr) {
|
|
833
|
+
// Cell fill — use explicit fill from tcPr first, then fall back to
|
|
834
|
+
// the table style fill based on row position and style flags.
|
|
835
|
+
const explicitFill = extractFill(tcPr, themeColors);
|
|
836
|
+
let styleFill;
|
|
837
|
+
if (styleInfo) {
|
|
838
|
+
if (hasFirstRow && rowIndex === 0) {
|
|
839
|
+
// Header row: use firstRowFill from the table style
|
|
840
|
+
styleFill = styleInfo.firstRowFill;
|
|
841
|
+
}
|
|
842
|
+
else if (hasBandRow) {
|
|
843
|
+
// Body rows with banding enabled: alternate between band fills.
|
|
844
|
+
// The "data row index" starts after the header (if present).
|
|
845
|
+
const dataRowIdx = hasFirstRow ? rowIndex - 1 : rowIndex;
|
|
846
|
+
styleFill =
|
|
847
|
+
dataRowIdx % 2 === 0
|
|
848
|
+
? styleInfo.band1Fill ?? styleInfo.wholeTblFill
|
|
849
|
+
: styleInfo.band2Fill ?? styleInfo.wholeTblFill;
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
styleFill = styleInfo.wholeTblFill;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
cell.fill = explicitFill ?? styleFill;
|
|
856
|
+
// Vertical alignment
|
|
857
|
+
const anchor = tcPr.getAttribute("anchor");
|
|
858
|
+
if (anchor === "ctr")
|
|
859
|
+
cell.verticalAlign = "center";
|
|
860
|
+
else if (anchor === "b")
|
|
861
|
+
cell.verticalAlign = "bottom";
|
|
862
|
+
else
|
|
863
|
+
cell.verticalAlign = "top";
|
|
864
|
+
// Cell margins (PPTX defaults: L/R=91440 EMU ~9.6px, T/B=45720 EMU ~4.8px)
|
|
865
|
+
const marL = tcPr.getAttribute("marL");
|
|
866
|
+
const marR = tcPr.getAttribute("marR");
|
|
867
|
+
const marT = tcPr.getAttribute("marT");
|
|
868
|
+
const marB = tcPr.getAttribute("marB");
|
|
869
|
+
cell.padding = {
|
|
870
|
+
left: Math.round(emuToPx(parseInt(marL ?? "91440", 10)) * scale),
|
|
871
|
+
right: Math.round(emuToPx(parseInt(marR ?? "91440", 10)) * scale),
|
|
872
|
+
top: Math.round(emuToPx(parseInt(marT ?? "45720", 10)) * scale),
|
|
873
|
+
bottom: Math.round(emuToPx(parseInt(marB ?? "45720", 10)) * scale),
|
|
874
|
+
};
|
|
875
|
+
// Cell borders
|
|
876
|
+
cell.borderLeft = parseCellBorder(tcPr, "lnL", scale, themeColors);
|
|
877
|
+
cell.borderRight = parseCellBorder(tcPr, "lnR", scale, themeColors);
|
|
878
|
+
cell.borderTop = parseCellBorder(tcPr, "lnT", scale, themeColors);
|
|
879
|
+
cell.borderBottom = parseCellBorder(tcPr, "lnB", scale, themeColors);
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
// No tcPr: use PPTX default margins
|
|
883
|
+
cell.padding = {
|
|
884
|
+
left: Math.round(emuToPx(91440) * scale),
|
|
885
|
+
right: Math.round(emuToPx(91440) * scale),
|
|
886
|
+
top: Math.round(emuToPx(45720) * scale),
|
|
887
|
+
bottom: Math.round(emuToPx(45720) * scale),
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
// Column span
|
|
891
|
+
const gridSpan = tc.getAttribute("gridSpan");
|
|
892
|
+
if (gridSpan) {
|
|
893
|
+
const span = parseInt(gridSpan, 10);
|
|
894
|
+
if (span > 1)
|
|
895
|
+
cell.colSpan = span;
|
|
896
|
+
}
|
|
897
|
+
// Row span
|
|
898
|
+
const rowSpan = tc.getAttribute("rowSpan");
|
|
899
|
+
if (rowSpan) {
|
|
900
|
+
const span = parseInt(rowSpan, 10);
|
|
901
|
+
if (span > 1)
|
|
902
|
+
cell.rowSpan = span;
|
|
903
|
+
}
|
|
904
|
+
// Skip merged cells (vMerge = vertically merged continuation)
|
|
905
|
+
const vMerge = tc.getAttribute("vMerge");
|
|
906
|
+
if (vMerge === "1" || vMerge === "true") {
|
|
907
|
+
// This cell is a continuation of a vertical merge - still add it but mark it
|
|
908
|
+
// We'll skip rendering it in the HTML
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
// Skip horizontally merged continuation cells
|
|
912
|
+
const hMerge = tc.getAttribute("hMerge");
|
|
913
|
+
if (hMerge === "1" || hMerge === "true") {
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
row.cells.push(cell);
|
|
917
|
+
}
|
|
918
|
+
table.rows.push(row);
|
|
919
|
+
rowIndex++;
|
|
920
|
+
}
|
|
921
|
+
return table;
|
|
922
|
+
}
|
|
923
|
+
// ============================================================================
|
|
622
924
|
// Rendering Functions
|
|
623
925
|
// ============================================================================
|
|
624
926
|
function renderSlideHtml(elements, bgColor) {
|
|
@@ -653,6 +955,121 @@ function renderSlideHtml(elements, bgColor) {
|
|
|
653
955
|
inner += `<img id="${elId}" data-elementType="image" src="${img.dataUri}"${altAttr} style="${styles}" />\n`;
|
|
654
956
|
continue;
|
|
655
957
|
}
|
|
958
|
+
if (el.kind === "table") {
|
|
959
|
+
const table = el.data;
|
|
960
|
+
const tableStyles = [
|
|
961
|
+
"position:absolute",
|
|
962
|
+
`left:${table.x}px`,
|
|
963
|
+
`top:${table.y}px`,
|
|
964
|
+
`width:${table.w}px`,
|
|
965
|
+
`height:${table.h}px`,
|
|
966
|
+
];
|
|
967
|
+
let tableHtml = `<table style="border-collapse:collapse;width:100%;height:100%;table-layout:fixed">`;
|
|
968
|
+
// Column widths via colgroup
|
|
969
|
+
if (table.colWidths.length > 0) {
|
|
970
|
+
tableHtml += `<colgroup>`;
|
|
971
|
+
for (const cw of table.colWidths) {
|
|
972
|
+
tableHtml += `<col style="width:${cw}px">`;
|
|
973
|
+
}
|
|
974
|
+
tableHtml += `</colgroup>`;
|
|
975
|
+
}
|
|
976
|
+
for (const row of table.rows) {
|
|
977
|
+
const rowStyle = row.height ? ` style="height:${row.height}px"` : "";
|
|
978
|
+
tableHtml += `<tr${rowStyle}>`;
|
|
979
|
+
for (const cell of row.cells) {
|
|
980
|
+
const cellPad = cell.padding ?? { top: 5, right: 10, bottom: 5, left: 10 };
|
|
981
|
+
const tdStyles = [
|
|
982
|
+
"box-sizing:border-box",
|
|
983
|
+
"overflow:hidden",
|
|
984
|
+
`padding:${cellPad.top}px ${cellPad.right}px ${cellPad.bottom}px ${cellPad.left}px`,
|
|
985
|
+
];
|
|
986
|
+
if (cell.fill) {
|
|
987
|
+
tdStyles.push(`background:${cell.fill}`);
|
|
988
|
+
}
|
|
989
|
+
if (cell.verticalAlign === "center") {
|
|
990
|
+
tdStyles.push("vertical-align:middle");
|
|
991
|
+
}
|
|
992
|
+
else if (cell.verticalAlign === "bottom") {
|
|
993
|
+
tdStyles.push("vertical-align:bottom");
|
|
994
|
+
}
|
|
995
|
+
else {
|
|
996
|
+
tdStyles.push("vertical-align:top");
|
|
997
|
+
}
|
|
998
|
+
// Cell borders
|
|
999
|
+
if (cell.borderTop) {
|
|
1000
|
+
tdStyles.push(`border-top:${cell.borderTop.width}px ${cell.borderTop.style} ${cell.borderTop.color}`);
|
|
1001
|
+
}
|
|
1002
|
+
if (cell.borderBottom) {
|
|
1003
|
+
tdStyles.push(`border-bottom:${cell.borderBottom.width}px ${cell.borderBottom.style} ${cell.borderBottom.color}`);
|
|
1004
|
+
}
|
|
1005
|
+
if (cell.borderLeft) {
|
|
1006
|
+
tdStyles.push(`border-left:${cell.borderLeft.width}px ${cell.borderLeft.style} ${cell.borderLeft.color}`);
|
|
1007
|
+
}
|
|
1008
|
+
if (cell.borderRight) {
|
|
1009
|
+
tdStyles.push(`border-right:${cell.borderRight.width}px ${cell.borderRight.style} ${cell.borderRight.color}`);
|
|
1010
|
+
}
|
|
1011
|
+
const spanAttrs = [];
|
|
1012
|
+
if (cell.colSpan && cell.colSpan > 1)
|
|
1013
|
+
spanAttrs.push(` colspan="${cell.colSpan}"`);
|
|
1014
|
+
if (cell.rowSpan && cell.rowSpan > 1)
|
|
1015
|
+
spanAttrs.push(` rowspan="${cell.rowSpan}"`);
|
|
1016
|
+
tableHtml += `<td style="${tdStyles.join(";")}"${spanAttrs.join("")}>`;
|
|
1017
|
+
// Render cell paragraphs
|
|
1018
|
+
for (const para of cell.paragraphs) {
|
|
1019
|
+
const pStyles = ["margin:0"];
|
|
1020
|
+
if (para.align)
|
|
1021
|
+
pStyles.push(`text-align:${para.align}`);
|
|
1022
|
+
if (para.spacingAfterPt)
|
|
1023
|
+
pStyles.push(`margin-bottom:${para.spacingAfterPt}pt`);
|
|
1024
|
+
if (para.spacingBeforePt)
|
|
1025
|
+
pStyles.push(`margin-top:${para.spacingBeforePt}pt`);
|
|
1026
|
+
if (para.lineSpacingPt)
|
|
1027
|
+
pStyles.push(`line-height:${para.lineSpacingPt}pt`);
|
|
1028
|
+
else if (para.lineSpacingPercent)
|
|
1029
|
+
pStyles.push(`line-height:${para.lineSpacingPercent}%`);
|
|
1030
|
+
let runHtml = "";
|
|
1031
|
+
for (const run of para.runs) {
|
|
1032
|
+
const rStyles = [];
|
|
1033
|
+
if (run.fontSize)
|
|
1034
|
+
rStyles.push(`font-size:${run.fontSize}px`);
|
|
1035
|
+
if (run.gradientFill) {
|
|
1036
|
+
rStyles.push(`background:${run.gradientFill}`);
|
|
1037
|
+
rStyles.push("-webkit-background-clip:text");
|
|
1038
|
+
rStyles.push("-webkit-text-fill-color:transparent");
|
|
1039
|
+
rStyles.push("background-clip:text");
|
|
1040
|
+
}
|
|
1041
|
+
else if (run.color) {
|
|
1042
|
+
rStyles.push(`color:${run.color}`);
|
|
1043
|
+
}
|
|
1044
|
+
if (run.bold)
|
|
1045
|
+
rStyles.push("font-weight:bold");
|
|
1046
|
+
if (run.italic)
|
|
1047
|
+
rStyles.push("font-style:italic");
|
|
1048
|
+
if (run.fontFamily)
|
|
1049
|
+
rStyles.push(`font-family:${cssFontFamily(run.fontFamily)}`);
|
|
1050
|
+
if (run.textShadow)
|
|
1051
|
+
rStyles.push(`text-shadow:${run.textShadow}`);
|
|
1052
|
+
const escapedText = run.text
|
|
1053
|
+
.replace(/&/g, "&")
|
|
1054
|
+
.replace(/</g, "<")
|
|
1055
|
+
.replace(/>/g, ">");
|
|
1056
|
+
if (rStyles.length > 0) {
|
|
1057
|
+
runHtml += `<span style="${rStyles.join(";")}">${escapedText}</span>`;
|
|
1058
|
+
}
|
|
1059
|
+
else {
|
|
1060
|
+
runHtml += escapedText;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
tableHtml += `<p style="${pStyles.join(";")}">${runHtml}</p>`;
|
|
1064
|
+
}
|
|
1065
|
+
tableHtml += `</td>`;
|
|
1066
|
+
}
|
|
1067
|
+
tableHtml += `</tr>`;
|
|
1068
|
+
}
|
|
1069
|
+
tableHtml += `</table>`;
|
|
1070
|
+
inner += `<div id="${elId}" data-elementType="table" style="${tableStyles.join(";")}">${tableHtml}</div>\n`;
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
656
1073
|
const shape = el.data;
|
|
657
1074
|
const styles = [
|
|
658
1075
|
"position:absolute",
|
|
@@ -717,13 +1134,20 @@ function renderSlideHtml(elements, bgColor) {
|
|
|
717
1134
|
pStyles.push(`margin-top:${para.spacingBeforePt}pt`);
|
|
718
1135
|
if (para.lineSpacingPt)
|
|
719
1136
|
pStyles.push(`line-height:${para.lineSpacingPt}pt`);
|
|
1137
|
+
// Note: lineSpacingPercent is intentionally NOT emitted for shapes.
|
|
1138
|
+
// Applying percentage-based line-height to fixed-height text boxes
|
|
1139
|
+
// often causes visual regressions (overlaps, clipping). It works
|
|
1140
|
+
// reliably only for tables where cell height auto-adjusts.
|
|
720
1141
|
if (para.marginLeftPx)
|
|
721
1142
|
pStyles.push(`padding-left:${para.marginLeftPx}px`);
|
|
722
1143
|
if (para.indentPx)
|
|
723
1144
|
pStyles.push(`text-indent:${para.indentPx}px`);
|
|
724
1145
|
let runHtml = "";
|
|
725
1146
|
if (para.bulletChar) {
|
|
726
|
-
|
|
1147
|
+
const bulletStyle = para.bulletColor
|
|
1148
|
+
? `margin-right:4px;color:${para.bulletColor}`
|
|
1149
|
+
: "margin-right:4px";
|
|
1150
|
+
runHtml += `<span style="${bulletStyle}">${para.bulletChar}</span>`;
|
|
727
1151
|
}
|
|
728
1152
|
for (const run of para.runs) {
|
|
729
1153
|
const rStyles = [];
|
|
@@ -880,7 +1304,41 @@ export default async function importPptx(arrayBuffer) {
|
|
|
880
1304
|
}
|
|
881
1305
|
}
|
|
882
1306
|
}
|
|
1307
|
+
// Add OOXML aliases: tx1/tx2 map to dk1/dk2, bg1/bg2 map to lt1/lt2
|
|
1308
|
+
const dk1Color = themeColors.get("dk1");
|
|
1309
|
+
if (dk1Color)
|
|
1310
|
+
themeColors.set("tx1", dk1Color);
|
|
1311
|
+
const dk2Color = themeColors.get("dk2");
|
|
1312
|
+
if (dk2Color)
|
|
1313
|
+
themeColors.set("tx2", dk2Color);
|
|
1314
|
+
const lt1Color = themeColors.get("lt1");
|
|
1315
|
+
if (lt1Color)
|
|
1316
|
+
themeColors.set("bg1", lt1Color);
|
|
1317
|
+
const lt2Color = themeColors.get("lt2");
|
|
1318
|
+
if (lt2Color)
|
|
1319
|
+
themeColors.set("bg2", lt2Color);
|
|
883
1320
|
}
|
|
1321
|
+
// Parse theme fonts
|
|
1322
|
+
let themeFonts;
|
|
1323
|
+
if (themeXml) {
|
|
1324
|
+
const themeDoc = parser.parseFromString(themeXml, "application/xml");
|
|
1325
|
+
const majorFont = themeDoc.getElementsByTagName("a:majorFont")[0];
|
|
1326
|
+
const minorFont = themeDoc.getElementsByTagName("a:minorFont")[0];
|
|
1327
|
+
if (majorFont || minorFont) {
|
|
1328
|
+
const majorLatin = majorFont ? findChild(majorFont, "latin")?.getAttribute("typeface") : null;
|
|
1329
|
+
const minorLatin = minorFont ? findChild(minorFont, "latin")?.getAttribute("typeface") : null;
|
|
1330
|
+
const majorEa = majorFont ? findChild(majorFont, "ea")?.getAttribute("typeface") : null;
|
|
1331
|
+
const minorEa = minorFont ? findChild(minorFont, "ea")?.getAttribute("typeface") : null;
|
|
1332
|
+
themeFonts = {
|
|
1333
|
+
majorLatin: majorLatin ?? "Calibri",
|
|
1334
|
+
minorLatin: minorLatin ?? "Calibri",
|
|
1335
|
+
majorEastAsian: majorEa ?? undefined,
|
|
1336
|
+
minorEastAsian: minorEa ?? undefined,
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
// Parse table styles from ppt/tableStyles.xml
|
|
1341
|
+
const tableStyleMap = await parseTableStyles(zip, parser, themeColors);
|
|
884
1342
|
// Parse embedded fonts from presentation.xml
|
|
885
1343
|
// PPTX files can embed TrueType fonts in ppt/fonts/ as .fntdata (EOT format)
|
|
886
1344
|
const embeddedFontNames = new Set();
|
|
@@ -961,7 +1419,7 @@ export default async function importPptx(arrayBuffer) {
|
|
|
961
1419
|
for (let i = 0; i < spTree.children.length; i++) {
|
|
962
1420
|
const child = spTree.children[i];
|
|
963
1421
|
if (child.localName === "sp") {
|
|
964
|
-
const shape = parseShape(child, scale, themeColors);
|
|
1422
|
+
const shape = parseShape(child, scale, themeColors, themeFonts);
|
|
965
1423
|
if (shape)
|
|
966
1424
|
elements.push({ kind: "shape", data: shape });
|
|
967
1425
|
}
|
|
@@ -970,6 +1428,11 @@ export default async function importPptx(arrayBuffer) {
|
|
|
970
1428
|
if (img)
|
|
971
1429
|
elements.push({ kind: "image", data: img });
|
|
972
1430
|
}
|
|
1431
|
+
else if (child.localName === "graphicFrame") {
|
|
1432
|
+
const table = parseTable(child, scale, themeColors, themeFonts, tableStyleMap);
|
|
1433
|
+
if (table)
|
|
1434
|
+
elements.push({ kind: "table", data: table });
|
|
1435
|
+
}
|
|
973
1436
|
}
|
|
974
1437
|
slides.push(renderSlideHtml(elements, slideBg));
|
|
975
1438
|
}
|