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,330 @@
1
+ /**
2
+ * Image serializer — converts PicNodeData into positioned HTML image/video/audio elements.
3
+ */
4
+
5
+ import type { PicNodeData } from '../model/nodes/PicNode';
6
+ import type { RenderContext } from './RenderContext';
7
+ import { SafeXmlNode } from '../parser/XmlParser';
8
+ import { encodeMediaForWebDisplay } from '../utils/mediaWebConvert';
9
+ import { lineStyleToBorder } from './borderMapper';
10
+ import type { Image, Video, Audio } from '../adapter/types';
11
+ import { getOrCreateBlobUrl, resolveMediaPath } from '../utils/media';
12
+
13
+ const PX_TO_PT = 0.75;
14
+
15
+ function pxToPt(px: number): number {
16
+ return px * PX_TO_PT;
17
+ }
18
+
19
+ /**
20
+ * Check if a file extension is an unsupported legacy format (WMF only now; EMF is handled).
21
+ */
22
+ function isUnsupportedFormat(path: string): boolean {
23
+ const ext = path.split('.').pop()?.toLowerCase() || '';
24
+ return ext === 'wmf';
25
+ }
26
+
27
+ /**
28
+ * Check if a file path is an EMF image.
29
+ */
30
+ function isEmfFormat(path: string): boolean {
31
+ const ext = path.split('.').pop()?.toLowerCase() || '';
32
+ return ext === 'emf';
33
+ }
34
+
35
+ /** OOXML fixed-point scale (100000 = 100%). */
36
+ const OOXML_100K = 100000;
37
+
38
+ /**
39
+ * Build `filters` for PPTist: `sharpen`, `colorTemperature`, `saturation`, `brightness`, `contrast`.
40
+ *
41
+ * - **ISO / DrawingML**: `<a:lum bright contrast>` on `<a:blip>`.
42
+ * - **Office 2010+** (same as legacy `src1/fill.js` `getPicFilters`): `a:extLst` → `ext` →
43
+ * `a14:imgProps` / `a14:imgLayer` / `a14:imgEffect` → `a14:saturation`, `a14:brightnessContrast`,
44
+ * `a14:sharpenSoften`, `a14:colorTemperature`.
45
+ *
46
+ * Extension effects are applied after `lum` and may override brightness/contrast when both exist.
47
+ */
48
+ function buildImageFilters(node: PicNodeData): Image['filters'] | undefined {
49
+ const blipFill = node.source.child('blipFill');
50
+ if (!blipFill.exists()) return undefined;
51
+ const blip = blipFill.child('blip');
52
+ if (!blip.exists()) return undefined;
53
+
54
+ const out: NonNullable<Image['filters']> = {};
55
+
56
+ applyLumToFilters(blip, out);
57
+ applyExtLstImageEffectsToFilters(blip, out);
58
+
59
+ return Object.keys(out).length > 0 ? out : undefined;
60
+ }
61
+
62
+ /** `<a:lum>` — brightness / contrast (values typically −100000…100000, scale 100000 = 100%). */
63
+ function applyLumToFilters(blip: SafeXmlNode, out: NonNullable<Image['filters']>): void {
64
+ const lum = blip.child('lum');
65
+ if (!lum.exists()) return;
66
+ const bright = lum.numAttr('bright');
67
+ const contrast = lum.numAttr('contrast');
68
+ if (bright !== undefined && bright !== 0) {
69
+ out.brightness = bright / OOXML_100K;
70
+ }
71
+ if (contrast !== undefined && contrast !== 0) {
72
+ out.contrast = contrast / OOXML_100K;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * `a:extLst` / `a14:img*` image adjustments (namespace-agnostic `localName` from DOM).
78
+ */
79
+ function applyExtLstImageEffectsToFilters(blip: SafeXmlNode, out: NonNullable<Image['filters']>): void {
80
+ const extLst = blip.child('extLst');
81
+ if (!extLst.exists()) return;
82
+
83
+ for (const ext of extLst.children()) {
84
+ if (ext.localName !== 'ext') continue;
85
+ const imgProps = ext.child('imgProps');
86
+ if (!imgProps.exists()) continue;
87
+ const imgLayer = imgProps.child('imgLayer');
88
+ if (!imgLayer.exists()) continue;
89
+
90
+ for (const imgEffect of imgLayer.children()) {
91
+ if (imgEffect.localName !== 'imgEffect') continue;
92
+ for (const el of imgEffect.allChildren()) {
93
+ switch (el.localName) {
94
+ case 'saturation': {
95
+ const sat = el.numAttr('sat');
96
+ if (sat !== undefined) {
97
+ out.saturation = sat / OOXML_100K;
98
+ }
99
+ break;
100
+ }
101
+ case 'brightnessContrast': {
102
+ const bright = el.numAttr('bright');
103
+ const contrast = el.numAttr('contrast');
104
+ if (bright !== undefined && bright !== 0) {
105
+ out.brightness = bright / OOXML_100K;
106
+ }
107
+ if (contrast !== undefined && contrast !== 0) {
108
+ out.contrast = contrast / OOXML_100K;
109
+ }
110
+ break;
111
+ }
112
+ case 'sharpenSoften': {
113
+ const amount = el.numAttr('amount');
114
+ if (amount !== undefined && amount !== 0) {
115
+ // Positive = sharpen, negative = soften (PPTist only has `sharpen`; use signed value).
116
+ out.sharpen = amount / OOXML_100K;
117
+ }
118
+ break;
119
+ }
120
+ case 'colorTemperature': {
121
+ const ct = el.numAttr('colorTemp');
122
+ if (ct !== undefined) {
123
+ out.colorTemperature = ct;
124
+ }
125
+ break;
126
+ }
127
+ default:
128
+ break;
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Resolve a media URL from a relationship ID.
137
+ */
138
+ function resolveMediaUrl(rId: string | undefined, ctx: RenderContext): string | undefined {
139
+ if (!rId) return undefined;
140
+
141
+ const rel = ctx.slide.rels.get(rId);
142
+ if (!rel) return undefined;
143
+
144
+ // Check if target is an external URL
145
+ if (rel.target.startsWith('http://') || rel.target.startsWith('https://')) {
146
+ return rel.target;
147
+ }
148
+
149
+ // Resolve from embedded media
150
+ const mediaPath = resolveMediaPath(rel.target);
151
+ const data = ctx.presentation.media.get(mediaPath);
152
+ if (!data) return undefined;
153
+
154
+ return getOrCreateBlobUrl(mediaPath, data, ctx.mediaUrlCache);
155
+ }
156
+
157
+ /**
158
+ * Render a video element.
159
+ */
160
+ function renderVideo(
161
+ node: PicNodeData,
162
+ ctx: RenderContext,
163
+ order: number,
164
+ box: { left: number; top: number; width: number; height: number },
165
+ ): Video {
166
+ // Try to get video URL from mediaRId
167
+ const videoUrl = resolveMediaUrl(node.mediaRId, ctx);
168
+
169
+ // Also try to show poster image from blipEmbed
170
+ let posterUrl: string | undefined;
171
+ if (node.blipEmbed) {
172
+ const rel = ctx.slide.rels.get(node.blipEmbed);
173
+ if (rel) {
174
+ const mediaPath = resolveMediaPath(rel.target);
175
+ const data = ctx.presentation.media.get(mediaPath);
176
+ if (data && !isUnsupportedFormat(mediaPath)) {
177
+ posterUrl = getOrCreateBlobUrl(mediaPath, data, ctx.mediaUrlCache);
178
+ }
179
+ }
180
+ }
181
+
182
+ const blob = videoUrl || undefined;
183
+ const src = posterUrl ?? videoUrl ?? undefined;
184
+
185
+ return {
186
+ type: 'video',
187
+ ...box,
188
+ blob,
189
+ src,
190
+ order,
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Render an audio element.
196
+ */
197
+ function renderAudio(
198
+ node: PicNodeData,
199
+ ctx: RenderContext,
200
+ order: number,
201
+ box: { left: number; top: number; width: number; height: number },
202
+ ): Audio {
203
+ const audioUrl = resolveMediaUrl(node.mediaRId, ctx);
204
+ const blob = audioUrl || '';
205
+ // TODO: optional cover image from blipEmbed
206
+
207
+ return {
208
+ type: 'audio',
209
+ ...box,
210
+ blob,
211
+ order,
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Render an image element.
217
+ */
218
+ function renderImage(
219
+ node: PicNodeData,
220
+ ctx: RenderContext,
221
+ order: number,
222
+ box: { left: number; top: number; width: number; height: number },
223
+ ): Image {
224
+ const embedId = node.blipEmbed;
225
+ let src = '';
226
+
227
+ if (!embedId) {
228
+ return buildImage(node, ctx, order, box, src, undefined);
229
+ }
230
+
231
+ const rel = ctx.slide.rels.get(embedId);
232
+ if (!rel) {
233
+ return buildImage(node, ctx, order, box, src, undefined);
234
+ }
235
+
236
+ const mediaPath = resolveMediaPath(rel.target);
237
+
238
+ const data = ctx.presentation.media.get(mediaPath);
239
+ if (!data) {
240
+ return buildImage(node, ctx, order, box, src, undefined);
241
+ }
242
+
243
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
244
+
245
+ src = encodeMediaForWebDisplay(mediaPath, bytes);
246
+ return buildImage(node, ctx, order, box, src, buildImageFilters(node));
247
+ }
248
+
249
+ function buildImage(
250
+ node: PicNodeData,
251
+ ctx: RenderContext,
252
+ order: number,
253
+ box: { left: number; top: number; width: number; height: number },
254
+ src: string,
255
+ filters: Image['filters'] | undefined,
256
+ ): Image {
257
+ const spPr = node.source.child('spPr');
258
+ const ln = spPr.exists() ? spPr.child('ln') : node.source.child('__none__');
259
+ const borderResult = ln.exists()
260
+ ? lineStyleToBorder(ln, ctx)
261
+ : {
262
+ border: { borderColor: '#000000', borderWidth: 0, borderType: 'solid' as const },
263
+ borderStrokeDasharray: '',
264
+ };
265
+
266
+ let rect: Image['rect'] | undefined;
267
+ if (
268
+ node.crop &&
269
+ (node.crop.top !== 0 || node.crop.bottom !== 0 || node.crop.left !== 0 || node.crop.right !== 0)
270
+ ) {
271
+ // OOXML srcRect
272
+ rect = {
273
+ t: node.crop.top,
274
+ b: node.crop.bottom,
275
+ l: node.crop.left,
276
+ r: node.crop.right,
277
+ };
278
+ }
279
+
280
+ return {
281
+ type: 'image',
282
+ ...box,
283
+ src,
284
+ rotate: node.rotation,
285
+ isFlipH: node.flipH,
286
+ isFlipV: node.flipV,
287
+ order,
288
+ rect,
289
+ geom: 'rect',
290
+ borderColor: borderResult.border.borderColor,
291
+ borderWidth: borderResult.border.borderWidth,
292
+ borderType: borderResult.border.borderType,
293
+ borderStrokeDasharray: borderResult.borderStrokeDasharray || '',
294
+ ...(filters && Object.keys(filters).length > 0 ? { filters } : {}),
295
+ };
296
+ }
297
+
298
+ /**
299
+ * Serialize picture node to Image, Video, or Audio element.
300
+ *
301
+ * Handles:
302
+ * - Standard images (png, jpg, gif, svg, bmp)
303
+ * - Unsupported formats (wmf) with placeholder
304
+ * - Video elements with controls
305
+ * - Audio elements with controls
306
+ * - Crop via `rect` (fractions)
307
+ * - Rotation and flip on Image
308
+ */
309
+ export function pictureToElement(
310
+ node: PicNodeData,
311
+ ctx: RenderContext,
312
+ order: number,
313
+ ): Image | Video | Audio {
314
+ const box = {
315
+ left: pxToPt(node.position.x),
316
+ top: pxToPt(node.position.y),
317
+ width: pxToPt(node.size.w),
318
+ height: pxToPt(node.size.h),
319
+ };
320
+
321
+ if (node.isVideo) {
322
+ return renderVideo(node, ctx, order, box);
323
+ }
324
+
325
+ if (node.isAudio) {
326
+ return renderAudio(node, ctx, order, box);
327
+ }
328
+
329
+ return renderImage(node, ctx, order, box);
330
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Serializer layer: converts PresentationData (and render context) into
3
+ * pptxtojson/PPTist JSON using the same resolution flow as the reference renderer.
4
+ */
5
+
6
+ export { createRenderContext, type RenderContext } from './RenderContext';
7
+ export { renderTextBody, type RenderTextBodyOptions } from './textSerializer';
8
+ export {
9
+ spPrToFill,
10
+ resolveShapeFill,
11
+ spPrHasExplicitFill,
12
+ gradientFillDataToValue,
13
+ type SpPrToFillOptions,
14
+ } from './fillMapper';
15
+ export { lineStyleToBorder, dashArrayForKind, type BorderResult } from './borderMapper';
16
+ export { slideToSlide, nodeToElement } from './slideSerializer';
17
+ export {
18
+ resolveSlideFill,
19
+ renderBgPr as bgPrToFill,
20
+ renderBgRef as bgRefToFill,
21
+ } from './backgroundSerializer';
22
+ export { renderShape, shapeToElement } from './shapeSerializer';
23
+ export { pictureToElement } from './imageSerializer';
24
+ export { tableToElement } from './tableSerializer';
25
+ export { chartToElement } from './chartSerializer';
26
+ export { groupToElement, type NodeToElement } from './groupSerializer';
27
+ export type { Slide as OutputSlide } from '../adapter/types';