pptx-browser 4.1.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/LICENSE +9 -0
- package/README.md +209 -0
- package/package.json +53 -0
- package/src/animation.js +817 -0
- package/src/charts.js +989 -0
- package/src/clipboard.js +416 -0
- package/src/colors.js +297 -0
- package/src/effects3d.js +312 -0
- package/src/extract.js +535 -0
- package/src/fntdata.js +265 -0
- package/src/fonts.js +676 -0
- package/src/index.js +751 -0
- package/src/pdf.js +298 -0
- package/src/render.js +1964 -0
- package/src/shapes.js +666 -0
- package/src/slideshow.js +492 -0
- package/src/smartart.js +696 -0
- package/src/svg.js +732 -0
- package/src/theme.js +88 -0
- package/src/utils.js +50 -0
- package/src/writer.js +1015 -0
- package/src/zip-writer.js +214 -0
- package/src/zip.js +194 -0
package/src/svg.js
ADDED
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* svg.js — PPTX → SVG serializer.
|
|
3
|
+
*
|
|
4
|
+
* Produces a faithful SVG for each slide: vector text (searchable / scalable),
|
|
5
|
+
* inline base-64 images, linearGradient / radialGradient fills, clip paths,
|
|
6
|
+
* shadows via <filter>, and the same preset geometry paths used by the canvas
|
|
7
|
+
* renderer.
|
|
8
|
+
*
|
|
9
|
+
* Public API (all re-exported from index.js):
|
|
10
|
+
* renderSlideToSvg(slideIndex, renderer) → Promise<string> SVG markup
|
|
11
|
+
* renderAllSlidesToSvg(renderer) → Promise<string[]>
|
|
12
|
+
*
|
|
13
|
+
* The SVG is self-contained (no external resources) and matches PowerPoint's
|
|
14
|
+
* "Save as SVG" output format.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { g1, gtn, attr, attrInt, parseXml, EMU_PER_PT } from './utils.js';
|
|
18
|
+
import { resolveColorElement, findFirstColorChild, colorToCss } from './colors.js';
|
|
19
|
+
import { buildFontInherited } from './fonts.js';
|
|
20
|
+
import { getRels } from './render.js';
|
|
21
|
+
|
|
22
|
+
// ── ID generator ──────────────────────────────────────────────────────────────
|
|
23
|
+
let _idSeq = 0;
|
|
24
|
+
function uid(prefix = 'el') { return `${prefix}${++_idSeq}`; }
|
|
25
|
+
|
|
26
|
+
// ── Attribute helpers ─────────────────────────────────────────────────────────
|
|
27
|
+
function esc(s) {
|
|
28
|
+
return String(s ?? '')
|
|
29
|
+
.replace(/&/g, '&').replace(/</g, '<')
|
|
30
|
+
.replace(/>/g, '>').replace(/"/g, '"');
|
|
31
|
+
}
|
|
32
|
+
function px(n) { return `${+n.toFixed(3)}`; }
|
|
33
|
+
|
|
34
|
+
// ── Colour helpers ────────────────────────────────────────────────────────────
|
|
35
|
+
function colStr(c) { return c ? colorToCss(c) : 'none'; }
|
|
36
|
+
|
|
37
|
+
function fillAttr(fillEl, defs, themeColors, x, y, w, h) {
|
|
38
|
+
if (!fillEl) return { fill: 'none', fillAttrs: '' };
|
|
39
|
+
const ln = fillEl.localName;
|
|
40
|
+
|
|
41
|
+
if (ln === 'noFill') return { fill: 'none', fillAttrs: '' };
|
|
42
|
+
|
|
43
|
+
if (ln === 'solidFill') {
|
|
44
|
+
const cc = findFirstColorChild(fillEl);
|
|
45
|
+
const c = resolveColorElement(cc, themeColors);
|
|
46
|
+
const css = colStr(c);
|
|
47
|
+
const opacity = c?.a != null ? c.a / 255 : 1;
|
|
48
|
+
return { fill: css, fillAttrs: opacity < 1 ? ` fill-opacity="${px(opacity)}"` : '' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (ln === 'gradFill') {
|
|
52
|
+
const gsLst = g1(fillEl, 'gsLst');
|
|
53
|
+
const stops = gsLst ? gtn(gsLst, 'gs').map(gs => {
|
|
54
|
+
const pos = attrInt(gs, 'pos', 0) / 100000;
|
|
55
|
+
const cc = findFirstColorChild(gs);
|
|
56
|
+
const c = resolveColorElement(cc, themeColors);
|
|
57
|
+
return { pos, color: colStr(c), opacity: c?.a != null ? c.a / 255 : 1 };
|
|
58
|
+
}) : [];
|
|
59
|
+
|
|
60
|
+
const linEl = g1(fillEl, 'lin');
|
|
61
|
+
const pathEl = g1(fillEl, 'path');
|
|
62
|
+
const gradId = uid('grad');
|
|
63
|
+
|
|
64
|
+
let gradDef;
|
|
65
|
+
if (pathEl) {
|
|
66
|
+
// Radial gradient
|
|
67
|
+
const fillToRect = g1(pathEl, 'fillToRect');
|
|
68
|
+
const fl = attrInt(fillToRect, 'l', 50000) / 100000;
|
|
69
|
+
const ft = attrInt(fillToRect, 't', 50000) / 100000;
|
|
70
|
+
const cx = x + w * fl;
|
|
71
|
+
const cy = y + h * ft;
|
|
72
|
+
const r = Math.sqrt(w * w + h * h) / 2;
|
|
73
|
+
gradDef = `<radialGradient id="${gradId}" cx="${px(cx)}" cy="${px(cy)}" r="${px(r)}" gradientUnits="userSpaceOnUse">`;
|
|
74
|
+
for (const s of stops) {
|
|
75
|
+
gradDef += `<stop offset="${px(s.pos)}" stop-color="${esc(s.color)}"${s.opacity < 1 ? ` stop-opacity="${px(s.opacity)}"` : ''}/>`;
|
|
76
|
+
}
|
|
77
|
+
gradDef += `</radialGradient>`;
|
|
78
|
+
} else {
|
|
79
|
+
// Linear gradient
|
|
80
|
+
const angRaw = attrInt(linEl, 'ang', 0);
|
|
81
|
+
const ang = ((angRaw / 60000) - 90) * (Math.PI / 180);
|
|
82
|
+
const cos = Math.cos(ang), sin = Math.sin(ang);
|
|
83
|
+
const half = Math.sqrt(w * w + h * h) / 2;
|
|
84
|
+
const cx2 = x + w / 2, cy2 = y + h / 2;
|
|
85
|
+
gradDef = `<linearGradient id="${gradId}" x1="${px(cx2 - cos * half)}" y1="${px(cy2 - sin * half)}" x2="${px(cx2 + cos * half)}" y2="${px(cy2 + sin * half)}" gradientUnits="userSpaceOnUse">`;
|
|
86
|
+
for (const s of stops) {
|
|
87
|
+
gradDef += `<stop offset="${px(s.pos)}" stop-color="${esc(s.color)}"${s.opacity < 1 ? ` stop-opacity="${px(s.opacity)}"` : ''}/>`;
|
|
88
|
+
}
|
|
89
|
+
gradDef += `</linearGradient>`;
|
|
90
|
+
}
|
|
91
|
+
defs.push(gradDef);
|
|
92
|
+
return { fill: `url(#${gradId})`, fillAttrs: '' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (ln === 'blipFill') {
|
|
96
|
+
// Image fill — handled separately (see renderPictureSvg)
|
|
97
|
+
return { fill: 'none', fillAttrs: '' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { fill: 'none', fillAttrs: '' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Stroke helper ─────────────────────────────────────────────────────────────
|
|
104
|
+
function strokeAttrs(lnEl, themeColors, scale) {
|
|
105
|
+
if (!lnEl) return '';
|
|
106
|
+
if (g1(lnEl, 'noFill')) return ' stroke="none"';
|
|
107
|
+
|
|
108
|
+
const solidFill = g1(lnEl, 'solidFill');
|
|
109
|
+
const cc = solidFill ? findFirstColorChild(solidFill) : null;
|
|
110
|
+
const c = resolveColorElement(cc, themeColors);
|
|
111
|
+
const color = colStr(c) || '#000';
|
|
112
|
+
const w = Math.max(0.5, attrInt(lnEl, 'w', 12700) / 914400 * 96); // px at 96dpi
|
|
113
|
+
|
|
114
|
+
const prstDash = g1(lnEl, 'prstDash');
|
|
115
|
+
const dash = prstDash ? attr(prstDash, 'val', 'solid') : 'solid';
|
|
116
|
+
let dashArr = '';
|
|
117
|
+
if (dash === 'dash') dashArr = ` stroke-dasharray="${px(w*4)},${px(w*2)}"`;
|
|
118
|
+
else if (dash === 'dot') dashArr = ` stroke-dasharray="${px(w)},${px(w*2)}"`;
|
|
119
|
+
else if (dash === 'dashDot')dashArr = ` stroke-dasharray="${px(w*4)},${px(w*2)},${px(w)},${px(w*2)}"`;
|
|
120
|
+
else if (dash === 'lgDash') dashArr = ` stroke-dasharray="${px(w*8)},${px(w*3)}"`;
|
|
121
|
+
|
|
122
|
+
const cap = attr(lnEl, 'cap', 'flat');
|
|
123
|
+
const capSvg = cap === 'rnd' ? 'round' : cap === 'sq' ? 'square' : 'butt';
|
|
124
|
+
|
|
125
|
+
return ` stroke="${esc(color)}" stroke-width="${px(w)}" stroke-linecap="${capSvg}" stroke-linejoin="round"${dashArr}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Shadow filter ──────────────────────────────────────────────────────────────
|
|
129
|
+
function shadowFilter(effectLst, defs) {
|
|
130
|
+
if (!effectLst) return '';
|
|
131
|
+
const outerShdw = g1(effectLst, 'outerShdw');
|
|
132
|
+
if (!outerShdw) return '';
|
|
133
|
+
|
|
134
|
+
const dist = attrInt(outerShdw, 'dist', 38100) / 914400 * 96;
|
|
135
|
+
const dir = attrInt(outerShdw, 'dir', 2700000) / 60000;
|
|
136
|
+
const blurR = attrInt(outerShdw, 'blurRad', 38100) / 914400 * 96;
|
|
137
|
+
const ang = (dir * Math.PI) / 180;
|
|
138
|
+
const dx = Math.cos(ang) * dist;
|
|
139
|
+
const dy = Math.sin(ang) * dist;
|
|
140
|
+
|
|
141
|
+
const cc = findFirstColorChild(outerShdw);
|
|
142
|
+
const c = resolveColorElement(cc, {});
|
|
143
|
+
const col = c ? colorToCss(c) : 'rgba(0,0,0,0.5)';
|
|
144
|
+
const opacity = c?.a != null ? c.a / 255 : 0.5;
|
|
145
|
+
|
|
146
|
+
const filterId = uid('shd');
|
|
147
|
+
defs.push(
|
|
148
|
+
`<filter id="${filterId}" x="-50%" y="-50%" width="200%" height="200%">` +
|
|
149
|
+
`<feDropShadow dx="${px(dx)}" dy="${px(dy)}" stdDeviation="${px(blurR / 2)}" flood-color="${esc(col)}" flood-opacity="${px(opacity)}"/>` +
|
|
150
|
+
`</filter>`
|
|
151
|
+
);
|
|
152
|
+
return ` filter="url(#${filterId})"`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Preset geometry path builder ──────────────────────────────────────────────
|
|
156
|
+
// We need SVG path strings for the same presets the canvas renderer draws.
|
|
157
|
+
// Strategy: draw to a mock path-capturing context, extract path commands.
|
|
158
|
+
|
|
159
|
+
function presetToSvgPath(prst, x, y, w, h, adjValues) {
|
|
160
|
+
// Use the same drawPresetGeom function but with a path-capturing mock ctx
|
|
161
|
+
const cmds = [];
|
|
162
|
+
const mock = {
|
|
163
|
+
beginPath() { cmds.length = 0; },
|
|
164
|
+
moveTo(px2, py2) { cmds.push(`M${px(px2)},${px(py2)}`); },
|
|
165
|
+
lineTo(px2, py2) { cmds.push(`L${px(px2)},${px(py2)}`); },
|
|
166
|
+
bezierCurveTo(c1x,c1y,c2x,c2y,ex,ey) {
|
|
167
|
+
cmds.push(`C${px(c1x)},${px(c1y)},${px(c2x)},${px(c2y)},${px(ex)},${px(ey)}`);
|
|
168
|
+
},
|
|
169
|
+
quadraticCurveTo(cpx,cpy,ex,ey) {
|
|
170
|
+
cmds.push(`Q${px(cpx)},${px(cpy)},${px(ex)},${px(ey)}`);
|
|
171
|
+
},
|
|
172
|
+
arc(cx2,cy2,r,start,end,ccw) {
|
|
173
|
+
// Convert to SVG arc
|
|
174
|
+
const startX = cx2 + r * Math.cos(start);
|
|
175
|
+
const startY = cy2 + r * Math.sin(start);
|
|
176
|
+
const endX = cx2 + r * Math.cos(end);
|
|
177
|
+
const endY = cy2 + r * Math.sin(end);
|
|
178
|
+
let sweep = end - start;
|
|
179
|
+
if (ccw) sweep = -((Math.PI * 2) - Math.abs(sweep));
|
|
180
|
+
const large = Math.abs(sweep) > Math.PI ? 1 : 0;
|
|
181
|
+
const sweepFlag = sweep > 0 ? 1 : 0;
|
|
182
|
+
if (cmds.length === 0) cmds.push(`M${px(startX)},${px(startY)}`);
|
|
183
|
+
cmds.push(`A${px(r)},${px(r)},0,${large},${sweepFlag},${px(endX)},${px(endY)}`);
|
|
184
|
+
},
|
|
185
|
+
arcTo(x1,y1,x2,y2,r) {
|
|
186
|
+
// Approximate with lineTo for SVG (arcTo is canvas-specific)
|
|
187
|
+
cmds.push(`L${px(x1)},${px(y1)} L${px(x2)},${px(y2)}`);
|
|
188
|
+
},
|
|
189
|
+
closePath() { cmds.push('Z'); },
|
|
190
|
+
fill() {},
|
|
191
|
+
stroke() {},
|
|
192
|
+
save() {}, restore() {}, translate() {}, rotate() {}, scale() {},
|
|
193
|
+
rect(rx,ry,rw,rh) {
|
|
194
|
+
cmds.push(`M${px(rx)},${px(ry)}L${px(rx+rw)},${px(ry)}L${px(rx+rw)},${px(ry+rh)}L${px(rx)},${px(ry+rh)}Z`);
|
|
195
|
+
},
|
|
196
|
+
setLineDash() {},
|
|
197
|
+
measureText() { return { width: 0 }; },
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
// Dynamically import shapes to avoid circular dep
|
|
202
|
+
// We'll call through a stored reference set at module init
|
|
203
|
+
if (_drawPresetGeom) {
|
|
204
|
+
_drawPresetGeom(mock, prst, x, y, w, h, adjValues || {});
|
|
205
|
+
}
|
|
206
|
+
} catch (e) {
|
|
207
|
+
// Fallback to rect
|
|
208
|
+
return `M${px(x)},${px(y)}L${px(x+w)},${px(y)}L${px(x+w)},${px(y+h)}L${px(x)},${px(y+h)}Z`;
|
|
209
|
+
}
|
|
210
|
+
return cmds.join(' ') || `M${px(x)},${px(y)}L${px(x+w)},${px(y)}L${px(x+w)},${px(y+h)}L${px(x)},${px(y+h)}Z`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let _drawPresetGeom = null;
|
|
214
|
+
export function initSvgShapeRenderer(drawPresetGeomFn) {
|
|
215
|
+
_drawPresetGeom = drawPresetGeomFn;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── Transform helper ──────────────────────────────────────────────────────────
|
|
219
|
+
function xfrmAttrs(xfrm) {
|
|
220
|
+
if (!xfrm) return '';
|
|
221
|
+
const rot = attrInt(xfrm, 'rot', 0) / 60000;
|
|
222
|
+
const flipH = attr(xfrm, 'flipH', '0') === '1';
|
|
223
|
+
const flipV = attr(xfrm, 'flipV', '0') === '1';
|
|
224
|
+
const off = g1(xfrm, 'off');
|
|
225
|
+
const ext = g1(xfrm, 'ext');
|
|
226
|
+
if (!off || !ext) return '';
|
|
227
|
+
const x = attrInt(off, 'x', 0) / 914400 * 96;
|
|
228
|
+
const y = attrInt(off, 'y', 0) / 914400 * 96;
|
|
229
|
+
const w = attrInt(ext, 'cx', 0) / 914400 * 96;
|
|
230
|
+
const h = attrInt(ext, 'cy', 0) / 914400 * 96;
|
|
231
|
+
const cx = x + w / 2, cy = y + h / 2;
|
|
232
|
+
|
|
233
|
+
const parts = [];
|
|
234
|
+
if (rot) parts.push(`rotate(${px(rot)},${px(cx)},${px(cy)})`);
|
|
235
|
+
if (flipH) parts.push(`scale(-1,1) translate(${px(-x*2-w)},0)`);
|
|
236
|
+
if (flipV) parts.push(`scale(1,-1) translate(0,${px(-y*2-h)})`);
|
|
237
|
+
|
|
238
|
+
return parts.length ? ` transform="${parts.join(' ')}"` : '';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Shape bounds from xfrm ────────────────────────────────────────────────────
|
|
242
|
+
function xfrmBounds(xfrm) {
|
|
243
|
+
if (!xfrm) return null;
|
|
244
|
+
const off = g1(xfrm, 'off'), ext = g1(xfrm, 'ext');
|
|
245
|
+
if (!off || !ext) return null;
|
|
246
|
+
return {
|
|
247
|
+
x: attrInt(off, 'x', 0) / 914400 * 96,
|
|
248
|
+
y: attrInt(off, 'y', 0) / 914400 * 96,
|
|
249
|
+
w: attrInt(ext, 'cx', 0) / 914400 * 96,
|
|
250
|
+
h: attrInt(ext, 'cy', 0) / 914400 * 96,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Text body → SVG ───────────────────────────────────────────────────────────
|
|
255
|
+
function textBodyToSvg(txBody, bx, by, bw, bh, themeColors, themeData, defs) {
|
|
256
|
+
if (!txBody) return '';
|
|
257
|
+
const bodyPr = g1(txBody, 'bodyPr');
|
|
258
|
+
const vert = attr(bodyPr, 'vert', 'horz');
|
|
259
|
+
const isVert = vert === 'vert' || vert === 'vert270' || vert === 'wordArtVert';
|
|
260
|
+
const anchor = attr(bodyPr, 'anchor', 't');
|
|
261
|
+
const lIns = attrInt(bodyPr, 'lIns', 91440) / 914400 * 96;
|
|
262
|
+
const rIns = attrInt(bodyPr, 'rIns', 91440) / 914400 * 96;
|
|
263
|
+
const tIns = attrInt(bodyPr, 'tIns', 45720) / 914400 * 96;
|
|
264
|
+
const bIns = attrInt(bodyPr, 'bIns', 45720) / 914400 * 96;
|
|
265
|
+
|
|
266
|
+
const tx = bx + lIns, tw = bw - lIns - rIns;
|
|
267
|
+
const ty = by + tIns, th = bh - tIns - bIns;
|
|
268
|
+
|
|
269
|
+
const defaultFontSz = 1800;
|
|
270
|
+
const lstStyle = g1(txBody, 'lstStyle');
|
|
271
|
+
const lstDefRPr = lstStyle ? g1(lstStyle, 'defRPr') : null;
|
|
272
|
+
|
|
273
|
+
const paragraphs = gtn(txBody, 'p');
|
|
274
|
+
let svgLines = '';
|
|
275
|
+
let curY = ty;
|
|
276
|
+
const clipId = uid('clip');
|
|
277
|
+
defs.push(`<clipPath id="${clipId}"><rect x="${px(bx)}" y="${px(by)}" width="${px(bw)}" height="${px(bh)}"/></clipPath>`);
|
|
278
|
+
|
|
279
|
+
// Vertical text: rotate the whole group
|
|
280
|
+
const vertTransform = isVert
|
|
281
|
+
? ` transform="rotate(-90,${px(bx + bw/2)},${px(by + bh/2)})"` : '';
|
|
282
|
+
|
|
283
|
+
// Auto-number counters
|
|
284
|
+
const autoNumCtrs = {};
|
|
285
|
+
|
|
286
|
+
for (const para of paragraphs) {
|
|
287
|
+
const pPr = g1(para, 'pPr');
|
|
288
|
+
const algn = attr(pPr, 'algn', 'l');
|
|
289
|
+
const marL = attrInt(pPr, 'marL', 0) / 914400 * 96;
|
|
290
|
+
const indent = attrInt(pPr, 'indent', 0) / 914400 * 96;
|
|
291
|
+
const defRPr = g1(pPr, 'defRPr');
|
|
292
|
+
let paraDefSz = defaultFontSz;
|
|
293
|
+
if (lstDefRPr) { const sz = lstDefRPr.getAttribute('sz'); if (sz) paraDefSz = parseInt(sz, 10); }
|
|
294
|
+
if (defRPr) { const sz = defRPr.getAttribute('sz'); if (sz) paraDefSz = parseInt(sz, 10); }
|
|
295
|
+
|
|
296
|
+
// Spacing
|
|
297
|
+
const spcBef = g1(pPr, 'spcBef');
|
|
298
|
+
const spcAft = g1(pPr, 'spcAft');
|
|
299
|
+
const lnSpc = g1(pPr, 'lnSpc');
|
|
300
|
+
|
|
301
|
+
let spaceBefore = 0, spaceAfter = 0;
|
|
302
|
+
if (spcBef) {
|
|
303
|
+
const sp = g1(spcBef, 'spcPct'), spp = g1(spcBef, 'spcPts');
|
|
304
|
+
if (sp) spaceBefore = (paraDefSz * EMU_PER_PT) / 914400 * 96 * (attrInt(sp, 'val', 0) / 100000);
|
|
305
|
+
else if (spp) spaceBefore = attrInt(spp, 'val', 0) / 100 / 72 * 96;
|
|
306
|
+
}
|
|
307
|
+
if (spcAft) {
|
|
308
|
+
const sp = g1(spcAft, 'spcPct'), spp = g1(spcAft, 'spcPts');
|
|
309
|
+
if (sp) spaceAfter = (paraDefSz * EMU_PER_PT) / 914400 * 96 * (attrInt(sp, 'val', 0) / 100000);
|
|
310
|
+
else if (spp) spaceAfter = attrInt(spp, 'val', 0) / 100 / 72 * 96;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Bullet
|
|
314
|
+
const buChar = pPr ? g1(pPr, 'buChar') : null;
|
|
315
|
+
const buAutoNum = pPr ? g1(pPr, 'buAutoNum') : null;
|
|
316
|
+
const buNone = pPr ? g1(pPr, 'buNone') : null;
|
|
317
|
+
const hasBullet = !buNone && (buChar || buAutoNum);
|
|
318
|
+
|
|
319
|
+
curY += spaceBefore;
|
|
320
|
+
|
|
321
|
+
const runEls = [];
|
|
322
|
+
for (const child of para.children) {
|
|
323
|
+
if (child.localName === 'r' || child.localName === 'br' || child.localName === 'fld')
|
|
324
|
+
runEls.push(child);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Empty paragraph
|
|
328
|
+
if (!runEls.length) {
|
|
329
|
+
const endRPr = g1(para, 'endParaRPr');
|
|
330
|
+
const sz = attrInt(endRPr || defRPr, 'sz', paraDefSz);
|
|
331
|
+
const szPx = sz / 100 / 72 * 96;
|
|
332
|
+
const lnH = szPx * 1.2;
|
|
333
|
+
curY += lnH + spaceAfter;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Build runs text
|
|
338
|
+
let lineText = '';
|
|
339
|
+
for (const rEl of runEls) {
|
|
340
|
+
if (rEl.localName === 'br') { lineText += '\n'; continue; }
|
|
341
|
+
const t = g1(rEl, 't') || g1(rEl, 'fldVal');
|
|
342
|
+
if (t) lineText += t.textContent;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Group consecutive runs by their rPr for tspan generation
|
|
346
|
+
const tspans = [];
|
|
347
|
+
for (const rEl of runEls) {
|
|
348
|
+
if (rEl.localName === 'br') {
|
|
349
|
+
tspans.push({ br: true });
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const rPr = g1(rEl, 'rPr') || g1(rEl, 'r')?.firstElementChild;
|
|
353
|
+
const tEl = g1(rEl, 't');
|
|
354
|
+
if (!tEl) continue;
|
|
355
|
+
const text = tEl.textContent;
|
|
356
|
+
if (!text) continue;
|
|
357
|
+
|
|
358
|
+
// Font info
|
|
359
|
+
const fi = buildFontInherited(rEl, defRPr, lstDefRPr, themeColors, themeData, paraDefSz);
|
|
360
|
+
const szPx = fi?.szPx || (paraDefSz / 100 / 72 * 96);
|
|
361
|
+
const family = fi?.family || 'sans-serif';
|
|
362
|
+
const bold = fi?.bold ? 'bold' : 'normal';
|
|
363
|
+
const italic = fi?.italic ? 'italic' : 'normal';
|
|
364
|
+
const color = fi?.color ? colorToCss(fi.color) : '#000000';
|
|
365
|
+
const underline = rPr ? (rPr.getAttribute('u') || 'none') !== 'none' : false;
|
|
366
|
+
const strike = rPr ? (rPr.getAttribute('strike') || 'noStrike') !== 'noStrike' : false;
|
|
367
|
+
const baseline = rPr ? parseInt(rPr.getAttribute('baseline') || '0', 10) : 0;
|
|
368
|
+
|
|
369
|
+
tspans.push({ text, szPx, family, bold, italic, color, underline, strike, baseline });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (!tspans.length) { curY += spaceAfter; continue; }
|
|
373
|
+
|
|
374
|
+
// Average font size for line height
|
|
375
|
+
const sizes = tspans.filter(t => !t.br && t.szPx).map(t => t.szPx);
|
|
376
|
+
const maxSzPx = sizes.length ? Math.max(...sizes) : paraDefSz / 100 / 72 * 96;
|
|
377
|
+
const lnH = maxSzPx * 1.2;
|
|
378
|
+
const baseline = curY + maxSzPx * 0.85;
|
|
379
|
+
|
|
380
|
+
// Text anchor
|
|
381
|
+
let textAnchor = 'start';
|
|
382
|
+
let xPos = tx + marL;
|
|
383
|
+
if (algn === 'ctr') { textAnchor = 'middle'; xPos = tx + tw / 2; }
|
|
384
|
+
else if (algn === 'r') { textAnchor = 'end'; xPos = tx + tw; }
|
|
385
|
+
|
|
386
|
+
// Bullet character
|
|
387
|
+
let bulletSvg = '';
|
|
388
|
+
if (hasBullet) {
|
|
389
|
+
const bx2 = tx + marL + indent;
|
|
390
|
+
let bulletChar = '';
|
|
391
|
+
if (buChar) {
|
|
392
|
+
bulletChar = esc(buChar.getAttribute('char') || '•');
|
|
393
|
+
} else if (buAutoNum) {
|
|
394
|
+
const numType = buAutoNum.getAttribute('type') || 'arabicPeriod';
|
|
395
|
+
const startAt = attrInt(buAutoNum, 'startAt', 1);
|
|
396
|
+
const key = numType + ':' + startAt;
|
|
397
|
+
if (!autoNumCtrs[key]) autoNumCtrs[key] = startAt;
|
|
398
|
+
bulletChar = esc(formatAutoNum(numType, autoNumCtrs[key]++));
|
|
399
|
+
}
|
|
400
|
+
const bSzPx = maxSzPx;
|
|
401
|
+
bulletSvg = `<text x="${px(bx2)}" y="${px(baseline)}" font-size="${px(bSzPx)}" font-family="sans-serif" fill="#000">${bulletChar}</text>`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Build <text> element with <tspan>s
|
|
405
|
+
let tspanSvg = '';
|
|
406
|
+
let firstSpan = true;
|
|
407
|
+
for (const ts of tspans) {
|
|
408
|
+
if (ts.br) {
|
|
409
|
+
curY += lnH;
|
|
410
|
+
tspanSvg += `<tspan x="${px(xPos)}" dy="${px(lnH)}">`;
|
|
411
|
+
firstSpan = false;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const dy = firstSpan ? 0 : 0;
|
|
415
|
+
const deco = ts.underline ? 'underline' : ts.strike ? 'line-through' : 'none';
|
|
416
|
+
let adjustedY = baseline;
|
|
417
|
+
if (ts.baseline > 0) adjustedY = baseline - ts.szPx * 0.38;
|
|
418
|
+
else if (ts.baseline < 0) adjustedY = baseline + ts.szPx * 0.12;
|
|
419
|
+
const subSzPx = ts.baseline !== 0 ? ts.szPx * 0.65 : ts.szPx;
|
|
420
|
+
|
|
421
|
+
tspanSvg += `<tspan` +
|
|
422
|
+
` font-family="${esc(ts.family)}, sans-serif"` +
|
|
423
|
+
` font-size="${px(subSzPx)}"` +
|
|
424
|
+
` font-weight="${ts.bold}"` +
|
|
425
|
+
` font-style="${ts.italic}"` +
|
|
426
|
+
` fill="${esc(ts.color)}"` +
|
|
427
|
+
(deco !== 'none' ? ` text-decoration="${deco}"` : '') +
|
|
428
|
+
(ts.baseline !== 0 ? ` dy="${px(adjustedY - baseline)}"` : '') +
|
|
429
|
+
`>${esc(ts.text)}</tspan>`;
|
|
430
|
+
firstSpan = false;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
svgLines += bulletSvg;
|
|
434
|
+
svgLines += `<text x="${px(xPos)}" y="${px(baseline)}" text-anchor="${textAnchor}">${tspanSvg}</text>`;
|
|
435
|
+
curY += lnH + spaceAfter;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return `<g clip-path="url(#${clipId})"${vertTransform}>${svgLines}</g>`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function formatAutoNum(type, n) {
|
|
442
|
+
switch (type) {
|
|
443
|
+
case 'arabicPeriod': return n + '.';
|
|
444
|
+
case 'arabicParenR': return n + ')';
|
|
445
|
+
case 'arabicParenBoth': return '(' + n + ')';
|
|
446
|
+
case 'romanLcPeriod': return toRoman(n).toLowerCase() + '.';
|
|
447
|
+
case 'romanUcPeriod': return toRoman(n) + '.';
|
|
448
|
+
case 'alphaLcParenR': return String.fromCharCode(96 + n) + ')';
|
|
449
|
+
case 'alphaUcParenR': return String.fromCharCode(64 + n) + ')';
|
|
450
|
+
default: return n + '.';
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function toRoman(n) {
|
|
454
|
+
const v=[1000,900,500,400,100,90,50,40,10,9,5,4,1];
|
|
455
|
+
const s=['M','CM','D','CD','C','XC','L','XL','X','IX','V','IV','I'];
|
|
456
|
+
let r='';
|
|
457
|
+
for(let i=0;i<v.length;i++) while(n>=v[i]){r+=s[i];n-=v[i];}
|
|
458
|
+
return r;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ── Shape → SVG ───────────────────────────────────────────────────────────────
|
|
462
|
+
async function shapesToSvg(spTreeEl, rels, imageCache, themeColors, themeData, defs) {
|
|
463
|
+
if (!spTreeEl) return '';
|
|
464
|
+
let out = '';
|
|
465
|
+
for (const child of spTreeEl.children) {
|
|
466
|
+
const ln = child.localName;
|
|
467
|
+
if (ln === 'sp') out += await shapeToSvg(child, themeColors, themeData, defs);
|
|
468
|
+
else if (ln === 'pic') out += await pictureToSvg(child, rels, imageCache, themeColors, defs);
|
|
469
|
+
else if (ln === 'cxnSp') out += await connectorToSvg(child, themeColors, defs);
|
|
470
|
+
else if (ln === 'grpSp') out += await groupToSvg(child, rels, imageCache, themeColors, themeData, defs);
|
|
471
|
+
else if (ln === 'graphicFrame') out += await graphicFrameToSvg(child, themeColors, defs);
|
|
472
|
+
}
|
|
473
|
+
return out;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function shapeToSvg(spEl, themeColors, themeData, defs) {
|
|
477
|
+
const spPr = g1(spEl, 'spPr');
|
|
478
|
+
const xfrm = g1(spPr, 'xfrm');
|
|
479
|
+
const b = xfrmBounds(xfrm);
|
|
480
|
+
if (!b) return '';
|
|
481
|
+
|
|
482
|
+
const prstGeom = g1(spPr, 'prstGeom');
|
|
483
|
+
const prst = prstGeom ? attr(prstGeom, 'prst', 'rect') : 'rect';
|
|
484
|
+
const custGeom = g1(spPr, 'custGeom');
|
|
485
|
+
|
|
486
|
+
// Fill
|
|
487
|
+
const fillNames = ['noFill','solidFill','gradFill','blipFill','pattFill'];
|
|
488
|
+
let fillElSource = null;
|
|
489
|
+
for (const fn of fillNames) { const el = g1(spPr, fn); if (el) { fillElSource = el; break; } }
|
|
490
|
+
if (!fillElSource) {
|
|
491
|
+
const styleEl = g1(spEl, 'style');
|
|
492
|
+
const fillRef = styleEl ? g1(styleEl, 'fillRef') : null;
|
|
493
|
+
if (fillRef && attrInt(fillRef, 'idx', 1) !== 0) {
|
|
494
|
+
const cc = findFirstColorChild(fillRef);
|
|
495
|
+
const c = resolveColorElement(cc, themeColors);
|
|
496
|
+
if (c) {
|
|
497
|
+
const ns = 'http://schemas.openxmlformats.org/drawingml/2006/main';
|
|
498
|
+
const doc2 = new DOMParser().parseFromString(`<solidFill xmlns="${ns}"><srgbClr val="${colorToCss(c).replace('#','')}"/></solidFill>`, 'application/xml');
|
|
499
|
+
fillElSource = doc2.documentElement;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const { fill, fillAttrs } = fillAttr(fillElSource, defs, themeColors, b.x, b.y, b.w, b.h);
|
|
505
|
+
|
|
506
|
+
// Stroke
|
|
507
|
+
let lnEl = g1(spPr, 'ln');
|
|
508
|
+
if (!lnEl) {
|
|
509
|
+
const styleEl = g1(spEl, 'style');
|
|
510
|
+
const lnRef = styleEl ? g1(styleEl, 'lnRef') : null;
|
|
511
|
+
if (lnRef && attrInt(lnRef, 'idx', 1) !== 0) {
|
|
512
|
+
const cc = findFirstColorChild(lnRef);
|
|
513
|
+
const c = resolveColorElement(cc, themeColors);
|
|
514
|
+
if (c) {
|
|
515
|
+
const ns = 'http://schemas.openxmlformats.org/drawingml/2006/main';
|
|
516
|
+
const doc2 = new DOMParser().parseFromString(`<ln xmlns="${ns}"><solidFill><srgbClr val="${colorToCss(c).replace('#','')}"/></solidFill></ln>`, 'application/xml');
|
|
517
|
+
lnEl = doc2.documentElement;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const stroke = strokeAttrs(lnEl, themeColors, 1);
|
|
522
|
+
|
|
523
|
+
// Shadow
|
|
524
|
+
const effectLst = g1(spPr, 'effectLst');
|
|
525
|
+
const filt = shadowFilter(effectLst, defs);
|
|
526
|
+
|
|
527
|
+
// Transform
|
|
528
|
+
const transform = xfrmAttrs(xfrm);
|
|
529
|
+
|
|
530
|
+
// Path
|
|
531
|
+
let pathSvg = '';
|
|
532
|
+
if (prst === 'rect' || (!prstGeom && !custGeom)) {
|
|
533
|
+
pathSvg = `<rect x="${px(b.x)}" y="${px(b.y)}" width="${px(b.w)}" height="${px(b.h)}" fill="${esc(fill)}"${fillAttrs}${stroke}${filt}${transform}/>`;
|
|
534
|
+
} else {
|
|
535
|
+
const d = presetToSvgPath(prst, b.x, b.y, b.w, b.h, {});
|
|
536
|
+
pathSvg = `<path d="${esc(d)}" fill="${esc(fill)}"${fillAttrs}${stroke}${filt}${transform}/>`;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Text
|
|
540
|
+
const txBody = g1(spEl, 'txBody');
|
|
541
|
+
const textSvg = txBody ? textBodyToSvg(txBody, b.x, b.y, b.w, b.h, themeColors, themeData, defs) : '';
|
|
542
|
+
|
|
543
|
+
return `<g>${pathSvg}${textSvg}</g>`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function pictureToSvg(picEl, rels, imageCache, themeColors, defs) {
|
|
547
|
+
const spPr = g1(picEl, 'spPr');
|
|
548
|
+
const xfrm = g1(spPr, 'xfrm');
|
|
549
|
+
const b = xfrmBounds(xfrm);
|
|
550
|
+
if (!b) return '';
|
|
551
|
+
|
|
552
|
+
const blipFill = g1(picEl, 'blipFill');
|
|
553
|
+
const blip = blipFill ? g1(blipFill, 'blip') : null;
|
|
554
|
+
const rId = blip ? (blip.getAttribute('r:embed') || blip.getAttribute('embed')) : null;
|
|
555
|
+
const rel = rId ? rels[rId] : null;
|
|
556
|
+
const imgData = rel ? imageCache[rel.fullPath] : null;
|
|
557
|
+
|
|
558
|
+
const transform = xfrmAttrs(xfrm);
|
|
559
|
+
const effectLst = g1(spPr, 'effectLst');
|
|
560
|
+
const filt = shadowFilter(effectLst, defs);
|
|
561
|
+
|
|
562
|
+
if (!imgData) {
|
|
563
|
+
return `<rect x="${px(b.x)}" y="${px(b.y)}" width="${px(b.w)}" height="${px(b.h)}" fill="#e0e0e0"${transform}/>`;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Convert to base64
|
|
567
|
+
const ext = rel.fullPath.split('.').pop().toLowerCase();
|
|
568
|
+
const mime = ext === 'png' ? 'image/png' : ext === 'gif' ? 'image/gif'
|
|
569
|
+
: ext === 'svg' ? 'image/svg+xml' : 'image/jpeg';
|
|
570
|
+
const b64 = btoa(String.fromCharCode(...(imgData instanceof Uint8Array ? imgData : new Uint8Array(imgData))));
|
|
571
|
+
|
|
572
|
+
// Clip to shape bounds
|
|
573
|
+
const clipId = uid('pic');
|
|
574
|
+
defs.push(`<clipPath id="${clipId}"><rect x="${px(b.x)}" y="${px(b.y)}" width="${px(b.w)}" height="${px(b.h)}"/></clipPath>`);
|
|
575
|
+
|
|
576
|
+
return `<image x="${px(b.x)}" y="${px(b.y)}" width="${px(b.w)}" height="${px(b.h)}" href="data:${mime};base64,${b64}" clip-path="url(#${clipId})"${filt}${transform} preserveAspectRatio="xMidYMid slice"/>`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async function connectorToSvg(cxnSpEl, themeColors, defs) {
|
|
580
|
+
const spPr = g1(cxnSpEl, 'spPr');
|
|
581
|
+
const xfrm = g1(spPr, 'xfrm');
|
|
582
|
+
const b = xfrmBounds(xfrm);
|
|
583
|
+
if (!b) return '';
|
|
584
|
+
const lnEl = g1(spPr, 'ln');
|
|
585
|
+
const stroke = strokeAttrs(lnEl, themeColors, 1);
|
|
586
|
+
const transform = xfrmAttrs(xfrm);
|
|
587
|
+
return `<line x1="${px(b.x)}" y1="${px(b.y)}" x2="${px(b.x+b.w)}" y2="${px(b.y+b.h)}" fill="none"${stroke}${transform}/>`;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function groupToSvg(grpSpEl, rels, imageCache, themeColors, themeData, defs) {
|
|
591
|
+
const spPr = g1(grpSpEl, 'grpSpPr');
|
|
592
|
+
const xfrm = g1(spPr, 'xfrm');
|
|
593
|
+
const b = xfrmBounds(xfrm);
|
|
594
|
+
const transform = xfrm ? xfrmAttrs(xfrm) : '';
|
|
595
|
+
const children = await shapesToSvg(grpSpEl, rels, imageCache, themeColors, themeData, defs);
|
|
596
|
+
return `<g${transform}>${children}</g>`;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async function graphicFrameToSvg(graphicFrame, themeColors, defs) {
|
|
600
|
+
const xfrm = g1(graphicFrame, 'xfrm');
|
|
601
|
+
const b = xfrmBounds(xfrm);
|
|
602
|
+
if (!b) return '';
|
|
603
|
+
// Placeholder (charts/SmartArt SVG rendering is complex; show clean box)
|
|
604
|
+
return `<rect x="${px(b.x)}" y="${px(b.y)}" width="${px(b.w)}" height="${px(b.h)}" fill="#f4f4f8" stroke="#ccc" stroke-width="1"/>` +
|
|
605
|
+
`<text x="${px(b.x+b.w/2)}" y="${px(b.y+b.h/2)}" text-anchor="middle" font-size="14" fill="#999">Chart</text>`;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// ── Background → SVG ──────────────────────────────────────────────────────────
|
|
609
|
+
async function backgroundToSvg(slideDoc, masterDoc, layoutDoc, imageCache, masterRels, themeColors, slideW, slideH, defs) {
|
|
610
|
+
const getbg = (doc) => {
|
|
611
|
+
const cSld = g1(doc, 'cSld');
|
|
612
|
+
const bg = cSld ? g1(cSld, 'bg') : null;
|
|
613
|
+
if (!bg) return null;
|
|
614
|
+
return { bgPr: g1(bg, 'bgPr'), bgRef: g1(bg, 'bgRef') };
|
|
615
|
+
};
|
|
616
|
+
const bgData = getbg(slideDoc) || getbg(layoutDoc) || getbg(masterDoc);
|
|
617
|
+
if (!bgData) return `<rect width="${px(slideW)}" height="${px(slideH)}" fill="white"/>`;
|
|
618
|
+
|
|
619
|
+
const { bgPr, bgRef } = bgData;
|
|
620
|
+
if (bgPr) {
|
|
621
|
+
const fills = ['noFill','solidFill','gradFill','blipFill','pattFill'];
|
|
622
|
+
for (const fn of fills) {
|
|
623
|
+
const fillEl = g1(bgPr, fn);
|
|
624
|
+
if (fillEl) {
|
|
625
|
+
if (fn === 'blipFill') {
|
|
626
|
+
const blip = g1(fillEl, 'blip');
|
|
627
|
+
const rId = blip ? (blip.getAttribute('r:embed') || blip.getAttribute('embed')) : null;
|
|
628
|
+
const rel = rId && masterRels ? masterRels[rId] : null;
|
|
629
|
+
const imgData = rel ? imageCache[rel.fullPath] : null;
|
|
630
|
+
if (imgData) {
|
|
631
|
+
const ext = rel.fullPath.split('.').pop().toLowerCase();
|
|
632
|
+
const mime = ext === 'png' ? 'image/png' : 'image/jpeg';
|
|
633
|
+
const b64 = btoa(String.fromCharCode(...new Uint8Array(imgData)));
|
|
634
|
+
return `<image width="${px(slideW)}" height="${px(slideH)}" href="data:${mime};base64,${b64}" preserveAspectRatio="xMidYMid slice"/>`;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const { fill, fillAttrs } = fillAttr(fillEl, defs, themeColors, 0, 0, slideW, slideH);
|
|
638
|
+
return `<rect width="${px(slideW)}" height="${px(slideH)}" fill="${esc(fill)}"${fillAttrs}/>`;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (bgRef) {
|
|
643
|
+
const cc = findFirstColorChild(bgRef);
|
|
644
|
+
const c = resolveColorElement(cc, themeColors);
|
|
645
|
+
if (c) return `<rect width="${px(slideW)}" height="${px(slideH)}" fill="${esc(colorToCss(c))}"/>`;
|
|
646
|
+
}
|
|
647
|
+
return `<rect width="${px(slideW)}" height="${px(slideH)}" fill="white"/>`;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ── Main entry ────────────────────────────────────────────────────────────────
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Render a single slide to an SVG string.
|
|
654
|
+
*
|
|
655
|
+
* @param {number} slideIndex
|
|
656
|
+
* @param {PptxRenderer} renderer — loaded renderer instance
|
|
657
|
+
* @returns {Promise<string>} — complete SVG markup
|
|
658
|
+
*/
|
|
659
|
+
export async function renderSlideToSvg(slideIndex, renderer) {
|
|
660
|
+
const { _files: files, slidePaths, slideSize, themeColors, themeData,
|
|
661
|
+
masterDoc, masterRels, masterImages } = renderer;
|
|
662
|
+
|
|
663
|
+
if (slideIndex < 0 || slideIndex >= slidePaths.length) throw new Error('Slide index out of range');
|
|
664
|
+
|
|
665
|
+
const slidePath = slidePaths[slideIndex];
|
|
666
|
+
const slideXml = files[slidePath] ? new TextDecoder().decode(files[slidePath]) : null;
|
|
667
|
+
if (!slideXml) throw new Error(`Cannot read slide ${slideIndex}`);
|
|
668
|
+
|
|
669
|
+
const slideDoc = parseXml(slideXml);
|
|
670
|
+
const slideRels = await getRels(files, slidePath);
|
|
671
|
+
|
|
672
|
+
// Layout
|
|
673
|
+
const layoutRel = Object.values(slideRels).find(r => r.type?.includes('slideLayout'));
|
|
674
|
+
const layoutDoc = layoutRel && files[layoutRel.fullPath]
|
|
675
|
+
? parseXml(new TextDecoder().decode(files[layoutRel.fullPath])) : null;
|
|
676
|
+
const layoutRels = layoutRel ? await getRels(files, layoutRel.fullPath) : {};
|
|
677
|
+
|
|
678
|
+
// Image cache
|
|
679
|
+
const { loadImages } = await import('./render.js');
|
|
680
|
+
const slideImages = await loadImages(files, slideRels);
|
|
681
|
+
const layoutImages = layoutRel ? await loadImages(files, layoutRels) : {};
|
|
682
|
+
const allImages = { ...masterImages, ...layoutImages, ...slideImages };
|
|
683
|
+
|
|
684
|
+
// Slide dimensions in px (96 dpi)
|
|
685
|
+
const W = slideSize.cx / 914400 * 96;
|
|
686
|
+
const H = slideSize.cy / 914400 * 96;
|
|
687
|
+
|
|
688
|
+
const defs = [];
|
|
689
|
+
|
|
690
|
+
// Background
|
|
691
|
+
const bgSvg = await backgroundToSvg(slideDoc, masterDoc, layoutDoc, allImages,
|
|
692
|
+
masterRels, themeColors, W, H, defs);
|
|
693
|
+
|
|
694
|
+
// Master / layout decorative shapes
|
|
695
|
+
const masterTree = g1(g1(masterDoc, 'cSld'), 'spTree');
|
|
696
|
+
const layoutTree = layoutDoc ? g1(g1(layoutDoc, 'cSld'), 'spTree') : null;
|
|
697
|
+
const slideTree = g1(g1(slideDoc, 'cSld'), 'spTree');
|
|
698
|
+
|
|
699
|
+
const masterSvg = masterTree
|
|
700
|
+
? await shapesToSvg(masterTree, masterRels, masterImages, themeColors, themeData, defs) : '';
|
|
701
|
+
const layoutSvg = layoutTree
|
|
702
|
+
? await shapesToSvg(layoutTree, layoutRels, layoutImages, themeColors, themeData, defs) : '';
|
|
703
|
+
const slideSvg = slideTree
|
|
704
|
+
? await shapesToSvg(slideTree, slideRels, allImages, themeColors, themeData, defs) : '';
|
|
705
|
+
|
|
706
|
+
const defsBlock = defs.length ? `<defs>${defs.join('\n')}</defs>` : '';
|
|
707
|
+
|
|
708
|
+
return [
|
|
709
|
+
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
710
|
+
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"`,
|
|
711
|
+
` width="${px(W)}" height="${px(H)}" viewBox="0 0 ${px(W)} ${px(H)}">`,
|
|
712
|
+
defsBlock,
|
|
713
|
+
bgSvg,
|
|
714
|
+
masterSvg,
|
|
715
|
+
layoutSvg,
|
|
716
|
+
slideSvg,
|
|
717
|
+
`</svg>`,
|
|
718
|
+
].join('\n');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Render all slides to SVG strings.
|
|
723
|
+
* @param {PptxRenderer} renderer
|
|
724
|
+
* @returns {Promise<string[]>}
|
|
725
|
+
*/
|
|
726
|
+
export async function renderAllSlidesToSvg(renderer) {
|
|
727
|
+
const results = [];
|
|
728
|
+
for (let i = 0; i < renderer.slideCount; i++) {
|
|
729
|
+
results.push(await renderSlideToSvg(i, renderer));
|
|
730
|
+
}
|
|
731
|
+
return results;
|
|
732
|
+
}
|