prismdeckjs 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/LICENSE +21 -0
- package/README.md +45 -0
- package/dist/chunks/rapier-D1P_-6v-.js +5727 -0
- package/dist/document/archive.d.ts +17 -0
- package/dist/document/archive.d.ts.map +1 -0
- package/dist/document/archive.js +105 -0
- package/dist/document/archive.js.map +1 -0
- package/dist/document/html.d.ts +10 -0
- package/dist/document/html.d.ts.map +1 -0
- package/dist/document/html.js +149 -0
- package/dist/document/html.js.map +1 -0
- package/dist/document/types.d.ts +188 -0
- package/dist/document/types.d.ts.map +1 -0
- package/dist/document/types.js +21 -0
- package/dist/document/types.js.map +1 -0
- package/dist/document/validate.d.ts +9 -0
- package/dist/document/validate.d.ts.map +1 -0
- package/dist/document/validate.js +77 -0
- package/dist/document/validate.js.map +1 -0
- package/dist/import/index.d.ts +9 -0
- package/dist/import/index.d.ts.map +1 -0
- package/dist/import/index.js +41 -0
- package/dist/import/index.js.map +1 -0
- package/dist/import/odp.d.ts +3 -0
- package/dist/import/odp.d.ts.map +1 -0
- package/dist/import/odp.js +529 -0
- package/dist/import/odp.js.map +1 -0
- package/dist/import/pptx.d.ts +3 -0
- package/dist/import/pptx.d.ts.map +1 -0
- package/dist/import/pptx.js +634 -0
- package/dist/import/pptx.js.map +1 -0
- package/dist/import/xml.d.ts +19 -0
- package/dist/import/xml.d.ts.map +1 -0
- package/dist/import/xml.js +100 -0
- package/dist/import/xml.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/physics/rapier.d.ts +36 -0
- package/dist/physics/rapier.d.ts.map +1 -0
- package/dist/physics/rapier.js +137 -0
- package/dist/physics/rapier.js.map +1 -0
- package/dist/prism-deck.min.js +61229 -0
- package/dist/render/renderer.d.ts +78 -0
- package/dist/render/renderer.d.ts.map +1 -0
- package/dist/render/renderer.js +512 -0
- package/dist/render/renderer.js.map +1 -0
- package/dist/render/stereo.d.ts +20 -0
- package/dist/render/stereo.d.ts.map +1 -0
- package/dist/render/stereo.js +26 -0
- package/dist/render/stereo.js.map +1 -0
- package/dist/runtime/player.d.ts +30 -0
- package/dist/runtime/player.d.ts.map +1 -0
- package/dist/runtime/player.js +91 -0
- package/dist/runtime/player.js.map +1 -0
- package/dist/runtime/session.d.ts +50 -0
- package/dist/runtime/session.d.ts.map +1 -0
- package/dist/runtime/session.js +212 -0
- package/dist/runtime/session.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +61 -0
- package/schema/prismdeck.schema.json +124 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
import { RECOMMENDED_ZIP_LIMITS, buildPresentation, materializeAllSlideNodes, parseZip, } from '@aiden0z/pptx-renderer';
|
|
2
|
+
import { strFromU8 } from 'fflate';
|
|
3
|
+
import { unzipWithLimits } from '../document/archive';
|
|
4
|
+
import { DEFAULT_TEXT_STYLE, DEFAULT_TRANSFORM, PRISMDECK_SCHEMA_VERSION, } from '../document/types';
|
|
5
|
+
import { attributeByLocalName, childElements, descendants, firstDescendant, localName, parseRelationships, parseXmlDocument, relationshipsPath, resolvePackagePath, } from './xml';
|
|
6
|
+
const MIME_BY_EXTENSION = {
|
|
7
|
+
png: 'image/png',
|
|
8
|
+
jpg: 'image/jpeg',
|
|
9
|
+
jpeg: 'image/jpeg',
|
|
10
|
+
gif: 'image/gif',
|
|
11
|
+
webp: 'image/webp',
|
|
12
|
+
svg: 'image/svg+xml',
|
|
13
|
+
emf: 'image/emf',
|
|
14
|
+
wmf: 'image/wmf',
|
|
15
|
+
mp3: 'audio/mpeg',
|
|
16
|
+
m4a: 'audio/mp4',
|
|
17
|
+
wav: 'audio/wav',
|
|
18
|
+
mp4: 'video/mp4',
|
|
19
|
+
webm: 'video/webm',
|
|
20
|
+
};
|
|
21
|
+
function stableId(prefix, value) {
|
|
22
|
+
let hash = 2166136261;
|
|
23
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
24
|
+
hash ^= value.charCodeAt(index);
|
|
25
|
+
hash = Math.imul(hash, 16777619);
|
|
26
|
+
}
|
|
27
|
+
return `${prefix}-${(hash >>> 0).toString(36)}`;
|
|
28
|
+
}
|
|
29
|
+
function decodeXml(files, path) {
|
|
30
|
+
const bytes = files[path];
|
|
31
|
+
return bytes ? strFromU8(bytes) : undefined;
|
|
32
|
+
}
|
|
33
|
+
function mimeForPath(path) {
|
|
34
|
+
return MIME_BY_EXTENSION[path.split('.').at(-1)?.toLowerCase() ?? ''] ?? 'application/octet-stream';
|
|
35
|
+
}
|
|
36
|
+
function fileName(path) {
|
|
37
|
+
return path.split('/').at(-1) ?? path;
|
|
38
|
+
}
|
|
39
|
+
function normalizeFrame(x, y, width, height, presentation) {
|
|
40
|
+
const finite = (value, fallback = 0) => (Number.isFinite(value) ? value : fallback);
|
|
41
|
+
const presentationWidth = finite(presentation.width, 1) || 1;
|
|
42
|
+
const presentationHeight = finite(presentation.height, 1) || 1;
|
|
43
|
+
return {
|
|
44
|
+
x: finite(x) / presentationWidth,
|
|
45
|
+
y: finite(y) / presentationHeight,
|
|
46
|
+
width: Math.max(0, finite(width)) / presentationWidth,
|
|
47
|
+
height: Math.max(0, finite(height)) / presentationHeight,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function normalizeXmlFrame(node, presentation) {
|
|
51
|
+
const xfrm = firstDescendant(node, 'xfrm');
|
|
52
|
+
const off = xfrm ? childElements(xfrm, 'off')[0] : undefined;
|
|
53
|
+
const ext = xfrm ? childElements(xfrm, 'ext')[0] : undefined;
|
|
54
|
+
const x = Number(off?.getAttribute('x') ?? 0) / 9_525;
|
|
55
|
+
const y = Number(off?.getAttribute('y') ?? 0) / 9_525;
|
|
56
|
+
const width = Number(ext?.getAttribute('cx') ?? 0) / 9_525;
|
|
57
|
+
const height = Number(ext?.getAttribute('cy') ?? 0) / 9_525;
|
|
58
|
+
return normalizeFrame(x, y, width, height, presentation);
|
|
59
|
+
}
|
|
60
|
+
function elementTransform(renderOrder, rotationZ = 0, flipH = false, flipV = false) {
|
|
61
|
+
return {
|
|
62
|
+
...DEFAULT_TRANSFORM,
|
|
63
|
+
z: renderOrder * 0.002,
|
|
64
|
+
rotationZ,
|
|
65
|
+
scaleX: flipH ? -1 : 1,
|
|
66
|
+
scaleY: flipV ? -1 : 1,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function textFromBody(body) {
|
|
70
|
+
if (!body)
|
|
71
|
+
return '';
|
|
72
|
+
return body.paragraphs.map((paragraph) => paragraph.runs.map((run) => run.text).join('')).join('\n');
|
|
73
|
+
}
|
|
74
|
+
function textFromXml(element) {
|
|
75
|
+
return descendants(element, 't')
|
|
76
|
+
.map((node) => node.textContent ?? '')
|
|
77
|
+
.join('');
|
|
78
|
+
}
|
|
79
|
+
function boolAttribute(node, name) {
|
|
80
|
+
const value = node?.attr(name);
|
|
81
|
+
return value === '1' || value === 'true';
|
|
82
|
+
}
|
|
83
|
+
function colorFromNode(node, theme) {
|
|
84
|
+
if (!node?.exists())
|
|
85
|
+
return undefined;
|
|
86
|
+
const element = node.element;
|
|
87
|
+
if (!element)
|
|
88
|
+
return undefined;
|
|
89
|
+
const srgb = firstDescendant(element, 'srgbClr');
|
|
90
|
+
if (srgb?.getAttribute('val'))
|
|
91
|
+
return `#${srgb.getAttribute('val')}`;
|
|
92
|
+
const system = firstDescendant(element, 'sysClr');
|
|
93
|
+
if (system?.getAttribute('lastClr'))
|
|
94
|
+
return `#${system.getAttribute('lastClr')}`;
|
|
95
|
+
const scheme = firstDescendant(element, 'schemeClr')?.getAttribute('val');
|
|
96
|
+
return scheme ? `#${theme?.colorScheme.get(scheme) ?? '111111'}` : undefined;
|
|
97
|
+
}
|
|
98
|
+
function themeForLayout(presentation, layoutPath) {
|
|
99
|
+
const masterPath = layoutPath ? presentation.layoutToMaster.get(layoutPath) : undefined;
|
|
100
|
+
const themePath = masterPath ? presentation.masterToTheme.get(masterPath) : undefined;
|
|
101
|
+
return themePath ? presentation.themes.get(themePath) : undefined;
|
|
102
|
+
}
|
|
103
|
+
function textStyle(body, presentation, theme = presentation.themes.values().next().value) {
|
|
104
|
+
const paragraph = body?.paragraphs[0];
|
|
105
|
+
const run = paragraph?.runs[0];
|
|
106
|
+
const properties = run?.properties;
|
|
107
|
+
const paragraphProperties = paragraph?.properties;
|
|
108
|
+
const sizeHundredths = Number(properties?.attr('sz') ?? 0);
|
|
109
|
+
const pointSize = sizeHundredths > 0 ? sizeHundredths / 100 : 24;
|
|
110
|
+
const fontElement = properties?.element ? firstDescendant(properties.element, 'latin') : undefined;
|
|
111
|
+
const alignment = paragraphProperties?.attr('algn');
|
|
112
|
+
return {
|
|
113
|
+
fontFamily: fontElement?.getAttribute('typeface') || theme?.minorFont.latin || 'Arial, sans-serif',
|
|
114
|
+
fontSize: Math.max(0.012, (pointSize * (96 / 72)) / presentation.height),
|
|
115
|
+
fontWeight: boolAttribute(properties, 'b') ? 700 : 400,
|
|
116
|
+
fontStyle: boolAttribute(properties, 'i') ? 'italic' : 'normal',
|
|
117
|
+
color: colorFromNode(properties, theme) ?? '#111111',
|
|
118
|
+
align: alignment === 'ctr' ? 'center' : alignment === 'r' ? 'right' : 'left',
|
|
119
|
+
verticalAlign: 'top',
|
|
120
|
+
lineHeight: 1.2,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function shapeKind(preset) {
|
|
124
|
+
if (!preset || preset === 'rect')
|
|
125
|
+
return 'rectangle';
|
|
126
|
+
if (preset === 'roundRect')
|
|
127
|
+
return 'roundedRectangle';
|
|
128
|
+
if (preset === 'ellipse')
|
|
129
|
+
return 'ellipse';
|
|
130
|
+
if (preset.includes('line') || preset.includes('Connector'))
|
|
131
|
+
return 'line';
|
|
132
|
+
return 'custom';
|
|
133
|
+
}
|
|
134
|
+
function sourceBase(id, name, type, frame, renderOrder, sourcePart, nativeType, rotation = 0, flipH = false, flipV = false) {
|
|
135
|
+
return {
|
|
136
|
+
id,
|
|
137
|
+
type,
|
|
138
|
+
name,
|
|
139
|
+
frame,
|
|
140
|
+
transform: elementTransform(renderOrder, rotation, flipH, flipV),
|
|
141
|
+
opacity: 1,
|
|
142
|
+
visible: true,
|
|
143
|
+
renderOrder,
|
|
144
|
+
source: { format: 'pptx', part: sourcePart, nativeId: id, nativeType },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function addMediaAssets(context) {
|
|
148
|
+
const pathToAsset = new Map();
|
|
149
|
+
for (const [path, data] of context.presentation.media) {
|
|
150
|
+
const id = stableId('asset', path);
|
|
151
|
+
pathToAsset.set(path, id);
|
|
152
|
+
context.assets.set(id, { id, fileName: fileName(path), mimeType: mimeForPath(path), data });
|
|
153
|
+
}
|
|
154
|
+
return pathToAsset;
|
|
155
|
+
}
|
|
156
|
+
function relationTarget(sourcePart, relationships, relationId) {
|
|
157
|
+
const relation = relationId ? relationships.get(relationId) : undefined;
|
|
158
|
+
return relation ? resolvePackagePath(sourcePart, relation.target) : undefined;
|
|
159
|
+
}
|
|
160
|
+
function parseCache(cache) {
|
|
161
|
+
if (!cache)
|
|
162
|
+
return [];
|
|
163
|
+
const count = Number(firstDescendant(cache, 'ptCount')?.getAttribute('val') ?? 0);
|
|
164
|
+
const points = new Map();
|
|
165
|
+
for (const point of descendants(cache, 'pt')) {
|
|
166
|
+
const index = Number(point.getAttribute('idx') ?? points.size);
|
|
167
|
+
const value = firstDescendant(point, 'v')?.textContent ?? '';
|
|
168
|
+
points.set(index, value);
|
|
169
|
+
}
|
|
170
|
+
const length = Math.max(count, ...Array.from(points.keys(), (index) => index + 1), 0);
|
|
171
|
+
return Array.from({ length }, (_, index) => points.get(index) ?? null);
|
|
172
|
+
}
|
|
173
|
+
function childCache(node) {
|
|
174
|
+
if (!node)
|
|
175
|
+
return undefined;
|
|
176
|
+
return firstDescendant(node, 'strCache') ?? firstDescendant(node, 'numCache') ?? firstDescendant(node, 'strLit') ?? firstDescendant(node, 'numLit');
|
|
177
|
+
}
|
|
178
|
+
function parseChart(node, context, base) {
|
|
179
|
+
const chart = context.presentation.charts.get(node.chartPath)?.element;
|
|
180
|
+
if (!chart) {
|
|
181
|
+
context.warnings.push({
|
|
182
|
+
code: 'PPTX_CHART_MISSING',
|
|
183
|
+
severity: 'warning',
|
|
184
|
+
message: `Chart data was not found for ${node.name}`,
|
|
185
|
+
elementId: base.id,
|
|
186
|
+
sourcePart: node.chartPath,
|
|
187
|
+
});
|
|
188
|
+
return { ...base, type: 'unsupported', reason: 'Chart data is missing', fallbackText: node.name };
|
|
189
|
+
}
|
|
190
|
+
const chartTypeElement = Array.from(chart.getElementsByTagName('*')).find((element) => localName(element).endsWith('Chart'));
|
|
191
|
+
const nativeType = chartTypeElement ? localName(chartTypeElement) : '';
|
|
192
|
+
const barDirection = firstDescendant(chart, 'barDir')?.getAttribute('val');
|
|
193
|
+
const chartType = nativeType.includes('bar')
|
|
194
|
+
? barDirection === 'col'
|
|
195
|
+
? 'column'
|
|
196
|
+
: 'bar'
|
|
197
|
+
: nativeType.includes('line')
|
|
198
|
+
? 'line'
|
|
199
|
+
: nativeType.includes('pie') || nativeType.includes('doughnut')
|
|
200
|
+
? 'pie'
|
|
201
|
+
: nativeType.includes('area')
|
|
202
|
+
? 'area'
|
|
203
|
+
: 'unknown';
|
|
204
|
+
if (chartType === 'unknown') {
|
|
205
|
+
context.warnings.push({
|
|
206
|
+
code: 'PPTX_CHART_UNSUPPORTED',
|
|
207
|
+
severity: 'warning',
|
|
208
|
+
message: `Chart type ${nativeType || 'unknown'} is not supported in this release`,
|
|
209
|
+
elementId: base.id,
|
|
210
|
+
sourcePart: node.chartPath,
|
|
211
|
+
});
|
|
212
|
+
return {
|
|
213
|
+
...base,
|
|
214
|
+
type: 'unsupported',
|
|
215
|
+
reason: `Unsupported PPTX chart type: ${nativeType || 'unknown'}`,
|
|
216
|
+
fallbackText: node.name,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const series = [];
|
|
220
|
+
let categories = [];
|
|
221
|
+
for (const seriesNode of descendants(chartTypeElement ?? chart, 'ser')) {
|
|
222
|
+
const txNode = childElements(seriesNode, 'tx')[0];
|
|
223
|
+
const catNode = childElements(seriesNode, 'cat')[0];
|
|
224
|
+
const valNode = childElements(seriesNode, 'val')[0] ?? childElements(seriesNode, 'yVal')[0];
|
|
225
|
+
const rawCategories = parseCache(childCache(catNode));
|
|
226
|
+
const values = parseCache(childCache(valNode)).map((value) => {
|
|
227
|
+
if (value === null || value === '')
|
|
228
|
+
return null;
|
|
229
|
+
const number = Number(value);
|
|
230
|
+
return Number.isFinite(number) ? number : null;
|
|
231
|
+
});
|
|
232
|
+
if (rawCategories.length > categories.length)
|
|
233
|
+
categories = rawCategories.map((value) => String(value ?? ''));
|
|
234
|
+
series.push({
|
|
235
|
+
name: String(parseCache(childCache(txNode)).find((value) => value !== null) ?? `Series ${series.length + 1}`),
|
|
236
|
+
values,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const title = firstDescendant(firstDescendant(chart, 'title') ?? chart, 't')?.textContent ?? undefined;
|
|
240
|
+
return { ...base, type: 'chart', chartType, categories, series, title };
|
|
241
|
+
}
|
|
242
|
+
function mapSlideNode(node, sourcePart, renderOrder, context, pathToAsset) {
|
|
243
|
+
const id = stableId('element', `${sourcePart}:${node.id}:${renderOrder}`);
|
|
244
|
+
const frame = normalizeFrame(node.position.x, node.position.y, node.size.w, node.size.h, context.presentation);
|
|
245
|
+
const common = sourceBase(id, node.name || `Element ${renderOrder + 1}`, 'unsupported', frame, renderOrder, sourcePart, node.nodeType, node.rotation, node.flipH, node.flipV);
|
|
246
|
+
const placeholder = node.placeholder
|
|
247
|
+
? { type: node.placeholder.type ?? 'body', index: node.placeholder.idx }
|
|
248
|
+
: undefined;
|
|
249
|
+
if (node.nodeType === 'shape') {
|
|
250
|
+
const shape = node;
|
|
251
|
+
const text = textFromBody(shape.textBody);
|
|
252
|
+
const style = textStyle(shape.textBody, context.presentation);
|
|
253
|
+
if (!shape.presetGeometry && text) {
|
|
254
|
+
return { ...common, type: 'text', text, style, placeholder };
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
...common,
|
|
258
|
+
type: 'shape',
|
|
259
|
+
shape: shapeKind(shape.presetGeometry),
|
|
260
|
+
fill: colorFromNode(shape.fill) ?? (text ? '#FFFFFF00' : '#E7E5E4'),
|
|
261
|
+
stroke: colorFromNode(shape.line) ?? '#44403C',
|
|
262
|
+
strokeWidth: 1,
|
|
263
|
+
text: text || undefined,
|
|
264
|
+
textStyle: text ? style : undefined,
|
|
265
|
+
placeholder,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (node.nodeType === 'picture') {
|
|
269
|
+
const picture = node;
|
|
270
|
+
const mediaPath = relationTarget(sourcePart, context.presentation.slides.find((slide) => slide.slidePath === sourcePart)?.rels ?? new Map(), picture.blipEmbed ?? picture.blipLink);
|
|
271
|
+
const assetId = mediaPath ? pathToAsset.get(mediaPath) : undefined;
|
|
272
|
+
if (!assetId) {
|
|
273
|
+
context.warnings.push({
|
|
274
|
+
code: 'PPTX_IMAGE_MISSING',
|
|
275
|
+
severity: 'warning',
|
|
276
|
+
message: `Image data was not found for ${node.name}`,
|
|
277
|
+
elementId: id,
|
|
278
|
+
sourcePart,
|
|
279
|
+
});
|
|
280
|
+
return { ...common, type: 'unsupported', reason: 'Missing image data', fallbackText: node.name, placeholder };
|
|
281
|
+
}
|
|
282
|
+
if (mediaPath?.toLowerCase().endsWith('.emf') || mediaPath?.toLowerCase().endsWith('.wmf')) {
|
|
283
|
+
context.warnings.push({
|
|
284
|
+
code: 'PPTX_VECTOR_IMAGE_FALLBACK',
|
|
285
|
+
severity: 'warning',
|
|
286
|
+
message: `${fileName(mediaPath)} requires EMF/WMF rasterization support`,
|
|
287
|
+
elementId: id,
|
|
288
|
+
sourcePart: mediaPath,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return { ...common, type: 'image', assetId, alt: node.name, fit: 'contain', placeholder };
|
|
292
|
+
}
|
|
293
|
+
if (node.nodeType === 'table') {
|
|
294
|
+
const table = node;
|
|
295
|
+
return {
|
|
296
|
+
...common,
|
|
297
|
+
type: 'table',
|
|
298
|
+
rows: table.rows.map((row) => row.cells.map((cell) => textFromBody(cell.textBody))),
|
|
299
|
+
headerRows: table.rows.length > 0 ? 1 : 0,
|
|
300
|
+
fill: '#FFFFFF',
|
|
301
|
+
stroke: '#78716C',
|
|
302
|
+
textStyle: { ...DEFAULT_TEXT_STYLE, fontSize: 0.026 },
|
|
303
|
+
placeholder,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (node.nodeType === 'chart') {
|
|
307
|
+
return { ...parseChart(node, context, common), placeholder };
|
|
308
|
+
}
|
|
309
|
+
if (node.nodeType === 'group') {
|
|
310
|
+
const group = node;
|
|
311
|
+
context.warnings.push({
|
|
312
|
+
code: 'PPTX_GROUP_FLATTENED',
|
|
313
|
+
severity: 'warning',
|
|
314
|
+
message: `Group ${node.name} is represented by a fallback in this release`,
|
|
315
|
+
elementId: id,
|
|
316
|
+
sourcePart,
|
|
317
|
+
});
|
|
318
|
+
return {
|
|
319
|
+
...common,
|
|
320
|
+
type: 'unsupported',
|
|
321
|
+
reason: `Grouped content with ${group.children.length} children is not yet flattened`,
|
|
322
|
+
fallbackText: node.name,
|
|
323
|
+
placeholder,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return { ...common, type: 'unsupported', reason: 'Unsupported PPTX node type', placeholder };
|
|
327
|
+
}
|
|
328
|
+
function xmlNodeFrame(node, presentation) {
|
|
329
|
+
return node.element ? normalizeXmlFrame(node.element, presentation) : { x: 0, y: 0, width: 0, height: 0 };
|
|
330
|
+
}
|
|
331
|
+
function mapTemplateNode(node, sourcePart, renderOrder, relationships, context, pathToAsset) {
|
|
332
|
+
const element = node.element;
|
|
333
|
+
if (!element || ['nvGrpSpPr', 'grpSpPr'].includes(node.localName))
|
|
334
|
+
return undefined;
|
|
335
|
+
const cNvPr = firstDescendant(element, 'cNvPr');
|
|
336
|
+
const nativeId = cNvPr?.getAttribute('id') ?? String(renderOrder + 1);
|
|
337
|
+
const name = cNvPr?.getAttribute('name') ?? `Layout element ${renderOrder + 1}`;
|
|
338
|
+
const id = stableId('layout-element', `${sourcePart}:${nativeId}:${renderOrder}`);
|
|
339
|
+
const frame = xmlNodeFrame(node, context.presentation);
|
|
340
|
+
const placeholderElement = firstDescendant(element, 'ph');
|
|
341
|
+
const placeholder = placeholderElement
|
|
342
|
+
? {
|
|
343
|
+
type: placeholderElement.getAttribute('type') ?? 'body',
|
|
344
|
+
index: Number(placeholderElement.getAttribute('idx') ?? 0) || undefined,
|
|
345
|
+
prompt: textFromXml(element) || undefined,
|
|
346
|
+
}
|
|
347
|
+
: undefined;
|
|
348
|
+
const common = sourceBase(id, name, 'unsupported', frame, renderOrder, sourcePart, node.localName);
|
|
349
|
+
if (node.localName === 'pic') {
|
|
350
|
+
const blip = firstDescendant(element, 'blip');
|
|
351
|
+
const relationId = blip ? attributeByLocalName(blip, 'embed') ?? attributeByLocalName(blip, 'link') : undefined;
|
|
352
|
+
const mediaPath = relationTarget(sourcePart, relationships, relationId);
|
|
353
|
+
const assetId = mediaPath ? pathToAsset.get(mediaPath) : undefined;
|
|
354
|
+
if (assetId)
|
|
355
|
+
return { ...common, type: 'image', assetId, alt: name, fit: 'contain', placeholder };
|
|
356
|
+
return { ...common, type: 'unsupported', reason: 'Layout picture has no resolved media', fallbackText: name, placeholder };
|
|
357
|
+
}
|
|
358
|
+
if (node.localName === 'sp' || node.localName === 'cxnSp') {
|
|
359
|
+
const text = textFromXml(element);
|
|
360
|
+
const preset = firstDescendant(element, 'prstGeom')?.getAttribute('prst') ?? undefined;
|
|
361
|
+
if (!preset && text) {
|
|
362
|
+
return {
|
|
363
|
+
...common,
|
|
364
|
+
type: 'text',
|
|
365
|
+
text,
|
|
366
|
+
style: { ...DEFAULT_TEXT_STYLE, fontSize: placeholder?.type === 'title' || placeholder?.type === 'ctrTitle' ? 0.065 : 0.035 },
|
|
367
|
+
placeholder,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
...common,
|
|
372
|
+
type: 'shape',
|
|
373
|
+
shape: shapeKind(preset),
|
|
374
|
+
fill: text ? '#FFFFFF00' : '#E7E5E4',
|
|
375
|
+
stroke: '#44403C',
|
|
376
|
+
strokeWidth: 1,
|
|
377
|
+
text: text || undefined,
|
|
378
|
+
textStyle: text ? DEFAULT_TEXT_STYLE : undefined,
|
|
379
|
+
placeholder,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
const IDENTITY_COORDINATE_MAP = {
|
|
385
|
+
scaleX: 1,
|
|
386
|
+
scaleY: 1,
|
|
387
|
+
translateX: 0,
|
|
388
|
+
translateY: 0,
|
|
389
|
+
rotationZ: 0,
|
|
390
|
+
};
|
|
391
|
+
function normalizedEmu(value, dimension) {
|
|
392
|
+
const emu = Number(value);
|
|
393
|
+
return Number.isFinite(emu) && dimension > 0 ? emu / 9_525 / dimension : 0;
|
|
394
|
+
}
|
|
395
|
+
function groupCoordinateMap(node, context, sourcePart) {
|
|
396
|
+
const xfrm = node.child('grpSpPr').child('xfrm');
|
|
397
|
+
const off = xfrm.child('off');
|
|
398
|
+
const extent = xfrm.child('ext');
|
|
399
|
+
const childOffset = xfrm.child('chOff');
|
|
400
|
+
const childExtent = xfrm.child('chExt');
|
|
401
|
+
const width = normalizedEmu(extent.attr('cx'), context.presentation.width);
|
|
402
|
+
const height = normalizedEmu(extent.attr('cy'), context.presentation.height);
|
|
403
|
+
const childWidth = normalizedEmu(childExtent.attr('cx'), context.presentation.width);
|
|
404
|
+
const childHeight = normalizedEmu(childExtent.attr('cy'), context.presentation.height);
|
|
405
|
+
const scaleX = childWidth > 0 ? width / childWidth : 1;
|
|
406
|
+
const scaleY = childHeight > 0 ? height / childHeight : 1;
|
|
407
|
+
const rotationZ = Number(xfrm.attr('rot') ?? 0) / 60_000;
|
|
408
|
+
const flipH = boolAttribute(xfrm, 'flipH');
|
|
409
|
+
const flipV = boolAttribute(xfrm, 'flipV');
|
|
410
|
+
if (rotationZ !== 0 || flipH || flipV) {
|
|
411
|
+
context.warnings.push({
|
|
412
|
+
code: 'PPTX_GROUP_TRANSFORM_PARTIAL',
|
|
413
|
+
severity: 'warning',
|
|
414
|
+
message: 'Rotated or flipped PowerPoint groups are flattened without rotating child positions',
|
|
415
|
+
sourcePart,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
scaleX,
|
|
420
|
+
scaleY,
|
|
421
|
+
translateX: normalizedEmu(off.attr('x'), context.presentation.width) -
|
|
422
|
+
normalizedEmu(childOffset.attr('x'), context.presentation.width) * scaleX,
|
|
423
|
+
translateY: normalizedEmu(off.attr('y'), context.presentation.height) -
|
|
424
|
+
normalizedEmu(childOffset.attr('y'), context.presentation.height) * scaleY,
|
|
425
|
+
rotationZ,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function composeCoordinateMaps(parent, child) {
|
|
429
|
+
return {
|
|
430
|
+
scaleX: parent.scaleX * child.scaleX,
|
|
431
|
+
scaleY: parent.scaleY * child.scaleY,
|
|
432
|
+
translateX: parent.translateX + child.translateX * parent.scaleX,
|
|
433
|
+
translateY: parent.translateY + child.translateY * parent.scaleY,
|
|
434
|
+
rotationZ: parent.rotationZ + child.rotationZ,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function applyCoordinateMap(frame, map) {
|
|
438
|
+
return {
|
|
439
|
+
x: map.translateX + frame.x * map.scaleX,
|
|
440
|
+
y: map.translateY + frame.y * map.scaleY,
|
|
441
|
+
width: Math.max(0, frame.width * Math.abs(map.scaleX)),
|
|
442
|
+
height: Math.max(0, frame.height * Math.abs(map.scaleY)),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
function flattenTemplateTreeNode(node, sourcePart, renderOrder, relationships, context, pathToAsset, parentMap = IDENTITY_COORDINATE_MAP) {
|
|
446
|
+
if (node.localName === 'grpSp') {
|
|
447
|
+
const coordinateMap = composeCoordinateMaps(parentMap, groupCoordinateMap(node, context, sourcePart));
|
|
448
|
+
const flattened = [];
|
|
449
|
+
for (const child of node.allChildren()) {
|
|
450
|
+
flattened.push(...flattenTemplateTreeNode(child, sourcePart, renderOrder + flattened.length, relationships, context, pathToAsset, coordinateMap));
|
|
451
|
+
}
|
|
452
|
+
return flattened;
|
|
453
|
+
}
|
|
454
|
+
const mapped = mapTemplateNode(node, sourcePart, renderOrder, relationships, context, pathToAsset);
|
|
455
|
+
if (mapped) {
|
|
456
|
+
mapped.frame = applyCoordinateMap(mapped.frame, parentMap);
|
|
457
|
+
mapped.transform.rotationZ += parentMap.rotationZ;
|
|
458
|
+
return [mapped];
|
|
459
|
+
}
|
|
460
|
+
if (node.localName === 'graphicFrame' && node.element) {
|
|
461
|
+
const cNvPr = firstDescendant(node.element, 'cNvPr');
|
|
462
|
+
const name = cNvPr?.getAttribute('name') ?? `Grouped object ${renderOrder + 1}`;
|
|
463
|
+
const id = stableId('group-element', `${sourcePart}:${cNvPr?.getAttribute('id') ?? renderOrder}`);
|
|
464
|
+
context.warnings.push({
|
|
465
|
+
code: 'PPTX_GROUP_CONTENT_UNSUPPORTED',
|
|
466
|
+
severity: 'warning',
|
|
467
|
+
message: `${name} is nested in a group and uses unsupported graphic-frame content`,
|
|
468
|
+
elementId: id,
|
|
469
|
+
sourcePart,
|
|
470
|
+
});
|
|
471
|
+
return [
|
|
472
|
+
{
|
|
473
|
+
...sourceBase(id, name, 'unsupported', applyCoordinateMap(normalizeXmlFrame(node.element, context.presentation), parentMap), renderOrder, sourcePart, 'graphicFrame', parentMap.rotationZ),
|
|
474
|
+
type: 'unsupported',
|
|
475
|
+
reason: 'Grouped table or chart content is unsupported',
|
|
476
|
+
fallbackText: name,
|
|
477
|
+
},
|
|
478
|
+
];
|
|
479
|
+
}
|
|
480
|
+
return [];
|
|
481
|
+
}
|
|
482
|
+
function layoutName(context, path) {
|
|
483
|
+
const xml = decodeXml(context.files, path);
|
|
484
|
+
if (!xml)
|
|
485
|
+
return fileName(path).replace(/\.xml$/i, '');
|
|
486
|
+
const document = parseXmlDocument(xml, path);
|
|
487
|
+
return firstDescendant(document, 'cSld')?.getAttribute('name') || fileName(path).replace(/\.xml$/i, '');
|
|
488
|
+
}
|
|
489
|
+
function mapLayouts(context, pathToAsset) {
|
|
490
|
+
const layouts = [];
|
|
491
|
+
for (const [layoutPath, layout] of context.presentation.layouts) {
|
|
492
|
+
const nodes = [];
|
|
493
|
+
const masterPath = context.presentation.layoutToMaster.get(layoutPath);
|
|
494
|
+
const master = masterPath ? context.presentation.masters.get(masterPath) : undefined;
|
|
495
|
+
if (layout.showMasterSp && master && masterPath) {
|
|
496
|
+
master.spTree.allChildren().forEach((node, index) => {
|
|
497
|
+
const mapped = flattenTemplateTreeNode(node, masterPath, nodes.length + index, master.rels, context, pathToAsset);
|
|
498
|
+
nodes.push(...mapped.filter((element) => !element.placeholder));
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
layout.spTree.allChildren().forEach((node, index) => {
|
|
502
|
+
const mapped = flattenTemplateTreeNode(node, layoutPath, nodes.length + index, layout.rels, context, pathToAsset);
|
|
503
|
+
nodes.push(...mapped);
|
|
504
|
+
});
|
|
505
|
+
layouts.push({ id: stableId('layout', layoutPath), name: layoutName(context, layoutPath), elements: nodes });
|
|
506
|
+
}
|
|
507
|
+
return layouts;
|
|
508
|
+
}
|
|
509
|
+
function directColor(background, theme) {
|
|
510
|
+
return colorFromNode(background, theme) ?? '#FFFFFF';
|
|
511
|
+
}
|
|
512
|
+
function notesForSlide(context, slidePath) {
|
|
513
|
+
const relsXml = decodeXml(context.files, relationshipsPath(slidePath));
|
|
514
|
+
if (!relsXml)
|
|
515
|
+
return undefined;
|
|
516
|
+
const notesRelation = Array.from(parseRelationships(relsXml, slidePath).values()).find((relation) => relation.type === 'notesSlide');
|
|
517
|
+
if (!notesRelation || notesRelation.external)
|
|
518
|
+
return undefined;
|
|
519
|
+
const notesXml = decodeXml(context.files, notesRelation.resolvedTarget);
|
|
520
|
+
if (!notesXml)
|
|
521
|
+
return undefined;
|
|
522
|
+
const document = parseXmlDocument(notesXml, notesRelation.resolvedTarget);
|
|
523
|
+
const values = descendants(document, 't').map((element) => element.textContent?.trim()).filter(Boolean);
|
|
524
|
+
return values.length > 0 ? values.join('\n') : undefined;
|
|
525
|
+
}
|
|
526
|
+
function hasRealTiming(context, slidePath) {
|
|
527
|
+
const xml = decodeXml(context.files, slidePath);
|
|
528
|
+
if (!xml)
|
|
529
|
+
return false;
|
|
530
|
+
const document = parseXmlDocument(xml, slidePath);
|
|
531
|
+
const timing = firstDescendant(document, 'timing');
|
|
532
|
+
return Boolean(timing &&
|
|
533
|
+
Array.from(timing.getElementsByTagName('*')).some((element) => ['anim', 'animEffect', 'animMotion', 'animRot', 'animScale', 'set', 'cmd'].includes(localName(element))));
|
|
534
|
+
}
|
|
535
|
+
function backgroundForSlide(context, slideIndex) {
|
|
536
|
+
const slide = context.presentation.slides[slideIndex];
|
|
537
|
+
const layoutPath = context.presentation.slideToLayout.get(slideIndex);
|
|
538
|
+
const layout = layoutPath ? context.presentation.layouts.get(layoutPath) : undefined;
|
|
539
|
+
const masterPath = layoutPath ? context.presentation.layoutToMaster.get(layoutPath) : undefined;
|
|
540
|
+
const master = masterPath ? context.presentation.masters.get(masterPath) : undefined;
|
|
541
|
+
const theme = themeForLayout(context.presentation, layoutPath);
|
|
542
|
+
return directColor((slide?.background ?? layout?.background ?? master?.background), theme);
|
|
543
|
+
}
|
|
544
|
+
function metadata(context) {
|
|
545
|
+
const coreXml = decodeXml(context.files, 'docProps/core.xml');
|
|
546
|
+
if (!coreXml)
|
|
547
|
+
return { title: context.sourceName?.replace(/\.pptx$/i, '') ?? 'Imported presentation', sourceFormat: 'pptx' };
|
|
548
|
+
const document = parseXmlDocument(coreXml, 'docProps/core.xml');
|
|
549
|
+
const value = (name) => firstDescendant(document, name)?.textContent?.trim() || undefined;
|
|
550
|
+
return {
|
|
551
|
+
title: value('title') ?? context.sourceName?.replace(/\.pptx$/i, '') ?? 'Imported presentation',
|
|
552
|
+
author: value('creator'),
|
|
553
|
+
createdAt: value('created'),
|
|
554
|
+
modifiedAt: value('modified'),
|
|
555
|
+
sourceFormat: 'pptx',
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function mapSlides(context, pathToAsset, layouts) {
|
|
559
|
+
return context.presentation.slides.map((slide, slideIndex) => {
|
|
560
|
+
const layoutPath = context.presentation.slideToLayout.get(slideIndex);
|
|
561
|
+
const layoutId = layoutPath ? stableId('layout', layoutPath) : undefined;
|
|
562
|
+
const elements = [];
|
|
563
|
+
for (const node of slide.nodes) {
|
|
564
|
+
if (node.nodeType === 'group') {
|
|
565
|
+
const relationships = slide.rels;
|
|
566
|
+
elements.push(...flattenTemplateTreeNode(node.source, slide.slidePath, elements.length + 1, relationships, context, pathToAsset));
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
elements.push(mapSlideNode(node, slide.slidePath, elements.length + 1, context, pathToAsset));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (hasRealTiming(context, slide.slidePath)) {
|
|
573
|
+
context.warnings.push({
|
|
574
|
+
code: 'PPTX_ANIMATION_UNSUPPORTED',
|
|
575
|
+
severity: 'warning',
|
|
576
|
+
message: 'PowerPoint animation timing is not imported in this release',
|
|
577
|
+
slideIndex,
|
|
578
|
+
sourcePart: slide.slidePath,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
const title = elements.find((element) => element.placeholder?.type === 'title' || element.placeholder?.type === 'ctrTitle');
|
|
582
|
+
const titleText = title?.type === 'text' ? title.text : title?.type === 'shape' ? title.text : undefined;
|
|
583
|
+
return {
|
|
584
|
+
id: stableId('slide', slide.slidePath),
|
|
585
|
+
name: titleText?.trim() || `Slide ${slideIndex + 1}`,
|
|
586
|
+
layoutId: layoutId && layouts.some((layout) => layout.id === layoutId) ? layoutId : undefined,
|
|
587
|
+
durationMs: 5_000,
|
|
588
|
+
background: backgroundForSlide(context, slideIndex),
|
|
589
|
+
notes: notesForSlide(context, slide.slidePath),
|
|
590
|
+
elements,
|
|
591
|
+
};
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
export async function importPptx(input, sourceName) {
|
|
595
|
+
const normalizedInput = Uint8Array.from(new Uint8Array(input)).buffer;
|
|
596
|
+
const [pptxFiles, rawFiles] = await Promise.all([
|
|
597
|
+
parseZip(normalizedInput, RECOMMENDED_ZIP_LIMITS),
|
|
598
|
+
Promise.resolve(unzipWithLimits(new Uint8Array(normalizedInput))),
|
|
599
|
+
]);
|
|
600
|
+
const presentation = buildPresentation(pptxFiles);
|
|
601
|
+
materializeAllSlideNodes(presentation);
|
|
602
|
+
const context = {
|
|
603
|
+
presentation,
|
|
604
|
+
files: rawFiles,
|
|
605
|
+
assets: new Map(),
|
|
606
|
+
warnings: [],
|
|
607
|
+
sourceName,
|
|
608
|
+
};
|
|
609
|
+
const pathToAsset = addMediaAssets(context);
|
|
610
|
+
const layouts = mapLayouts(context, pathToAsset);
|
|
611
|
+
const slides = mapSlides(context, pathToAsset, layouts);
|
|
612
|
+
const document = {
|
|
613
|
+
schemaVersion: PRISMDECK_SCHEMA_VERSION,
|
|
614
|
+
id: stableId('deck', `${sourceName ?? 'presentation'}:${presentation.width}x${presentation.height}`),
|
|
615
|
+
kind: slides.length === 0 && layouts.length > 0 ? 'template' : 'presentation',
|
|
616
|
+
metadata: metadata(context),
|
|
617
|
+
size: { width: presentation.width, height: presentation.height },
|
|
618
|
+
layouts,
|
|
619
|
+
slides,
|
|
620
|
+
};
|
|
621
|
+
if (document.kind === 'template') {
|
|
622
|
+
context.warnings.push({
|
|
623
|
+
code: 'PPTX_TEMPLATE_IMPORTED',
|
|
624
|
+
severity: 'info',
|
|
625
|
+
message: `Imported ${layouts.length} layouts from a zero-slide PowerPoint file`,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
document,
|
|
630
|
+
assets: context.assets,
|
|
631
|
+
report: { format: 'pptx', sourceName, warnings: context.warnings },
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
//# sourceMappingURL=pptx.js.map
|