reamkit 1.22.0 → 1.24.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 (101) hide show
  1. package/dist/esm/core/bmp.d.ts +33 -0
  2. package/dist/esm/core/bmp.js +276 -0
  3. package/dist/esm/core/converter/ream.d.ts +16 -0
  4. package/dist/esm/core/converter/ream.js +59 -9
  5. package/dist/esm/core/document-model/types.d.ts +55 -0
  6. package/dist/esm/core/drawingml/chart-geometry.js +14 -7
  7. package/dist/esm/core/drawingml/colors.d.ts +4 -2
  8. package/dist/esm/core/drawingml/colors.js +15 -0
  9. package/dist/esm/core/drawingml/diagram/colors.d.ts +52 -0
  10. package/dist/esm/core/drawingml/diagram/colors.js +125 -0
  11. package/dist/esm/core/drawingml/diagram/data-model.d.ts +47 -0
  12. package/dist/esm/core/drawingml/diagram/data-model.js +119 -0
  13. package/dist/esm/core/drawingml/diagram/layout-engine.d.ts +46 -0
  14. package/dist/esm/core/drawingml/diagram/layout-engine.js +948 -0
  15. package/dist/esm/core/drawingml/diagram/run.d.ts +37 -0
  16. package/dist/esm/core/drawingml/diagram/run.js +50 -0
  17. package/dist/esm/core/drawingml/diagram/to-drawing.d.ts +16 -0
  18. package/dist/esm/core/drawingml/diagram/to-drawing.js +86 -0
  19. package/dist/esm/core/drawingml/preset-geometry.js +116 -6
  20. package/dist/esm/core/drawingml/text-warp.d.ts +58 -0
  21. package/dist/esm/core/drawingml/text-warp.js +355 -0
  22. package/dist/esm/core/drawingml/theme-parser.d.ts +38 -0
  23. package/dist/esm/core/drawingml/theme-parser.js +73 -4
  24. package/dist/esm/core/font/measure.d.ts +12 -0
  25. package/dist/esm/core/font/measure.js +36 -0
  26. package/dist/esm/core/fonts/families.d.ts +10 -0
  27. package/dist/esm/core/fonts/families.js +44 -0
  28. package/dist/esm/core/fonts/index.d.ts +3 -2
  29. package/dist/esm/core/fonts/remote-fonts.d.ts +50 -0
  30. package/dist/esm/core/fonts/remote-fonts.js +114 -8
  31. package/dist/esm/core/fonts/scripts.d.ts +26 -0
  32. package/dist/esm/core/fonts/scripts.js +177 -0
  33. package/dist/esm/core/images.d.ts +30 -2
  34. package/dist/esm/core/images.js +121 -5
  35. package/dist/esm/core/metafile/blit.d.ts +33 -0
  36. package/dist/esm/core/metafile/blit.js +67 -0
  37. package/dist/esm/core/metafile/dib.d.ts +61 -0
  38. package/dist/esm/core/metafile/dib.js +205 -0
  39. package/dist/esm/core/metafile/emf.js +280 -38
  40. package/dist/esm/core/metafile/picture.d.ts +61 -1
  41. package/dist/esm/core/metafile/picture.js +70 -1
  42. package/dist/esm/core/metafile/wmf.js +202 -13
  43. package/dist/esm/core/ole/escher-blip.d.ts +12 -0
  44. package/dist/esm/core/ole/escher-blip.js +98 -0
  45. package/dist/esm/{pdf-reader → core}/png-encode.js +1 -1
  46. package/dist/esm/excel/print-model.js +5 -3
  47. package/dist/esm/excel/sheet-drawing.js +1 -1
  48. package/dist/esm/excel/sheet-to-flow.js +16 -2
  49. package/dist/esm/excel/styles-parser.js +1 -1
  50. package/dist/esm/excel/xls/biff-reader.js +53 -18
  51. package/dist/esm/excel/xls/biff-styles.d.ts +5 -2
  52. package/dist/esm/excel/xls/biff-styles.js +9 -7
  53. package/dist/esm/excel/xls/escher.js +90 -24
  54. package/dist/esm/html/html-writer.js +2 -1
  55. package/dist/esm/layout/page-doc.d.ts +74 -0
  56. package/dist/esm/layout/page-doc.js +14 -1
  57. package/dist/esm/layout/styled-layout.d.ts +2 -2
  58. package/dist/esm/layout/styled-layout.js +475 -48
  59. package/dist/esm/pdf/shading.js +8 -6
  60. package/dist/esm/pdf/styled-page-emitter.js +77 -5
  61. package/dist/esm/pdf/vector-graphics.js +1 -1
  62. package/dist/esm/pdf-reader/image-decode.js +1 -1
  63. package/dist/esm/pptx/embedded-fonts.d.ts +31 -0
  64. package/dist/esm/pptx/embedded-fonts.js +104 -0
  65. package/dist/esm/pptx/placeholder-cascade.d.ts +73 -6
  66. package/dist/esm/pptx/placeholder-cascade.js +114 -18
  67. package/dist/esm/pptx/ppt/ppt-reader.js +191 -13
  68. package/dist/esm/pptx/ppt/ppt-text.d.ts +107 -1
  69. package/dist/esm/pptx/ppt/ppt-text.js +873 -144
  70. package/dist/esm/pptx/pptx-reader.d.ts +8 -0
  71. package/dist/esm/pptx/pptx-reader.js +117 -15
  72. package/dist/esm/pptx/preset-table-styles.d.ts +9 -0
  73. package/dist/esm/pptx/preset-table-styles.js +27 -0
  74. package/dist/esm/pptx/slide-parser.d.ts +11 -2
  75. package/dist/esm/pptx/slide-parser.js +203 -44
  76. package/dist/esm/pptx/sp-helpers.d.ts +9 -4
  77. package/dist/esm/pptx/sp-helpers.js +48 -9
  78. package/dist/esm/pptx/table-style.d.ts +16 -1
  79. package/dist/esm/pptx/table-style.js +104 -10
  80. package/dist/esm/svg/svg-writer.js +1 -0
  81. package/dist/esm/word/document-parser.d.ts +11 -2
  82. package/dist/esm/word/document-parser.js +10 -7
  83. package/dist/esm/word/docx-reader.js +32 -16
  84. package/dist/esm/word/docx-to-pdf.d.ts +8 -7
  85. package/dist/esm/word/docx-to-pdf.js +25 -7
  86. package/dist/esm/word/docx-writer.js +4 -0
  87. package/dist/esm/word/drawing-parser.d.ts +20 -0
  88. package/dist/esm/word/drawing-parser.js +67 -22
  89. package/dist/esm/word/numbering-parser.d.ts +2 -1
  90. package/dist/esm/word/numbering-parser.js +6 -6
  91. package/dist/esm/word/paragraph-properties.d.ts +2 -1
  92. package/dist/esm/word/paragraph-properties.js +2 -2
  93. package/dist/esm/word/run-properties.d.ts +5 -2
  94. package/dist/esm/word/run-properties.js +13 -8
  95. package/dist/esm/word/styles-parser.d.ts +2 -1
  96. package/dist/esm/word/styles-parser.js +12 -12
  97. package/dist/esm/word/table-parser.js +6 -3
  98. package/dist/esm/word/theme-fonts.d.ts +10 -0
  99. package/dist/esm/word/theme-fonts.js +28 -0
  100. package/package.json +1 -1
  101. /package/dist/esm/{pdf-reader → core}/png-encode.d.ts +0 -0
