@opencraw/office-reader 0.0.2

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 (34) hide show
  1. package/README.md +201 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.esm.js +6 -0
  4. package/dist/pptx.d.ts +1 -0
  5. package/dist/pptx.esm.js +5 -0
  6. package/dist/read-pptx.use-case.esm.js +591 -0
  7. package/dist/read-source.client.esm.js +289 -0
  8. package/dist/read-xlsx.use-case.esm.js +381 -0
  9. package/dist/src/index.d.ts +9 -0
  10. package/dist/src/ooxml-package/index.d.ts +7 -0
  11. package/dist/src/ooxml-package/ooxml-package.client.d.ts +47 -0
  12. package/dist/src/ooxml-package/relationships.mapper.d.ts +27 -0
  13. package/dist/src/ooxml-package/xml-walk.algorithm.d.ts +37 -0
  14. package/dist/src/presentation/chart.mapper.d.ts +13 -0
  15. package/dist/src/presentation/deck.model.d.ts +56 -0
  16. package/dist/src/presentation/index.d.ts +6 -0
  17. package/dist/src/presentation/notes.mapper.d.ts +9 -0
  18. package/dist/src/presentation/placeholder-geometry.mapper.d.ts +53 -0
  19. package/dist/src/presentation/read-pptx.use-case.d.ts +32 -0
  20. package/dist/src/presentation/slide.mapper.d.ts +24 -0
  21. package/dist/src/read-error/index.d.ts +3 -0
  22. package/dist/src/read-error/office-read.error.d.ts +29 -0
  23. package/dist/src/source-bytes/index.d.ts +3 -0
  24. package/dist/src/source-bytes/read-source.client.d.ts +18 -0
  25. package/dist/src/spreadsheet/cell-value.algorithm.d.ts +47 -0
  26. package/dist/src/spreadsheet/index.d.ts +6 -0
  27. package/dist/src/spreadsheet/number-formats.mapper.d.ts +10 -0
  28. package/dist/src/spreadsheet/read-xlsx.use-case.d.ts +27 -0
  29. package/dist/src/spreadsheet/shared-strings.mapper.d.ts +10 -0
  30. package/dist/src/spreadsheet/workbook.model.d.ts +35 -0
  31. package/dist/src/spreadsheet/worksheet.mapper.d.ts +23 -0
  32. package/dist/xlsx.d.ts +1 -0
  33. package/dist/xlsx.esm.js +5 -0
  34. package/package.json +77 -0
