@aivorynet/slaide 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/LICENSE +201 -0
- package/README.md +87 -0
- package/bin/slaide.js +25 -0
- package/dist/assets.d.ts +10 -0
- package/dist/assets.js +54 -0
- package/dist/assets.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +433 -0
- package/dist/cli.js.map +1 -0
- package/dist/compare/index.d.ts +24 -0
- package/dist/compare/index.js +246 -0
- package/dist/compare/index.js.map +1 -0
- package/dist/compare/ssim.d.ts +2 -0
- package/dist/compare/ssim.js +46 -0
- package/dist/compare/ssim.js.map +1 -0
- package/dist/compiler/chrome.d.ts +7 -0
- package/dist/compiler/chrome.js +94 -0
- package/dist/compiler/chrome.js.map +1 -0
- package/dist/compiler/compile.d.ts +2 -0
- package/dist/compiler/compile.js +432 -0
- package/dist/compiler/compile.js.map +1 -0
- package/dist/compiler/markdown.d.ts +26 -0
- package/dist/compiler/markdown.js +439 -0
- package/dist/compiler/markdown.js.map +1 -0
- package/dist/compiler/tokens.d.ts +13 -0
- package/dist/compiler/tokens.js +127 -0
- package/dist/compiler/tokens.js.map +1 -0
- package/dist/container.d.ts +47 -0
- package/dist/container.js +309 -0
- package/dist/container.js.map +1 -0
- package/dist/engine-entry.d.ts +1 -0
- package/dist/engine-entry.js +71 -0
- package/dist/engine-entry.js.map +1 -0
- package/dist/export-keynote/keynote.d.ts +10 -0
- package/dist/export-keynote/keynote.js +58 -0
- package/dist/export-keynote/keynote.js.map +1 -0
- package/dist/export-pptx/embed-fonts.d.ts +5 -0
- package/dist/export-pptx/embed-fonts.js +159 -0
- package/dist/export-pptx/embed-fonts.js.map +1 -0
- package/dist/export-pptx/inject-anim.d.ts +28 -0
- package/dist/export-pptx/inject-anim.js +150 -0
- package/dist/export-pptx/inject-anim.js.map +1 -0
- package/dist/export-pptx/pptx.d.ts +10 -0
- package/dist/export-pptx/pptx.js +425 -0
- package/dist/export-pptx/pptx.js.map +1 -0
- package/dist/export-pptx/transitions.d.ts +16 -0
- package/dist/export-pptx/transitions.js +82 -0
- package/dist/export-pptx/transitions.js.map +1 -0
- package/dist/import/emit.d.ts +5 -0
- package/dist/import/emit.js +274 -0
- package/dist/import/emit.js.map +1 -0
- package/dist/import/index.d.ts +21 -0
- package/dist/import/index.js +192 -0
- package/dist/import/index.js.map +1 -0
- package/dist/import/pptx.d.ts +117 -0
- package/dist/import/pptx.js +1043 -0
- package/dist/import/pptx.js.map +1 -0
- package/dist/import/raster-com.d.ts +27 -0
- package/dist/import/raster-com.js +113 -0
- package/dist/import/raster-com.js.map +1 -0
- package/dist/import/raster-extension.d.ts +35 -0
- package/dist/import/raster-extension.js +27 -0
- package/dist/import/raster-extension.js.map +1 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +174 -0
- package/dist/index.js.map +1 -0
- package/dist/install/detect.d.ts +17 -0
- package/dist/install/detect.js +82 -0
- package/dist/install/detect.js.map +1 -0
- package/dist/install/index.d.ts +20 -0
- package/dist/install/index.js +126 -0
- package/dist/install/index.js.map +1 -0
- package/dist/install/registry.d.ts +33 -0
- package/dist/install/registry.js +169 -0
- package/dist/install/registry.js.map +1 -0
- package/dist/install/skills.d.ts +17 -0
- package/dist/install/skills.js +72 -0
- package/dist/install/skills.js.map +1 -0
- package/dist/master-io.d.ts +7 -0
- package/dist/master-io.js +55 -0
- package/dist/master-io.js.map +1 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.js +114 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/optimize/export-optimize.d.ts +21 -0
- package/dist/optimize/export-optimize.js +167 -0
- package/dist/optimize/export-optimize.js.map +1 -0
- package/dist/parser/frontmatter.d.ts +8 -0
- package/dist/parser/frontmatter.js +45 -0
- package/dist/parser/frontmatter.js.map +1 -0
- package/dist/parser/parse.d.ts +2 -0
- package/dist/parser/parse.js +184 -0
- package/dist/parser/parse.js.map +1 -0
- package/dist/render/anim.d.ts +50 -0
- package/dist/render/anim.js +213 -0
- package/dist/render/anim.js.map +1 -0
- package/dist/render/charts.d.ts +3 -0
- package/dist/render/charts.js +171 -0
- package/dist/render/charts.js.map +1 -0
- package/dist/render/css.d.ts +3 -0
- package/dist/render/css.js +183 -0
- package/dist/render/css.js.map +1 -0
- package/dist/render/echarts-lib.generated.d.ts +1 -0
- package/dist/render/echarts-lib.generated.js +7 -0
- package/dist/render/echarts-lib.generated.js.map +1 -0
- package/dist/render/html.d.ts +5 -0
- package/dist/render/html.js +186 -0
- package/dist/render/html.js.map +1 -0
- package/dist/render/inject.d.ts +12 -0
- package/dist/render/inject.js +19 -0
- package/dist/render/inject.js.map +1 -0
- package/dist/render/mermaid-lib.generated.d.ts +1 -0
- package/dist/render/mermaid-lib.generated.js +7 -0
- package/dist/render/mermaid-lib.generated.js.map +1 -0
- package/dist/render/runtime.d.ts +1 -0
- package/dist/render/runtime.js +393 -0
- package/dist/render/runtime.js.map +1 -0
- package/dist/render-pdf/pdf.d.ts +9 -0
- package/dist/render-pdf/pdf.js +53 -0
- package/dist/render-pdf/pdf.js.map +1 -0
- package/dist/render-png/montage.d.ts +14 -0
- package/dist/render-png/montage.js +75 -0
- package/dist/render-png/montage.js.map +1 -0
- package/dist/render-png/shoot.d.ts +25 -0
- package/dist/render-png/shoot.js +79 -0
- package/dist/render-png/shoot.js.map +1 -0
- package/dist/scaffold.d.ts +9 -0
- package/dist/scaffold.js +36 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/types.d.ts +212 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +6 -0
- package/dist/util.js +28 -0
- package/dist/util.js.map +1 -0
- package/package.json +66 -0
- package/skills/slaide/SKILL.md +64 -0
- package/skills/slaide/agents/openai.yaml +12 -0
- package/skills/slaide/examples/branded-deck.slaide +198 -0
- package/skills/slaide/examples/branded-theme.slaide.yaml +187 -0
- package/skills/slaide/examples/sample.slaide +98 -0
- package/skills/slaide/reference.md +177 -0
- package/skills/slaide/themes.md +171 -0
- package/themes/aurora.slaide.yaml +136 -0
|
@@ -0,0 +1,1043 @@
|
|
|
1
|
+
// Copyright 2026 AIVory, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// PPTX (OOXML) -> ImportIR. Direct zip+XML parse with EMU->px, theme/scheme-color
|
|
4
|
+
// + clrMap + autofit resolution, recursive shape-tree traversal (groups/connectors/
|
|
5
|
+
// tables), placeholder inheritance (slide -> layout -> master), per-run font/color/
|
|
6
|
+
// gradient/decoration capture, a:br/a:fld, and slide/layout/master backgrounds.
|
|
7
|
+
//
|
|
8
|
+
// Two parsers are used: a flat parser (attribute access) for theme/master/layout/rels,
|
|
9
|
+
// and a preserveOrder parser for slide shape trees so paint order (z-index) and the
|
|
10
|
+
// interleaving of runs / line-breaks / fields are preserved exactly.
|
|
11
|
+
import JSZip from 'jszip';
|
|
12
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { todayFormatted } from '../compiler/chrome.js';
|
|
15
|
+
const EMU_PER_PX = 12700; // PowerPoint points map 1:1 to px in this design space
|
|
16
|
+
const px = (emu) => Math.round(emu / EMU_PER_PX);
|
|
17
|
+
const flat = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_', removeNSPrefix: false, trimValues: false });
|
|
18
|
+
const ordered = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_', preserveOrder: true, trimValues: false });
|
|
19
|
+
function arr(v) {
|
|
20
|
+
if (v === undefined || v === null)
|
|
21
|
+
return [];
|
|
22
|
+
return Array.isArray(v) ? v : [v];
|
|
23
|
+
}
|
|
24
|
+
function tagOf(n) {
|
|
25
|
+
for (const k of Object.keys(n))
|
|
26
|
+
if (k !== ':@')
|
|
27
|
+
return k;
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
function kids(n) {
|
|
31
|
+
if (!n)
|
|
32
|
+
return [];
|
|
33
|
+
const t = tagOf(n);
|
|
34
|
+
return Array.isArray(n[t]) ? n[t] : [];
|
|
35
|
+
}
|
|
36
|
+
function oattr(n, a) {
|
|
37
|
+
const v = n?.[':@']?.['@_' + a];
|
|
38
|
+
return v === undefined ? undefined : String(v);
|
|
39
|
+
}
|
|
40
|
+
function ochild(n, tag) {
|
|
41
|
+
return kids(n).find((c) => tagOf(c) === tag);
|
|
42
|
+
}
|
|
43
|
+
function ochildren(n, tag) {
|
|
44
|
+
return kids(n).filter((c) => tagOf(c) === tag);
|
|
45
|
+
}
|
|
46
|
+
/** First descendant (depth-first) with the given tag. */
|
|
47
|
+
function odeep(n, tag) {
|
|
48
|
+
for (const c of kids(n)) {
|
|
49
|
+
if (tagOf(c) === tag)
|
|
50
|
+
return c;
|
|
51
|
+
const d = odeep(c, tag);
|
|
52
|
+
if (d)
|
|
53
|
+
return d;
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
function otext(n) {
|
|
58
|
+
let s = '';
|
|
59
|
+
for (const c of kids(n)) {
|
|
60
|
+
if ('#text' in c)
|
|
61
|
+
s += c['#text'];
|
|
62
|
+
else
|
|
63
|
+
s += otext(c);
|
|
64
|
+
}
|
|
65
|
+
return s;
|
|
66
|
+
}
|
|
67
|
+
const IDENTITY = { ax: 0, bx: 1, ay: 0, by: 1, rot: 0, flipH: false, flipV: false };
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Color resolution (works on the mini-DOM). `scheme` = theme clrScheme resolved to
|
|
70
|
+
// hex; `clrMap` remaps logical names (bg1/tx1/...) to scheme slots per the master.
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
function sysToHex(last, name) {
|
|
73
|
+
if (last)
|
|
74
|
+
return '#' + last;
|
|
75
|
+
return name === 'window' ? '#FFFFFF' : '#000000';
|
|
76
|
+
}
|
|
77
|
+
function resolveScheme(clrSchemeNode) {
|
|
78
|
+
const out = {};
|
|
79
|
+
for (const c of kids(clrSchemeNode)) {
|
|
80
|
+
const key = tagOf(c).replace(/^a:/, '');
|
|
81
|
+
const srgb = ochild(c, 'a:srgbClr');
|
|
82
|
+
const sys = ochild(c, 'a:sysClr');
|
|
83
|
+
if (srgb)
|
|
84
|
+
out[key] = '#' + oattr(srgb, 'val');
|
|
85
|
+
else if (sys)
|
|
86
|
+
out[key] = sysToHex(oattr(sys, 'lastClr'), oattr(sys, 'val'));
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
function clamp(n) {
|
|
91
|
+
return Math.max(0, Math.min(255, Math.round(n)));
|
|
92
|
+
}
|
|
93
|
+
function hexToRgb(hex) {
|
|
94
|
+
const m = hex.replace('#', '');
|
|
95
|
+
return [parseInt(m.slice(0, 2), 16), parseInt(m.slice(2, 4), 16), parseInt(m.slice(4, 6), 16)];
|
|
96
|
+
}
|
|
97
|
+
function rgbToHex(r, g, b) {
|
|
98
|
+
return '#' + [r, g, b].map((v) => clamp(v).toString(16).padStart(2, '0')).join('').toUpperCase();
|
|
99
|
+
}
|
|
100
|
+
// --- HSL conversions (lumMod/lumOff/satMod/hueMod operate in HSL, like PowerPoint) ---
|
|
101
|
+
function rgbToHsl(r, g, b) {
|
|
102
|
+
r /= 255;
|
|
103
|
+
g /= 255;
|
|
104
|
+
b /= 255;
|
|
105
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
106
|
+
let h = 0, s = 0;
|
|
107
|
+
const l = (max + min) / 2;
|
|
108
|
+
const d = max - min;
|
|
109
|
+
if (d !== 0) {
|
|
110
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
111
|
+
if (max === r)
|
|
112
|
+
h = ((g - b) / d + (g < b ? 6 : 0));
|
|
113
|
+
else if (max === g)
|
|
114
|
+
h = (b - r) / d + 2;
|
|
115
|
+
else
|
|
116
|
+
h = (r - g) / d + 4;
|
|
117
|
+
h /= 6;
|
|
118
|
+
}
|
|
119
|
+
return [h, s, l];
|
|
120
|
+
}
|
|
121
|
+
function hue2rgb(p, q, t) {
|
|
122
|
+
if (t < 0)
|
|
123
|
+
t += 1;
|
|
124
|
+
if (t > 1)
|
|
125
|
+
t -= 1;
|
|
126
|
+
if (t < 1 / 6)
|
|
127
|
+
return p + (q - p) * 6 * t;
|
|
128
|
+
if (t < 1 / 2)
|
|
129
|
+
return q;
|
|
130
|
+
if (t < 2 / 3)
|
|
131
|
+
return p + (q - p) * (2 / 3 - t) * 6;
|
|
132
|
+
return p;
|
|
133
|
+
}
|
|
134
|
+
function hslToRgb(h, s, l) {
|
|
135
|
+
if (s === 0)
|
|
136
|
+
return [l * 255, l * 255, l * 255];
|
|
137
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
138
|
+
const p = 2 * l - q;
|
|
139
|
+
return [hue2rgb(p, q, h + 1 / 3) * 255, hue2rgb(p, q, h) * 255, hue2rgb(p, q, h - 1 / 3) * 255];
|
|
140
|
+
}
|
|
141
|
+
/** Apply OOXML color modifiers (children of a color node) in document order.
|
|
142
|
+
* shade/tint act in sRGB; lumMod/lumOff/satMod/hueMod act in HSL — matching how
|
|
143
|
+
* PowerPoint derives the tinted/shaded variants used throughout theme palettes. */
|
|
144
|
+
function applyMods(hex, colorNode) {
|
|
145
|
+
let [r, g, b] = hexToRgb(hex);
|
|
146
|
+
for (const child of kids(colorNode)) {
|
|
147
|
+
const tag = tagOf(child);
|
|
148
|
+
const v = oattr(child, 'val');
|
|
149
|
+
if (v === undefined)
|
|
150
|
+
continue;
|
|
151
|
+
const f = Number(v) / 100000;
|
|
152
|
+
switch (tag) {
|
|
153
|
+
case 'a:shade':
|
|
154
|
+
r *= f;
|
|
155
|
+
g *= f;
|
|
156
|
+
b *= f;
|
|
157
|
+
break;
|
|
158
|
+
case 'a:tint':
|
|
159
|
+
r = r * f + 255 * (1 - f);
|
|
160
|
+
g = g * f + 255 * (1 - f);
|
|
161
|
+
b = b * f + 255 * (1 - f);
|
|
162
|
+
break;
|
|
163
|
+
case 'a:lumMod': {
|
|
164
|
+
const [h, s, l] = rgbToHsl(r, g, b);
|
|
165
|
+
[r, g, b] = hslToRgb(h, s, Math.min(1, l * f));
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case 'a:lumOff': {
|
|
169
|
+
const [h, s, l] = rgbToHsl(r, g, b);
|
|
170
|
+
[r, g, b] = hslToRgb(h, s, Math.min(1, Math.max(0, l + f)));
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case 'a:satMod': {
|
|
174
|
+
const [h, s, l] = rgbToHsl(r, g, b);
|
|
175
|
+
[r, g, b] = hslToRgb(h, Math.min(1, s * f), l);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case 'a:hueMod': {
|
|
179
|
+
const [h, s, l] = rgbToHsl(r, g, b);
|
|
180
|
+
[r, g, b] = hslToRgb((h * f) % 1, s, l);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return rgbToHex(r, g, b);
|
|
186
|
+
}
|
|
187
|
+
/** Apply an a:alpha child (1/1000 %) -> rgba() string; otherwise return the hex. */
|
|
188
|
+
function applyAlpha(hex, colorNode) {
|
|
189
|
+
const a = ochild(colorNode, 'a:alpha');
|
|
190
|
+
if (!a)
|
|
191
|
+
return hex;
|
|
192
|
+
const alpha = Math.round((Number(oattr(a, 'val')) / 100000) * 1000) / 1000;
|
|
193
|
+
if (alpha >= 1)
|
|
194
|
+
return hex;
|
|
195
|
+
const [r, g, b] = hexToRgb(hex);
|
|
196
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
197
|
+
}
|
|
198
|
+
/** Resolve a node that *holds* a color child (a:srgbClr | a:schemeClr). May be rgba(). */
|
|
199
|
+
function colorFrom(holder, ctx) {
|
|
200
|
+
const srgb = ochild(holder, 'a:srgbClr');
|
|
201
|
+
if (srgb)
|
|
202
|
+
return applyAlpha(applyMods('#' + oattr(srgb, 'val'), srgb), srgb);
|
|
203
|
+
const sc = ochild(holder, 'a:schemeClr');
|
|
204
|
+
if (sc) {
|
|
205
|
+
const val = oattr(sc, 'val');
|
|
206
|
+
const mapped = ctx.clrMap[val] ?? val;
|
|
207
|
+
const base = ctx.scheme[mapped] ?? ctx.scheme[val];
|
|
208
|
+
if (base)
|
|
209
|
+
return applyAlpha(applyMods(base, sc), sc);
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
/** Solid fill color of a parent node that may contain a:solidFill. */
|
|
214
|
+
function solidFill(parent, ctx) {
|
|
215
|
+
return colorFrom(ochild(parent, 'a:solidFill'), ctx);
|
|
216
|
+
}
|
|
217
|
+
/** Returns {color} for solid fills or {gradient} for gradient fills. */
|
|
218
|
+
function fillInfo(parent, ctx) {
|
|
219
|
+
const solid = solidFill(parent, ctx);
|
|
220
|
+
if (solid)
|
|
221
|
+
return { color: solid };
|
|
222
|
+
const g = ochild(parent, 'a:gradFill');
|
|
223
|
+
if (g) {
|
|
224
|
+
const stops = ochildren(ochild(g, 'a:gsLst'), 'a:gs');
|
|
225
|
+
const cols = stops.map((s) => colorFrom(s, ctx)).filter(Boolean);
|
|
226
|
+
const lin = ochild(g, 'a:lin');
|
|
227
|
+
const ang = lin ? Math.round(Number(oattr(lin, 'ang')) / 60000) : 135;
|
|
228
|
+
if (cols.length >= 2)
|
|
229
|
+
return { gradient: `linear-gradient(${ang}deg, ${cols.join(', ')})` };
|
|
230
|
+
if (cols.length === 1)
|
|
231
|
+
return { color: cols[0] };
|
|
232
|
+
}
|
|
233
|
+
return {};
|
|
234
|
+
}
|
|
235
|
+
/** Border/outline of a shape (a:ln) -> {color, width px}, or undefined if none. */
|
|
236
|
+
function strokeOf(spPr, ctx) {
|
|
237
|
+
const ln = ochild(spPr, 'a:ln');
|
|
238
|
+
if (!ln || ochild(ln, 'a:noFill'))
|
|
239
|
+
return undefined;
|
|
240
|
+
const color = colorFrom(ochild(ln, 'a:solidFill'), ctx);
|
|
241
|
+
if (!color)
|
|
242
|
+
return undefined;
|
|
243
|
+
const w = oattr(ln, 'w');
|
|
244
|
+
return { color, width: w ? Math.max(1, px(Number(w))) : 1 };
|
|
245
|
+
}
|
|
246
|
+
function fontFromLatin(latin, major, minor) {
|
|
247
|
+
const tf = oattr(latin, 'typeface');
|
|
248
|
+
if (!tf)
|
|
249
|
+
return undefined;
|
|
250
|
+
if (tf === '+mj-lt')
|
|
251
|
+
return major;
|
|
252
|
+
if (tf === '+mn-lt')
|
|
253
|
+
return minor;
|
|
254
|
+
return tf;
|
|
255
|
+
}
|
|
256
|
+
function runDefaultsFromRPr(rPr, ctx, major, minor) {
|
|
257
|
+
if (!rPr)
|
|
258
|
+
return {};
|
|
259
|
+
const fi = fillInfo(rPr, ctx);
|
|
260
|
+
const szRaw = oattr(rPr, 'sz');
|
|
261
|
+
const rawFont = fontFromLatin(ochild(rPr, 'a:latin'), major, minor);
|
|
262
|
+
const nf = rawFont ? normalizeFont(rawFont) : undefined;
|
|
263
|
+
return {
|
|
264
|
+
size: szRaw ? Number(szRaw) / 100 : undefined,
|
|
265
|
+
font: nf?.family,
|
|
266
|
+
weight: nf?.weight,
|
|
267
|
+
color: fi.color,
|
|
268
|
+
gradient: fi.gradient,
|
|
269
|
+
bold: oattr(rPr, 'b') === '1' || undefined,
|
|
270
|
+
italic: oattr(rPr, 'i') === '1' || nf?.italic || undefined,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function anchorToValign(a) {
|
|
274
|
+
if (a === 'ctr')
|
|
275
|
+
return 'center';
|
|
276
|
+
if (a === 'b')
|
|
277
|
+
return 'bottom';
|
|
278
|
+
if (a === 't')
|
|
279
|
+
return 'top';
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
function boxFromXfrm(xf) {
|
|
283
|
+
const off = ochild(xf, 'a:off');
|
|
284
|
+
const ext = ochild(xf, 'a:ext');
|
|
285
|
+
if (!off || !ext)
|
|
286
|
+
return undefined;
|
|
287
|
+
return {
|
|
288
|
+
x: px(Number(oattr(off, 'x'))),
|
|
289
|
+
y: px(Number(oattr(off, 'y'))),
|
|
290
|
+
w: px(Number(oattr(ext, 'cx'))),
|
|
291
|
+
h: px(Number(oattr(ext, 'cy'))),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/** Field-wise merge of run defaults: defined fields in `b` (the higher-priority
|
|
295
|
+
* source, e.g. the layout) win, but `b`'s undefined fields keep `a`'s value. */
|
|
296
|
+
function mergeRunDefaults(a, b) {
|
|
297
|
+
if (!a)
|
|
298
|
+
return b;
|
|
299
|
+
if (!b)
|
|
300
|
+
return a;
|
|
301
|
+
const out = { ...a };
|
|
302
|
+
for (const k of Object.keys(b))
|
|
303
|
+
if (b[k] !== undefined)
|
|
304
|
+
out[k] = b[k];
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
function mergePhInfo(a, b) {
|
|
308
|
+
if (!a)
|
|
309
|
+
return b;
|
|
310
|
+
return { box: b.box ?? a.box, valign: b.valign ?? a.valign, run: mergeRunDefaults(a.run, b.run) };
|
|
311
|
+
}
|
|
312
|
+
/** Parse a slideLayout or slideMaster spTree for placeholder geometry + run defaults.
|
|
313
|
+
* Call master first, then layout — layout placeholders OVERRIDE the master per field
|
|
314
|
+
* (e.g. a dark layout recolours the title to bg1/white over the master's dark default). */
|
|
315
|
+
function indexPlaceholders(spTree, ctx, major, minor, into) {
|
|
316
|
+
for (const sp of ochildren(spTree, 'p:sp')) {
|
|
317
|
+
const ph = odeep(ochild(sp, 'p:nvSpPr'), 'p:ph');
|
|
318
|
+
if (!ph)
|
|
319
|
+
continue;
|
|
320
|
+
const type = oattr(ph, 'type') ?? 'body';
|
|
321
|
+
const idx = oattr(ph, 'idx') ?? '';
|
|
322
|
+
const spPr = ochild(sp, 'p:spPr');
|
|
323
|
+
const box = boxFromXfrm(ochild(spPr, 'a:xfrm'));
|
|
324
|
+
const bodyPr = odeep(ochild(sp, 'p:txBody'), 'a:bodyPr');
|
|
325
|
+
const lvl1 = odeep(ochild(sp, 'p:txBody'), 'a:lvl1pPr');
|
|
326
|
+
const defRPr = lvl1 ? ochild(lvl1, 'a:defRPr') : undefined;
|
|
327
|
+
const info = {
|
|
328
|
+
box,
|
|
329
|
+
valign: anchorToValign(oattr(bodyPr, 'anchor')),
|
|
330
|
+
run: defRPr ? runDefaultsFromRPr(defRPr, ctx, major, minor) : undefined,
|
|
331
|
+
};
|
|
332
|
+
const keyFull = `${type}#${idx}`;
|
|
333
|
+
into[keyFull] = mergePhInfo(into[keyFull], info);
|
|
334
|
+
if (idx === '' || !into[type])
|
|
335
|
+
into[type] = mergePhInfo(into[type], info); // by-type fallback
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function txStylesFrom(masterRoot, ctx, major, minor) {
|
|
339
|
+
const ts = odeep(masterRoot, 'p:txStyles');
|
|
340
|
+
const role = (tag) => {
|
|
341
|
+
const styleNode = ochild(ts, tag);
|
|
342
|
+
const out = [];
|
|
343
|
+
for (let i = 1; i <= 9; i++) {
|
|
344
|
+
const lvl = ochild(styleNode, `a:lvl${i}pPr`);
|
|
345
|
+
const defRPr = lvl ? ochild(lvl, 'a:defRPr') : undefined;
|
|
346
|
+
out[i - 1] = defRPr ? runDefaultsFromRPr(defRPr, ctx, major, minor) : {};
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
};
|
|
350
|
+
return { title: role('p:titleStyle'), body: role('p:bodyStyle'), other: role('p:otherStyle') };
|
|
351
|
+
}
|
|
352
|
+
function bgFrom(node, ctx, mediaResolve) {
|
|
353
|
+
const bg = ochild(node, 'p:bg');
|
|
354
|
+
if (!bg)
|
|
355
|
+
return undefined;
|
|
356
|
+
const bgPr = ochild(bg, 'p:bgPr');
|
|
357
|
+
if (bgPr) {
|
|
358
|
+
const fi = fillInfo(bgPr, ctx);
|
|
359
|
+
if (fi.gradient)
|
|
360
|
+
return { kind: 'gradient', gradient: fi.gradient };
|
|
361
|
+
if (fi.color)
|
|
362
|
+
return { kind: 'solid', color: fi.color };
|
|
363
|
+
const blip = odeep(bgPr, 'a:blip');
|
|
364
|
+
const src = mediaResolve(oattr(blip, 'r:embed'));
|
|
365
|
+
if (src)
|
|
366
|
+
return { kind: 'image', src };
|
|
367
|
+
}
|
|
368
|
+
// bgRef (scheme fill index) — approximate with the mapped scheme color.
|
|
369
|
+
const bgRef = ochild(bg, 'p:bgRef');
|
|
370
|
+
if (bgRef) {
|
|
371
|
+
const col = colorFrom(bgRef, ctx);
|
|
372
|
+
if (col)
|
|
373
|
+
return { kind: 'solid', color: col };
|
|
374
|
+
}
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
// Merge consecutive runs whose every style-bearing field is identical. PowerPoint
|
|
378
|
+
// frequently splits a single styled phrase into many runs (spell-check spans, edit
|
|
379
|
+
// history); rendering each as its own inline box breaks cross-run kerning/ligatures,
|
|
380
|
+
// so the text width drifts from the original. Merging restores one flow box.
|
|
381
|
+
const RUN_STYLE_KEYS = [
|
|
382
|
+
'bold', 'italic', 'underline', 'strike', 'size', 'color', 'gradient', 'font', 'weight', 'spacing', 'caps', 'baseline', 'href',
|
|
383
|
+
];
|
|
384
|
+
function sameStyle(a, b) {
|
|
385
|
+
if (a.br || b.br)
|
|
386
|
+
return false;
|
|
387
|
+
return RUN_STYLE_KEYS.every((k) => a[k] === b[k]);
|
|
388
|
+
}
|
|
389
|
+
function mergeRuns(runs) {
|
|
390
|
+
for (let i = runs.length - 1; i > 0; i--) {
|
|
391
|
+
if (sameStyle(runs[i - 1], runs[i])) {
|
|
392
|
+
runs[i - 1].text += runs[i].text;
|
|
393
|
+
runs.splice(i, 1);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
// Text body -> paragraphs (ordered: runs, breaks, fields interleaved).
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
function parseTxBody(txBody, ctx, major, minor, def, nonGoogle, fieldText) {
|
|
401
|
+
if (!txBody)
|
|
402
|
+
return [];
|
|
403
|
+
const bodyPr = ochild(txBody, 'a:bodyPr');
|
|
404
|
+
const na = ochild(bodyPr, 'a:normAutofit');
|
|
405
|
+
const scale = na ? (Number(oattr(na, 'fontScale')) || 100000) / 100000 : 1;
|
|
406
|
+
const paras = [];
|
|
407
|
+
for (const p of ochildren(txBody, 'a:p')) {
|
|
408
|
+
const pPr = ochild(p, 'a:pPr');
|
|
409
|
+
const buChar = ochild(pPr, 'a:buChar');
|
|
410
|
+
const buAuto = ochild(pPr, 'a:buAutoNum');
|
|
411
|
+
const buNone = ochild(pPr, 'a:buNone');
|
|
412
|
+
const lnSpcPct = odeep(ochild(pPr, 'a:lnSpc'), 'a:spcPct');
|
|
413
|
+
const spcBef = odeep(ochild(pPr, 'a:spcBef'), 'a:spcPts');
|
|
414
|
+
const spcAft = odeep(ochild(pPr, 'a:spcAft'), 'a:spcPts');
|
|
415
|
+
const runs = [];
|
|
416
|
+
for (const node of kids(p)) {
|
|
417
|
+
const t = tagOf(node);
|
|
418
|
+
if (t === 'a:br') {
|
|
419
|
+
runs.push({ text: '', br: true });
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (t !== 'a:r' && t !== 'a:fld')
|
|
423
|
+
continue;
|
|
424
|
+
const rPr = ochild(node, 'a:rPr');
|
|
425
|
+
let text;
|
|
426
|
+
if (t === 'a:fld') {
|
|
427
|
+
text = fieldText(oattr(node, 'type'), otext(ochild(node, 'a:t')));
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
text = otext(ochild(node, 'a:t'));
|
|
431
|
+
}
|
|
432
|
+
if (text === '')
|
|
433
|
+
continue;
|
|
434
|
+
const fi = fillInfo(rPr, ctx);
|
|
435
|
+
const szRaw = oattr(rPr, 'sz');
|
|
436
|
+
const latinFont = fontFromLatin(ochild(rPr, 'a:latin'), major, minor);
|
|
437
|
+
const nf = latinFont ? normalizeFont(latinFont) : undefined;
|
|
438
|
+
const font = nf?.family ?? def?.font;
|
|
439
|
+
const weight = nf ? nf.weight : def?.weight;
|
|
440
|
+
if (font && !isGoogleFont(font))
|
|
441
|
+
nonGoogle.add(font);
|
|
442
|
+
const spc = oattr(rPr, 'spc');
|
|
443
|
+
const cap = oattr(rPr, 'cap');
|
|
444
|
+
const baseline = Number(oattr(rPr, 'baseline') || 0);
|
|
445
|
+
const href = ochild(rPr, 'a:hlinkClick') ? 'href' : undefined; // presence flag (target resolution omitted)
|
|
446
|
+
const sizePx = szRaw ? Number(szRaw) / 100 : def?.size;
|
|
447
|
+
runs.push({
|
|
448
|
+
text,
|
|
449
|
+
bold: oattr(rPr, 'b') === '1' || (szRaw ? undefined : def?.bold),
|
|
450
|
+
italic: oattr(rPr, 'i') === '1' || nf?.italic || (szRaw ? undefined : def?.italic),
|
|
451
|
+
underline: oattr(rPr, 'u') !== undefined && oattr(rPr, 'u') !== 'none',
|
|
452
|
+
strike: oattr(rPr, 'strike') !== undefined && oattr(rPr, 'strike') !== 'noStrike',
|
|
453
|
+
size: sizePx !== undefined ? Math.round(sizePx * scale * 10) / 10 : undefined,
|
|
454
|
+
color: fi.color ?? def?.color,
|
|
455
|
+
gradient: fi.gradient ?? def?.gradient,
|
|
456
|
+
font,
|
|
457
|
+
weight,
|
|
458
|
+
spacing: spc ? Math.round((Number(spc) / 100) * 10) / 10 : undefined,
|
|
459
|
+
caps: cap === 'all' ? 'all' : cap === 'small' ? 'small' : undefined,
|
|
460
|
+
baseline: baseline > 0 ? 'sup' : baseline < 0 ? 'sub' : undefined,
|
|
461
|
+
href,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
if (!runs.length)
|
|
465
|
+
continue;
|
|
466
|
+
mergeRuns(runs);
|
|
467
|
+
paras.push({
|
|
468
|
+
runs,
|
|
469
|
+
bullet: !!(buChar || buAuto) && !buNone,
|
|
470
|
+
bulletChar: buChar ? oattr(buChar, 'char') : undefined,
|
|
471
|
+
ordered: !!buAuto,
|
|
472
|
+
level: Number(oattr(pPr, 'lvl')) || 0,
|
|
473
|
+
align: oattr(pPr, 'algn'),
|
|
474
|
+
lineHeight: lnSpcPct ? Number(oattr(lnSpcPct, 'val')) / 100000 : undefined,
|
|
475
|
+
spaceBefore: spcBef ? Math.round(Number(oattr(spcBef, 'val')) / 100) : undefined,
|
|
476
|
+
spaceAfter: spcAft ? Math.round(Number(oattr(spcAft, 'val')) / 100) : undefined,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
return paras;
|
|
480
|
+
}
|
|
481
|
+
// Heuristic: which families Google Fonts can serve. Anything else -> system provider.
|
|
482
|
+
const GOOGLE_FONTS = new Set([
|
|
483
|
+
'Inter', 'Roboto', 'Open Sans', 'Lato', 'Montserrat', 'Poppins', 'Raleway', 'Nunito', 'Nunito Sans',
|
|
484
|
+
'Work Sans', 'Source Sans Pro', 'Source Sans 3', 'Source Serif 4', 'Merriweather', 'Playfair Display',
|
|
485
|
+
'Oswald', 'Rubik', 'Mulish', 'Manrope', 'DM Sans', 'DM Serif Display', 'Space Grotesk', 'Karla',
|
|
486
|
+
'Fira Sans', 'Fira Code', 'JetBrains Mono', 'IBM Plex Sans', 'IBM Plex Mono', 'IBM Plex Serif',
|
|
487
|
+
'Quicksand', 'Josefin Sans', 'Barlow', 'Cabin', 'PT Sans', 'PT Serif', 'Noto Sans', 'Noto Serif',
|
|
488
|
+
'Archivo', 'Libre Franklin', 'Libre Baskerville', 'Bitter', 'Crimson Text', 'EB Garamond', 'Lora',
|
|
489
|
+
'Hind', 'Titillium Web', 'Heebo', 'Assistant', 'Exo 2', 'Teko', 'Anton', 'Bebas Neue',
|
|
490
|
+
].map((f) => f.toLowerCase()));
|
|
491
|
+
function isGoogleFont(family) {
|
|
492
|
+
return GOOGLE_FONTS.has(family.trim().toLowerCase());
|
|
493
|
+
}
|
|
494
|
+
// ---------------------------------------------------------------------------
|
|
495
|
+
// Font-family normalization. PowerPoint stores weight/style variants as part of
|
|
496
|
+
// the family name ("Open Sans Extrabold", "Lato Black", "Open Sans Light"). The
|
|
497
|
+
// web has no such families — the browser falls back to system-ui and every glyph
|
|
498
|
+
// gets the wrong width, which is the single biggest source of text drift. Peel the
|
|
499
|
+
// trailing weight/style words to a real base family + a numeric weight so the
|
|
500
|
+
// actual web font loads and renders at the intended weight.
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
const WEIGHT_SUFFIX = /[\s-]+(thin|hairline|extra[\s-]?light|ultra[\s-]?light|semi[\s-]?light|light|regular|normal|book|medium|semi[\s-]?bold|demi[\s-]?bold|extra[\s-]?bold|ultra[\s-]?bold|bold|extra[\s-]?black|ultra[\s-]?black|black|heavy)$/i;
|
|
503
|
+
const WEIGHT_MAP = {
|
|
504
|
+
thin: 100, hairline: 100, extralight: 200, ultralight: 200, semilight: 350, light: 300,
|
|
505
|
+
regular: 400, normal: 400, book: 400, medium: 500, semibold: 600, demibold: 600,
|
|
506
|
+
bold: 700, extrabold: 800, ultrabold: 800, black: 900, heavy: 900, extrablack: 950, ultrablack: 950,
|
|
507
|
+
};
|
|
508
|
+
const normCache = new Map();
|
|
509
|
+
export function normalizeFont(raw) {
|
|
510
|
+
const key = raw.trim();
|
|
511
|
+
const hit = normCache.get(key);
|
|
512
|
+
if (hit)
|
|
513
|
+
return hit;
|
|
514
|
+
let family = key;
|
|
515
|
+
let weight;
|
|
516
|
+
let italic;
|
|
517
|
+
// Don't decompose a name the web already serves verbatim (e.g. "Archivo Black",
|
|
518
|
+
// "DM Serif Display", "Bebas Neue") — those are real, distinct families.
|
|
519
|
+
if (!isGoogleFont(family)) {
|
|
520
|
+
// Peel a trailing italic/oblique word, then any number of weight words.
|
|
521
|
+
const it = family.match(/[\s-]+(italic|oblique)$/i);
|
|
522
|
+
if (it) {
|
|
523
|
+
italic = true;
|
|
524
|
+
family = family.slice(0, it.index).trim();
|
|
525
|
+
}
|
|
526
|
+
let m;
|
|
527
|
+
while ((m = family.match(WEIGHT_SUFFIX))) {
|
|
528
|
+
const word = m[1].toLowerCase().replace(/[\s-]/g, '');
|
|
529
|
+
weight = WEIGHT_MAP[word] ?? weight;
|
|
530
|
+
family = family.slice(0, m.index).trim();
|
|
531
|
+
if (!family) {
|
|
532
|
+
family = key;
|
|
533
|
+
break;
|
|
534
|
+
} // never strip to nothing
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
const out = { family: family || key, weight, italic };
|
|
538
|
+
normCache.set(key, out);
|
|
539
|
+
return out;
|
|
540
|
+
}
|
|
541
|
+
// ---------------------------------------------------------------------------
|
|
542
|
+
// Geometry helpers operating under a transform context.
|
|
543
|
+
// ---------------------------------------------------------------------------
|
|
544
|
+
function composedBox(off, ext, T) {
|
|
545
|
+
const ox = Number(oattr(off, 'x'));
|
|
546
|
+
const oy = Number(oattr(off, 'y'));
|
|
547
|
+
const ex = Number(oattr(ext, 'cx'));
|
|
548
|
+
const ey = Number(oattr(ext, 'cy'));
|
|
549
|
+
const f = (emu) => Math.round((emu / EMU_PER_PX) * 10) / 10; // 0.1px precision
|
|
550
|
+
return {
|
|
551
|
+
x: f(T.ax + ox * T.bx),
|
|
552
|
+
y: f(T.ay + oy * T.by),
|
|
553
|
+
w: f(ex * T.bx),
|
|
554
|
+
h: f(ey * T.by),
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
/** Composed box from an a:xfrm node, or undefined if it lacks off/ext. */
|
|
558
|
+
function composedBoxOf(xf, T) {
|
|
559
|
+
const off = ochild(xf, 'a:off');
|
|
560
|
+
const ext = ochild(xf, 'a:ext');
|
|
561
|
+
if (!off || !ext)
|
|
562
|
+
return undefined;
|
|
563
|
+
return composedBox(off, ext, T);
|
|
564
|
+
}
|
|
565
|
+
function composeGroup(T, grpXfrm) {
|
|
566
|
+
const off = ochild(grpXfrm, 'a:off');
|
|
567
|
+
const ext = ochild(grpXfrm, 'a:ext');
|
|
568
|
+
const chOff = ochild(grpXfrm, 'a:chOff') ?? off;
|
|
569
|
+
const chExt = ochild(grpXfrm, 'a:chExt') ?? ext;
|
|
570
|
+
const offX = Number(oattr(off, 'x'));
|
|
571
|
+
const offY = Number(oattr(off, 'y'));
|
|
572
|
+
const sx = Number(oattr(ext, 'cx')) / (Number(oattr(chExt, 'cx')) || 1);
|
|
573
|
+
const sy = Number(oattr(ext, 'cy')) / (Number(oattr(chExt, 'cy')) || 1);
|
|
574
|
+
const aPrimeX = offX - Number(oattr(chOff, 'x')) * sx;
|
|
575
|
+
const aPrimeY = offY - Number(oattr(chOff, 'y')) * sy;
|
|
576
|
+
const rotDeg = Number(oattr(grpXfrm, 'rot') || 0) / 60000;
|
|
577
|
+
return {
|
|
578
|
+
ax: T.ax + aPrimeX * T.bx,
|
|
579
|
+
bx: sx * T.bx,
|
|
580
|
+
ay: T.ay + aPrimeY * T.by,
|
|
581
|
+
by: sy * T.by,
|
|
582
|
+
rot: T.rot + rotDeg,
|
|
583
|
+
flipH: T.flipH !== (oattr(grpXfrm, 'flipH') === '1'),
|
|
584
|
+
flipV: T.flipV !== (oattr(grpXfrm, 'flipV') === '1'),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function insetsOf(bodyPr) {
|
|
588
|
+
if (!bodyPr)
|
|
589
|
+
return undefined;
|
|
590
|
+
const has = ['lIns', 'tIns', 'rIns', 'bIns'].some((a) => oattr(bodyPr, a) !== undefined);
|
|
591
|
+
if (!has)
|
|
592
|
+
return undefined;
|
|
593
|
+
const v = (a, d) => (oattr(bodyPr, a) !== undefined ? px(Number(oattr(bodyPr, a))) : d);
|
|
594
|
+
return { l: v('lIns', 7), t: v('tIns', 4), r: v('rIns', 7), b: v('bIns', 4) };
|
|
595
|
+
}
|
|
596
|
+
function rolePh(ph) {
|
|
597
|
+
if (ph === 'title' || ph === 'ctrTitle')
|
|
598
|
+
return 'title';
|
|
599
|
+
if (ph === 'body' || ph === 'subTitle')
|
|
600
|
+
return 'body';
|
|
601
|
+
return 'other';
|
|
602
|
+
}
|
|
603
|
+
function walkTree(node, T, ctx, out) {
|
|
604
|
+
for (const ch of kids(node)) {
|
|
605
|
+
const t = tagOf(ch);
|
|
606
|
+
if (t === 'p:sp')
|
|
607
|
+
handleSp(ch, T, ctx, out);
|
|
608
|
+
else if (t === 'p:pic')
|
|
609
|
+
handlePic(ch, T, ctx, out);
|
|
610
|
+
else if (t === 'p:cxnSp')
|
|
611
|
+
handleCxn(ch, T, ctx, out);
|
|
612
|
+
else if (t === 'p:graphicFrame')
|
|
613
|
+
handleGraphicFrame(ch, T, ctx, out);
|
|
614
|
+
else if (t === 'p:grpSp')
|
|
615
|
+
handleGroup(ch, T, ctx, out);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
function cNvName(nv) {
|
|
619
|
+
const cNvPr = odeep(nv, 'p:cNvPr');
|
|
620
|
+
return { name: oattr(cNvPr, 'name'), id: oattr(cNvPr, 'id') };
|
|
621
|
+
}
|
|
622
|
+
function handleSp(sp, T, ctx, out) {
|
|
623
|
+
const nv = ochild(sp, 'p:nvSpPr');
|
|
624
|
+
const ph = odeep(nv, 'p:ph');
|
|
625
|
+
const phType = ph ? oattr(ph, 'type') ?? 'body' : undefined;
|
|
626
|
+
const phIdx = ph ? oattr(ph, 'idx') ?? '' : undefined;
|
|
627
|
+
if (ctx.skipPlaceholders && ph)
|
|
628
|
+
return; // layout/master placeholders are filled by the slide
|
|
629
|
+
const { name, id } = cNvName(nv);
|
|
630
|
+
const spPr = ochild(sp, 'p:spPr');
|
|
631
|
+
const xf = ochild(spPr, 'a:xfrm');
|
|
632
|
+
// Geometry: explicit, else inherited from placeholder.
|
|
633
|
+
let box = composedBoxOf(xf, T);
|
|
634
|
+
const inh = phType ? ctx.inherit.ph[`${phType}#${phIdx}`] ?? ctx.inherit.ph[phType] : undefined;
|
|
635
|
+
if (!box && inh?.box)
|
|
636
|
+
box = inh.box;
|
|
637
|
+
if (!box) {
|
|
638
|
+
// Last resort: skip but warn (truly geometry-less, e.g. an off-slide placeholder).
|
|
639
|
+
const probe = parseTxBody(ochild(sp, 'p:txBody'), ctx.color, ctx.major, ctx.minor, undefined, ctx.nonGoogle, ctx.fieldText);
|
|
640
|
+
if (probe.length)
|
|
641
|
+
ctx.warnings.push(`Skipped a ${phType ?? 'text'} shape with no resolvable geometry.`);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const rot = (xf ? Number(oattr(xf, 'rot') || 0) / 60000 : 0) + T.rot;
|
|
645
|
+
const flipH = (oattr(xf, 'flipH') === '1') !== T.flipH;
|
|
646
|
+
const flipV = (oattr(xf, 'flipV') === '1') !== T.flipV;
|
|
647
|
+
// Custom geometry -> rasterize (we can't reproduce arbitrary paths).
|
|
648
|
+
const prst = oattr(ochild(spPr, 'a:prstGeom'), 'prst');
|
|
649
|
+
if (ochild(spPr, 'a:custGeom')) {
|
|
650
|
+
out.push({ kind: 'raster', ...box, rotation: rot || undefined, name, id, rasterReq: { name, id, reason: 'custom-geometry' } });
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
// Run defaults, lowest→highest priority: master txStyles < inherited placeholder
|
|
654
|
+
// (layout over master) < the shape's own lstStyle defRPr. mergeRunDefaults keeps a
|
|
655
|
+
// lower layer's value where the higher layer leaves a field undefined (so e.g. a
|
|
656
|
+
// placeholder that only recolours doesn't wipe the inherited size).
|
|
657
|
+
const role = rolePh(phType);
|
|
658
|
+
let def = mergeRunDefaults(ctx.inherit.txStyles[role]?.[0], inh?.run) ?? {};
|
|
659
|
+
const lstDefRPr = odeep(ochild(ochild(sp, 'p:txBody'), 'a:lstStyle'), 'a:defRPr');
|
|
660
|
+
if (lstDefRPr)
|
|
661
|
+
def = mergeRunDefaults(def, runDefaultsFromRPr(lstDefRPr, ctx.color, ctx.major, ctx.minor)) ?? def;
|
|
662
|
+
const paras = parseTxBody(ochild(sp, 'p:txBody'), ctx.color, ctx.major, ctx.minor, def, ctx.nonGoogle, ctx.fieldText);
|
|
663
|
+
const bodyPr = odeep(ochild(sp, 'p:txBody'), 'a:bodyPr');
|
|
664
|
+
const valign = anchorToValign(oattr(bodyPr, 'anchor')) ?? inh?.valign;
|
|
665
|
+
const fi = fillInfo(spPr, ctx.color);
|
|
666
|
+
let fillColor = fi.gradient ?? fi.color;
|
|
667
|
+
let stroke = strokeOf(spPr, ctx.color);
|
|
668
|
+
// Fall back to the shape's theme style references (p:style/fillRef,lnRef) when the
|
|
669
|
+
// shape has no direct fill/line — idx "0" means none. Many template shapes (cards,
|
|
670
|
+
// pills) are coloured this way; without it they reconstruct as empty boxes.
|
|
671
|
+
const styleNode = ochild(sp, 'p:style');
|
|
672
|
+
if (!fillColor && styleNode && !ochild(spPr, 'a:noFill')) {
|
|
673
|
+
const fillRef = ochild(styleNode, 'a:fillRef');
|
|
674
|
+
if (fillRef && oattr(fillRef, 'idx') && oattr(fillRef, 'idx') !== '0')
|
|
675
|
+
fillColor = colorFrom(fillRef, ctx.color);
|
|
676
|
+
}
|
|
677
|
+
if (!stroke && styleNode && !ochild(ochild(spPr, 'a:ln'), 'a:noFill')) {
|
|
678
|
+
const lnRef = ochild(styleNode, 'a:lnRef');
|
|
679
|
+
if (lnRef && oattr(lnRef, 'idx') && oattr(lnRef, 'idx') !== '0') {
|
|
680
|
+
const lc = colorFrom(lnRef, ctx.color);
|
|
681
|
+
if (lc)
|
|
682
|
+
stroke = { color: lc, width: 1 };
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const ellipse = prst === 'ellipse';
|
|
686
|
+
const radius = prst === 'roundRect' ? Math.round(Math.min(box.w, box.h) * 0.12) : undefined;
|
|
687
|
+
if (paras.length) {
|
|
688
|
+
out.push({
|
|
689
|
+
kind: 'text',
|
|
690
|
+
...box,
|
|
691
|
+
ph: phType,
|
|
692
|
+
name,
|
|
693
|
+
id,
|
|
694
|
+
paras,
|
|
695
|
+
valign,
|
|
696
|
+
insets: insetsOf(bodyPr),
|
|
697
|
+
rotation: rot || undefined,
|
|
698
|
+
flipH: flipH || undefined,
|
|
699
|
+
flipV: flipV || undefined,
|
|
700
|
+
// A text box can also carry a fill (e.g. a chip/pill behind the text).
|
|
701
|
+
fill: fillColor,
|
|
702
|
+
stroke,
|
|
703
|
+
radius,
|
|
704
|
+
ellipse: ellipse || undefined,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
else if (fillColor || stroke) {
|
|
708
|
+
out.push({ kind: 'rect', ...box, name, id, fill: fillColor, stroke, rotation: rot || undefined, radius, ellipse: ellipse || undefined });
|
|
709
|
+
}
|
|
710
|
+
// else: an unfilled, textless shape — nothing to render.
|
|
711
|
+
}
|
|
712
|
+
function handlePic(pic, T, ctx, out) {
|
|
713
|
+
const spPr = ochild(pic, 'p:spPr');
|
|
714
|
+
const xf = ochild(spPr, 'a:xfrm');
|
|
715
|
+
const box = composedBoxOf(xf, T);
|
|
716
|
+
if (!box)
|
|
717
|
+
return;
|
|
718
|
+
const blip = odeep(ochild(pic, 'p:blipFill'), 'a:blip');
|
|
719
|
+
const src = ctx.resolveMedia(oattr(blip, 'r:embed'));
|
|
720
|
+
if (!src)
|
|
721
|
+
return;
|
|
722
|
+
const { name, id } = cNvName(ochild(pic, 'p:nvPicPr'));
|
|
723
|
+
const rot = (xf ? Number(oattr(xf, 'rot') || 0) / 60000 : 0) + T.rot;
|
|
724
|
+
out.push({ kind: 'image', ...box, src, name, id, rotation: rot || undefined });
|
|
725
|
+
}
|
|
726
|
+
function handleCxn(cxn, T, ctx, out) {
|
|
727
|
+
// Connector: render a straight one as a thin rotated rect; otherwise rasterize.
|
|
728
|
+
const spPr = ochild(cxn, 'p:spPr');
|
|
729
|
+
const xf = ochild(spPr, 'a:xfrm');
|
|
730
|
+
const box = composedBoxOf(xf, T);
|
|
731
|
+
if (!box)
|
|
732
|
+
return;
|
|
733
|
+
const { name, id } = cNvName(ochild(cxn, 'p:nvCxnSpPr'));
|
|
734
|
+
const prst = oattr(ochild(spPr, 'a:prstGeom'), 'prst');
|
|
735
|
+
const lnColor = colorFrom(ochild(ochild(spPr, 'a:ln'), 'a:solidFill'), ctx.color);
|
|
736
|
+
if (prst === 'straightConnector1' || prst === 'line') {
|
|
737
|
+
// Thin rect spanning the bounding box diagonal is overkill; for axis-aligned
|
|
738
|
+
// straight connectors the bbox is the line. Good enough for most diagrams.
|
|
739
|
+
out.push({ kind: 'rect', ...box, name, id, fill: lnColor ?? '#888', rotation: T.rot || undefined });
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
out.push({ kind: 'raster', ...box, name, id, rasterReq: { name, id, reason: 'connector' } });
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function handleGraphicFrame(gf, T, ctx, out) {
|
|
746
|
+
const xfrm = odeep(gf, 'p:xfrm') ?? odeep(gf, 'a:xfrm');
|
|
747
|
+
const box = composedBoxOf(xfrm, T);
|
|
748
|
+
const { name, id } = cNvName(ochild(gf, 'p:nvGraphicFramePr'));
|
|
749
|
+
const tbl = odeep(gf, 'a:tbl');
|
|
750
|
+
if (tbl && box) {
|
|
751
|
+
const table = parseTable(tbl, ctx);
|
|
752
|
+
if (table) {
|
|
753
|
+
out.push({ kind: 'table', ...box, name, id, table });
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
// Charts, SmartArt (dgm), OLE -> rasterize.
|
|
758
|
+
if (box)
|
|
759
|
+
out.push({ kind: 'raster', ...box, name, id, rasterReq: { name, id, reason: 'graphic-frame' } });
|
|
760
|
+
}
|
|
761
|
+
function parseTable(tbl, ctx) {
|
|
762
|
+
const grid = ochild(tbl, 'a:tblGrid');
|
|
763
|
+
const widths = ochildren(grid, 'a:gridCol').map((c) => px(Number(oattr(c, 'w'))));
|
|
764
|
+
const rows = [];
|
|
765
|
+
const heights = [];
|
|
766
|
+
for (const tr of ochildren(tbl, 'a:tr')) {
|
|
767
|
+
heights.push(px(Number(oattr(tr, 'h'))));
|
|
768
|
+
const cells = [];
|
|
769
|
+
for (const tc of ochildren(tr, 'a:tc')) {
|
|
770
|
+
const paras = parseTxBody(ochild(tc, 'a:txBody'), ctx.color, ctx.major, ctx.minor, undefined, ctx.nonGoogle, ctx.fieldText);
|
|
771
|
+
const fill = solidFill(ochild(tc, 'a:tcPr'), ctx.color);
|
|
772
|
+
cells.push({ paras, fill });
|
|
773
|
+
}
|
|
774
|
+
rows.push(cells);
|
|
775
|
+
}
|
|
776
|
+
if (!rows.length)
|
|
777
|
+
return undefined;
|
|
778
|
+
return { rows, widths, heights };
|
|
779
|
+
}
|
|
780
|
+
/** A group is "complex" when reconstructing its children individually would drift —
|
|
781
|
+
* connectors (curved diagram links), custom geometry, embedded objects, or many
|
|
782
|
+
* pieces. Such groups are rasterized as a single image (pixel-perfect, anchored). */
|
|
783
|
+
function groupIsComplex(grp) {
|
|
784
|
+
let connectors = 0, custom = 0, shapes = 0, frames = 0;
|
|
785
|
+
const visit = (n) => {
|
|
786
|
+
for (const c of kids(n)) {
|
|
787
|
+
const t = tagOf(c);
|
|
788
|
+
if (t === 'p:cxnSp')
|
|
789
|
+
connectors++;
|
|
790
|
+
else if (t === 'p:graphicFrame')
|
|
791
|
+
frames++;
|
|
792
|
+
else if (t === 'p:sp') {
|
|
793
|
+
shapes++;
|
|
794
|
+
if (ochild(ochild(c, 'p:spPr'), 'a:custGeom'))
|
|
795
|
+
custom++;
|
|
796
|
+
}
|
|
797
|
+
else if (t === 'p:grpSp')
|
|
798
|
+
visit(c);
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
visit(grp);
|
|
802
|
+
return connectors >= 2 || custom >= 1 || frames >= 1 || shapes >= 10;
|
|
803
|
+
}
|
|
804
|
+
function handleGroup(grp, T, ctx, out) {
|
|
805
|
+
const grpSpPr = ochild(grp, 'p:grpSpPr');
|
|
806
|
+
const xf = ochild(grpSpPr, 'a:xfrm');
|
|
807
|
+
if (!xf) {
|
|
808
|
+
walkTree(grp, T, ctx, out);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const childT = composeGroup(T, xf);
|
|
812
|
+
const box = composedBoxOf(xf, T);
|
|
813
|
+
const rotDeg = Number(oattr(xf, 'rot') || 0) / 60000;
|
|
814
|
+
// Rotated OR diagram-like groups -> rasterize the whole group as one image
|
|
815
|
+
// (recursing would shatter a curved mind-map into dozens of mismatched pieces).
|
|
816
|
+
if (box && (rotDeg !== 0 || groupIsComplex(grp))) {
|
|
817
|
+
const { name, id } = cNvName(ochild(grp, 'p:nvGrpSpPr'));
|
|
818
|
+
out.push({
|
|
819
|
+
kind: 'raster',
|
|
820
|
+
...box,
|
|
821
|
+
name,
|
|
822
|
+
id,
|
|
823
|
+
rotation: rotDeg || undefined,
|
|
824
|
+
rasterReq: { name, id, reason: rotDeg ? 'rotated-group' : 'complex-group' },
|
|
825
|
+
});
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
walkTree(grp, childT, ctx, out);
|
|
829
|
+
}
|
|
830
|
+
// ---------------------------------------------------------------------------
|
|
831
|
+
// Orchestration
|
|
832
|
+
// ---------------------------------------------------------------------------
|
|
833
|
+
export async function parsePptx(path) {
|
|
834
|
+
const warnings = [];
|
|
835
|
+
const zip = await JSZip.loadAsync(readFileSync(path));
|
|
836
|
+
const readStr = async (p) => {
|
|
837
|
+
const f = zip.file(p);
|
|
838
|
+
return f ? f.async('string') : null;
|
|
839
|
+
};
|
|
840
|
+
const parseOrdered = async (p) => {
|
|
841
|
+
const s = await readStr(p);
|
|
842
|
+
return s ? { 'p:_root': ordered.parse(s) } : undefined;
|
|
843
|
+
};
|
|
844
|
+
// canvas
|
|
845
|
+
const presFlat = flat.parse((await readStr('ppt/presentation.xml')) ?? '');
|
|
846
|
+
const sldSz = presFlat?.['p:presentation']?.['p:sldSz'];
|
|
847
|
+
const canvas = { w: px(Number(sldSz?.['@_cx'])) || 1280, h: px(Number(sldSz?.['@_cy'])) || 720 };
|
|
848
|
+
// theme (first)
|
|
849
|
+
const themeFile = Object.keys(zip.files).find((f) => /ppt\/theme\/theme\d+\.xml$/.test(f));
|
|
850
|
+
const themeRoot = themeFile ? { 'p:_root': ordered.parse((await readStr(themeFile)) ?? '') } : undefined;
|
|
851
|
+
const themeEl = odeep(themeRoot, 'a:themeElements');
|
|
852
|
+
const scheme = resolveScheme(ochild(themeEl, 'a:clrScheme'));
|
|
853
|
+
const fontScheme = ochild(themeEl, 'a:fontScheme');
|
|
854
|
+
const fontMajor = oattr(odeep(ochild(fontScheme, 'a:majorFont'), 'a:latin'), 'typeface') ?? 'Inter';
|
|
855
|
+
const fontMinor = oattr(odeep(ochild(fontScheme, 'a:minorFont'), 'a:latin'), 'typeface') ?? 'Inter';
|
|
856
|
+
// slide order via presentation rels
|
|
857
|
+
const presRels = flat.parse((await readStr('ppt/_rels/presentation.xml.rels')) ?? '');
|
|
858
|
+
const relMap = {};
|
|
859
|
+
for (const rel of arr(presRels?.['Relationships']?.['Relationship']))
|
|
860
|
+
relMap[rel['@_Id']] = rel['@_Target'];
|
|
861
|
+
const slidePaths = [];
|
|
862
|
+
for (const sldId of arr(presFlat?.['p:presentation']?.['p:sldIdLst']?.['p:sldId'])) {
|
|
863
|
+
const target = relMap[sldId['@_r:id']];
|
|
864
|
+
if (target)
|
|
865
|
+
slidePaths.push('ppt/' + target.replace(/^\.\.\//, '').replace(/^\//, ''));
|
|
866
|
+
}
|
|
867
|
+
if (!slidePaths.length) {
|
|
868
|
+
Object.keys(zip.files)
|
|
869
|
+
.filter((f) => /ppt\/slides\/slide\d+\.xml$/.test(f))
|
|
870
|
+
.sort((a, b) => parseInt(a.match(/\d+/)[0]) - parseInt(b.match(/\d+/)[0]))
|
|
871
|
+
.forEach((f) => slidePaths.push(f));
|
|
872
|
+
}
|
|
873
|
+
// media catalog
|
|
874
|
+
const mediaName = {};
|
|
875
|
+
for (const f of Object.keys(zip.files)) {
|
|
876
|
+
if (/ppt\/media\//.test(f) && !zip.files[f].dir)
|
|
877
|
+
mediaName[f] = f.split('/').pop();
|
|
878
|
+
}
|
|
879
|
+
const usedMedia = new Set();
|
|
880
|
+
// relationship resolver for a given part's .rels
|
|
881
|
+
const relsFor = async (partPath) => {
|
|
882
|
+
const relsPath = partPath.replace(/([^/]+)\.xml$/, '_rels/$1.xml.rels');
|
|
883
|
+
const xml = flat.parse((await readStr(relsPath)) ?? '');
|
|
884
|
+
const map = {};
|
|
885
|
+
for (const rel of arr(xml?.['Relationships']?.['Relationship']))
|
|
886
|
+
map[rel['@_Id']] = rel['@_Target'];
|
|
887
|
+
return map;
|
|
888
|
+
};
|
|
889
|
+
// cache layout/master inheritance per layout part
|
|
890
|
+
const inheritCache = {};
|
|
891
|
+
const colorCtx = { scheme, clrMap: {} };
|
|
892
|
+
const today = todayFormatted();
|
|
893
|
+
const fieldText = (type, cached) => {
|
|
894
|
+
if (!type)
|
|
895
|
+
return cached;
|
|
896
|
+
if (/^slidenum/i.test(type))
|
|
897
|
+
return cached || ''; // resolved per-slide below
|
|
898
|
+
if (/^datetime/i.test(type))
|
|
899
|
+
return cached || today;
|
|
900
|
+
return cached;
|
|
901
|
+
};
|
|
902
|
+
const slides = [];
|
|
903
|
+
const deckNonGoogle = new Set();
|
|
904
|
+
let slideNo = 0;
|
|
905
|
+
for (const sp of slidePaths) {
|
|
906
|
+
slideNo++;
|
|
907
|
+
const slideRels = await relsFor(sp);
|
|
908
|
+
const resolveMedia = (embed) => {
|
|
909
|
+
if (!embed)
|
|
910
|
+
return undefined;
|
|
911
|
+
const target = slideRels[embed];
|
|
912
|
+
if (!target)
|
|
913
|
+
return undefined;
|
|
914
|
+
const mediaPath = 'ppt/' + target.replace(/^\.\.\//, '');
|
|
915
|
+
const fname = mediaName[mediaPath];
|
|
916
|
+
if (fname)
|
|
917
|
+
usedMedia.add(mediaPath);
|
|
918
|
+
return fname;
|
|
919
|
+
};
|
|
920
|
+
// resolve layout + master for this slide, and build inheritance once per layout
|
|
921
|
+
const layoutTarget = Object.entries(slideRels).find(([, t]) => /slideLayout\d+\.xml$/.test(t))?.[1];
|
|
922
|
+
const layoutPath = layoutTarget ? 'ppt/slides/' + layoutTarget : undefined;
|
|
923
|
+
const layoutNorm = layoutPath ? layoutPath.replace(/ppt\/slides\/\.\.\//, 'ppt/') : undefined;
|
|
924
|
+
let inherit = inheritCache['__none__'] ?? { ph: {}, txStyles: { title: [], body: [], other: [] }, decorations: [] };
|
|
925
|
+
let masterColorMap = {};
|
|
926
|
+
if (layoutNorm) {
|
|
927
|
+
if (!inheritCache[layoutNorm]) {
|
|
928
|
+
const layoutRoot = await parseOrdered(layoutNorm);
|
|
929
|
+
const layoutRels = await relsFor(layoutNorm);
|
|
930
|
+
const masterTarget = Object.entries(layoutRels).find(([, t]) => /slideMaster\d+\.xml$/.test(t))?.[1];
|
|
931
|
+
const masterNorm = masterTarget ? 'ppt/slideMasters/' + masterTarget.replace(/^\.\.\/slideMasters\//, '') : undefined;
|
|
932
|
+
const masterRoot = masterNorm ? await parseOrdered(masterNorm) : undefined;
|
|
933
|
+
const masterRels = masterNorm ? await relsFor(masterNorm) : {};
|
|
934
|
+
// clrMap from master
|
|
935
|
+
const cm = odeep(masterRoot, 'p:clrMap');
|
|
936
|
+
if (cm)
|
|
937
|
+
for (const [k, v] of Object.entries(cm[':@'] ?? {}))
|
|
938
|
+
masterColorMap[k.replace(/^@_/, '')] = String(v);
|
|
939
|
+
const ictx = { scheme, clrMap: masterColorMap };
|
|
940
|
+
const ph = {};
|
|
941
|
+
indexPlaceholders(odeep(masterRoot, 'p:spTree'), ictx, fontMajor, fontMinor, ph);
|
|
942
|
+
indexPlaceholders(odeep(layoutRoot, 'p:spTree'), ictx, fontMajor, fontMinor, ph); // layout overrides master
|
|
943
|
+
const txStyles = txStylesFrom(masterRoot, ictx, fontMajor, fontMinor);
|
|
944
|
+
const masterBg = bgFrom(odeep(masterRoot, 'p:cSld'), ictx, () => undefined);
|
|
945
|
+
const layoutBg = bgFrom(odeep(layoutRoot, 'p:cSld'), ictx, () => undefined);
|
|
946
|
+
// Design shapes from master + layout (non-placeholder) painted behind slide content.
|
|
947
|
+
const mkResolve = (rels) => (embed) => {
|
|
948
|
+
if (!embed)
|
|
949
|
+
return undefined;
|
|
950
|
+
const target = rels[embed];
|
|
951
|
+
if (!target)
|
|
952
|
+
return undefined;
|
|
953
|
+
const mediaPath = 'ppt/' + target.replace(/^\.\.\//, '');
|
|
954
|
+
const fname = mediaName[mediaPath];
|
|
955
|
+
if (fname)
|
|
956
|
+
usedMedia.add(mediaPath);
|
|
957
|
+
return fname;
|
|
958
|
+
};
|
|
959
|
+
const decoCtx = (rels) => ({
|
|
960
|
+
color: ictx, major: fontMajor, minor: fontMinor,
|
|
961
|
+
inherit: { ph, txStyles, decorations: [] },
|
|
962
|
+
resolveMedia: mkResolve(rels), nonGoogle: new Set(), fieldText, warnings, skipPlaceholders: true,
|
|
963
|
+
});
|
|
964
|
+
const decorations = [];
|
|
965
|
+
const showMaster = oattr(odeep(layoutRoot, 'p:sldLayout'), 'showMasterSp');
|
|
966
|
+
if (masterRoot && showMaster !== '0')
|
|
967
|
+
walkTree(odeep(masterRoot, 'p:spTree'), IDENTITY, decoCtx(masterRels), decorations);
|
|
968
|
+
const lTree = odeep(layoutRoot, 'p:spTree');
|
|
969
|
+
if (lTree)
|
|
970
|
+
walkTree(lTree, IDENTITY, decoCtx(layoutRels), decorations);
|
|
971
|
+
inheritCache[layoutNorm] = { ph, txStyles, background: layoutBg ?? masterBg, decorations };
|
|
972
|
+
inheritCache[layoutNorm].__clrMap = masterColorMap;
|
|
973
|
+
}
|
|
974
|
+
inherit = inheritCache[layoutNorm];
|
|
975
|
+
masterColorMap = inherit.__clrMap ?? {};
|
|
976
|
+
}
|
|
977
|
+
colorCtx.clrMap = masterColorMap;
|
|
978
|
+
const slideRoot = await parseOrdered(sp);
|
|
979
|
+
const cSld = odeep(slideRoot, 'p:cSld');
|
|
980
|
+
const spTree = odeep(cSld, 'p:spTree');
|
|
981
|
+
if (!spTree)
|
|
982
|
+
continue;
|
|
983
|
+
const nonGoogle = new Set();
|
|
984
|
+
const slideFieldText = (type, cached) => {
|
|
985
|
+
if (type && /^slidenum/i.test(type))
|
|
986
|
+
return cached || String(slideNo);
|
|
987
|
+
return fieldText(type, cached);
|
|
988
|
+
};
|
|
989
|
+
const walk = {
|
|
990
|
+
color: colorCtx,
|
|
991
|
+
major: fontMajor,
|
|
992
|
+
minor: fontMinor,
|
|
993
|
+
inherit,
|
|
994
|
+
resolveMedia,
|
|
995
|
+
nonGoogle,
|
|
996
|
+
fieldText: slideFieldText,
|
|
997
|
+
warnings,
|
|
998
|
+
};
|
|
999
|
+
// Layout/master design shapes paint behind the slide's own content.
|
|
1000
|
+
const shapes = inherit.decorations.map((d) => ({ ...d }));
|
|
1001
|
+
walkTree(spTree, IDENTITY, walk, shapes);
|
|
1002
|
+
// background: slide > layout > master
|
|
1003
|
+
const slideBg = bgFrom(cSld, colorCtx, resolveMedia) ?? inherit.background;
|
|
1004
|
+
// notes
|
|
1005
|
+
let notes;
|
|
1006
|
+
const notesTarget = Object.entries(slideRels).find(([, t]) => /notesSlide\d+\.xml$/.test(t))?.[1];
|
|
1007
|
+
if (notesTarget) {
|
|
1008
|
+
const notesPath = 'ppt/notesSlides/' + notesTarget.replace(/^\.\.\/notesSlides\//, '');
|
|
1009
|
+
const notesRoot = await parseOrdered(notesPath);
|
|
1010
|
+
const body = odeep(notesRoot, 'p:notes');
|
|
1011
|
+
const ntext = body
|
|
1012
|
+
? ochildren(odeep(body, 'p:spTree'), 'p:sp')
|
|
1013
|
+
.map((s) => parseTxBody(ochild(s, 'p:txBody'), colorCtx, fontMajor, fontMinor, undefined, nonGoogle, slideFieldText))
|
|
1014
|
+
.flat()
|
|
1015
|
+
.map((p) => p.runs.map((r) => (r.br ? '\n' : r.text)).join(''))
|
|
1016
|
+
.filter((l) => l.trim() && !/^\d+$/.test(l.trim()))
|
|
1017
|
+
.join('\n')
|
|
1018
|
+
: '';
|
|
1019
|
+
if (ntext.trim())
|
|
1020
|
+
notes = ntext.trim();
|
|
1021
|
+
}
|
|
1022
|
+
for (const f of nonGoogle)
|
|
1023
|
+
deckNonGoogle.add(f);
|
|
1024
|
+
slides.push({ shapes, notes, background: slideBg });
|
|
1025
|
+
}
|
|
1026
|
+
// assets (only used media)
|
|
1027
|
+
const assets = [];
|
|
1028
|
+
for (const [mpath, fname] of Object.entries(mediaName)) {
|
|
1029
|
+
if (!usedMedia.has(mpath))
|
|
1030
|
+
continue;
|
|
1031
|
+
assets.push({ name: fname, data: await zip.files[mpath].async('nodebuffer') });
|
|
1032
|
+
}
|
|
1033
|
+
const palette = { ...scheme };
|
|
1034
|
+
return {
|
|
1035
|
+
canvas,
|
|
1036
|
+
theme: { palette, fontMajor, fontMinor, nonGoogleFonts: [...deckNonGoogle] },
|
|
1037
|
+
slides,
|
|
1038
|
+
assets,
|
|
1039
|
+
warnings,
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
export { isGoogleFont, mergeRuns };
|
|
1043
|
+
//# sourceMappingURL=pptx.js.map
|