reamkit 1.22.0 → 1.23.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 (72) hide show
  1. package/dist/esm/core/converter/ream.d.ts +16 -0
  2. package/dist/esm/core/converter/ream.js +59 -9
  3. package/dist/esm/core/document-model/types.d.ts +18 -0
  4. package/dist/esm/core/drawingml/chart-geometry.js +11 -5
  5. package/dist/esm/core/drawingml/colors.d.ts +4 -2
  6. package/dist/esm/core/drawingml/colors.js +15 -0
  7. package/dist/esm/core/drawingml/theme-parser.d.ts +38 -0
  8. package/dist/esm/core/drawingml/theme-parser.js +64 -1
  9. package/dist/esm/core/fonts/families.d.ts +10 -0
  10. package/dist/esm/core/fonts/families.js +44 -0
  11. package/dist/esm/core/fonts/index.d.ts +3 -2
  12. package/dist/esm/core/fonts/remote-fonts.d.ts +50 -0
  13. package/dist/esm/core/fonts/remote-fonts.js +114 -8
  14. package/dist/esm/core/fonts/scripts.d.ts +26 -0
  15. package/dist/esm/core/fonts/scripts.js +177 -0
  16. package/dist/esm/core/metafile/blit.d.ts +33 -0
  17. package/dist/esm/core/metafile/blit.js +67 -0
  18. package/dist/esm/core/metafile/dib.d.ts +61 -0
  19. package/dist/esm/core/metafile/dib.js +205 -0
  20. package/dist/esm/core/metafile/emf.js +137 -23
  21. package/dist/esm/core/metafile/picture.d.ts +12 -1
  22. package/dist/esm/core/metafile/wmf.js +149 -9
  23. package/dist/esm/core/ole/escher-blip.d.ts +12 -0
  24. package/dist/esm/core/ole/escher-blip.js +88 -0
  25. package/dist/esm/{pdf-reader → core}/png-encode.js +1 -1
  26. package/dist/esm/excel/print-model.js +3 -1
  27. package/dist/esm/excel/styles-parser.js +1 -1
  28. package/dist/esm/excel/xls/biff-reader.js +53 -18
  29. package/dist/esm/excel/xls/biff-styles.d.ts +5 -2
  30. package/dist/esm/excel/xls/biff-styles.js +9 -7
  31. package/dist/esm/excel/xls/escher.js +90 -24
  32. package/dist/esm/layout/page-doc.d.ts +39 -0
  33. package/dist/esm/layout/styled-layout.d.ts +2 -2
  34. package/dist/esm/layout/styled-layout.js +301 -35
  35. package/dist/esm/pdf/styled-page-emitter.js +35 -2
  36. package/dist/esm/pdf-reader/image-decode.js +1 -1
  37. package/dist/esm/pptx/embedded-fonts.d.ts +31 -0
  38. package/dist/esm/pptx/embedded-fonts.js +104 -0
  39. package/dist/esm/pptx/placeholder-cascade.d.ts +73 -6
  40. package/dist/esm/pptx/placeholder-cascade.js +114 -18
  41. package/dist/esm/pptx/ppt/ppt-reader.js +120 -10
  42. package/dist/esm/pptx/ppt/ppt-text.d.ts +66 -1
  43. package/dist/esm/pptx/ppt/ppt-text.js +576 -137
  44. package/dist/esm/pptx/pptx-reader.d.ts +8 -0
  45. package/dist/esm/pptx/pptx-reader.js +27 -5
  46. package/dist/esm/pptx/preset-table-styles.d.ts +9 -0
  47. package/dist/esm/pptx/preset-table-styles.js +27 -0
  48. package/dist/esm/pptx/slide-parser.d.ts +7 -1
  49. package/dist/esm/pptx/slide-parser.js +106 -33
  50. package/dist/esm/pptx/sp-helpers.d.ts +2 -1
  51. package/dist/esm/pptx/sp-helpers.js +3 -2
  52. package/dist/esm/pptx/table-style.d.ts +9 -1
  53. package/dist/esm/pptx/table-style.js +49 -7
  54. package/dist/esm/word/document-parser.d.ts +7 -1
  55. package/dist/esm/word/document-parser.js +6 -6
  56. package/dist/esm/word/docx-reader.js +6 -3
  57. package/dist/esm/word/docx-to-pdf.d.ts +8 -7
  58. package/dist/esm/word/docx-to-pdf.js +25 -7
  59. package/dist/esm/word/drawing-parser.js +31 -19
  60. package/dist/esm/word/numbering-parser.d.ts +2 -1
  61. package/dist/esm/word/numbering-parser.js +6 -6
  62. package/dist/esm/word/paragraph-properties.d.ts +2 -1
  63. package/dist/esm/word/paragraph-properties.js +2 -2
  64. package/dist/esm/word/run-properties.d.ts +5 -2
  65. package/dist/esm/word/run-properties.js +13 -8
  66. package/dist/esm/word/styles-parser.d.ts +2 -1
  67. package/dist/esm/word/styles-parser.js +12 -12
  68. package/dist/esm/word/table-parser.js +3 -3
  69. package/dist/esm/word/theme-fonts.d.ts +10 -0
  70. package/dist/esm/word/theme-fonts.js +28 -0
  71. package/package.json +1 -1
  72. /package/dist/esm/{pdf-reader → core}/png-encode.d.ts +0 -0
