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.
- package/.babelrc.cjs +15 -0
- package/.eslintignore +3 -0
- package/.eslintrc.cjs +78 -0
- package/LICENSE +21 -0
- package/README.md +294 -0
- package/favicon.ico +0 -0
- package/index.html +541 -0
- package/package.json +56 -0
- package/rollup.config.js +42 -0
- package/scripts/extract-pptx-structure.js +115 -0
- package/scripts/transvert.js +34 -0
- package/src/adapter/toPptxtojson.ts +46 -0
- package/src/adapter/types.ts +330 -0
- package/src/export/serializePresentation.ts +200 -0
- package/src/index.ts +27 -0
- package/src/model/Layout.ts +218 -0
- package/src/model/Master.ts +114 -0
- package/src/model/Presentation.ts +502 -0
- package/src/model/Slide.ts +386 -0
- package/src/model/Theme.ts +95 -0
- package/src/model/nodes/BaseNode.ts +169 -0
- package/src/model/nodes/ChartNode.ts +55 -0
- package/src/model/nodes/GroupNode.ts +62 -0
- package/src/model/nodes/PicNode.ts +102 -0
- package/src/model/nodes/ShapeNode.ts +289 -0
- package/src/model/nodes/TableNode.ts +135 -0
- package/src/parser/RelParser.ts +81 -0
- package/src/parser/XmlParser.ts +101 -0
- package/src/parser/ZipParser.ts +277 -0
- package/src/parser/units.ts +59 -0
- package/src/serializer/RenderContext.ts +79 -0
- package/src/serializer/StyleResolver.ts +821 -0
- package/src/serializer/backgroundSerializer.ts +143 -0
- package/src/serializer/borderMapper.ts +93 -0
- package/src/serializer/chartSerializer.ts +97 -0
- package/src/serializer/fillMapper.ts +224 -0
- package/src/serializer/groupSerializer.ts +94 -0
- package/src/serializer/imageSerializer.ts +330 -0
- package/src/serializer/index.ts +27 -0
- package/src/serializer/shapeSerializer.ts +694 -0
- package/src/serializer/slideSerializer.ts +250 -0
- package/src/serializer/tableSerializer.ts +66 -0
- package/src/serializer/textSerializer.md +70 -0
- package/src/serializer/textSerializer.ts +1019 -0
- package/src/shapes/customGeometry.ts +178 -0
- package/src/shapes/presets.ts +6587 -0
- package/src/shapes/shapeArc.ts +44 -0
- package/src/types/vendor-shims.d.ts +20 -0
- package/src/utils/color.ts +488 -0
- package/src/utils/emfParser.ts +298 -0
- package/src/utils/media.ts +73 -0
- package/src/utils/mediaWebConvert.ts +100 -0
- package/src/utils/rgbaToPng.ts +33 -0
- package/src/utils/urlSafety.ts +17 -0
- 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
|
+
}
|