pptxtojson-pro 0.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.
Files changed (55) hide show
  1. package/.babelrc.cjs +15 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc.cjs +78 -0
  4. package/LICENSE +21 -0
  5. package/README.md +294 -0
  6. package/favicon.ico +0 -0
  7. package/index.html +541 -0
  8. package/package.json +56 -0
  9. package/rollup.config.js +42 -0
  10. package/scripts/extract-pptx-structure.js +115 -0
  11. package/scripts/transvert.js +34 -0
  12. package/src/adapter/toPptxtojson.ts +46 -0
  13. package/src/adapter/types.ts +330 -0
  14. package/src/export/serializePresentation.ts +200 -0
  15. package/src/index.ts +27 -0
  16. package/src/model/Layout.ts +218 -0
  17. package/src/model/Master.ts +114 -0
  18. package/src/model/Presentation.ts +502 -0
  19. package/src/model/Slide.ts +386 -0
  20. package/src/model/Theme.ts +95 -0
  21. package/src/model/nodes/BaseNode.ts +169 -0
  22. package/src/model/nodes/ChartNode.ts +55 -0
  23. package/src/model/nodes/GroupNode.ts +62 -0
  24. package/src/model/nodes/PicNode.ts +102 -0
  25. package/src/model/nodes/ShapeNode.ts +289 -0
  26. package/src/model/nodes/TableNode.ts +135 -0
  27. package/src/parser/RelParser.ts +81 -0
  28. package/src/parser/XmlParser.ts +101 -0
  29. package/src/parser/ZipParser.ts +277 -0
  30. package/src/parser/units.ts +59 -0
  31. package/src/serializer/RenderContext.ts +79 -0
  32. package/src/serializer/StyleResolver.ts +821 -0
  33. package/src/serializer/backgroundSerializer.ts +143 -0
  34. package/src/serializer/borderMapper.ts +93 -0
  35. package/src/serializer/chartSerializer.ts +97 -0
  36. package/src/serializer/fillMapper.ts +224 -0
  37. package/src/serializer/groupSerializer.ts +94 -0
  38. package/src/serializer/imageSerializer.ts +330 -0
  39. package/src/serializer/index.ts +27 -0
  40. package/src/serializer/shapeSerializer.ts +694 -0
  41. package/src/serializer/slideSerializer.ts +250 -0
  42. package/src/serializer/tableSerializer.ts +66 -0
  43. package/src/serializer/textSerializer.md +70 -0
  44. package/src/serializer/textSerializer.ts +1019 -0
  45. package/src/shapes/customGeometry.ts +178 -0
  46. package/src/shapes/presets.ts +6587 -0
  47. package/src/shapes/shapeArc.ts +44 -0
  48. package/src/types/vendor-shims.d.ts +20 -0
  49. package/src/utils/color.ts +488 -0
  50. package/src/utils/emfParser.ts +298 -0
  51. package/src/utils/media.ts +73 -0
  52. package/src/utils/mediaWebConvert.ts +100 -0
  53. package/src/utils/rgbaToPng.ts +33 -0
  54. package/src/utils/urlSafety.ts +17 -0
  55. package/tsconfig.json +24 -0
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 解压 PPTX 并展示其内部文件结构
4
+ * 用法: node scripts/extract-pptx-structure.js <pptx路径> [输出目录]
5
+ * 示例: node scripts/extract-pptx-structure.js ./xxx.pptx
6
+ * node scripts/extract-pptx-structure.js ./xxx.pptx ./output
7
+ */
8
+
9
+ import JSZip from 'jszip'
10
+ import fs from 'fs'
11
+ import path from 'path'
12
+
13
+ const args = process.argv.slice(2)
14
+ const pptxPath = args[0]
15
+ const outDir = args[1] || null
16
+
17
+ if (!pptxPath) {
18
+ console.error('用法: node scripts/extract-pptx-structure.js <pptx路径> [输出目录]')
19
+ process.exit(1)
20
+ }
21
+
22
+ const resolvedPath = path.resolve(pptxPath)
23
+ if (!fs.existsSync(resolvedPath)) {
24
+ console.error('文件不存在:', resolvedPath)
25
+ process.exit(1)
26
+ }
27
+
28
+ /**
29
+ * 将扁平路径列表转成树形结构
30
+ * @param {string[]} paths - 如 ['ppt/slides/slide1.xml', 'ppt/slides/slide2.xml']
31
+ * @returns {Object} 树形对象
32
+ */
33
+ function pathsToTree(paths) {
34
+ const tree = {}
35
+ for (const p of paths) {
36
+ const parts = p.split('/').filter(Boolean)
37
+ let current = tree
38
+ for (let i = 0; i < parts.length; i++) {
39
+ const name = parts[i]
40
+ const isLast = i === parts.length - 1
41
+ if (!current[name]) {
42
+ current[name] = isLast ? null : {}
43
+ }
44
+ if (!isLast) {
45
+ current = current[name]
46
+ }
47
+ }
48
+ }
49
+ return tree
50
+ }
51
+
52
+ /**
53
+ * 递归打印树
54
+ */
55
+ function printTree(obj, prefix = '') {
56
+ const entries = Object.entries(obj).sort((a, b) => {
57
+ const aIsFile = a[1] === null
58
+ const bIsFile = b[1] === null
59
+ if (aIsFile !== bIsFile) return aIsFile ? 1 : -1
60
+ return a[0].localeCompare(b[0])
61
+ })
62
+ for (let i = 0; i < entries.length; i++) {
63
+ const [name, value] = entries[i]
64
+ const isLast = i === entries.length - 1
65
+ const branch = isLast ? '└── ' : '├── '
66
+ const nextPrefix = isLast ? ' ' : '│ '
67
+ if (value === null) {
68
+ console.log(prefix + branch + name)
69
+ } else {
70
+ console.log(prefix + branch + name + '/')
71
+ printTree(value, prefix + nextPrefix)
72
+ }
73
+ }
74
+ }
75
+
76
+ async function main() {
77
+ console.log('正在读取:', resolvedPath)
78
+ const buffer = fs.readFileSync(resolvedPath)
79
+ const zip = await JSZip.loadAsync(buffer)
80
+
81
+ const filePaths = []
82
+ zip.forEach((relativePath) => {
83
+ filePaths.push(relativePath)
84
+ })
85
+ filePaths.sort()
86
+
87
+ console.log('\n========== PPTX 内部文件结构 ==========\n')
88
+ const tree = pathsToTree(filePaths)
89
+ printTree(tree)
90
+
91
+ console.log('\n---------- 扁平文件列表 ----------')
92
+ filePaths.forEach((p) => console.log(p))
93
+ console.log('\n总文件数:', filePaths.length)
94
+
95
+ if (outDir) {
96
+ const outResolved = path.resolve(outDir)
97
+ fs.mkdirSync(outResolved, { recursive: true })
98
+ console.log('\n正在解压到:', outResolved)
99
+ for (const relativePath of filePaths) {
100
+ const file = zip.file(relativePath)
101
+ if (!file) continue
102
+ const fullPath = path.join(outResolved, relativePath)
103
+ const dir = path.dirname(fullPath)
104
+ fs.mkdirSync(dir, { recursive: true })
105
+ const content = await file.async('nodebuffer')
106
+ fs.writeFileSync(fullPath, content)
107
+ }
108
+ console.log('解压完成.')
109
+ }
110
+ }
111
+
112
+ main().catch((err) => {
113
+ console.error(err)
114
+ process.exit(1)
115
+ })
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 命令行脚本:接受一个 .pptx 文件路径,解析并输出 JSON。
4
+ * 用法: node scripts/transvert.js <path-to.pptx>
5
+ * 或: pnpm run transvert <path-to.pptx>
6
+ */
7
+
8
+ import { parse } from '../dist/index.js'
9
+ import fs from 'fs'
10
+ import path from 'path'
11
+
12
+ const pptxPath = process.argv[2]
13
+ if (!pptxPath) {
14
+ console.error('用法: node scripts/transvert.js <path-to.pptx>')
15
+ process.exit(1)
16
+ }
17
+
18
+ const resolved = path.resolve(process.cwd(), pptxPath)
19
+ if (!fs.existsSync(resolved)) {
20
+ console.error('文件不存在:', resolved)
21
+ process.exit(1)
22
+ }
23
+
24
+ const buf = fs.readFileSync(resolved)
25
+ const arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)
26
+
27
+ parse(arrayBuffer)
28
+ .then((json) => {
29
+ console.log(JSON.stringify(json, null, 2))
30
+ })
31
+ .catch((err) => {
32
+ console.error('解析失败:', err.message)
33
+ process.exit(1)
34
+ })
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Adapter: PresentationData + PptxFiles → pptxtojson/PPTist output format.
3
+ * All dimensions in output are in pt (px * 0.75).
4
+ * Delegates slide serialization to the serializer layer (slideToSlide).
5
+ */
6
+
7
+ import type { PresentationData } from '../model/Presentation';
8
+ import type { PptxFiles } from '../parser/ZipParser';
9
+ import type { Output, Slide, Size } from './types';
10
+ import { slideToSlide } from '../serializer/slideSerializer';
11
+
12
+ const PX_TO_PT = 0.75;
13
+
14
+ function pxToPt(px: number): number {
15
+ return px * PX_TO_PT;
16
+ }
17
+
18
+ function getThemeColors(presentation: PresentationData): string[] {
19
+ const themeColors: string[] = [];
20
+ const firstTheme = presentation.themes.values().next().value;
21
+ if (!firstTheme) return ['#000000', '#000000', '#000000', '#000000', '#000000', '#000000'];
22
+ for (let i = 1; i <= 6; i++) {
23
+ const hex = firstTheme.colorScheme.get(`accent${i}`) ?? '000000';
24
+ themeColors.push(hex.startsWith('#') ? hex : `#${hex}`);
25
+ }
26
+ return themeColors;
27
+ }
28
+
29
+ export function toPptxtojsonFormat(
30
+ presentation: PresentationData,
31
+ files: PptxFiles,
32
+ ): Output {
33
+ const size: Size = {
34
+ width: pxToPt(presentation.width),
35
+ height: pxToPt(presentation.height),
36
+ };
37
+ const themeColors = getThemeColors(presentation);
38
+ const slides: Slide[] = presentation.slides.map((slide) =>
39
+ slideToSlide(presentation, slide, files),
40
+ );
41
+ return {
42
+ slides,
43
+ themeColors,
44
+ size,
45
+ };
46
+ }
@@ -0,0 +1,330 @@
1
+ /**
2
+ * pptxtojson / PPTist 输出格式类型定义
3
+ * 长度与坐标单位均为 pt。
4
+ */
5
+
6
+ export interface Size {
7
+ width: number
8
+ height: number
9
+ }
10
+
11
+ export interface Shadow {
12
+ h: number
13
+ v: number
14
+ blur: number
15
+ color: string
16
+ }
17
+
18
+ export interface ColorFill {
19
+ type: 'color'
20
+ value: string
21
+ }
22
+
23
+ export interface ImageFill {
24
+ type: 'image'
25
+ value: {
26
+ picBase64: string
27
+ opacity: number
28
+ }
29
+ }
30
+
31
+ export interface GradientFill {
32
+ type: 'gradient'
33
+ value: {
34
+ path: 'line' | 'circle' | 'rect' | 'shape'
35
+ rot: number
36
+ colors: {
37
+ pos: string
38
+ color: string
39
+ }[]
40
+ }
41
+ }
42
+
43
+ export interface PatternFill {
44
+ type: 'pattern'
45
+ value: {
46
+ type: string
47
+ foregroundColor: string
48
+ backgroundColor: string
49
+ }
50
+ }
51
+
52
+ export type Fill = ColorFill | ImageFill | GradientFill | PatternFill
53
+
54
+ export interface Border {
55
+ borderColor: string
56
+ borderWidth: number
57
+ borderType: 'solid' | 'dashed' | 'dotted'
58
+ }
59
+
60
+ export interface AutoFit {
61
+ type: 'shape' | 'text'
62
+ fontScale?: number
63
+ }
64
+
65
+ export interface Shape {
66
+ type: 'shape'
67
+ left: number
68
+ top: number
69
+ width: number
70
+ height: number
71
+ borderColor: string
72
+ borderWidth: number
73
+ borderType: 'solid' | 'dashed' | 'dotted'
74
+ borderStrokeDasharray: string
75
+ shadow?: Shadow
76
+ fill: Fill
77
+ content: string
78
+ isFlipV: boolean
79
+ isFlipH: boolean
80
+ rotate: number
81
+ shapType: string
82
+ vAlign: string
83
+ path?: string
84
+ keypoints?: Record<string, number>
85
+ name: string
86
+ order: number
87
+ autoFit?: AutoFit
88
+ link?: string
89
+ }
90
+
91
+ export interface Text {
92
+ type: 'text'
93
+ left: number
94
+ top: number
95
+ width: number
96
+ height: number
97
+ borderColor: string
98
+ borderWidth: number
99
+ borderType: 'solid' | 'dashed' | 'dotted'
100
+ borderStrokeDasharray: string
101
+ shadow?: Shadow
102
+ fill: Fill
103
+ isFlipV: boolean
104
+ isFlipH: boolean
105
+ isVertical: boolean
106
+ rotate: number
107
+ content: string
108
+ vAlign: string
109
+ name: string
110
+ order: number
111
+ autoFit?: AutoFit
112
+ link?: string
113
+ }
114
+
115
+ export interface Image {
116
+ type: 'image'
117
+ left: number
118
+ top: number
119
+ width: number
120
+ height: number
121
+ src: string
122
+ rotate: number
123
+ isFlipH: boolean
124
+ isFlipV: boolean
125
+ order: number
126
+ rect?: {
127
+ t?: number
128
+ b?: number
129
+ l?: number
130
+ r?: number
131
+ }
132
+ geom: string
133
+ borderColor: string
134
+ borderWidth: number
135
+ borderType: 'solid' | 'dashed' | 'dotted'
136
+ borderStrokeDasharray: string
137
+ filters?: {
138
+ sharpen?: number
139
+ colorTemperature?: number
140
+ saturation?: number
141
+ brightness?: number
142
+ contrast?: number
143
+ }
144
+ link?: string
145
+ }
146
+
147
+ export interface TableCell {
148
+ text: string
149
+ rowSpan?: number
150
+ colSpan?: number
151
+ vMerge?: number
152
+ hMerge?: number
153
+ fillColor?: string
154
+ fontColor?: string
155
+ fontBold?: boolean
156
+ borders: {
157
+ top?: Border
158
+ bottom?: Border
159
+ left?: Border
160
+ right?: Border
161
+ }
162
+ }
163
+
164
+ export interface Table {
165
+ type: 'table'
166
+ left: number
167
+ top: number
168
+ width: number
169
+ height: number
170
+ data: TableCell[][]
171
+ borders: {
172
+ top?: Border
173
+ bottom?: Border
174
+ left?: Border
175
+ right?: Border
176
+ }
177
+ order: number
178
+ rowHeights: number[]
179
+ colWidths: number[]
180
+ }
181
+
182
+ export type ChartType = 'lineChart' |
183
+ 'line3DChart' |
184
+ 'barChart' |
185
+ 'bar3DChart' |
186
+ 'pieChart' |
187
+ 'pie3DChart' |
188
+ 'doughnutChart' |
189
+ 'areaChart' |
190
+ 'area3DChart' |
191
+ 'scatterChart' |
192
+ 'bubbleChart' |
193
+ 'radarChart' |
194
+ 'surfaceChart' |
195
+ 'surface3DChart' |
196
+ 'stockChart'
197
+
198
+ export interface ChartValue {
199
+ x: string
200
+ y: number
201
+ }
202
+
203
+ export interface ChartXLabel {
204
+ [key: string]: string
205
+ }
206
+
207
+ export interface ChartItem {
208
+ key: string
209
+ values: ChartValue[]
210
+ xlabels: ChartXLabel
211
+ }
212
+
213
+ export type ScatterChartData = [number[], number[]]
214
+
215
+ export interface CommonChart {
216
+ type: 'chart'
217
+ left: number
218
+ top: number
219
+ width: number
220
+ height: number
221
+ data: ChartItem[]
222
+ colors: string[]
223
+ chartType: Exclude<ChartType, 'scatterChart' | 'bubbleChart'>
224
+ barDir?: 'bar' | 'col'
225
+ marker?: boolean
226
+ holeSize?: string
227
+ grouping?: string
228
+ style?: string
229
+ order: number
230
+ }
231
+
232
+ export interface ScatterChart {
233
+ type: 'chart'
234
+ left: number
235
+ top: number
236
+ width: number
237
+ height: number
238
+ data: ScatterChartData
239
+ colors: string[]
240
+ chartType: 'scatterChart' | 'bubbleChart'
241
+ order: number
242
+ }
243
+
244
+ export type Chart = CommonChart | ScatterChart
245
+
246
+ export interface Video {
247
+ type: 'video'
248
+ left: number
249
+ top: number
250
+ width: number
251
+ height: number
252
+ blob?: string
253
+ src?: string
254
+ order: number
255
+ }
256
+
257
+ export interface Audio {
258
+ type: 'audio'
259
+ left: number
260
+ top: number
261
+ width: number
262
+ height: number
263
+ blob: string
264
+ order: number
265
+ }
266
+
267
+ export interface Diagram {
268
+ type: 'diagram'
269
+ left: number
270
+ top: number
271
+ width: number
272
+ height: number
273
+ elements: (Shape | Text)[]
274
+ textList: string[]
275
+ order: number
276
+ }
277
+
278
+ export interface Math {
279
+ type: 'math'
280
+ left: number
281
+ top: number
282
+ width: number
283
+ height: number
284
+ latex: string
285
+ picBase64: string
286
+ order: number
287
+ text?: string
288
+ }
289
+
290
+ export type BaseElement = Shape | Text | Image | Table | Chart | Video | Audio | Diagram | Math
291
+
292
+ export interface Group {
293
+ type: 'group'
294
+ left: number
295
+ top: number
296
+ width: number
297
+ height: number
298
+ rotate: number
299
+ elements: BaseElement[]
300
+ order: number
301
+ isFlipH: boolean
302
+ isFlipV: boolean
303
+ }
304
+
305
+ export type Element = BaseElement | Group
306
+
307
+ export interface SlideTransition {
308
+ type: string
309
+ duration: number
310
+ direction: string | null
311
+ }
312
+
313
+ export interface Slide {
314
+ fill: Fill
315
+ elements: Element[]
316
+ layoutElements: Element[]
317
+ note: string
318
+ transition?: SlideTransition | null
319
+ }
320
+
321
+ export interface Options {
322
+ slideFactor?: number
323
+ fontsizeFactor?: number
324
+ }
325
+
326
+ export interface Output {
327
+ slides: Slide[]
328
+ themeColors: string[]
329
+ size: Size
330
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Serialize PresentationData into a plain JSON-serializable structure.
3
+ * Strips all SafeXmlNode references and re-parses group children.
4
+ */
5
+
6
+ import { PresentationData } from '../model/Presentation';
7
+ import { SlideNode } from '../model/Slide';
8
+ import { ShapeNodeData, TextBody } from '../model/nodes/ShapeNode';
9
+ import { PicNodeData } from '../model/nodes/PicNode';
10
+ import { TableNodeData, TableRow, TableCell } from '../model/nodes/TableNode';
11
+ import { GroupNodeData } from '../model/nodes/GroupNode';
12
+ import { ChartNodeData } from '../model/nodes/ChartNode';
13
+ import { BaseNodeData } from '../model/nodes/BaseNode';
14
+ import { parseShapeNode } from '../model/nodes/ShapeNode';
15
+ import { parsePicNode } from '../model/nodes/PicNode';
16
+ import { parseTableNode } from '../model/nodes/TableNode';
17
+ import { parseGroupNode } from '../model/nodes/GroupNode';
18
+ import { SafeXmlNode } from '../parser/XmlParser';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Serialized Types (JSON-safe)
22
+ // ---------------------------------------------------------------------------
23
+
24
+ interface SerializedParagraph {
25
+ level: number;
26
+ text: string;
27
+ }
28
+
29
+ interface SerializedTextBody {
30
+ paragraphs: SerializedParagraph[];
31
+ totalText: string;
32
+ }
33
+
34
+ interface SerializedCell {
35
+ text: string;
36
+ gridSpan: number;
37
+ rowSpan: number;
38
+ }
39
+
40
+ interface SerializedRow {
41
+ height: number;
42
+ cells: SerializedCell[];
43
+ }
44
+
45
+ export interface SerializedNode {
46
+ id: string;
47
+ name: string;
48
+ nodeType: string;
49
+ position: { x: number; y: number };
50
+ size: { w: number; h: number };
51
+ rotation: number;
52
+ flipH: boolean;
53
+ flipV: boolean;
54
+ presetGeometry?: string;
55
+ textBody?: SerializedTextBody;
56
+ columns?: number[];
57
+ rows?: SerializedRow[];
58
+ tableStyleId?: string;
59
+ blipEmbed?: string;
60
+ chartPath?: string;
61
+ children?: SerializedNode[];
62
+ }
63
+
64
+ export interface SerializedSlide {
65
+ index: number;
66
+ nodes: SerializedNode[];
67
+ }
68
+
69
+ export interface SerializedPresentation {
70
+ width: number;
71
+ height: number;
72
+ slideCount: number;
73
+ slides: SerializedSlide[];
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Helpers
78
+ // ---------------------------------------------------------------------------
79
+
80
+ function serializeTextBody(tb: TextBody | undefined): SerializedTextBody | undefined {
81
+ if (!tb) return undefined;
82
+ const paragraphs: SerializedParagraph[] = tb.paragraphs.map((p) => ({
83
+ level: p.level,
84
+ text: p.runs.map((r) => r.text).join(''),
85
+ }));
86
+ const totalText = paragraphs.map((p) => p.text).join('\n');
87
+ if (!totalText.trim()) return undefined;
88
+ return { paragraphs, totalText };
89
+ }
90
+
91
+ function serializeCell(cell: TableCell): SerializedCell {
92
+ const text = cell.textBody
93
+ ? cell.textBody.paragraphs.map((p) => p.runs.map((r) => r.text).join('')).join('\n')
94
+ : '';
95
+ return { text, gridSpan: cell.gridSpan, rowSpan: cell.rowSpan };
96
+ }
97
+
98
+ function serializeRow(row: TableRow): SerializedRow {
99
+ return {
100
+ height: row.height,
101
+ cells: row.cells.map(serializeCell),
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Parse a raw XML child node from a group into a typed node.
107
+ */
108
+ function parseGroupChild(childXml: SafeXmlNode): BaseNodeData | undefined {
109
+ const tag = childXml.localName;
110
+ switch (tag) {
111
+ case 'sp':
112
+ case 'cxnSp':
113
+ return parseShapeNode(childXml);
114
+ case 'pic':
115
+ return parsePicNode(childXml);
116
+ case 'grpSp':
117
+ return parseGroupNode(childXml);
118
+ case 'graphicFrame': {
119
+ const graphic = childXml.child('graphic');
120
+ const graphicData = graphic.child('graphicData');
121
+ if (graphicData.child('tbl').exists()) {
122
+ return parseTableNode(childXml);
123
+ }
124
+ return undefined;
125
+ }
126
+ default:
127
+ return undefined;
128
+ }
129
+ }
130
+
131
+ function serializeNode(node: SlideNode | BaseNodeData): SerializedNode {
132
+ const base: SerializedNode = {
133
+ id: node.id,
134
+ name: node.name,
135
+ nodeType: node.nodeType,
136
+ position: { x: node.position.x, y: node.position.y },
137
+ size: { w: node.size.w, h: node.size.h },
138
+ rotation: node.rotation,
139
+ flipH: node.flipH,
140
+ flipV: node.flipV,
141
+ };
142
+
143
+ switch (node.nodeType) {
144
+ case 'shape': {
145
+ const s = node as ShapeNodeData;
146
+ base.presetGeometry = s.presetGeometry;
147
+ base.textBody = serializeTextBody(s.textBody);
148
+ break;
149
+ }
150
+ case 'picture': {
151
+ const p = node as PicNodeData;
152
+ base.blipEmbed = p.blipEmbed;
153
+ break;
154
+ }
155
+ case 'table': {
156
+ const t = node as TableNodeData;
157
+ base.columns = [...t.columns];
158
+ base.rows = t.rows.map(serializeRow);
159
+ base.tableStyleId = t.tableStyleId;
160
+ break;
161
+ }
162
+ case 'chart': {
163
+ const c = node as ChartNodeData;
164
+ base.chartPath = c.chartPath;
165
+ break;
166
+ }
167
+ case 'group': {
168
+ const g = node as GroupNodeData;
169
+ const children: SerializedNode[] = [];
170
+ for (const childXml of g.children) {
171
+ try {
172
+ const parsed = parseGroupChild(childXml);
173
+ if (parsed) children.push(serializeNode(parsed));
174
+ } catch {
175
+ // skip unparseable group children
176
+ }
177
+ }
178
+ base.children = children;
179
+ break;
180
+ }
181
+ }
182
+
183
+ return base;
184
+ }
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // Main Export
188
+ // ---------------------------------------------------------------------------
189
+
190
+ export function serializePresentation(pres: PresentationData): SerializedPresentation {
191
+ return {
192
+ width: pres.width,
193
+ height: pres.height,
194
+ slideCount: pres.slides.length,
195
+ slides: pres.slides.map((slide, i) => ({
196
+ index: i,
197
+ nodes: slide.nodes.map(serializeNode),
198
+ })),
199
+ };
200
+ }