@@ -0,0 +1,31 @@
1
+ import { Loss } from '../core/ir/index.js';
2
+ import { OpcPackage } from '../core/opc/index.js';
3
+ import { PoNode } from '../core/po-helpers.js';
4
+ import { FontRegistry } from '../core/font/index.js';
5
+ /** What an `fntdata` part turned out to hold. */
6
+ type FaceBytes = {
7
+ readonly kind: 'font';
8
+ readonly bytes: Uint8Array;
9
+ } | {
10
+ readonly kind: 'compressed';
11
+ } | {
12
+ readonly kind: 'unreadable';
13
+ };
14
+ /**
15
+ * Read one `fntdata` part: the font itself when the container stores it
16
+ * plainly, and what stopped it otherwise.
17
+ *
18
+ * @param data The part's bytes.
19
+ */
20
+ export declare function readFntData(data: Uint8Array): FaceBytes;
21
+ /**
22
+ * §19.2.1.13 — the fonts a deck embeds, by normalized family name.
23
+ *
24
+ * @param pkg The opened `.pptx` package.
25
+ * @param presPath The presentation part's path (relationships resolve from it).
26
+ * @param pres Its parsed `p:presentation` element.
27
+ * @param onLoss Where a face that cannot be unpacked records itself.
28
+ * @returns The families whose faces were readable (empty when none were).
29
+ */
30
+ export declare function loadPptxEmbeddedFonts(pkg: OpcPackage, presPath: string, pres: PoNode | undefined, onLoss: (loss: Loss) => void): Map<string, FontRegistry>;
31
+ export {};
@@ -0,0 +1,104 @@
1
+ import { FontRegistry } from "../core/font/font-registry.js";
2
+ import { poAttr, poChildren, poIs } from "../core/po-helpers.js";
3
+ //#region src/pptx/embedded-fonts.ts
4
+ var REL_FONT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font";
5
+ /** §19.2.1.13 — the four faces a `p:embeddedFont` may name, by element. */
6
+ var VARIANTS = [
7
+ ["p:regular", "regular"],
8
+ ["p:bold", "bold"],
9
+ ["p:italic", "italic"],
10
+ ["p:boldItalic", "boldItalic"]
11
+ ];
12
+ var EOT_VERSIONS = new Set([
13
+ 65536,
14
+ 131073,
15
+ 131074
16
+ ]);
17
+ var TTEMBED_TTCOMPRESSED = 4;
18
+ var TTEMBED_XORENCRYPTDATA = 268435456;
19
+ /**
20
+ * Read one `fntdata` part: the font itself when the container stores it
21
+ * plainly, and what stopped it otherwise.
22
+ *
23
+ * @param data The part's bytes.
24
+ */
25
+ function readFntData(data) {
26
+ if (isSfnt(data)) return {
27
+ kind: "font",
28
+ bytes: data
29
+ };
30
+ if (data.length < 82) return { kind: "unreadable" };
31
+ const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
32
+ const eotSize = v.getUint32(0, true);
33
+ const version = v.getUint32(8, true);
34
+ const flags = v.getUint32(12, true);
35
+ if (eotSize !== data.length || !EOT_VERSIONS.has(version)) return { kind: "unreadable" };
36
+ if ((flags & (TTEMBED_TTCOMPRESSED | TTEMBED_XORENCRYPTDATA)) !== 0) return { kind: "compressed" };
37
+ let at = 82;
38
+ for (let i = 0; i < 4; i++) {
39
+ if (at + 2 > data.length) return { kind: "unreadable" };
40
+ at += 2 + v.getUint16(at, true) + 2;
41
+ }
42
+ if (version !== 65536) {
43
+ if (at + 2 > data.length) return { kind: "unreadable" };
44
+ at += 2 + v.getUint16(at, true);
45
+ at = data.length - v.getUint32(4, true);
46
+ }
47
+ const bytes = data.subarray(at);
48
+ return isSfnt(bytes) ? {
49
+ kind: "font",
50
+ bytes
51
+ } : { kind: "unreadable" };
52
+ }
53
+ /**
54
+ * §19.2.1.13 — the fonts a deck embeds, by normalized family name.
55
+ *
56
+ * @param pkg The opened `.pptx` package.
57
+ * @param presPath The presentation part's path (relationships resolve from it).
58
+ * @param pres Its parsed `p:presentation` element.
59
+ * @param onLoss Where a face that cannot be unpacked records itself.
60
+ * @returns The families whose faces were readable (empty when none were).
61
+ */
62
+ function loadPptxEmbeddedFonts(pkg, presPath, pres, onLoss) {
63
+ const out = /* @__PURE__ */ new Map();
64
+ const list = pres ? poChildren(pres).find((c) => poIs(c, "p:embeddedFontLst")) : void 0;
65
+ if (!list) return out;
66
+ const relById = new Map(pkg.getPartRelationships(presPath).map((r) => [r.id, r]));
67
+ for (const font of poChildren(list).filter((c) => poIs(c, "p:embeddedFont"))) {
68
+ const bytes = {};
69
+ let compressed = false;
70
+ for (const [tag, variant] of VARIANTS) {
71
+ const node = poChildren(font).find((c) => poIs(c, tag));
72
+ const rId = node ? poAttr(node, "r:id") : void 0;
73
+ const rel = rId !== void 0 ? relById.get(rId) : void 0;
74
+ if (!rel || rel.type !== REL_FONT) continue;
75
+ const part = pkg.resolveRelatedPart(presPath, rel);
76
+ if (!part) continue;
77
+ const face = readFntData(part.data);
78
+ if (face.kind === "compressed") compressed = true;
79
+ if (face.kind === "font") bytes[variant] = face.bytes;
80
+ }
81
+ const typeface = poChildren(font).find((c) => poIs(c, "p:font"));
82
+ const name = (typeface ? poAttr(typeface, "typeface") : void 0)?.trim() ?? "";
83
+ if (bytes.regular) try {
84
+ out.set(name.toLowerCase(), FontRegistry.fromBytes({
85
+ ...bytes,
86
+ regular: bytes.regular
87
+ }));
88
+ continue;
89
+ } catch {}
90
+ if (compressed) onLoss({
91
+ feature: "font",
92
+ detail: `the deck embeds ${name || "a font"}, packed with MicroType Express (EOT TTEMBED_TTCOMPRESSED); it renders in a substitute`,
93
+ severity: "substituted"
94
+ });
95
+ }
96
+ return out;
97
+ }
98
+ function isSfnt(data) {
99
+ if (data.length < 4) return false;
100
+ const sig = (data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]) >>> 0;
101
+ return sig === 65536 || sig === 1330926671 || sig === 1953658213 || sig === 1953784678;
102
+ }
103
+ //#endregion
104
+ export { loadPptxEmbeddedFonts };
@@ -1,7 +1,9 @@
1
1
  import { ParagraphProperties, RunProperties } from '../core/document-model/index.js';