@@ -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,9 @@
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 { knockOutColor } from "../../core/images.js";
5
+ import { isEmf } from "../../core/metafile/emf.js";
6
+ import { isWmf } from "../../core/metafile/wmf.js";
4
7
  import { EMPTY_STYLE_SHEET, resolveBodyStyles } from "../../core/style-cascade/resolver.js";
5
8
  import "../../core/style-cascade/index.js";
6
9
  import { isCfb, openCfb } from "../../core/ole/cfb.js";
@@ -48,7 +51,7 @@ function readPpt(bytes) {
48
51
  return {
49
52
  doc: {
50
53
  kind: "flow",
51
- body: resolveBodyStyles(buildBody(content, resources), EMPTY_STYLE_SHEET),
54
+ body: resolveBodyStyles(buildBody(content, resources, width, height), EMPTY_STYLE_SHEET),
52
55
  sections: [],
53
56
  section: {
54
57
  pageSize: {
@@ -70,19 +73,23 @@ function readPpt(bytes) {
70
73
  losses: [content.encrypted ? PPT_ENCRYPTED_LOSS : PPT_TEXT_LOSS]
71
74
  };
72
75
  }
73
- function buildBody(content, resources) {
76
+ function buildBody(content, resources, pageW, pageH) {
74
77
  const body = [];
75
78
  content.slides.forEach((slide, slideIndex) => {
76
79
  const inFlowParas = [];
77
80
  const inFlowImages = [];
78
81
  const floats = [];
82
+ if (slide.background) {
83
+ const backdrop = slideBackdrop(slide.background, resources, pageW, pageH);
84
+ if (backdrop) floats.push(backdrop);
85
+ }
79
86
  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));
87
+ if (shape.autoShape) floats.push(positionedAutoShape(shape.rectPt, shape.autoShape, resources));
81
88
  if (shape.image) {
82
89
  const block = positionedImage(shape.rectPt, shape.image, resources);
83
90
  if (block) floats.push(block);
84
91
  }
85
- if (shape.autoShape) floats.push(positionedAutoShape(shape.rectPt, shape.autoShape));
92
+ if (shape.paragraphs?.some((p) => paragraphText(p).length > 0)) floats.push(positionedTextShape(shape.rectPt, shape.paragraphs, shape.wordArt === true));
86
93
  } else {
87
94
  for (const para of shape.paragraphs ?? []) if (paragraphText(para).length > 0) inFlowParas.push(flowParagraph(para));
88
95
  if (shape.image) {
@@ -122,10 +129,78 @@ function flowParagraph(para) {
122
129
  kind: "paragraph",
123
130
  paragraph: {
124
131
  properties: toParaProperties(para, false),
125
- runs: para.runs.map(toRun)
132
+ runs: bulletedRuns(para)
126
133
  }
127
134
  };
128
135
  }
136
+ function bulletedRuns(para) {
137
+ const runs = para.runs.map(toRun);
138
+ if (para.bullet === void 0) return runs;
139
+ const { bold: _b, italic: _i, ...rest } = runs[0]?.properties ?? {};
140
+ const gap = hangs(para) ? " " : " ";
141
+ return [{
142
+ text: `${para.bullet}${gap}`,
143
+ properties: rest
144
+ }, ...runs];
145
+ }
146
+ function slideBackdrop(bg, resources, pageW, pageH) {
147
+ const fill = backdropFill(bg, resources);
148
+ if (!fill) return void 0;
149
+ return {
150
+ kind: "shape",
151
+ shape: {
152
+ float: {
153
+ wrap: "none",
154
+ behind: true,
155
+ posH: {
156
+ relativeFrom: "page",
157
+ offsetPt: pt(0)
158
+ },
159
+ posV: {
160
+ relativeFrom: "page",
161
+ offsetPt: pt(0)
162
+ }
163
+ },
164
+ width: pageW,
165
+ height: pageH,
166
+ geometry: {
167
+ kind: "preset",
168
+ preset: "rect",
169
+ adjust: /* @__PURE__ */ new Map()
170
+ },
171
+ fill,
172
+ paragraphProperties: {}
173
+ }
174
+ };
175
+ }
176
+ function backdropFill(bg, resources) {
177
+ if (bg.image) return {
178
+ kind: "picture",
179
+ imageResource: resources.put(bg.image.bytes),
180
+ ...bg.imageTiled ? {
181
+ tiled: true,
182
+ tileFromCentre: true
183
+ } : {}
184
+ };
185
+ if (bg.gradient) {
186
+ const stops = gradientStops(bg.gradient);
187
+ return {
188
+ kind: "gradient",
189
+ gradient: bg.gradient.radial ? {
190
+ kind: "radial",
191
+ stops
192
+ } : {
193
+ kind: "linear",
194
+ angle: bg.gradient.angleDeg,
195
+ stops
196
+ }
197
+ };
198
+ }
199
+ return bg.fillColorHex ? {
200
+ kind: "solid",
201
+ colorHex: bg.fillColorHex
202
+ } : void 0;
203
+ }
129
204
  function floatAt(rect) {
130
205
  return {
131
206
  wrap: "none",
@@ -139,7 +214,7 @@ function floatAt(rect) {
139
214
  }
140
215
  };
141
216
  }
142
- function positionedTextShape(rect, paragraphs) {
217
+ function positionedTextShape(rect, paragraphs, centred = false) {
143
218
  return {
144
219
  kind: "shape",
145
220
  shape: {
@@ -154,21 +229,22 @@ function positionedTextShape(rect, paragraphs) {
154
229
  fill: { kind: "none" },
155
230
  text: {
156
231
  content: paragraphs.filter((p) => paragraphText(p).length > 0).map(flowParagraph),
157
- anchor: "t"
232
+ anchor: centred ? "ctr" : "t"
158
233
  },
159
234
  paragraphProperties: {}
160
235
  }
161
236
  };
162
237
  }
163
238
  function positionedImage(rect, image, resources) {
164
- if (!imagePixelSize(image.bytes)) return void 0;
239
+ if (!isMetafile(image.bytes) && !imagePixelSize(image.bytes)) return void 0;
165
240
  return {
166
241
  kind: "image",
167
242
  image: {
168
243
  float: floatAt(rect),
169
- resource: resources.put(image.bytes),
244
+ resource: resources.put(drawnBytes(image)),
170
245
  width: pt(rect.w),
171
246
  height: pt(rect.h),
247
+ ...image.crop ? { crop: image.crop } : {},
172
248
  paragraphProperties: {}
173
249
  }
174
250
  };
@@ -214,9 +290,74 @@ function toCustomGeometry(g) {
214
290
  commands
215
291
  };
216
292
  }
217
- function positionedAutoShape(rect, auto) {
293
+ /**
294
+ * §2.3.7.6 `fillFocus` — the stops a shaded fill's two colours make.
295
+ *
296
+ * A focus of zero is the plain ramp everyone expects: the first colour at one
297
+ * end, the second at the other. Any other value means the sweep runs OUT from
298
+ * one of them — a focus of 50 puts the first colour halfway along with the
299
+ * second at BOTH ends, which is what 23884's every slide asks for and why its
300
+ * background read as a flat wash into violet where the reference is blue
301
+ * across the middle and dark only at the corners.
302
+ *
303
+ * @param g The gradient as the `.ppt` states it.
304
+ * @returns The stops, in order.
305
+ */
306
+ function gradientStops(g) {
307
+ const focus = g.focusPct ?? 0;
308
+ if (focus === 0) return [{
309
+ offset: 0,
310
+ colorHex: g.fromHex
311
+ }, {
312
+ offset: 1,
313
+ colorHex: g.toHex
314
+ }];
315
+ if (Math.abs(focus) >= 100) return [{
316
+ offset: 0,
317
+ colorHex: g.toHex
318
+ }, {
319
+ offset: 1,
320
+ colorHex: g.fromHex
321
+ }];
322
+ const peak = focus > 0 ? g.fromHex : g.toHex;
323
+ const ends = focus > 0 ? g.toHex : g.fromHex;
324
+ const at = focus > 0 ? focus / 100 : 1 + focus / 100;
325
+ return [
326
+ {
327
+ offset: 0,
328
+ colorHex: ends
329
+ },
330
+ {
331
+ offset: at,
332
+ colorHex: peak
333
+ },
334
+ {
335
+ offset: 1,
336
+ colorHex: ends
337
+ }
338
+ ];
339
+ }
340
+ function positionedAutoShape(rect, auto, resources) {
218
341
  const line = isLineShape(auto.shapeType);
219
- const fill = auto.fillColorHex && !line ? {
342
+ const fill = line ? { kind: "none" } : auto.image ? {
343
+ kind: "picture",
344
+ imageResource: resources.put(auto.image.bytes),
345
+ ...auto.imageTiled ? {
346
+ tiled: true,
347
+ tileFromCentre: true
348
+ } : {},
349
+ ...auto.tileSizePt ? { tileSizePt: auto.tileSizePt } : {}
350
+ } : auto.gradient ? {
351
+ kind: "gradient",
352
+ gradient: auto.gradient.radial ? {
353
+ kind: "radial",
354
+ stops: gradientStops(auto.gradient)
355
+ } : {
356
+ kind: "linear",
357
+ angle: auto.gradient.angleDeg,
358
+ stops: gradientStops(auto.gradient)
359
+ }
360
+ } : auto.fillColorHex ? {
220
361
  kind: "solid",
221
362
  colorHex: auto.fillColorHex
222
363
  } : { kind: "none" };
@@ -250,13 +391,33 @@ function inFlowImage(image, resources) {
250
391
  return {
251
392
  kind: "image",
252
393
  image: {
253
- resource: resources.put(image.bytes),
394
+ resource: resources.put(drawnBytes(image)),
254
395
  width: pt(size.w * .75),
255
396
  height: pt(size.h * .75),
397
+ ...image.crop ? { crop: image.crop } : {},
256
398
  paragraphProperties: {}
257
399
  }
258
400
  };
259
401
  }
402
+ /**
403
+ * The bytes a picture is actually DRAWN from.
404
+ *
405
+ * §2.3.23 `pictureTransparent` names a colour the picture is drawn without, and
406
+ * a raster carries no alpha channel to say so — so the knock-out is baked into
407
+ * bytes of its own. The resource store hashes content, so two shapes knocking
408
+ * different colours out of the same blip stay two resources by themselves, and
409
+ * one that knocks out nothing keeps the bytes it arrived with.
410
+ *
411
+ * @param image The picture as the shape states it.
412
+ * @returns The bytes to store, knocked out where the shape asks for it.
413
+ */
414
+ function drawnBytes(image) {
415
+ if (image.transparentHex === void 0 || isMetafile(image.bytes)) return image.bytes;
416
+ return knockOutColor(image.bytes, image.transparentHex) ?? image.bytes;
417
+ }
418
+ function isMetafile(d) {
419
+ return isEmf(d) || isWmf(d);
420
+ }
260
421
  function imagePixelSize(d) {
261
422
  if (d.length >= 24 && d[0] === 137 && d[1] === 80 && d[2] === 78 && d[3] === 71) {
262
423
  const w = u32be(d, 16);
@@ -298,6 +459,10 @@ function toRun(r) {
298
459
  ...r.italic ? { italic: true } : {},
299
460
  ...underline ? { underline } : {},
300
461
  ...r.sizePt ? { fontSizePt: pt(r.sizePt) } : {},
462
+ ...r.fontFamily ? { fontFamily: {
463
+ ascii: r.fontFamily,
464
+ hAnsi: r.fontFamily
465
+ } } : {},
301
466
  ...r.colorHex ? { colorHex: r.colorHex } : {}
302
467
  };
303
468
  return {
@@ -308,12 +473,25 @@ function toRun(r) {
308
473
  function toParaProperties(p, breakBefore) {
309
474
  const alignment = p.align !== void 0 ? alignmentFrom(p.align) : void 0;
310
475
  const level = p.level ?? 0;
476
+ const leftPt = p.leftMarginPt ?? (level > 0 ? level * LEVEL_INDENT_PT : void 0);
477
+ const firstPt = p.indentPt !== void 0 ? p.indentPt - (leftPt ?? 0) : void 0;
311
478
  return {
312
479
  ...breakBefore ? { pageBreakBefore: true } : {},
313
480
  ...alignment ? { alignment } : {},
314
- ...level > 0 ? { indentLeft: pt(level * LEVEL_INDENT_PT) } : {}
481
+ ...leftPt !== void 0 ? { indentLeft: pt(leftPt) } : {},
482
+ ...firstPt !== void 0 && firstPt !== 0 ? { indentFirstLine: pt(firstPt) } : {},
483
+ ...p.spaceBeforePt !== void 0 ? { spacingBefore: pt(p.spaceBeforePt) } : {},
484
+ ...p.spaceAfterPt !== void 0 ? { spacingAfter: pt(p.spaceAfterPt) } : {},
485
+ ...p.lineSpacing !== void 0 && p.lineSpacing > 0 ? {
486
+ spacingLineRule: "auto",
487
+ spacingLine: pt(p.lineSpacing / 100 * 12)
488
+ } : {}
315
489
  };
316
490
  }
491
+ /** Whether the paragraph's first line starts LEFT of its body — a hanging bullet. */
492
+ function hangs(p) {
493
+ return p.indentPt !== void 0 && p.leftMarginPt !== void 0 && p.indentPt < p.leftMarginPt;
494
+ }
317
495
  function alignmentFrom(a) {
318
496
  switch (a) {
319
497
  case 1: return "center";
@@ -1,6 +1,8 @@
1
1
  /** A run of uniformly-formatted text within a paragraph (PPT-2). */
2
2
  export interface PptRun {
3
3
  readonly text: string;
4
+ /** The typeface the run names, resolved through the deck's font collection. */
5
+ readonly fontFamily?: string;
4
6
  readonly bold?: boolean;
5
7
  readonly italic?: boolean;
6
8
  readonly underline?: boolean;
@@ -20,6 +22,24 @@ export interface PptParagraph {
20
22
  readonly align?: number;
21
23
  /** The 0-based outline/indent level. */
22
24
  readonly level?: number;
25
+ /**
26
+ * §2.9.20 — the bullet character this paragraph is marked with, already
27
+ * translated out of whatever symbol font stated it. Absent when the paragraph
28
+ * carries none.
29
+ */
30
+ readonly bullet?: string;
31
+ /**
32
+ * §2.9.20 `leftMargin` — where the paragraph's BODY sits, in points from the
33
+ * text box's own left edge.
34
+ */
35
+ readonly leftMarginPt?: number;
36
+ /** §2.9.20 `indent` — where its FIRST line starts. Left of the body ⇒ hanging. */
37
+ readonly indentPt?: number;
38
+ /** §2.9.32 `lineSpacing`, as a PERCENTAGE of the line's height — 100 is single. */
39
+ readonly lineSpacing?: number;
40
+ /** §2.9.32 `spaceBefore` / `spaceAfter`, in points. */
41
+ readonly spaceBeforePt?: number;
42
+ readonly spaceAfterPt?: number;
23
43
  }
24
44
  /**
25
45
  * An embedded picture referenced by a slide shape — the raw image bytes pulled
@@ -27,6 +47,22 @@ export interface PptParagraph {
27
47
  */
28
48
  export interface PptImage {
29
49
  readonly bytes: Uint8Array;
50
+ /**
51
+ * §2.3.23 `cropFrom*` — the fraction of each edge of the SOURCE cut away
52
+ * before it is fitted to the shape's box. Absent ⇒ the whole picture.
53
+ */
54
+ readonly crop?: {
55
+ readonly left: number;
56
+ readonly top: number;
57
+ readonly right: number;
58
+ readonly bottom: number;
59
+ };
60
+ /**
61
+ * §2.3.23 `pictureTransparent` — the colour the picture is drawn WITHOUT,
62
+ * 6-hex and no leading `#`. Clip art of this age states its ground here
63
+ * rather than carrying an alpha channel.
64
+ */
65
+ readonly transparentHex?: string;
30
66
  }
31
67
  /** A shape's rectangle on the slide, in points (from the OfficeArtClientAnchor). */
32
68
  export interface PptRect {
@@ -80,6 +116,41 @@ export interface PptAutoShape {
80
116
  readonly fillColorHex?: string;
81
117
  /** The resolved line colour as 6-hex RGB, when present. */
82
118
  readonly lineColorHex?: string;
119
+ /**
120
+ * MS-ODRAW §2.3.7.1 — the two-colour sweep a `fillType` of 4..8 asks for.
121
+ * `fillColor` is one end and `fillBackColor` the other; `fillAngle` is where
122
+ * it runs. Read as its fillColor alone, 41246-2's title slide came out a flat
123
+ * teal where every reader fades it to white.
124
+ */
125
+ readonly gradient?: {
126
+ readonly fromHex: string;
127
+ readonly toHex: string;
128
+ readonly angleDeg: number;
129
+ readonly radial: boolean;
130
+ /**
131
+ * §2.3.7.6 `fillFocus` — where along the sweep the FIRST colour peaks, as a
132
+ * percentage. Zero is a plain ramp; 50 puts it in the middle with the
133
+ * second colour at both ends, which is a different picture entirely.
134
+ */
135
+ readonly focusPct?: number;
136
+ };
137
+ /**
138
+ * §2.3.7.1 MSOFILLTYPE 3 — a picture stretched over the shape, its blip named
139
+ * by `fillBlip`. A `.ppt` table is a group of cell rectangles, and a cell with
140
+ * a picture background states it exactly this way.
141
+ */
142
+ readonly image?: PptImage;
143
+ /** §2.3.7.1 MSOFILLTYPE 2 — the picture REPEATS at its own size, not stretched. */
144
+ readonly imageTiled?: boolean;
145
+ /**
146
+ * §2.3.7.11/.12 `fillWidth` / `fillHeight` — the size one copy occupies, in
147
+ * points, when the shape states it rather than leaving the tile at the
148
+ * picture's own size.
149
+ */
150
+ readonly tileSizePt?: {
151
+ readonly widthPt: number;
152
+ readonly heightPt: number;
153
+ };
83
154
  readonly geometry?: PptCustomGeometry;
84
155
  }
85
156
  /**
@@ -89,13 +160,48 @@ export interface PptAutoShape {
89
160
  */
90
161
  export interface PptShape {
91
162
  readonly rectPt?: PptRect;
163
+ /**
164
+ * §2.3.22 — the shape IS a piece of WordArt: its text is the shape, centred
165
+ * in its box rather than flowed from the top. Drawn as an autoshape it was a
166
+ * coloured rectangle where the reference has a word.
167
+ */
168
+ readonly wordArt?: boolean;
169
+ /**
170
+ * Whether the shape is a placeholder (§2.9.24). On a MASTER that means it is
171
+ * a prototype, not decoration: drawn on the slides that follow it, every one
172
+ * of them would carry "Click to edit Master title style".
173
+ */
174
+ readonly placeholder?: boolean;
175
+ /**
176
+ * §2.9.51 `OEPlaceholderAtom.placeholderId` — WHICH placeholder it is. The
177
+ * three a master keeps for the slide's furniture — 7 the date, 8 the slide
178
+ * number, 9 the footer — are the only ones whose text does not live in the
179
+ * shape at all (§2.4.15).
180
+ */
181
+ readonly placeholderId?: number;
92
182
  readonly paragraphs?: ReadonlyArray<PptParagraph>;
93
183
  readonly image?: PptImage;
94
184
  readonly autoShape?: PptAutoShape;
95
185
  }
96
- /** One slide: its shapes in document order. */
186
+ /**
187
+ * A slide's background (PPT-12) — the fill on the shape MS-ODRAW marks with
188
+ * `fBackground`. A `.ppt` states it as a shape like any other; drawn as one it
189
+ * would be a rectangle among the content, and dropped it leaves a black slide
190
+ * white.
191
+ */
192
+ export interface PptBackground {
193
+ /** A solid fill's colour, as 6-hex RGB. */
194
+ readonly fillColorHex?: string;
195
+ /** A shaded fill, in the same terms an autoshape's is (PPT-9). */
196
+ readonly gradient?: PptAutoShape['gradient'];
197
+ /** A picture fill, stretched over the slide (or tiled across it). */
198
+ readonly image?: PptImage;
199
+ readonly imageTiled?: boolean;
200
+ }
201
+ /** One slide: its shapes in document order, over its background. */
97
202
  export interface PptSlide {
98
203
  readonly shapes: ReadonlyArray<PptShape>;
204
+ readonly background?: PptBackground;
99
205
  }
100
206
  /** The extracted content of a `.ppt`: its slides, deck page size and encryption flag. */
101
207
  export interface PptContent {