@@ -0,0 +1,591 @@
1
+ import { w as walkXml, n as namespacedAttribute, i as isOn, a as OoxmlPackage, r as readSource, b as relationshipOfType, c as relationshipsOf, O as OfficeReadError } from './read-source.client.esm.js';
2
+ import 'fflate';
3
+
4
+ /**
5
+ * Reads a chart part: its type, title and series, from the values the chart
6
+ * caches next to its formulas (`c:strCache`, `c:numCache`), so the embedded
7
+ * workbook is never needed.
8
+ *
9
+ * @param xml - The chart part.
10
+ * @param mode - `typed`: values as numbers (`null` where missing); `text`: as text.
11
+ * @returns The chart.
12
+ */
13
+ function readChart(xml, mode) {
14
+ let type = '';
15
+ let title;
16
+ const series = [];
17
+ let current;
18
+ let target;
19
+ let point = 0;
20
+ let capturing = false;
21
+ let inPlotArea = false;
22
+ let titleText;
23
+ walkXml(xml, {
24
+ open: (name, attributes) => {
25
+ if (name === 'plotArea') {
26
+ inPlotArea = true;
27
+ } else if (!inPlotArea && name === 'title' && title === undefined) {
28
+ titleText = '';
29
+ } else if (inPlotArea && type === '' && name.endsWith('Chart')) {
30
+ type = name.slice(0, -'Chart'.length);
31
+ } else if (name === 'ser') {
32
+ current = {
33
+ name: [],
34
+ categories: [],
35
+ values: []
36
+ };
37
+ } else if (current !== undefined) {
38
+ const next = targetOf(name);
39
+ if (next !== undefined) {
40
+ target = next;
41
+ point = 0;
42
+ } else if (name === 'ptCount' && target !== undefined) {
43
+ // Points left out of the cache are missing values: keep their places.
44
+ current[target].length = Math.max(current[target].length, Number(attributes.val ?? 0));
45
+ } else if (name === 'pt') {
46
+ point = Number(attributes.idx ?? 0);
47
+ } else if (name === 'v') {
48
+ capturing = true;
49
+ if (target !== undefined) current[target][point] = '';
50
+ }
51
+ }
52
+ },
53
+ text: text => {
54
+ if (titleText !== undefined) titleText += text;else if (capturing && current !== undefined && target !== undefined) current[target][point] += text;
55
+ },
56
+ close: name => {
57
+ if (name === 'v') {
58
+ capturing = false;
59
+ } else if (name === 'title' && titleText !== undefined) {
60
+ title = titleText.trim();
61
+ titleText = undefined;
62
+ } else if (name === 'ser' && current !== undefined) {
63
+ series.push(current);
64
+ current = undefined;
65
+ } else if (targetOf(name) !== undefined) {
66
+ target = undefined;
67
+ }
68
+ }
69
+ });
70
+ return {
71
+ type,
72
+ ...(!(title === undefined || title === '') && {
73
+ title
74
+ }),
75
+ series: series.map(open => ({
76
+ name: open.name.join('').trim(),
77
+ categories: Array.from(open.categories, text => text ?? ''),
78
+ values: Array.from(open.values, text => valueOf(text, mode))
79
+ }))
80
+ };
81
+ }
82
+ /** Where a series element's cached points go: its name, its categories (x values of a scatter), its values. */
83
+ function targetOf(name) {
84
+ if (name === 'tx') return 'name';
85
+ if (name === 'cat' || name === 'xVal') return 'categories';
86
+ return name === 'val' || name === 'yVal' ? 'values' : undefined;
87
+ }
88
+ function valueOf(text, mode) {
89
+ const number = text === undefined || text.trim() === '' ? NaN : Number(text);
90
+ if (mode === 'text') return Number.isNaN(number) ? text ?? '' : String(number);
91
+ return Number.isNaN(number) ? null : number;
92
+ }
93
+
94
+ /**
95
+ * The speaker notes of a notes-slide part: the text of its body placeholder
96
+ * (the slide image and the slide number placeholders are left out).
97
+ *
98
+ * @param xml - The notes-slide part.
99
+ * @returns The notes, paragraphs joined by line breaks.
100
+ */
101
+ function readNotes(xml) {
102
+ const paragraphs = [];
103
+ let inBody = false;
104
+ let paragraph;
105
+ let inText = false;
106
+ walkXml(xml, {
107
+ open: (name, attributes) => {
108
+ if (name === 'sp') inBody = false;else if (name === 'ph') inBody = (attributes.type ?? 'body') === 'body';else if (inBody && name === 'p') paragraph = '';else if (name === 't') inText = paragraph !== undefined;else if (name === 'br' && paragraph !== undefined) paragraph += '\n';
109
+ },
110
+ text: text => {
111
+ if (inText && paragraph !== undefined) paragraph += text;
112
+ },
113
+ close: name => {
114
+ if (name === 't') {
115
+ inText = false;
116
+ } else if (name === 'p' && paragraph !== undefined) {
117
+ paragraphs.push(paragraph);
118
+ paragraph = undefined;
119
+ }
120
+ }
121
+ });
122
+ return paragraphs.join('\n').trim();
123
+ }
124
+
125
+ /**
126
+ * The positioned placeholders of a slide layout or master. A slide's title or
127
+ * body usually carries no position of its own: it inherits it from the
128
+ * placeholder with the same index in its layout, else the same type, else the
129
+ * master's.
130
+ *
131
+ * @param xml - The layout or master part.
132
+ * @returns Its placeholder boxes.
133
+ */
134
+ function placeholderBoxes(xml) {
135
+ const boxes = {
136
+ byIdx: new Map(),
137
+ byType: new Map()
138
+ };
139
+ let key;
140
+ let box;
141
+ let inShape = false;
142
+ let inXfrm = false;
143
+ walkXml(xml, {
144
+ open: (name, attributes) => {
145
+ if (name === 'sp') {
146
+ inShape = true;
147
+ key = undefined;
148
+ box = undefined;
149
+ } else if (inShape && name === 'ph') {
150
+ key = {
151
+ type: attributes.type ?? 'obj',
152
+ idx: attributes.idx
153
+ };
154
+ } else if (inShape && name === 'xfrm') {
155
+ inXfrm = true;
156
+ box = {};
157
+ } else if (inXfrm && box !== undefined) {
158
+ readGeometry(name, attributes, box);
159
+ }
160
+ },
161
+ close: name => {
162
+ if (name === 'xfrm') {
163
+ inXfrm = false;
164
+ } else if (name === 'sp') {
165
+ inShape = false;
166
+ if (key !== undefined && isBox(box)) {
167
+ if (key.idx !== undefined) boxes.byIdx.set(key.idx, box);
168
+ if (!boxes.byType.has(key.type)) boxes.byType.set(key.type, box);
169
+ }
170
+ }
171
+ }
172
+ });
173
+ return boxes;
174
+ }
175
+ /**
176
+ * Where an unpositioned placeholder sits: its layout's placeholder with the
177
+ * same index, else the same type, else the master's of that type.
178
+ *
179
+ * @param key - The placeholder.
180
+ * @param layout - The slide's layout.
181
+ * @param master - The layout's master.
182
+ * @returns The box, or `undefined` when neither places it.
183
+ */
184
+ function inheritedBox(key, layout, master) {
185
+ const fromIdx = key.idx === undefined ? undefined : layout.byIdx.get(key.idx);
186
+ return fromIdx ?? layout.byType.get(key.type) ?? master.byType.get(masterType(key.type)) ?? master.byType.get(key.type);
187
+ }
188
+ /**
189
+ * Reads `a:off` and `a:ext` into a box.
190
+ *
191
+ * @param name - The element's local name.
192
+ * @param attributes - Its attributes.
193
+ * @param box - The box to fill.
194
+ */
195
+ function readGeometry(name, attributes, box) {
196
+ if (name === 'off') {
197
+ box.x = Number(attributes.x ?? 0);
198
+ box.y = Number(attributes.y ?? 0);
199
+ } else if (name === 'ext') {
200
+ box.width = Number(attributes.cx ?? 0);
201
+ box.height = Number(attributes.cy ?? 0);
202
+ }
203
+ }
204
+ /**
205
+ * Whether a box has all four numbers.
206
+ *
207
+ * @param box - A box being read.
208
+ * @returns Whether it is complete.
209
+ */
210
+ function isBox(box) {
211
+ return box?.x !== undefined && box.y !== undefined && box.width !== undefined && box.height !== undefined;
212
+ }
213
+ /** A master has only title, body and the footer placeholders: a centred title is a title, anything else body text. */
214
+ function masterType(type) {
215
+ if (type === 'ctrTitle') return 'title';
216
+ return ['subTitle', 'obj', 'body'].includes(type) ? 'body' : type;
217
+ }
218
+
219
+ const EMU_PER_POINT = 12_700;
220
+ /** Placeholders that repeat on every slide and carry no content. */
221
+ const LAYOUT_NOISE = new Set(['sldNum', 'dt', 'ftr']);
222
+ const TITLES = new Set(['title', 'ctrTitle']);
223
+ /**
224
+ * Reads a slide part: its text boxes with their positions (group transforms
225
+ * applied; a placeholder without a position of its own takes the one
226
+ * `placeholderBox` gives), its tables with their merged cells, the ids of its
227
+ * charts, its title and whether it is hidden.
228
+ *
229
+ * @param xml - The slide part.
230
+ * @param placeholderBox - Where an unpositioned placeholder sits (from the layout and master).
231
+ * @returns The content.
232
+ */
233
+ function readSlide(xml, placeholderBox) {
234
+ const content = {
235
+ hidden: false,
236
+ shapes: [],
237
+ tables: [],
238
+ chartIds: []
239
+ };
240
+ const groups = [];
241
+ let shape;
242
+ let table;
243
+ let row;
244
+ let cell;
245
+ let paragraph;
246
+ let inText = false;
247
+ let xfrm;
248
+ const finishShape = open => {
249
+ const text = open.paragraphs.join('\n').trim();
250
+ const type = open.placeholder?.type;
251
+ if (text === '' || type !== undefined && LAYOUT_NOISE.has(type)) return;
252
+ if (type !== undefined && TITLES.has(type)) content.title ??= text;
253
+ const own = isBox(open.box) ? transformed(open.box, groups) : undefined;
254
+ const box = own ?? (open.placeholder === undefined ? undefined : placeholderBox(open.placeholder)) ?? {
255
+ x: 0,
256
+ y: 0,
257
+ width: 0,
258
+ height: 0
259
+ };
260
+ content.shapes.push({
261
+ ...points(box),
262
+ text,
263
+ ...(type !== undefined && {
264
+ placeholder: type
265
+ })
266
+ });
267
+ };
268
+ walkXml(xml, {
269
+ open: (name, attributes) => {
270
+ switch (name) {
271
+ case 'sld':
272
+ {
273
+ content.hidden = attributes.show === '0' || attributes.show === 'false';
274
+ break;
275
+ }
276
+ case 'grpSp':
277
+ {
278
+ groups.push({
279
+ box: {},
280
+ childBox: {},
281
+ properties: false
282
+ });
283
+ break;
284
+ }
285
+ case 'grpSpPr':
286
+ {
287
+ const group = groups.at(-1);
288
+ if (group !== undefined) group.properties = true;
289
+ break;
290
+ }
291
+ case 'sp':
292
+ case 'graphicFrame':
293
+ {
294
+ shape = {
295
+ paragraphs: []
296
+ };
297
+ break;
298
+ }
299
+ case 'ph':
300
+ {
301
+ if (shape !== undefined) shape.placeholder = {
302
+ type: attributes.type ?? 'obj',
303
+ idx: attributes.idx
304
+ };
305
+ break;
306
+ }
307
+ case 'xfrm':
308
+ {
309
+ const group = groups.at(-1);
310
+ if (group?.properties === true) xfrm = group.box;else if (shape !== undefined && table === undefined) xfrm = shape.box = {};
311
+ break;
312
+ }
313
+ case 'chOff':
314
+ case 'chExt':
315
+ {
316
+ const group = groups.at(-1);
317
+ if (group?.properties === true) readGeometry(name === 'chOff' ? 'off' : 'ext', attributes, group.childBox);
318
+ break;
319
+ }
320
+ case 'off':
321
+ case 'ext':
322
+ {
323
+ if (xfrm !== undefined) readGeometry(name, attributes, xfrm);
324
+ break;
325
+ }
326
+ case 'tbl':
327
+ {
328
+ table = {
329
+ rows: [],
330
+ merges: []
331
+ };
332
+ break;
333
+ }
334
+ case 'tr':
335
+ {
336
+ if (table !== undefined) row = [];
337
+ break;
338
+ }
339
+ case 'tc':
340
+ {
341
+ if (row !== undefined) cell = {
342
+ paragraphs: [],
343
+ columnSpan: Number(attributes.gridSpan ?? 1),
344
+ rowSpan: Number(attributes.rowSpan ?? 1),
345
+ continued: isOn(attributes.hMerge) || isOn(attributes.vMerge)
346
+ };
347
+ break;
348
+ }
349
+ case 'p':
350
+ {
351
+ paragraph = '';
352
+ break;
353
+ }
354
+ case 't':
355
+ {
356
+ inText = paragraph !== undefined;
357
+ break;
358
+ }
359
+ case 'br':
360
+ {
361
+ if (paragraph !== undefined) paragraph += '\n';
362
+ break;
363
+ }
364
+ case 'chart':
365
+ {
366
+ const id = namespacedAttribute(attributes, 'id');
367
+ if (id !== undefined) content.chartIds.push(id);
368
+ break;
369
+ }
370
+ // No default
371
+ }
372
+ },
373
+ text: text => {
374
+ if (inText && paragraph !== undefined) paragraph += text;
375
+ },
376
+ close: name => {
377
+ switch (name) {
378
+ case 't':
379
+ {
380
+ inText = false;
381
+ break;
382
+ }
383
+ case 'p':
384
+ {
385
+ if (paragraph !== undefined) (cell ?? shape)?.paragraphs.push(paragraph);
386
+ paragraph = undefined;
387
+ break;
388
+ }
389
+ case 'xfrm':
390
+ {
391
+ xfrm = undefined;
392
+ break;
393
+ }
394
+ case 'grpSpPr':
395
+ {
396
+ const group = groups.at(-1);
397
+ if (group !== undefined) group.properties = false;
398
+ break;
399
+ }
400
+ case 'grpSp':
401
+ {
402
+ groups.pop();
403
+ break;
404
+ }
405
+ case 'tc':
406
+ {
407
+ if (cell !== undefined && row !== undefined && table !== undefined) {
408
+ if (cell.columnSpan > 1 || cell.rowSpan > 1) table.merges.push(rangeOf(table.rows.length, row.length, cell.rowSpan, cell.columnSpan));
409
+ row.push(cell.continued ? '' : cell.paragraphs.join('\n').trim());
410
+ }
411
+ cell = undefined;
412
+ break;
413
+ }
414
+ case 'tr':
415
+ {
416
+ if (row !== undefined) table?.rows.push(row);
417
+ row = undefined;
418
+ break;
419
+ }
420
+ case 'tbl':
421
+ {
422
+ if (table !== undefined) content.tables.push({
423
+ name: `table ${content.tables.length + 1}`,
424
+ hidden: false,
425
+ rows: table.rows,
426
+ hiddenRows: [],
427
+ merges: table.merges
428
+ });
429
+ table = undefined;
430
+ break;
431
+ }
432
+ case 'sp':
433
+ case 'graphicFrame':
434
+ {
435
+ if (shape !== undefined) finishShape(shape);
436
+ shape = undefined;
437
+ break;
438
+ }
439
+ // No default
440
+ }
441
+ }
442
+ });
443
+ content.shapes.sort((first, second) => Math.round(first.y) - Math.round(second.y) || first.x - second.x);
444
+ return content;
445
+ }
446
+ /** A child's box in slide coordinates: each enclosing group maps its child space onto its own box, innermost first. */
447
+ function transformed(box, groups) {
448
+ let mapped = box;
449
+ for (let index = groups.length - 1; index >= 0; index -= 1) {
450
+ const {
451
+ box: outer,
452
+ childBox: inner
453
+ } = groups[index];
454
+ if (!isBox(outer) || !isBox(inner) || inner.width === 0 || inner.height === 0) continue;
455
+ const scaleX = outer.width / inner.width;
456
+ const scaleY = outer.height / inner.height;
457
+ mapped = {
458
+ x: outer.x + (mapped.x - inner.x) * scaleX,
459
+ y: outer.y + (mapped.y - inner.y) * scaleY,
460
+ width: mapped.width * scaleX,
461
+ height: mapped.height * scaleY
462
+ };
463
+ }
464
+ return mapped;
465
+ }
466
+ function toPoints(emu) {
467
+ return Math.round(emu / EMU_PER_POINT * 100) / 100;
468
+ }
469
+ function points(box) {
470
+ const round = toPoints;
471
+ return {
472
+ x: round(box.x),
473
+ y: round(box.y),
474
+ width: round(box.width),
475
+ height: round(box.height)
476
+ };
477
+ }
478
+ /** A merged range as an A1 reference (`A1:D1`). */
479
+ function rangeOf(row, column, rowSpan, columnSpan) {
480
+ return `${columnLetter(column)}${row + 1}:${columnLetter(column + columnSpan - 1)}${row + rowSpan}`;
481
+ }
482
+ function columnLetter(index) {
483
+ let letters = '';
484
+ for (let rest = index + 1; rest > 0; rest = Math.floor((rest - 1) / 26)) letters = String.fromCodePoint(65 + (rest - 1) % 26) + letters;
485
+ return letters;
486
+ }
487
+
488
+ const POINTS = 12_700;
489
+ /** 10 × 7.5 inches, PowerPoint's default before 16:9. */
490
+ const DEFAULT_SIZE = {
491
+ width: 9_144_000,
492
+ height: 6_858_000
493
+ };
494
+ async function readPptx(source, options = {}) {
495
+ const pkg = OoxmlPackage.open(await readSource(source), options.limits);
496
+ const main = relationshipOfType(relationshipsOf(pkg, ''), 'officeDocument')?.target ?? (pkg.has('ppt/presentation.xml') ? 'ppt/presentation.xml' : '');
497
+ const ids = [];
498
+ const size = {
499
+ ...DEFAULT_SIZE
500
+ };
501
+ let isPresentation = false;
502
+ walkXml(pkg.text(main), {
503
+ open: (name, attributes) => {
504
+ switch (name) {
505
+ case 'presentation':
506
+ {
507
+ isPresentation = true;
508
+ break;
509
+ }
510
+ case 'sldId':
511
+ {
512
+ ids.push(namespacedAttribute(attributes, 'id') ?? '');
513
+ break;
514
+ }
515
+ case 'sldSz':
516
+ {
517
+ {
518
+ Object.assign(size, {
519
+ width: Number(attributes.cx ?? DEFAULT_SIZE.width),
520
+ height: Number(attributes.cy ?? DEFAULT_SIZE.height)
521
+ });
522
+ // No default
523
+ }
524
+ break;
525
+ }
526
+ }
527
+ }
528
+ });
529
+ if (!isPresentation) throw new OfficeReadError('not-pptx', `not a presentation: the package's main part is ${main === '' ? 'missing' : main}${main.endsWith('workbook.xml') ? ' (a spreadsheet: read it with readXlsx)' : ''}`);
530
+ const relationships = relationshipsOf(pkg, main);
531
+ const layouts = new Map();
532
+ const mode = options.values ?? 'typed';
533
+ const selected = selector(options.slides);
534
+ const slides = [];
535
+ for (const [index, id] of ids.entries()) {
536
+ const part = relationships.get(id);
537
+ if (part?.type !== 'slide') continue;
538
+ const slideRelationships = relationshipsOf(pkg, part.target);
539
+ const layoutPart = relationshipOfType(slideRelationships, 'slideLayout')?.target;
540
+ const geometry = layoutPart === undefined ? undefined : geometryOf(pkg, layoutPart, layouts);
541
+ const content = readSlide(pkg.text(part.target), key => geometry === undefined ? undefined : inheritedBox(key, geometry.layout, geometry.master));
542
+ const number = index + 1;
543
+ if (!selected({
544
+ number,
545
+ title: content.title ?? '',
546
+ hidden: content.hidden
547
+ })) continue;
548
+ const charts = options.charts === false ? [] : content.chartIds.flatMap(chartId => {
549
+ const chart = slideRelationships.get(chartId);
550
+ return chart === undefined ? [] : [readChart(pkg.text(chart.target), mode)];
551
+ });
552
+ const notesPart = options.notes === false ? undefined : relationshipOfType(slideRelationships, 'notesSlide')?.target;
553
+ slides.push({
554
+ number,
555
+ ...(content.title !== undefined && {
556
+ title: content.title
557
+ }),
558
+ hidden: content.hidden,
559
+ shapes: content.shapes,
560
+ tables: content.tables,
561
+ charts,
562
+ notes: notesPart === undefined ? '' : readNotes(pkg.text(notesPart))
563
+ });
564
+ }
565
+ return {
566
+ width: Math.round(size.width / POINTS * 100) / 100,
567
+ height: Math.round(size.height / POINTS * 100) / 100,
568
+ slides
569
+ };
570
+ }
571
+ /** A layout's and its master's placeholder boxes, read once per layout. */
572
+ function geometryOf(pkg, layoutPart, cache) {
573
+ const cached = cache.get(layoutPart);
574
+ if (cached !== undefined) return cached;
575
+ const masterPart = relationshipOfType(relationshipsOf(pkg, layoutPart), 'slideMaster')?.target;
576
+ const geometry = {
577
+ layout: placeholderBoxes(pkg.text(layoutPart)),
578
+ master: placeholderBoxes(masterPart === undefined ? '' : pkg.text(masterPart))
579
+ };
580
+ cache.set(layoutPart, geometry);
581
+ return geometry;
582
+ }
583
+ function selector(filter) {
584
+ if (filter === undefined) return () => true;
585
+ if (Array.isArray(filter)) return slide => filter.includes(slide.number);
586
+ if (filter instanceof RegExp) return slide => filter.test(slide.title);
587
+ return filter;
588
+ }
589
+
590
+ export { readPptx as r };
591
+ //# sourceMappingURL=read-pptx.use-case.esm.js.map