2
2
  import { ColorResolver } from '../core/drawingml/colors.js';
3
+ import { ThemeFonts } from '../core/drawingml/theme-parser.js';
3
4
  import { PoNode } from '../core/po-helpers.js';
4
5
  import { PlaceholderRef, ShapeBoxEmu } from './sp-helpers.js';
6
+ import { Pt } from '../core/ir/index.js';
5
7
  /**
6
8
  * The placeholder cascade (PX2): the resolver a slide parser consults whenever a
7
9
  * slide placeholder shape (`p:sp` with a `p:ph`) lacks its own geometry or run
@@ -27,17 +29,32 @@ export interface PlaceholderCascade {
27
29
  */
28
30
  readonly anchorFor: (ph: PlaceholderRef) => 't' | 'ctr' | 'b' | undefined;
29
31
  /**
30
- * The prototype's own `p:spPr`, for the properties a slide placeholder
31
- * inherits rather than states: its fill, its outline, its geometry. The
32
- * layout's before the master's.
32
+ * The prototypes' own `p:spPr`, nearest first — the layout's, then the
33
+ * master's — for the properties a slide placeholder inherits rather than
34
+ * states: its fill, its outline, its geometry.
35
+ *
36
+ * A CHAIN and not one node, because the inheritance is per property. The
37
+ * layout's prototype may state a box and nothing else, and the fill then
38
+ * comes from the master: tdf104015's title is red in every reader and was a
39
+ * bare outline here, because the layout's `p:spPr` existed and stopped the
40
+ * search.
33
41
  */
34
- readonly shapePropsFor: (ph: PlaceholderRef) => PoNode | undefined;
42
+ readonly shapePropsFor: (ph: PlaceholderRef) => ReadonlyArray<PoNode>;
35
43
  }
36
44
  /** One level of a text style: how its paragraphs sit and how their runs read. */
37
45
  interface LevelStyle {
38
46
  readonly run: RunProperties;
39
47
  readonly paragraph: ParagraphProperties;
40
48
  readonly bullet?: LevelBullet;
49
+ /**
50
+ * §21.1.2.2.9/.10 — the space around the paragraph stated as a FRACTION of a
51
+ * line rather than a distance. A fraction of WHAT is only known once the
52
+ * levels have merged and the size is settled, so it travels this far raw.
53
+ */
54
+ readonly spacingPct?: {
55
+ readonly before?: number;
56
+ readonly after?: number;
57
+ };
41
58
  }
42
59
  /**
43
60
  * §21.1.2.4.4/.5/.6 — the bullet a level declares: none at all, a literal
@@ -78,8 +95,24 @@ export declare function parseBullet(pPr: PoNode | undefined, colors?: ColorResol
78
95
  * @param layoutTree The parsed `p:sldLayout` part.
79
96
  * @param masterTree The parsed `p:sldMaster` part, when present.
80
97
  * @param colors The deck's colour resolver, for scheme colours in the text styles.
98
+ * @param deckDefaults The presentation's own default text style, as the floor.
99
+ * @param themeFonts The theme's font scheme, for the `+mn-lt` tokens.
100
+ */
101
+ export declare function buildPlaceholderCascade(layoutTree: ReadonlyArray<PoNode>, masterTree: ReadonlyArray<PoNode> | undefined, colors: ColorResolver, deckDefaults?: ReadonlyArray<LevelStyle>, themeFonts?: ThemeFonts): PlaceholderCascade;
102
+ /**
103
+ * §21.1.2.2.9/.10 — the space a paragraph states as a FRACTION of a line, in
104
+ * points.
105
+ *
106
+ * A "line" here is the paragraph's own text at its natural height, which is the
107
+ * size times the same 1.2 the layout gives an unstated line. A level that
108
+ * states the fraction and leaves the size to another level is resolved once
109
+ * they have merged, which is why this is not read where the fraction is.
110
+ *
111
+ * @param pct The fraction, `0..n` (20% arrives as 0.2).
112
+ * @param sizePt The paragraph's resolved font size.
113
+ * @returns The distance, or `undefined` when the size is not known.
81
114
  */
