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,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slide parser — converts a slide XML into a structured SlideData
|
|
3
|
+
* with typed node objects for each shape on the slide.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SafeXmlNode, parseXml } from '../parser/XmlParser';
|
|
7
|
+
import { RelEntry, resolveRelTarget } from '../parser/RelParser';
|
|
8
|
+
import { emuToPx } from '../parser/units';
|
|
9
|
+
import { parseBaseProps } from './nodes/BaseNode';
|
|
10
|
+
import { ShapeNodeData, parseShapeNode } from './nodes/ShapeNode';
|
|
11
|
+
import { PicNodeData, parsePicNode } from './nodes/PicNode';
|
|
12
|
+
import { TableNodeData, parseTableNode } from './nodes/TableNode';
|
|
13
|
+
import { GroupNodeData, parseGroupNode } from './nodes/GroupNode';
|
|
14
|
+
import { ChartNodeData, parseChartNode } from './nodes/ChartNode';
|
|
15
|
+
|
|
16
|
+
export type SlideNode = ShapeNodeData | PicNodeData | TableNodeData | GroupNodeData | ChartNodeData;
|
|
17
|
+
|
|
18
|
+
export interface SlideData {
|
|
19
|
+
index: number;
|
|
20
|
+
nodes: SlideNode[];
|
|
21
|
+
background?: SafeXmlNode;
|
|
22
|
+
layoutIndex: string;
|
|
23
|
+
rels: Map<string, RelEntry>;
|
|
24
|
+
/** Full path to the slide file (e.g. "ppt/slides/slide3.xml"). */
|
|
25
|
+
slidePath: string;
|
|
26
|
+
/** When false, shapes from the layout and master should NOT be rendered on this slide. */
|
|
27
|
+
showMasterSp: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check whether a graphicFrame contains a table (`a:tbl`).
|
|
32
|
+
*/
|
|
33
|
+
function isTableFrame(node: SafeXmlNode): boolean {
|
|
34
|
+
const graphic = node.child('graphic');
|
|
35
|
+
const graphicData = graphic.child('graphicData');
|
|
36
|
+
return graphicData.child('tbl').exists();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check whether a graphicFrame contains a chart.
|
|
41
|
+
*/
|
|
42
|
+
function isChartFrame(node: SafeXmlNode): boolean {
|
|
43
|
+
const graphic = node.child('graphic');
|
|
44
|
+
const graphicData = graphic.child('graphicData');
|
|
45
|
+
const uri = graphicData.attr('uri') || '';
|
|
46
|
+
return uri.includes('chart');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Find p:pic inside OLE graphicData (mc:AlternateContent > mc:Fallback or mc:Choice > p:oleObj > p:pic).
|
|
51
|
+
* Returns the pic node if it has blipFill with embed (so we can render the preview image).
|
|
52
|
+
*/
|
|
53
|
+
function findOleFallbackPic(graphicFrame: SafeXmlNode): SafeXmlNode | null {
|
|
54
|
+
const graphic = graphicFrame.child('graphic');
|
|
55
|
+
const graphicData = graphic.child('graphicData');
|
|
56
|
+
const uri = graphicData.attr('uri') || '';
|
|
57
|
+
if (!uri.includes('ole')) return null;
|
|
58
|
+
|
|
59
|
+
const altContent = graphicData.child('AlternateContent');
|
|
60
|
+
if (!altContent.exists()) return null;
|
|
61
|
+
|
|
62
|
+
for (const branch of ['Fallback', 'Choice'] as const) {
|
|
63
|
+
const oleObj = altContent.child(branch).child('oleObj');
|
|
64
|
+
if (!oleObj.exists()) continue;
|
|
65
|
+
const pic = oleObj.child('pic');
|
|
66
|
+
if (!pic.exists()) continue;
|
|
67
|
+
const blipFill = pic.child('blipFill');
|
|
68
|
+
const blip = blipFill.child('blip');
|
|
69
|
+
const embed = blip.attr('embed') ?? blip.attr('r:embed');
|
|
70
|
+
if (embed) return pic;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parse a graphicFrame that contains an OLE object with a fallback picture (preview image).
|
|
77
|
+
* Uses the frame's position/size and the inner pic's blip embed.
|
|
78
|
+
* Exported for use in GroupRenderer when parsing group children.
|
|
79
|
+
*/
|
|
80
|
+
export function parseOleFrameAsPicture(graphicFrame: SafeXmlNode): PicNodeData | undefined {
|
|
81
|
+
const pic = findOleFallbackPic(graphicFrame);
|
|
82
|
+
if (!pic) return undefined;
|
|
83
|
+
|
|
84
|
+
const base = parseBaseProps(graphicFrame);
|
|
85
|
+
const blipFill = pic.child('blipFill');
|
|
86
|
+
const blip = blipFill.child('blip');
|
|
87
|
+
const blipEmbed = blip.attr('embed') ?? blip.attr('r:embed');
|
|
88
|
+
const blipLink = blip.attr('link') ?? blip.attr('r:link');
|
|
89
|
+
if (!blipEmbed) return undefined;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
...base,
|
|
93
|
+
nodeType: 'picture',
|
|
94
|
+
blipEmbed,
|
|
95
|
+
blipLink,
|
|
96
|
+
source: graphicFrame,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check whether a graphicFrame contains a SmartArt diagram.
|
|
102
|
+
*/
|
|
103
|
+
function isDiagramFrame(node: SafeXmlNode): boolean {
|
|
104
|
+
const graphic = node.child('graphic');
|
|
105
|
+
const graphicData = graphic.child('graphicData');
|
|
106
|
+
const uri = graphicData.attr('uri') || '';
|
|
107
|
+
return uri.includes('diagram');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse a SmartArt diagram graphicFrame by resolving the diagram drawing fallback XML.
|
|
112
|
+
* The drawing XML contains pre-rendered shapes in a spTree that we can display as a group.
|
|
113
|
+
*/
|
|
114
|
+
function parseDiagramFrame(
|
|
115
|
+
graphicFrame: SafeXmlNode,
|
|
116
|
+
rels: Map<string, RelEntry>,
|
|
117
|
+
slidePath: string,
|
|
118
|
+
diagramDrawings: Map<string, string>,
|
|
119
|
+
): GroupNodeData | undefined {
|
|
120
|
+
const base = parseBaseProps(graphicFrame);
|
|
121
|
+
const slideDir = slidePath.substring(0, slidePath.lastIndexOf('/'));
|
|
122
|
+
const drawingCandidates = Array.from(rels.values())
|
|
123
|
+
.filter(
|
|
124
|
+
(entry) => entry.type.includes('diagramDrawing') || entry.target.includes('diagrams/drawing'),
|
|
125
|
+
)
|
|
126
|
+
.map((entry) => {
|
|
127
|
+
const target = entry.target;
|
|
128
|
+
const match = target.match(/drawing(\d+)/);
|
|
129
|
+
return {
|
|
130
|
+
target,
|
|
131
|
+
num: match ? Number.parseInt(match[1], 10) : undefined,
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Extract the diagram data rId from the relIds element to identify which diagram this is
|
|
136
|
+
const graphic = graphicFrame.child('graphic');
|
|
137
|
+
const graphicData = graphic.child('graphicData');
|
|
138
|
+
const relIds = graphicData.child('relIds');
|
|
139
|
+
|
|
140
|
+
// Strategy 1: Match data file number to drawing file number
|
|
141
|
+
// e.g. data3.xml → drawing3.xml
|
|
142
|
+
if (relIds.exists()) {
|
|
143
|
+
const dmRId = relIds.attr('r:dm') ?? relIds.attr('dm');
|
|
144
|
+
if (dmRId) {
|
|
145
|
+
const dmRel = rels.get(dmRId);
|
|
146
|
+
if (dmRel) {
|
|
147
|
+
// Extract the number from the data target (e.g. "data3" → "3")
|
|
148
|
+
const numMatch = dmRel.target.match(/data(\d+)/);
|
|
149
|
+
if (numMatch) {
|
|
150
|
+
const drawingNum = Number.parseInt(numMatch[1], 10);
|
|
151
|
+
// Prefer exact drawingN; if absent, use the nearest numbered drawing relation.
|
|
152
|
+
const ordered = drawingCandidates.slice().sort((a, b) => {
|
|
153
|
+
const da =
|
|
154
|
+
a.num === undefined ? Number.POSITIVE_INFINITY : Math.abs(a.num - drawingNum);
|
|
155
|
+
const db =
|
|
156
|
+
b.num === undefined ? Number.POSITIVE_INFINITY : Math.abs(b.num - drawingNum);
|
|
157
|
+
return da - db;
|
|
158
|
+
});
|
|
159
|
+
for (const candidate of ordered) {
|
|
160
|
+
const drawingPath = resolveRelTarget(slideDir, candidate.target);
|
|
161
|
+
const drawingXml = diagramDrawings.get(drawingPath);
|
|
162
|
+
if (drawingXml) {
|
|
163
|
+
return buildDiagramGroup(base, drawingXml);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Strategy 2: Fallback - find any diagramDrawing relationship
|
|
172
|
+
for (const candidate of drawingCandidates) {
|
|
173
|
+
const drawingPath = resolveRelTarget(slideDir, candidate.target);
|
|
174
|
+
const drawingXml = diagramDrawings.get(drawingPath);
|
|
175
|
+
if (drawingXml) {
|
|
176
|
+
return buildDiagramGroup(base, drawingXml);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Read xfrm off/ext from a shape-like node (dsp:sp uses dsp:spPr > a:xfrm).
|
|
185
|
+
*/
|
|
186
|
+
function readShapeBounds(node: SafeXmlNode): { x: number; y: number; w: number; h: number } | null {
|
|
187
|
+
const spPr = node.child('spPr');
|
|
188
|
+
if (!spPr.exists()) return null;
|
|
189
|
+
const xfrm = spPr.child('xfrm');
|
|
190
|
+
if (!xfrm.exists()) return null;
|
|
191
|
+
const off = xfrm.child('off');
|
|
192
|
+
const ext = xfrm.child('ext');
|
|
193
|
+
const x = emuToPx(off.numAttr('x') ?? 0);
|
|
194
|
+
const y = emuToPx(off.numAttr('y') ?? 0);
|
|
195
|
+
const w = emuToPx(ext.numAttr('cx') ?? 0);
|
|
196
|
+
const h = emuToPx(ext.numAttr('cy') ?? 0);
|
|
197
|
+
return { x, y, w, h };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Build a GroupNodeData from a diagram drawing XML string.
|
|
202
|
+
* Diagram drawings use dsp: namespace (drawingml 2008); structure is dsp:drawing > dsp:spTree > dsp:sp.
|
|
203
|
+
* Diagram shapes use their own coordinate space; we compute childOffset/childExtent from
|
|
204
|
+
* the actual bounding box of all shapes so remapping preserves layout and spacing.
|
|
205
|
+
*/
|
|
206
|
+
function buildDiagramGroup(
|
|
207
|
+
base: ReturnType<typeof parseBaseProps>,
|
|
208
|
+
drawingXml: string,
|
|
209
|
+
): GroupNodeData {
|
|
210
|
+
const drawingRoot = parseXml(drawingXml);
|
|
211
|
+
const spTree = drawingRoot.child('spTree');
|
|
212
|
+
if (!spTree.exists()) {
|
|
213
|
+
return {
|
|
214
|
+
...base,
|
|
215
|
+
nodeType: 'group',
|
|
216
|
+
childOffset: { x: 0, y: 0 },
|
|
217
|
+
childExtent: { w: base.size.w, h: base.size.h },
|
|
218
|
+
children: [],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const CHILD_TAGS = new Set(['sp', 'pic', 'grpSp', 'graphicFrame', 'cxnSp']);
|
|
223
|
+
// Circular presets need isotropic scaling; tree/org-chart style diagrams should keep native axis scaling.
|
|
224
|
+
const CIRCULAR_PRESETS = new Set(['pie', 'arc', 'blockArc', 'donut', 'circularArrow']);
|
|
225
|
+
const children: SafeXmlNode[] = [];
|
|
226
|
+
let minX = Infinity;
|
|
227
|
+
let minY = Infinity;
|
|
228
|
+
let maxRight = -Infinity;
|
|
229
|
+
let maxBottom = -Infinity;
|
|
230
|
+
let hasCircularPreset = false;
|
|
231
|
+
|
|
232
|
+
for (const child of spTree.allChildren()) {
|
|
233
|
+
if (CHILD_TAGS.has(child.localName)) {
|
|
234
|
+
children.push(child);
|
|
235
|
+
const prst = child.child('spPr').child('prstGeom').attr('prst');
|
|
236
|
+
if (prst && CIRCULAR_PRESETS.has(prst)) hasCircularPreset = true;
|
|
237
|
+
const b = readShapeBounds(child);
|
|
238
|
+
if (b) {
|
|
239
|
+
minX = Math.min(minX, b.x);
|
|
240
|
+
minY = Math.min(minY, b.y);
|
|
241
|
+
maxRight = Math.max(maxRight, b.x + b.w);
|
|
242
|
+
maxBottom = Math.max(maxBottom, b.y + b.h);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const hasBounds =
|
|
248
|
+
minX !== Infinity && minY !== Infinity && maxRight !== -Infinity && maxBottom !== -Infinity;
|
|
249
|
+
|
|
250
|
+
// Check if shapes extend significantly beyond the diagram frame (negative offsets or huge extents).
|
|
251
|
+
// When decorative shapes (e.g. blockArc) have large negative coordinates, including them in
|
|
252
|
+
// the bounding box distorts the layout. Fall back to frame-based coordinates in that case.
|
|
253
|
+
const bboxSpansNegative = hasBounds && (minX < 0 || minY < 0);
|
|
254
|
+
const bboxMuchLargerThanFrame =
|
|
255
|
+
hasBounds && (maxRight - minX > base.size.w * 2 || maxBottom - minY > base.size.h * 2);
|
|
256
|
+
const useFrameCoords = bboxSpansNegative || bboxMuchLargerThanFrame;
|
|
257
|
+
|
|
258
|
+
// Use the graphicFrame's own dimensions as the child coordinate space.
|
|
259
|
+
// Diagram shapes are positioned in the frame's coordinate space (EMU converted to px).
|
|
260
|
+
// Using frame dimensions gives a 1:1 scale, preserving original positions and sizes.
|
|
261
|
+
// This avoids enlarging shapes when the bounding box is smaller than the frame.
|
|
262
|
+
let extentW = Math.max(1, base.size.w);
|
|
263
|
+
let extentH = Math.max(1, base.size.h);
|
|
264
|
+
let offX = 0;
|
|
265
|
+
let offY = 0;
|
|
266
|
+
|
|
267
|
+
if (!hasBounds) {
|
|
268
|
+
extentW = Math.max(1, base.size.w);
|
|
269
|
+
extentH = Math.max(1, base.size.h);
|
|
270
|
+
offX = 0;
|
|
271
|
+
offY = 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
...base,
|
|
276
|
+
nodeType: 'group',
|
|
277
|
+
childOffset: { x: offX, y: offY },
|
|
278
|
+
childExtent: { w: extentW, h: extentH },
|
|
279
|
+
children,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Parse a single child node from spTree, dispatching to the appropriate parser.
|
|
285
|
+
*/
|
|
286
|
+
export function parseChildNode(
|
|
287
|
+
child: SafeXmlNode,
|
|
288
|
+
rels: Map<string, RelEntry>,
|
|
289
|
+
slidePath: string,
|
|
290
|
+
diagramDrawings?: Map<string, string>,
|
|
291
|
+
): SlideNode | undefined {
|
|
292
|
+
const tag = child.localName;
|
|
293
|
+
|
|
294
|
+
switch (tag) {
|
|
295
|
+
case 'sp':
|
|
296
|
+
case 'cxnSp':
|
|
297
|
+
return parseShapeNode(child);
|
|
298
|
+
case 'pic':
|
|
299
|
+
return parsePicNode(child);
|
|
300
|
+
case 'grpSp':
|
|
301
|
+
return parseGroupNode(child);
|
|
302
|
+
case 'graphicFrame':
|
|
303
|
+
if (isTableFrame(child)) {
|
|
304
|
+
return parseTableNode(child);
|
|
305
|
+
}
|
|
306
|
+
if (isChartFrame(child)) {
|
|
307
|
+
return parseChartNode(child, rels, slidePath);
|
|
308
|
+
}
|
|
309
|
+
// SmartArt diagram with drawing fallback
|
|
310
|
+
if (isDiagramFrame(child) && diagramDrawings) {
|
|
311
|
+
return parseDiagramFrame(child, rels, slidePath, diagramDrawings);
|
|
312
|
+
}
|
|
313
|
+
// OLE object with fallback picture (e.g. embedded PDF preview on slide 34)
|
|
314
|
+
{
|
|
315
|
+
const olePic = parseOleFrameAsPicture(child);
|
|
316
|
+
if (olePic) return olePic;
|
|
317
|
+
}
|
|
318
|
+
// Non-table/chart/ole graphic frames — skip
|
|
319
|
+
return undefined;
|
|
320
|
+
default:
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Find the layout relationship target from a slide's rels map.
|
|
327
|
+
* The relationship type URI for slide layouts ends with "slideLayout".
|
|
328
|
+
*/
|
|
329
|
+
function findLayoutRel(rels: Map<string, RelEntry>): string {
|
|
330
|
+
for (const [, entry] of rels) {
|
|
331
|
+
if (entry.type.includes('slideLayout')) {
|
|
332
|
+
return entry.target;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return '';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Parse a slide XML root (`p:sld`) into SlideData.
|
|
340
|
+
*
|
|
341
|
+
* @param root Parsed XML root of the slide
|
|
342
|
+
* @param index Zero-based slide index
|
|
343
|
+
* @param rels Relationship entries for this slide
|
|
344
|
+
* @param slidePath Full path to the slide file (e.g. "ppt/slides/slide1.xml")
|
|
345
|
+
*/
|
|
346
|
+
export function parseSlide(
|
|
347
|
+
root: SafeXmlNode,
|
|
348
|
+
index: number,
|
|
349
|
+
rels: Map<string, RelEntry>,
|
|
350
|
+
slidePath: string = '',
|
|
351
|
+
diagramDrawings?: Map<string, string>,
|
|
352
|
+
): SlideData {
|
|
353
|
+
const cSld = root.child('cSld');
|
|
354
|
+
|
|
355
|
+
// --- Background ---
|
|
356
|
+
const bg = cSld.child('bg');
|
|
357
|
+
const background = bg.exists() ? bg : undefined;
|
|
358
|
+
|
|
359
|
+
// --- Parse shape tree children ---
|
|
360
|
+
const spTree = cSld.child('spTree');
|
|
361
|
+
const nodes: SlideNode[] = [];
|
|
362
|
+
|
|
363
|
+
for (const child of spTree.allChildren()) {
|
|
364
|
+
const node = parseChildNode(child, rels, slidePath, diagramDrawings);
|
|
365
|
+
if (node) {
|
|
366
|
+
nodes.push(node);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// --- Layout relationship ---
|
|
371
|
+
const layoutIndex = findLayoutRel(rels);
|
|
372
|
+
|
|
373
|
+
// --- showMasterSp: if "0", layout/master shapes should not be rendered on this slide ---
|
|
374
|
+
const showMasterSpAttr = root.attr('showMasterSp');
|
|
375
|
+
const showMasterSp = showMasterSpAttr !== '0';
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
index,
|
|
379
|
+
nodes,
|
|
380
|
+
background,
|
|
381
|
+
layoutIndex,
|
|
382
|
+
rels,
|
|
383
|
+
slidePath,
|
|
384
|
+
showMasterSp,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme parser — extracts color scheme and font definitions from a:theme XML.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SafeXmlNode } from '../parser/XmlParser';
|
|
6
|
+
|
|
7
|
+
export interface ThemeData {
|
|
8
|
+
colorScheme: Map<string, string>;
|
|
9
|
+
majorFont: { latin: string; ea: string; cs: string };
|
|
10
|
+
minorFont: { latin: string; ea: string; cs: string };
|
|
11
|
+
fillStyles: SafeXmlNode[]; // from a:fillStyleLst children (indexed 1-based)
|
|
12
|
+
lineStyles: SafeXmlNode[]; // from a:lnStyleLst children (indexed 1-based)
|
|
13
|
+
effectStyles: SafeXmlNode[]; // from a:effectStyleLst children (indexed 1-based)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Known color scheme slot names in a:clrScheme. */
|
|
17
|
+
const COLOR_SLOTS = [
|
|
18
|
+
'dk1',
|
|
19
|
+
'dk2',
|
|
20
|
+
'lt1',
|
|
21
|
+
'lt2',
|
|
22
|
+
'accent1',
|
|
23
|
+
'accent2',
|
|
24
|
+
'accent3',
|
|
25
|
+
'accent4',
|
|
26
|
+
'accent5',
|
|
27
|
+
'accent6',
|
|
28
|
+
'hlink',
|
|
29
|
+
'folHlink',
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extract a hex color value from a color definition node.
|
|
34
|
+
* Handles both `a:srgbClr@val` and `a:sysClr@lastClr`.
|
|
35
|
+
*/
|
|
36
|
+
function extractColor(node: SafeXmlNode): string | undefined {
|
|
37
|
+
const srgb = node.child('srgbClr');
|
|
38
|
+
if (srgb.exists()) {
|
|
39
|
+
return srgb.attr('val');
|
|
40
|
+
}
|
|
41
|
+
const sys = node.child('sysClr');
|
|
42
|
+
if (sys.exists()) {
|
|
43
|
+
return sys.attr('lastClr') ?? sys.attr('val');
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse font info from a majorFont or minorFont node.
|
|
50
|
+
* Extracts typeface attributes from latin, ea, and cs child elements.
|
|
51
|
+
*/
|
|
52
|
+
function parseFontInfo(fontNode: SafeXmlNode): { latin: string; ea: string; cs: string } {
|
|
53
|
+
return {
|
|
54
|
+
latin: fontNode.child('latin').attr('typeface') ?? '',
|
|
55
|
+
ea: fontNode.child('ea').attr('typeface') ?? '',
|
|
56
|
+
cs: fontNode.child('cs').attr('typeface') ?? '',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse a theme XML root (`a:theme`) into ThemeData.
|
|
62
|
+
*/
|
|
63
|
+
export function parseTheme(root: SafeXmlNode): ThemeData {
|
|
64
|
+
const themeElements = root.child('themeElements');
|
|
65
|
+
|
|
66
|
+
// --- Color scheme ---
|
|
67
|
+
const clrScheme = themeElements.child('clrScheme');
|
|
68
|
+
const colorScheme = new Map<string, string>();
|
|
69
|
+
|
|
70
|
+
for (const slot of COLOR_SLOTS) {
|
|
71
|
+
const slotNode = clrScheme.child(slot);
|
|
72
|
+
if (slotNode.exists()) {
|
|
73
|
+
const hex = extractColor(slotNode);
|
|
74
|
+
if (hex !== undefined) {
|
|
75
|
+
colorScheme.set(slot, hex);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- Font scheme ---
|
|
81
|
+
const fontScheme = themeElements.child('fontScheme');
|
|
82
|
+
const majorFont = parseFontInfo(fontScheme.child('majorFont'));
|
|
83
|
+
const minorFont = parseFontInfo(fontScheme.child('minorFont'));
|
|
84
|
+
|
|
85
|
+
// --- Format scheme ---
|
|
86
|
+
const fmtScheme = themeElements.child('fmtScheme');
|
|
87
|
+
const fillStyleLst = fmtScheme.child('fillStyleLst');
|
|
88
|
+
const fillStyles: SafeXmlNode[] = fillStyleLst.allChildren();
|
|
89
|
+
const lnStyleLst = fmtScheme.child('lnStyleLst');
|
|
90
|
+
const lineStyles: SafeXmlNode[] = lnStyleLst.allChildren();
|
|
91
|
+
const effectStyleLst = fmtScheme.child('effectStyleLst');
|
|
92
|
+
const effectStyles: SafeXmlNode[] = effectStyleLst.allChildren();
|
|
93
|
+
|
|
94
|
+
return { colorScheme, majorFont, minorFont, fillStyles, lineStyles, effectStyles };
|
|
95
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base node types and property parser shared by all slide node kinds.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SafeXmlNode } from '../../parser/XmlParser';
|
|
6
|
+
import { emuToPx, angleToDeg } from '../../parser/units';
|
|
7
|
+
|
|
8
|
+
export type NodeType = 'shape' | 'picture' | 'table' | 'group' | 'chart' | 'unknown';
|
|
9
|
+
|
|
10
|
+
export interface Position {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Size {
|
|
16
|
+
w: number;
|
|
17
|
+
h: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PlaceholderInfo {
|
|
21
|
+
type?: string;
|
|
22
|
+
idx?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Shape-level hyperlink click action (from cNvPr > a:hlinkClick). */
|
|
26
|
+
export interface HlinkAction {
|
|
27
|
+
/** Action URI, e.g. "ppaction://hlinksldjump", "ppaction://hlinkpres", or empty for URL links. */
|
|
28
|
+
action?: string;
|
|
29
|
+
/** Relationship ID for the target (slide, URL, etc.). */
|
|
30
|
+
rId?: string;
|
|
31
|
+
/** Optional tooltip text. */
|
|
32
|
+
tooltip?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BaseNodeData {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
nodeType: NodeType;
|
|
39
|
+
position: Position;
|
|
40
|
+
size: Size;
|
|
41
|
+
rotation: number;
|
|
42
|
+
flipH: boolean;
|
|
43
|
+
flipV: boolean;
|
|
44
|
+
placeholder?: PlaceholderInfo;
|
|
45
|
+
/** Shape-level hyperlink/click action (action buttons, clickable shapes). */
|
|
46
|
+
hlinkClick?: HlinkAction;
|
|
47
|
+
/** @internal Raw XML node — opaque to consumers. Use serializePresentation() for JSON-safe data. */
|
|
48
|
+
source: SafeXmlNode;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Try to find the non-visual properties container in the given node.
|
|
53
|
+
* PPTX uses different wrapper names depending on the shape kind:
|
|
54
|
+
* p:nvSpPr (shapes/connectors), p:nvPicPr (pictures),
|
|
55
|
+
* p:nvGrpSpPr (groups), p:nvGraphicFramePr (tables/charts).
|
|
56
|
+
*/
|
|
57
|
+
function findNvProps(node: SafeXmlNode): { cNvPr: SafeXmlNode; nvPr: SafeXmlNode } {
|
|
58
|
+
const wrappers = ['nvSpPr', 'nvPicPr', 'nvGrpSpPr', 'nvGraphicFramePr', 'nvCxnSpPr'];
|
|
59
|
+
for (const name of wrappers) {
|
|
60
|
+
const wrapper = node.child(name);
|
|
61
|
+
if (wrapper.exists()) {
|
|
62
|
+
return {
|
|
63
|
+
cNvPr: wrapper.child('cNvPr'),
|
|
64
|
+
nvPr: wrapper.child('nvPr'),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
cNvPr: node.child('cNvPr'),
|
|
70
|
+
nvPr: node.child('nvPr'),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Find the transform (xfrm) node. Shapes use `p:spPr > a:xfrm`,
|
|
76
|
+
* groups use `p:grpSpPr > a:xfrm`, graphic frames use `p:xfrm`.
|
|
77
|
+
*/
|
|
78
|
+
function findXfrm(node: SafeXmlNode): SafeXmlNode {
|
|
79
|
+
// Try spPr first (most shapes)
|
|
80
|
+
const spPr = node.child('spPr');
|
|
81
|
+
if (spPr.exists()) {
|
|
82
|
+
const xfrm = spPr.child('xfrm');
|
|
83
|
+
if (xfrm.exists()) return xfrm;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Try grpSpPr (groups)
|
|
87
|
+
const grpSpPr = node.child('grpSpPr');
|
|
88
|
+
if (grpSpPr.exists()) {
|
|
89
|
+
const xfrm = grpSpPr.child('xfrm');
|
|
90
|
+
if (xfrm.exists()) return xfrm;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Try direct xfrm (graphic frames)
|
|
94
|
+
const directXfrm = node.child('xfrm');
|
|
95
|
+
if (directXfrm.exists()) return directXfrm;
|
|
96
|
+
|
|
97
|
+
// Return empty node — all reads will return defaults
|
|
98
|
+
return node.child('__nonexistent__');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Parse placeholder info from nvPr > p:ph.
|
|
103
|
+
*/
|
|
104
|
+
function parsePlaceholder(nvPr: SafeXmlNode): PlaceholderInfo | undefined {
|
|
105
|
+
const ph = nvPr.child('ph');
|
|
106
|
+
if (!ph.exists()) return undefined;
|
|
107
|
+
|
|
108
|
+
const type = ph.attr('type');
|
|
109
|
+
const idx = ph.numAttr('idx');
|
|
110
|
+
|
|
111
|
+
return { type, idx };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Parse the base properties common to all node types from a shape-like XML node.
|
|
116
|
+
* Returns everything except `nodeType`, which the caller must set.
|
|
117
|
+
*/
|
|
118
|
+
export function parseBaseProps(spNode: SafeXmlNode): Omit<BaseNodeData, 'nodeType'> {
|
|
119
|
+
const { cNvPr, nvPr } = findNvProps(spNode);
|
|
120
|
+
|
|
121
|
+
const id = cNvPr.attr('id') ?? '';
|
|
122
|
+
const name = cNvPr.attr('name') ?? '';
|
|
123
|
+
|
|
124
|
+
// --- Transform ---
|
|
125
|
+
const xfrm = findXfrm(spNode);
|
|
126
|
+
const off = xfrm.child('off');
|
|
127
|
+
const ext = xfrm.child('ext');
|
|
128
|
+
|
|
129
|
+
const position: Position = {
|
|
130
|
+
x: emuToPx(off.numAttr('x') ?? 0),
|
|
131
|
+
y: emuToPx(off.numAttr('y') ?? 0),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const size: Size = {
|
|
135
|
+
w: emuToPx(ext.numAttr('cx') ?? 0),
|
|
136
|
+
h: emuToPx(ext.numAttr('cy') ?? 0),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const rotation = angleToDeg(xfrm.numAttr('rot') ?? 0);
|
|
140
|
+
const flipH = xfrm.attr('flipH') === '1' || xfrm.attr('flipH') === 'true';
|
|
141
|
+
const flipV = xfrm.attr('flipV') === '1' || xfrm.attr('flipV') === 'true';
|
|
142
|
+
|
|
143
|
+
// --- Placeholder ---
|
|
144
|
+
const placeholder = parsePlaceholder(nvPr);
|
|
145
|
+
|
|
146
|
+
// --- Shape-level hyperlink action (cNvPr > a:hlinkClick) ---
|
|
147
|
+
let hlinkClick: HlinkAction | undefined;
|
|
148
|
+
const hlinkNode = cNvPr.child('hlinkClick');
|
|
149
|
+
if (hlinkNode.exists()) {
|
|
150
|
+
hlinkClick = {
|
|
151
|
+
action: hlinkNode.attr('action') ?? undefined,
|
|
152
|
+
rId: hlinkNode.attr('id') ?? hlinkNode.attr('r:id') ?? undefined,
|
|
153
|
+
tooltip: hlinkNode.attr('tooltip') ?? undefined,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
id,
|
|
159
|
+
name,
|
|
160
|
+
position,
|
|
161
|
+
size,
|
|
162
|
+
rotation,
|
|
163
|
+
flipH,
|
|
164
|
+
flipV,
|
|
165
|
+
placeholder,
|
|
166
|
+
hlinkClick,
|
|
167
|
+
source: spNode,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart node — represents a chart embedded in a graphicFrame element.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SafeXmlNode } from '../../parser/XmlParser';
|
|
6
|
+
import { RelEntry, resolveRelTarget } from '../../parser/RelParser';
|
|
7
|
+
import { BaseNodeData, parseBaseProps } from './BaseNode';
|
|
8
|
+
|
|
9
|
+
export interface ChartNodeData extends BaseNodeData {
|
|
10
|
+
nodeType: 'chart';
|
|
11
|
+
chartPath: string; // e.g. "ppt/charts/chart1.xml"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse a graphicFrame containing a chart reference into a ChartNodeData.
|
|
16
|
+
*
|
|
17
|
+
* @param graphicFrame The graphicFrame XML node
|
|
18
|
+
* @param slideRels Relationship entries for the containing slide
|
|
19
|
+
* @param slidePath Full path of the slide (e.g. "ppt/slides/slide1.xml")
|
|
20
|
+
*/
|
|
21
|
+
export function parseChartNode(
|
|
22
|
+
graphicFrame: SafeXmlNode,
|
|
23
|
+
slideRels: Map<string, RelEntry>,
|
|
24
|
+
slidePath: string,
|
|
25
|
+
): ChartNodeData | undefined {
|
|
26
|
+
const base = parseBaseProps(graphicFrame);
|
|
27
|
+
|
|
28
|
+
// Find chart relationship
|
|
29
|
+
const graphic = graphicFrame.child('graphic');
|
|
30
|
+
const graphicData = graphic.child('graphicData');
|
|
31
|
+
|
|
32
|
+
// Find the chart reference - look for c:chart element with r:id
|
|
33
|
+
let chartRId: string | undefined;
|
|
34
|
+
for (const child of graphicData.allChildren()) {
|
|
35
|
+
if (child.localName === 'chart') {
|
|
36
|
+
chartRId = child.attr('r:id') || child.attr('id');
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!chartRId) return undefined;
|
|
42
|
+
|
|
43
|
+
const rel = slideRels.get(chartRId);
|
|
44
|
+
if (!rel) return undefined;
|
|
45
|
+
|
|
46
|
+
// Resolve chart path relative to slide
|
|
47
|
+
const slideDir = slidePath.substring(0, slidePath.lastIndexOf('/'));
|
|
48
|
+
const chartPath = resolveRelTarget(slideDir, rel.target);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...base,
|
|
52
|
+
nodeType: 'chart' as const,
|
|
53
|
+
chartPath,
|
|
54
|
+
};
|
|
55
|
+
}
|