82
- export declare function buildPlaceholderCascade(layoutTree: ReadonlyArray<PoNode>, masterTree: ReadonlyArray<PoNode> | undefined, colors: ColorResolver, deckDefaults?: ReadonlyArray<LevelStyle>): PlaceholderCascade;
115
+ export declare function spacingFromLineFraction(pct: number | undefined, sizePt: number | undefined): Pt | undefined;
83
116
  /**
84
117
  * `a:lstStyle` (or a `p:txStyles` family) → its nine levels. Shared by the
85
118
  * deck's default text style, the master's families and a prototype's own list,
@@ -90,5 +123,39 @@ export declare function buildPlaceholderCascade(layoutTree: ReadonlyArray<PoNode
90
123
  * @param prefix The level element prefix: `a:lvl` here, always.
91
124
  * @returns The nine levels, or an empty list when there is no such node.
92
125
  */
93
- export declare function parseLevelStyles(list: PoNode | undefined, colors: ColorResolver): Array<LevelStyle>;
126
+ export declare function parseLevelStyles(list: PoNode | undefined, colors: ColorResolver, themeFonts?: ThemeFonts): Array<LevelStyle>;
127
+ /**
128
+ * A level's paragraph shape — §21.1.2.2.7 `@algn`, `@marL`/`@indent`, and the
129
+ * space around and between its lines.
130
+ *
131
+ * Both spacings come in two spellings: `a:spcPts` is a distance (hundredths of
132
+ * a point) and `a:spcPct` a FRACTION of a line (thousandths of a percent). Only
133
+ * the distance was read, so a deck that spaces its bullets the usual way — by
134
+ * fraction — set them solid.
135
+ */
136
+ export declare function pPrToParagraphProps(lvlPr: PoNode | undefined): ParagraphProperties;
137
+ /**
138
+ * §21.1.2.2.5 `a:lnSpc` — the height of each line: a fraction of the natural
139
+ * one (`a:spcPct`) or a distance (`a:spcPts`).
140
+ *
141
+ * The model states a multiple the way §17.3.1.33 does — 12pt under the `auto`
142
+ * rule IS single — so a fraction needs no font size to express, which is why
143
+ * this resolves here and the paragraph SPACING has to wait for the size.
144
+ *
145
+ * @param pPr The paragraph properties (a level's or a paragraph's own).
146
+ * @returns The line spacing, or nothing when the node states none.
147
+ */
148
+ export declare function lineSpacing(pPr: PoNode | undefined): ParagraphProperties;
149
+ /**
150
+ * §21.1.2.2.9/.10 — the fraction of a line a paragraph puts before and after
151
+ * itself, when it states one. The distance form is read beside it; this is what
152
+ * the caller must still resolve against the size.
153
+ *
154
+ * @param pPr The paragraph properties.
155
+ * @returns The two fractions (`0..n`), each present only when stated.
156
+ */
157
+ export declare function spacingFractions(pPr: PoNode | undefined): {
158
+ readonly before?: number;
159
+ readonly after?: number;
160
+ } | undefined;
94
161
  export {};
@@ -55,11 +55,13 @@ function categoryOf(type) {
55
55
  * @param layoutTree The parsed `p:sldLayout` part.
56
56
  * @param masterTree The parsed `p:sldMaster` part, when present.
57
57
  * @param colors The deck's colour resolver, for scheme colours in the text styles.
58
+ * @param deckDefaults The presentation's own default text style, as the floor.
59
+ * @param themeFonts The theme's font scheme, for the `+mn-lt` tokens.
58
60
  */
59
- function buildPlaceholderCascade(layoutTree, masterTree, colors, deckDefaults = []) {
60
- const layoutPhs = collectPlaceholders(layoutTree, "p:sldLayout", colors);
61
- const masterPhs = masterTree ? collectPlaceholders(masterTree, "p:sldMaster", colors) : [];
62
- const txStyles = masterTree ? collectTxStyles(masterTree, colors) : {
61
+ function buildPlaceholderCascade(layoutTree, masterTree, colors, deckDefaults = [], themeFonts) {
62
+ const layoutPhs = collectPlaceholders(layoutTree, "p:sldLayout", colors, themeFonts);
63
+ const masterPhs = masterTree ? collectPlaceholders(masterTree, "p:sldMaster", colors, themeFonts) : [];
64
+ const txStyles = masterTree ? collectTxStyles(masterTree, colors, themeFonts) : {
63
65
  title: [],
64
66
  body: [],
65
67
  other: []
@@ -67,7 +69,8 @@ function buildPlaceholderCascade(layoutTree, masterTree, colors, deckDefaults =
67
69
  const styleAt = (ph, level) => {
68
70
  const layers = [at(deckDefaults, level)];
69
71
  if (ph) layers.push(at(txStyles[categoryOf(ph.type)], level), at(matchPlaceholder(masterPhs, ph)?.levels ?? [], level), at(matchPlaceholder(layoutPhs, ph)?.levels ?? [], level));
70
- return layers.reduce(mergeLevels, EMPTY_LEVEL);
72
+ const merged = layers.reduce(mergeLevels, EMPTY_LEVEL);
73
+ return merged.spacingPct === void 0 ? merged : withResolvedSpacing(merged);
71
74
  };
72
75
  return {
73
76
  geometryFor(ph) {
@@ -86,7 +89,7 @@ function buildPlaceholderCascade(layoutTree, masterTree, colors, deckDefaults =
86
89
  return matchPlaceholder(layoutPhs, ph)?.anchor ?? matchPlaceholder(masterPhs, ph)?.anchor;
87
90
  },
88
91
  shapePropsFor(ph) {
89
- return matchPlaceholder(layoutPhs, ph)?.spPr ?? matchPlaceholder(masterPhs, ph)?.spPr;
92
+ return [matchPlaceholder(layoutPhs, ph)?.spPr, matchPlaceholder(masterPhs, ph)?.spPr].filter((n) => n !== void 0);
90
93
  }
91
94
  };
92
95
  }
@@ -94,6 +97,39 @@ var EMPTY_LEVEL = {
94
97
  run: {},
95
98
  paragraph: {}
96
99
  };
100
+ /**
101
+ * §21.1.2.2.9/.10 — the space a paragraph states as a FRACTION of a line, in
102
+ * points.
103
+ *
104
+ * A "line" here is the paragraph's own text at its natural height, which is the
105
+ * size times the same 1.2 the layout gives an unstated line. A level that
106
+ * states the fraction and leaves the size to another level is resolved once
107
+ * they have merged, which is why this is not read where the fraction is.
108
+ *
109
+ * @param pct The fraction, `0..n` (20% arrives as 0.2).
110
+ * @param sizePt The paragraph's resolved font size.
111
+ * @returns The distance, or `undefined` when the size is not known.
112
+ */
113
+ function spacingFromLineFraction(pct, sizePt) {
114
+ if (pct === void 0 || sizePt === void 0) return void 0;
115
+ return pt(pct * sizePt * NATURAL_LINE);
116
+ }
117
+ /** What the layout gives a line of text with nothing said about its height. */
118
+ var NATURAL_LINE = 1.2;
119
+ function withResolvedSpacing(level) {
120
+ const size = level.run.fontSizePt;
121
+ const before = spacingFromLineFraction(level.spacingPct?.before, size);
122
+ const after = spacingFromLineFraction(level.spacingPct?.after, size);
123
+ if (before === void 0 && after === void 0) return level;
124
+ return {
125
+ ...level,
126
+ paragraph: {
127
+ ...level.paragraph,
128
+ ...before !== void 0 ? { spacingBefore: before } : {},
129
+ ...after !== void 0 ? { spacingAfter: after } : {}
130
+ }
131
+ };
132
+ }
97
133
  /** The style at `level`, clamped to the list — a deeper level keeps the last. */
98
134
  function at(levels, level) {
99
135
  if (levels.length === 0) return EMPTY_LEVEL;
@@ -105,6 +141,10 @@ function mergeLevels(base, next) {
105
141
  ...base.bullet,
106
142
  ...next.bullet
107
143
  } : void 0;
144
+ const spacingPct = base.spacingPct || next.spacingPct ? {
145
+ ...base.spacingPct,
146
+ ...next.spacingPct
147
+ } : void 0;
108
148
  return {
109
149
  run: {
110
150
  ...base.run,
@@ -114,7 +154,8 @@ function mergeLevels(base, next) {
114
154
  ...base.paragraph,
115
155
  ...next.paragraph
116
156
  },
117
- ...bullet ? { bullet } : {}
157
+ ...bullet ? { bullet } : {},
158
+ ...spacingPct ? { spacingPct } : {}
118
159
  };
119
160
  }
120
161
  /**
@@ -127,17 +168,19 @@ function mergeLevels(base, next) {
127
168
  * @param prefix The level element prefix: `a:lvl` here, always.
128
169
  * @returns The nine levels, or an empty list when there is no such node.
129
170
  */
130
- function parseLevelStyles(list, colors) {
171
+ function parseLevelStyles(list, colors, themeFonts) {
131
172
  if (!list) return [];
132
173
  const out = [];
133
174
  for (let lvl = 1; lvl <= 9; lvl++) {
134
175
  const lvlPr = poChildren(list).find((c) => poIs(c, `a:lvl${String(lvl)}pPr`));
135
176
  const defRPr = lvlPr ? poChildren(lvlPr).find((c) => poIs(c, "a:defRPr")) : void 0;
136
177
  const bullet = parseBullet(lvlPr, colors);
178
+ const spacingPct = spacingFractions(lvlPr);
137
179
  out.push({
138
- run: rPrToRunProps(defRPr, colors),
180
+ run: rPrToRunProps(defRPr, colors, themeFonts),
139
181
  paragraph: pPrToParagraphProps(lvlPr),
140
- ...bullet ? { bullet } : {}
182
+ ...bullet ? { bullet } : {},
183
+ ...spacingPct ? { spacingPct } : {}
141
184
  });
142
185
  }
143
186
  return out;
@@ -151,8 +194,12 @@ var ALGN = {
151
194
  };
152
195
  /**
153
196
  * A level's paragraph shape — §21.1.2.2.7 `@algn`, `@marL`/`@indent`, and the
154
- * space around it. `a:spcPct` is a fraction of the line rather than a distance,
155
- * which this model has no room for, so only the stated POINTS are read.
197
+ * space around and between its lines.
198
+ *
199
+ * Both spacings come in two spellings: `a:spcPts` is a distance (hundredths of
200
+ * a point) and `a:spcPct` a FRACTION of a line (thousandths of a percent). Only
201
+ * the distance was read, so a deck that spaces its bullets the usual way — by
202
+ * fraction — set them solid.
156
203
  */
157
204
  function pPrToParagraphProps(lvlPr) {
158
205
  if (!lvlPr) return {};
@@ -164,7 +211,56 @@ function pPrToParagraphProps(lvlPr) {
164
211
  ...marL !== void 0 ? { indentLeft: emuToPt(marL) } : {},
165
212
  ...indent !== void 0 ? { indentFirstLine: emuToPt(indent) } : {},
166
213
  ...spacingPt(lvlPr, "a:spcBef", "spacingBefore"),
167
- ...spacingPt(lvlPr, "a:spcAft", "spacingAfter")
214
+ ...spacingPt(lvlPr, "a:spcAft", "spacingAfter"),
215
+ ...lineSpacing(lvlPr)
216
+ };
217
+ }
218
+ /**
219
+ * §21.1.2.2.5 `a:lnSpc` — the height of each line: a fraction of the natural
220
+ * one (`a:spcPct`) or a distance (`a:spcPts`).
221
+ *
222
+ * The model states a multiple the way §17.3.1.33 does — 12pt under the `auto`
223
+ * rule IS single — so a fraction needs no font size to express, which is why
224
+ * this resolves here and the paragraph SPACING has to wait for the size.
225
+ *
226
+ * @param pPr The paragraph properties (a level's or a paragraph's own).
227
+ * @returns The line spacing, or nothing when the node states none.
228
+ */
229
+ function lineSpacing(pPr) {
230
+ const holder = pPr ? poChildren(pPr).find((c) => poIs(c, "a:lnSpc")) : void 0;
231
+ if (!holder) return {};
232
+ const pctVal = poIntAttr(poChildren(holder).find((c) => poIs(c, "a:spcPct")), "val");
233
+ if (pctVal !== void 0 && pctVal > 0) return {
234
+ spacingLine: pt(pctVal / 1e5 * 12),
235
+ spacingLineRule: "auto"
236
+ };
237
+ const ptsVal = poIntAttr(poChildren(holder).find((c) => poIs(c, "a:spcPts")), "val");
238
+ if (ptsVal !== void 0 && ptsVal > 0) return {
239
+ spacingLine: pt(ptsVal / 100),
240
+ spacingLineRule: "exact"
241
+ };
242
+ return {};
243
+ }
244
+ /**
245
+ * §21.1.2.2.9/.10 — the fraction of a line a paragraph puts before and after
246
+ * itself, when it states one. The distance form is read beside it; this is what
247
+ * the caller must still resolve against the size.
248
+ *
249
+ * @param pPr The paragraph properties.
250
+ * @returns The two fractions (`0..n`), each present only when stated.
251
+ */
252
+ function spacingFractions(pPr) {
253
+ const frac = (tag) => {
254
+ const holder = pPr ? poChildren(pPr).find((c) => poIs(c, tag)) : void 0;
255
+ const pctNode = holder ? poChildren(holder).find((c) => poIs(c, "a:spcPct")) : void 0;
256
+ const val = pctNode ? poIntAttr(pctNode, "val") : void 0;
257
+ return val === void 0 || val < 0 ? void 0 : val / 1e5;
258
+ };
259
+ const [before, after] = [frac("a:spcBef"), frac("a:spcAft")];
260
+ if (before === void 0 && after === void 0) return void 0;
261
+ return {
262
+ ...before !== void 0 ? { before } : {},
263
+ ...after !== void 0 ? { after } : {}
168
264
  };
169
265
  }
170
266
  function spacingPt(lvlPr, tag, key) {
@@ -173,7 +269,7 @@ function spacingPt(lvlPr, tag, key) {
173
269
  const val = pts ? poIntAttr(pts, "val") : void 0;
174
270
  return val === void 0 ? {} : { [key]: pt(val / 100) };
175
271
  }
176
- function collectPlaceholders(tree, root, colors) {
272
+ function collectPlaceholders(tree, root, colors, themeFonts) {
177
273
  const sld = tree.find((n) => poIs(n, root));
178
274
  const cSld = sld ? poChildren(sld).find((c) => poIs(c, "p:cSld")) : void 0;
179
275
  const spTree = cSld ? poChildren(cSld).find((c) => poIs(c, "p:spTree")) : void 0;
@@ -194,7 +290,7 @@ function collectPlaceholders(tree, root, colors) {
194
290
  ref,
195
291
  ...box ? { box } : {},
196
292
  ...spPr ? { spPr } : {},
197
- levels: parseLevelStyles(lstStyle, colors),
293
+ levels: parseLevelStyles(lstStyle, colors, themeFonts),
198
294
  ...anchor ? { anchor } : {}
199
295
  });
200
296
  }
@@ -210,10 +306,10 @@ function matchPlaceholder(list, ph) {
210
306
  const cat = categoryOf(ph.type);
211
307
  return list.find((p) => categoryOf(p.ref.type) === cat);
212
308
  }
213
- function collectTxStyles(masterTree, colors) {
309
+ function collectTxStyles(masterTree, colors, themeFonts) {
214
310
  const sld = masterTree.find((n) => poIs(n, "p:sldMaster"));
215
311
  const txStyles = sld ? poChildren(sld).find((c) => poIs(c, "p:txStyles")) : void 0;
216
- const family = (tag) => parseLevelStyles(txStyles ? poChildren(txStyles).find((c) => poIs(c, tag)) : void 0, colors);
312
+ const family = (tag) => parseLevelStyles(txStyles ? poChildren(txStyles).find((c) => poIs(c, tag)) : void 0, colors, themeFonts);
217
313
  return {
218
314
  title: family("p:titleStyle"),
219
315
  body: family("p:bodyStyle"),
@@ -221,4 +317,4 @@ function collectTxStyles(masterTree, colors) {
221
317
  };
222
318
  }
223
319
  //#endregion
224
- export { buildPlaceholderCascade, parseBullet, parseLevelStyles };
320
+ export { buildPlaceholderCascade, lineSpacing, parseBullet, parseLevelStyles, spacingFractions, spacingFromLineFraction };
@@ -1,6 +1,8 @@
1
1
  import { pt } from "../../core/ir/units.js";
2
2
  import { ResourceStore } from "../../core/ir/resources.js";
3
3
  import { FEATURES } from "../../core/ir/features.js";
4
+ import { isEmf } from "../../core/metafile/emf.js";
5
+ import { isWmf } from "../../core/metafile/wmf.js";
4
6
  import { EMPTY_STYLE_SHEET, resolveBodyStyles } from "../../core/style-cascade/resolver.js";
5
7
  import "../../core/style-cascade/index.js";
6
8
  import { isCfb, openCfb } from "../../core/ole/cfb.js";
@@ -48,7 +50,7 @@ function readPpt(bytes) {
48
50
  return {
49
51
  doc: {
50
52
  kind: "flow",
51
- body: resolveBodyStyles(buildBody(content, resources), EMPTY_STYLE_SHEET),
53
+ body: resolveBodyStyles(buildBody(content, resources, width, height), EMPTY_STYLE_SHEET),
52
54
  sections: [],
53
55
  section: {
54
56
  pageSize: {
@@ -70,19 +72,23 @@ function readPpt(bytes) {
70
72
  losses: [content.encrypted ? PPT_ENCRYPTED_LOSS : PPT_TEXT_LOSS]
71
73
  };
72
74
  }
73
- function buildBody(content, resources) {
75
+ function buildBody(content, resources, pageW, pageH) {
74
76
  const body = [];
75
77
  content.slides.forEach((slide, slideIndex) => {
76
78
  const inFlowParas = [];
77
79
  const inFlowImages = [];
78
80
  const floats = [];
81
+ if (slide.background) {
82
+ const backdrop = slideBackdrop(slide.background, resources, pageW, pageH);
83
+ if (backdrop) floats.push(backdrop);
84
+ }
79
85
  for (const shape of slide.shapes) if (shape.rectPt) {
80
- if (shape.paragraphs?.some((p) => paragraphText(p).length > 0)) floats.push(positionedTextShape(shape.rectPt, shape.paragraphs));
86
+ if (shape.autoShape) floats.push(positionedAutoShape(shape.rectPt, shape.autoShape, resources));
81
87
  if (shape.image) {
82
88
  const block = positionedImage(shape.rectPt, shape.image, resources);
83
89
  if (block) floats.push(block);
84
90
  }
85
- if (shape.autoShape) floats.push(positionedAutoShape(shape.rectPt, shape.autoShape));
91
+ if (shape.paragraphs?.some((p) => paragraphText(p).length > 0)) floats.push(positionedTextShape(shape.rectPt, shape.paragraphs, shape.wordArt === true));
86
92
  } else {
87
93
  for (const para of shape.paragraphs ?? []) if (paragraphText(para).length > 0) inFlowParas.push(flowParagraph(para));
88
94
  if (shape.image) {
@@ -122,10 +128,80 @@ function flowParagraph(para) {
122
128
  kind: "paragraph",
123
129
  paragraph: {
124
130
  properties: toParaProperties(para, false),
125
- runs: para.runs.map(toRun)
131
+ runs: bulletedRuns(para)
126
132
  }
127
133
  };
128
134
  }
135
+ function bulletedRuns(para) {
136
+ const runs = para.runs.map(toRun);
137
+ if (para.bullet === void 0) return runs;
138
+ const { bold: _b, italic: _i, ...rest } = runs[0]?.properties ?? {};
139
+ return [{
140
+ text: `${para.bullet} `,
141
+ properties: rest
142
+ }, ...runs];
143
+ }
144
+ function slideBackdrop(bg, resources, pageW, pageH) {
145
+ const fill = backdropFill(bg, resources);
146
+ if (!fill) return void 0;
147
+ return {
148
+ kind: "shape",
149
+ shape: {
150
+ float: {
151
+ wrap: "none",
152
+ behind: true,
153
+ posH: {
154
+ relativeFrom: "page",
155
+ offsetPt: pt(0)
156
+ },
157
+ posV: {
158
+ relativeFrom: "page",
159
+ offsetPt: pt(0)
160
+ }
161
+ },
162
+ width: pageW,
163
+ height: pageH,
164
+ geometry: {
165
+ kind: "preset",
166
+ preset: "rect",
167
+ adjust: /* @__PURE__ */ new Map()
168
+ },
169
+ fill,
170
+ paragraphProperties: {}
171
+ }
172
+ };
173
+ }
174
+ function backdropFill(bg, resources) {
175
+ if (bg.image) return {
176
+ kind: "picture",
177
+ imageResource: resources.put(bg.image.bytes),
178
+ ...bg.imageTiled ? { tiled: true } : {}
179
+ };
180
+ if (bg.gradient) {
181
+ const stops = [{
182
+ offset: 0,
183
+ colorHex: bg.gradient.fromHex
184
+ }, {
185
+ offset: 1,
186
+ colorHex: bg.gradient.toHex
187
+ }];
188
+ return {
189
+ kind: "gradient",
190
+ gradient: bg.gradient.radial ? {
191
+ kind: "radial",
192
+ stops
193
+ } : {
194
+ kind: "linear",
195
+ angle: bg.gradient.angleDeg,
196
+ stops
197
+ }
198
+ };
199
+ }
200
+ return bg.fillColorHex ? {
201
+ kind: "solid",
202
+ colorHex: bg.fillColorHex
203
+ } : void 0;
204
+ }
129
205
  function floatAt(rect) {
130
206
  return {
131
207
  wrap: "none",
@@ -139,7 +215,7 @@ function floatAt(rect) {
139
215
  }
140
216
  };
141
217
  }
142
- function positionedTextShape(rect, paragraphs) {
218
+ function positionedTextShape(rect, paragraphs, centred = false) {
143
219
  return {
144
220
  kind: "shape",
145
221
  shape: {
@@ -154,14 +230,14 @@ function positionedTextShape(rect, paragraphs) {
154
230
  fill: { kind: "none" },
155
231
  text: {
156
232
  content: paragraphs.filter((p) => paragraphText(p).length > 0).map(flowParagraph),
157
- anchor: "t"
233
+ anchor: centred ? "ctr" : "t"
158
234
  },
159
235
  paragraphProperties: {}
160
236
  }
161
237
  };
162
238
  }
163
239
  function positionedImage(rect, image, resources) {
164
- if (!imagePixelSize(image.bytes)) return void 0;
240
+ if (!isMetafile(image.bytes) && !imagePixelSize(image.bytes)) return void 0;
165
241
  return {
166
242
  kind: "image",
167
243
  image: {
@@ -214,9 +290,36 @@ function toCustomGeometry(g) {
214
290
  commands
215
291
  };
216
292
  }
217
- function positionedAutoShape(rect, auto) {
293
+ function positionedAutoShape(rect, auto, resources) {
218
294
  const line = isLineShape(auto.shapeType);
219
- const fill = auto.fillColorHex && !line ? {
295
+ const fill = line ? { kind: "none" } : auto.image ? {
296
+ kind: "picture",
297
+ imageResource: resources.put(auto.image.bytes),
298
+ ...auto.imageTiled ? { tiled: true } : {},
299
+ ...auto.tileSizePt ? { tileSizePt: auto.tileSizePt } : {}
300
+ } : auto.gradient ? {
301
+ kind: "gradient",
302
+ gradient: auto.gradient.radial ? {
303
+ kind: "radial",
304
+ stops: [{
305
+ offset: 0,
306
+ colorHex: auto.gradient.fromHex
307
+ }, {
308
+ offset: 1,
309
+ colorHex: auto.gradient.toHex
310
+ }]
311
+ } : {
312
+ kind: "linear",
313
+ angle: auto.gradient.angleDeg,
314
+ stops: [{
315
+ offset: 0,
316
+ colorHex: auto.gradient.fromHex
317
+ }, {
318
+ offset: 1,
319
+ colorHex: auto.gradient.toHex
320
+ }]
321
+ }
322
+ } : auto.fillColorHex ? {
220
323
  kind: "solid",
221
324
  colorHex: auto.fillColorHex
222
325
  } : { kind: "none" };
@@ -257,6 +360,9 @@ function inFlowImage(image, resources) {
257
360
  }
258
361
  };
259
362
  }
363
+ function isMetafile(d) {
364
+ return isEmf(d) || isWmf(d);
365
+ }
260
366
  function imagePixelSize(d) {
261
367
  if (d.length >= 24 && d[0] === 137 && d[1] === 80 && d[2] === 78 && d[3] === 71) {
262
368
  const w = u32be(d, 16);
@@ -298,6 +404,10 @@ function toRun(r) {
298
404
  ...r.italic ? { italic: true } : {},
299
405
  ...underline ? { underline } : {},
300
406
  ...r.sizePt ? { fontSizePt: pt(r.sizePt) } : {},
407
+ ...r.fontFamily ? { fontFamily: {
408
+ ascii: r.fontFamily,
409
+ hAnsi: r.fontFamily
410
+ } } : {},
301
411
  ...r.colorHex ? { colorHex: r.colorHex } : {}
302
412
  };
303
413
  return {