reamkit 1.21.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 (90) hide show
  1. package/dist/esm/core/bytes.d.ts +22 -4
  2. package/dist/esm/core/bytes.js +73 -5
  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 +78 -0
  6. package/dist/esm/core/drawingml/chart-geometry.js +11 -5
  7. package/dist/esm/core/drawingml/chart-parser.d.ts +2 -1
  8. package/dist/esm/core/drawingml/chart-parser.js +23 -1
  9. package/dist/esm/core/drawingml/colors.d.ts +30 -4
  10. package/dist/esm/core/drawingml/colors.js +45 -6
  11. package/dist/esm/core/drawingml/preset-geometry.js +236 -0
  12. package/dist/esm/core/drawingml/theme-parser.d.ts +38 -0
  13. package/dist/esm/core/drawingml/theme-parser.js +64 -1
  14. package/dist/esm/core/fonts/families.d.ts +10 -0
  15. package/dist/esm/core/fonts/families.js +44 -0
  16. package/dist/esm/core/fonts/index.d.ts +3 -2
  17. package/dist/esm/core/fonts/remote-fonts.d.ts +50 -0
  18. package/dist/esm/core/fonts/remote-fonts.js +114 -8
  19. package/dist/esm/core/fonts/scripts.d.ts +26 -0
  20. package/dist/esm/core/fonts/scripts.js +177 -0
  21. package/dist/esm/core/metafile/blit.d.ts +33 -0
  22. package/dist/esm/core/metafile/blit.js +67 -0
  23. package/dist/esm/core/metafile/dib.d.ts +61 -0
  24. package/dist/esm/core/metafile/dib.js +205 -0
  25. package/dist/esm/core/metafile/emf.js +139 -24
  26. package/dist/esm/core/metafile/picture.d.ts +12 -1
  27. package/dist/esm/core/metafile/symbol-fonts.d.ts +42 -0
  28. package/dist/esm/core/metafile/symbol-fonts.js +154 -0
  29. package/dist/esm/core/metafile/wmf.js +183 -10
  30. package/dist/esm/core/ole/escher-blip.d.ts +12 -0
  31. package/dist/esm/core/ole/escher-blip.js +88 -0
  32. package/dist/esm/{pdf-reader → core}/png-encode.js +1 -1
  33. package/dist/esm/core/vector.d.ts +5 -0
  34. package/dist/esm/excel/print-model.js +3 -1
  35. package/dist/esm/excel/sheet-shape-parser.js +1 -12
  36. package/dist/esm/excel/styles-parser.js +1 -1
  37. package/dist/esm/excel/xls/biff-reader.js +53 -18
  38. package/dist/esm/excel/xls/biff-styles.d.ts +5 -2
  39. package/dist/esm/excel/xls/biff-styles.js +9 -7
  40. package/dist/esm/excel/xls/escher.js +90 -24
  41. package/dist/esm/excel/xlsx-reader.d.ts +1 -1
  42. package/dist/esm/excel/xlsx-reader.js +3 -3
  43. package/dist/esm/layout/page-doc.d.ts +77 -0
  44. package/dist/esm/layout/page-doc.js +34 -18
  45. package/dist/esm/layout/styled-layout.d.ts +2 -2
  46. package/dist/esm/layout/styled-layout.js +467 -76
  47. package/dist/esm/pdf/shading.d.ts +19 -0
  48. package/dist/esm/pdf/shading.js +84 -11
  49. package/dist/esm/pdf/styled-page-emitter.js +647 -444
  50. package/dist/esm/pdf-reader/image-decode.js +1 -1
  51. package/dist/esm/pptx/embedded-fonts.d.ts +31 -0
  52. package/dist/esm/pptx/embedded-fonts.js +104 -0
  53. package/dist/esm/pptx/ole-preview.d.ts +10 -0
  54. package/dist/esm/pptx/ole-preview.js +52 -0
  55. package/dist/esm/pptx/placeholder-cascade.d.ts +136 -5
  56. package/dist/esm/pptx/placeholder-cascade.js +260 -27
  57. package/dist/esm/pptx/ppt/ppt-reader.js +120 -10
  58. package/dist/esm/pptx/ppt/ppt-text.d.ts +66 -1
  59. package/dist/esm/pptx/ppt/ppt-text.js +576 -137
  60. package/dist/esm/pptx/pptx-reader.d.ts +8 -0
  61. package/dist/esm/pptx/pptx-reader.js +271 -41
  62. package/dist/esm/pptx/preset-table-styles.d.ts +9 -0
  63. package/dist/esm/pptx/preset-table-styles.js +27 -0
  64. package/dist/esm/pptx/slide-parser.d.ts +78 -10
  65. package/dist/esm/pptx/slide-parser.js +396 -95
  66. package/dist/esm/pptx/sp-helpers.d.ts +2 -1
  67. package/dist/esm/pptx/sp-helpers.js +22 -6
  68. package/dist/esm/pptx/table-style.d.ts +64 -0
  69. package/dist/esm/pptx/table-style.js +203 -0
  70. package/dist/esm/svg/svg-writer.js +53 -5
  71. package/dist/esm/word/document-parser.d.ts +7 -1
  72. package/dist/esm/word/document-parser.js +6 -6
  73. package/dist/esm/word/docx-reader.js +8 -5
  74. package/dist/esm/word/docx-to-pdf.d.ts +8 -7
  75. package/dist/esm/word/docx-to-pdf.js +25 -7
  76. package/dist/esm/word/drawing-parser.d.ts +34 -0
  77. package/dist/esm/word/drawing-parser.js +173 -15
  78. package/dist/esm/word/numbering-parser.d.ts +2 -1
  79. package/dist/esm/word/numbering-parser.js +6 -6
  80. package/dist/esm/word/paragraph-properties.d.ts +2 -1
  81. package/dist/esm/word/paragraph-properties.js +2 -2
  82. package/dist/esm/word/run-properties.d.ts +5 -2
  83. package/dist/esm/word/run-properties.js +13 -8
  84. package/dist/esm/word/styles-parser.d.ts +2 -1
  85. package/dist/esm/word/styles-parser.js +12 -12
  86. package/dist/esm/word/table-parser.js +3 -3
  87. package/dist/esm/word/theme-fonts.d.ts +10 -0
  88. package/dist/esm/word/theme-fonts.js +28 -0
  89. package/package.json +1 -1
  90. /package/dist/esm/{pdf-reader → core}/png-encode.d.ts +0 -0
@@ -117,6 +117,242 @@ function presetPaths(preset, w, h, adjust) {
117
117
  case "leftArrow": return [blockArrow(w, h, adjust, "left")];
118
118
  case "upArrow": return [blockArrow(w, h, adjust, "up")];
119
119
  case "downArrow": return [blockArrow(w, h, adjust, "down")];
120
+ case "leftRightArrow": {
121
+ const ss = Math.min(w, h);
122
+ const t = clamp(frac(adjust, "adj1", 5e4), 0, 1) * ss;
123
+ const hl = clamp(frac(adjust, "adj2", 5e4), 0, 1) * ss;
124
+ const [sb, st] = [(h - t) / 2, (h + t) / 2];
125
+ return [polygon([
126
+ [0, h / 2],
127
+ [hl, h],
128
+ [hl, st],
129
+ [w - hl, st],
130
+ [w - hl, h],
131
+ [w, h / 2],
132
+ [w - hl, 0],
133
+ [w - hl, sb],
134
+ [hl, sb],
135
+ [hl, 0]
136
+ ])];
137
+ }
138
+ case "notchedRightArrow": {
139
+ const ss = Math.min(w, h);
140
+ const t = clamp(frac(adjust, "adj1", 5e4), 0, 1) * ss;
141
+ const hl = clamp(frac(adjust, "adj2", 5e4), 0, 1) * ss;
142
+ const [sb, st] = [(h - t) / 2, (h + t) / 2];
143
+ const notch = Math.min(hl, w / 2);
144
+ return [polygon([
145
+ [0, sb],
146
+ [w - hl, sb],
147
+ [w - hl, 0],
148
+ [w, h / 2],
149
+ [w - hl, h],
150
+ [w - hl, st],
151
+ [0, st],
152
+ [notch, h / 2]
153
+ ])];
154
+ }
155
+ case "stripedRightArrow": {
156
+ const ss = Math.min(w, h);
157
+ const t = clamp(frac(adjust, "adj1", 5e4), 0, 1) * ss;
158
+ const hl = clamp(frac(adjust, "adj2", 5e4), 0, 1) * ss;
159
+ const [sb, st] = [(h - t) / 2, (h + t) / 2];
160
+ const unit = ss / 12;
161
+ return [
162
+ polygon([
163
+ [0, sb],
164
+ [unit, sb],
165
+ [unit, st],
166
+ [0, st]
167
+ ]),
168
+ polygon([
169
+ [unit * 2, sb],
170
+ [unit * 4, sb],
171
+ [unit * 4, st],
172
+ [unit * 2, st]
173
+ ]),
174
+ polygon([
175
+ [unit * 5, sb],
176
+ [w - hl, sb],
177
+ [w - hl, 0],
178
+ [w, h / 2],
179
+ [w - hl, h],
180
+ [w - hl, st],
181
+ [unit * 5, st]
182
+ ])
183
+ ];
184
+ }
185
+ case "bentUpArrow": {
186
+ const ss = Math.min(w, h);
187
+ const t = clamp(frac(adjust, "adj2", 25e3), 0, 1) * ss;
188
+ const hw = clamp(frac(adjust, "adj3", 25e3), 0, 1) * ss * 2;
189
+ const hh = Math.min(hw, h);
190
+ const cx = w - hw / 2;
191
+ return [polygon([
192
+ [0, 0],
193
+ [cx + t / 2, 0],
194
+ [cx + t / 2, h - hh],
195
+ [w, h - hh],
196
+ [cx, h],
197
+ [cx - hw / 2, h - hh],
198
+ [cx - t / 2, h - hh],
199
+ [cx - t / 2, t],
200
+ [0, t]
201
+ ])];
202
+ }
203
+ case "chevron":
204
+ case "homePlate": {
205
+ const ss = Math.min(w, h);
206
+ const x = Math.min(clamp(frac(adjust, "adj", 5e4), 0, 1) * ss, w);
207
+ const points = [
208
+ [0, 0],
209
+ [w - x, 0],
210
+ [w, h / 2],
211
+ [w - x, h],
212
+ [0, h]
213
+ ];
214
+ if (preset === "chevron") points.push([x, h / 2]);
215
+ return [polygon(points)];
216
+ }
217
+ case "corner": {
218
+ const ss = Math.min(w, h);
219
+ const th = clamp(frac(adjust, "adj1", 5e4), 0, 1) * ss;
220
+ const tw = clamp(frac(adjust, "adj2", 5e4), 0, 1) * ss;
221
+ return [polygon([
222
+ [0, 0],
223
+ [w, 0],
224
+ [w, th],
225
+ [tw, th],
226
+ [tw, h],
227
+ [0, h]
228
+ ])];
229
+ }
230
+ case "pie":
231
+ case "blockArc": {
232
+ const [rx, ry] = [w / 2, h / 2];
233
+ const start = -((adjust.get("adj1") ?? 0) / 6e4) * (Math.PI / 180);
234
+ let sweep = -((adjust.get("adj2") ?? (preset === "pie" ? 162e5 : 108e5)) / 6e4) * (Math.PI / 180) - start;
235
+ if (sweep > 0) sweep -= 2 * Math.PI;
236
+ const at = (a, r) => [rx + rx * r * Math.cos(a), ry + ry * r * Math.sin(a)];
237
+ const b = new PathBuilder();
238
+ const [sx0, sy0] = at(start, 1);
239
+ b.moveTo(sx0, sy0).append(arcToBeziers(rx, ry, rx, ry, start, sweep));
240
+ if (preset === "pie") return [b.lineTo(rx, ry).close().build()];
241
+ const inner = 1 - clamp(frac(adjust, "adj3", 25e3), 0, 1);
242
+ const [ix, iy] = at(start + sweep, inner);
243
+ return [b.lineTo(ix, iy).append(arcToBeziers(rx, ry, rx * inner, ry * inner, start + sweep, -sweep)).close().build()];
244
+ }
245
+ case "gear6":
246
+ case "gear9": {
247
+ const teeth = preset === "gear6" ? 6 : 9;
248
+ const ss = Math.min(w, h);
249
+ const depth = clamp(frac(adjust, "adj1", 15e3), 0, .4) * ss;
250
+ const [cx, cy] = [w / 2, h / 2];
251
+ const outer = Math.min(w, h) / 2;
252
+ const root = outer - depth;
253
+ const half = Math.PI / teeth / 2.6;
254
+ const pts = [];
255
+ for (let i = 0; i < teeth; i++) {
256
+ const a = i * 2 * Math.PI / teeth;
257
+ const gap = Math.PI / teeth;
258
+ pts.push([cx + root * Math.cos(a - gap + half), cy + root * Math.sin(a - gap + half)]);
259
+ pts.push([cx + outer * Math.cos(a - half), cy + outer * Math.sin(a - half)]);
260
+ pts.push([cx + outer * Math.cos(a + half), cy + outer * Math.sin(a + half)]);
261
+ pts.push([cx + root * Math.cos(a + gap - half), cy + root * Math.sin(a + gap - half)]);
262
+ }
263
+ return [polygon(pts)];
264
+ }
265
+ case "circularArrow":
266
+ case "leftCircularArrow": {
267
+ const ss = Math.min(w, h);
268
+ const t = clamp(frac(adjust, "adj1", 12500), .02, .45) * ss;
269
+ const [rx, ry] = [Math.max(t, w / 2 - t), Math.max(t, h / 2 - t)];
270
+ const [cx, cy] = [w / 2, h / 2];
271
+ const deg = (v) => -(v / 6e4) * (Math.PI / 180);
272
+ const tail = deg(adjust.get("adj3") ?? (preset === "circularArrow" ? 20457681 : 125e5));
273
+ let sweep = deg(adjust.get("adj2") ?? (preset === "circularArrow" ? 1142319 : 1142319)) - tail;
274
+ const turn = preset === "circularArrow" ? -1 : 1;
275
+ while (sweep * turn < 0) sweep += turn * 2 * Math.PI;
276
+ while (Math.abs(sweep) > 2 * Math.PI) sweep -= turn * 2 * Math.PI;
277
+ const headSweep = turn * Math.min(Math.abs(sweep) / 3, .35);
278
+ const bandEnd = tail + sweep - headSweep;
279
+ const at = (a, r) => [clamp(cx + (rx + r) * Math.cos(a), 0, w), clamp(cy + (ry + r) * Math.sin(a), 0, h)];
280
+ const b = new PathBuilder();
281
+ const [ox, oy] = at(tail, t / 2);
282
+ b.moveTo(ox, oy).append(arcToBeziers(cx, cy, rx + t / 2, ry + t / 2, tail, bandEnd - tail).map((seg) => seg.op === "cubic" ? {
283
+ op: "cubic",
284
+ x1: clamp(seg.x1, 0, w),
285
+ y1: clamp(seg.y1, 0, h),
286
+ x2: clamp(seg.x2, 0, w),
287
+ y2: clamp(seg.y2, 0, h),
288
+ x: clamp(seg.x, 0, w),
289
+ y: clamp(seg.y, 0, h)
290
+ } : seg));
291
+ const [hx1, hy1] = at(bandEnd, t);
292
+ const [tipX, tipY] = at(tail + sweep, 0);
293
+ const [hx2, hy2] = at(bandEnd, -t);
294
+ const [ix, iy] = at(bandEnd, -t / 2);
295
+ return [b.lineTo(hx1, hy1).lineTo(tipX, tipY).lineTo(hx2, hy2).lineTo(ix, iy).append(arcToBeziers(cx, cy, rx - t / 2, ry - t / 2, bandEnd, tail - bandEnd)).close().build()];
296
+ }
297
+ case "actionButtonBlank": return [{ segments: roundRectSegments(w, h, Math.min(w, h) * .1) }];
298
+ case "flowChartMerge": return [polygon([
299
+ [0, h],
300
+ [w, h],
301
+ [w / 2, 0]
302
+ ])];
303
+ case "flowChartExtract": return [polygon([
304
+ [0, 0],
305
+ [w, 0],
306
+ [w / 2, h]
307
+ ])];
308
+ case "flowChartDelay": return [new PathBuilder().moveTo(0, 0).lineTo(w / 2, 0).append(arcToBeziers(w / 2, h / 2, w / 2, h / 2, -Math.PI / 2, Math.PI)).lineTo(0, h).close().build()];
309
+ case "flowChartMagneticDisk": {
310
+ const ry = h / 6;
311
+ return [new PathBuilder().moveTo(0, h - ry).append(arcToBeziers(w / 2, h - ry, w / 2, ry, Math.PI, -2 * Math.PI)).moveTo(0, h - ry).lineTo(0, ry).append(arcToBeziers(w / 2, ry, w / 2, ry, Math.PI, -Math.PI)).lineTo(w, h - ry).build()];
312
+ }
313
+ case "flowChartMagneticTape": {
314
+ const foot = h / 8;
315
+ return [{ segments: ellipseSegments(w, h) }, polygon([
316
+ [w / 2, 0],
317
+ [w, 0],
318
+ [w, foot],
319
+ [w / 2, foot]
320
+ ])];
321
+ }
322
+ case "flowChartPunchedTape": {
323
+ const wave = h / 5;
324
+ return [new PathBuilder().moveTo(0, wave).cubicTo(w / 4, wave * 2, w * 3 / 4, 0, w, wave).lineTo(w, h - wave).cubicTo(w * 3 / 4, h - wave * 2, w / 4, h, 0, h - wave).close().build()];
325
+ }
326
+ case "wedgeRoundRectCallout": {
327
+ const r = clamp(frac(adjust, "adj3", 16667), 0, .5) * Math.min(w, h);
328
+ const tipX = w / 2 + frac(adjust, "adj1", -20833) * w;
329
+ const tipY = h / 2 - frac(adjust, "adj2", 62500) * h;
330
+ const [cx, cy] = [w / 2, h / 2];
331
+ const len = Math.hypot(tipX - cx, tipY - cy) || 1;
332
+ const [ux, uy] = [(tipX - cx) / len, (tipY - cy) / len];
333
+ const base = Math.min(w, h) / 6;
334
+ return [{ segments: roundRectSegments(w, h, r) }, polygon([
335
+ [cx - uy * base, cy + ux * base],
336
+ [cx + uy * base, cy - ux * base],
337
+ [tipX, tipY]
338
+ ])];
339
+ }
340
+ case "leftBracket":
341
+ case "rightBracket":
342
+ case "leftBrace":
343
+ case "rightBrace": {
344
+ const brace = preset === "leftBrace" || preset === "rightBrace";
345
+ const r = Math.min(clamp(frac(adjust, "adj1", 8333), 0, .5) * h, brace ? w / 2 : w);
346
+ const mirrored = preset === "rightBracket" || preset === "rightBrace";
347
+ const x = (v) => mirrored ? w - v : v;
348
+ const spine = brace ? r : 0;
349
+ const b = new PathBuilder().moveTo(x(w), 0).lineTo(x(spine + r), 0).lineTo(x(spine), r);
350
+ if (brace) {
351
+ const mid = clamp(frac(adjust, "adj2", 5e4), 0, 1) * h;
352
+ b.lineTo(x(spine), Math.max(r, mid - r)).lineTo(x(0), mid).lineTo(x(spine), Math.min(h - r, mid + r));
353
+ }
354
+ return [b.lineTo(x(spine), h - r).lineTo(x(spine + r), h).lineTo(x(w), h).build()];
355
+ }
120
356
  default: return null;
121
357
  }
122
358
  }
@@ -1,4 +1,42 @@
1
1
  import { PoNode } from '../po-helpers.js';
2
+ /**
3
+ * §20.1.4.1.16 `a:fontScheme` — the two typefaces a theme names, each in three
4
+ * scripts. A document does not repeat them: it refers to them by TOKEN
5
+ * (`+mj-lt`, `+mn-ea`, …), which every reader is expected to resolve.
6
+ */
7
+ export interface ThemeFonts {
8
+ /** `a:majorFont` — headings. */
9
+ readonly major: ThemeFontSlots;
10
+ /** `a:minorFont` — body text. */
11
+ readonly minor: ThemeFontSlots;
12
+ }
13
+ /** One font of a scheme, in the three scripts a run may pick from. */
14
+ export interface ThemeFontSlots {
15
+ readonly latin?: string;
16
+ readonly ea?: string;
17
+ readonly cs?: string;
18
+ }
19
+ /**
20
+ * Parse a theme's font scheme (§20.1.4.1.16).
21
+ *
22
+ * @param themeXml The raw theme part bytes, UTF-8.
23
+ * @returns The two fonts; slots the theme leaves empty are absent.
24
+ */
25
+ export declare function parseThemeFonts(themeXml: Uint8Array): ThemeFonts;
26
+ /**
27
+ * Resolve a typeface written as a theme TOKEN to the name it stands for.
28
+ *
29
+ * A slide states its fonts as `+mn-lt` far more often than by name, and left
30
+ * unresolved that string travels into the model as if it WERE a typeface: no
31
+ * substitution table knows it, so a deck whose theme is Times came out in a
32
+ * grotesque (45541_Header).
33
+ *
34
+ * @param typeface The `@typeface` as the file writes it.
35
+ * @param fonts The theme's font scheme, when the part was read.
36
+ * @returns The resolved name; the input unchanged when it is not a token, and
37
+ * `undefined` when it is a token the theme leaves empty.
38
+ */
39
+ export declare function resolveThemeFont(typeface: string | undefined, fonts: ThemeFonts | undefined): string | undefined;
2
40
  /**
3
41
  * Parse a DrawingML theme part (ECMA-376 §20.1.6.2, `a:clrScheme`) into a
4
42
  * `name → hex` colour map. Reads the twelve scheme slots — `dk1`/`lt1`/`dk2`/
@@ -26,6 +26,69 @@ var SCHEME_SLOTS = new Set([
26
26
  "folHlink"
27
27
  ]);
28
28
  /**
29
+ * Parse a theme's font scheme (§20.1.4.1.16).
30
+ *
31
+ * @param themeXml The raw theme part bytes, UTF-8.
32
+ * @returns The two fonts; slots the theme leaves empty are absent.
33
+ */
34
+ function parseThemeFonts(themeXml) {
35
+ const scheme = poFindByPath(parser.parse(decoder.decode(themeXml)), [
36
+ "a:theme",
37
+ "a:themeElements",
38
+ "a:fontScheme"
39
+ ]);
40
+ const read = (tag) => {
41
+ const font = scheme ? poChildren(scheme).find((c) => poIs(c, tag)) : void 0;
42
+ const slot = (name) => {
43
+ const node = font ? poChildren(font).find((c) => poIs(c, name)) : void 0;
44
+ const face = node ? poAttr(node, "typeface")?.trim() : void 0;
45
+ return face === void 0 || face === "" ? void 0 : face;
46
+ };
47
+ const [latin, ea, cs] = [
48
+ slot("a:latin"),
49
+ slot("a:ea"),
50
+ slot("a:cs")
51
+ ];
52
+ return {
53
+ ...latin !== void 0 ? { latin } : {},
54
+ ...ea !== void 0 ? { ea } : {},
55
+ ...cs !== void 0 ? { cs } : {}
56
+ };
57
+ };
58
+ return {
59
+ major: read("a:majorFont"),
60
+ minor: read("a:minorFont")
61
+ };
62
+ }
63
+ var THEME_FONT_TOKENS = new Map([
64
+ ["+mj-lt", ["major", "latin"]],
65
+ ["+mn-lt", ["minor", "latin"]],
66
+ ["+mj-ea", ["major", "ea"]],
67
+ ["+mn-ea", ["minor", "ea"]],
68
+ ["+mj-cs", ["major", "cs"]],
69
+ ["+mn-cs", ["minor", "cs"]]
70
+ ]);
71
+ /**
72
+ * Resolve a typeface written as a theme TOKEN to the name it stands for.
73
+ *
74
+ * A slide states its fonts as `+mn-lt` far more often than by name, and left
75
+ * unresolved that string travels into the model as if it WERE a typeface: no
76
+ * substitution table knows it, so a deck whose theme is Times came out in a
77
+ * grotesque (45541_Header).
78
+ *
79
+ * @param typeface The `@typeface` as the file writes it.
80
+ * @param fonts The theme's font scheme, when the part was read.
81
+ * @returns The resolved name; the input unchanged when it is not a token, and
82
+ * `undefined` when it is a token the theme leaves empty.
83
+ */
84
+ function resolveThemeFont(typeface, fonts) {
85
+ if (typeface === void 0 || !typeface.startsWith("+")) return typeface;
86
+ const slot = THEME_FONT_TOKENS.get(typeface.toLowerCase());
87
+ if (!slot || !fonts) return void 0;
88
+ const [which, script] = slot;
89
+ return fonts[which][script];
90
+ }
91
+ /**
29
92
  * Parse a DrawingML theme part (ECMA-376 §20.1.6.2, `a:clrScheme`) into a
30
93
  * `name → hex` colour map. Reads the twelve scheme slots — `dk1`/`lt1`/`dk2`/
31
94
  * `lt2`, `accent1`–`accent6`, `hlink`/`folHlink` — taking each slot's
@@ -154,4 +217,4 @@ function colorOf(slot) {
154
217
  }
155
218
  }
156
219
  //#endregion
157
- export { parseTheme, parseThemeBgFillStyles, parseThemeEffectStyles, parseThemeFillStyles, parseThemeLineWidths };
220
+ export { parseTheme, parseThemeBgFillStyles, parseThemeEffectStyles, parseThemeFillStyles, parseThemeFonts, parseThemeLineWidths, resolveThemeFont };
@@ -0,0 +1,10 @@
1
+ import { FamilyKey } from './remote-fonts.js';
2
+ import { FlowDoc } from '../ir/flow.js';
3
+ /**
4
+ * The curated substitute families a document's text needs, always including the
5
+ * sans default as the fallback for unstyled runs, math and chart labels.
6
+ *
7
+ * @param flow The parsed document.
8
+ * @returns One {@link FamilyKey} per distinct family the document names.
9
+ */
10
+ export declare function familiesInFlow(flow: FlowDoc): Set<FamilyKey>;
@@ -0,0 +1,44 @@
1
+ import { resolveFamilyKey } from "./remote-fonts.js";
2
+ //#region src/core/fonts/families.ts
3
+ /**
4
+ * The curated substitute families a document's text needs, always including the
5
+ * sans default as the fallback for unstyled runs, math and chart labels.
6
+ *
7
+ * @param flow The parsed document.
8
+ * @returns One {@link FamilyKey} per distinct family the document names.
9
+ */
10
+ function familiesInFlow(flow) {
11
+ const names = /* @__PURE__ */ new Set();
12
+ const add = (name) => {
13
+ if (name !== void 0 && name.trim() !== "") names.add(name.trim().toLowerCase());
14
+ };
15
+ add(flow.styles.defaultRunProperties.fontFamily?.ascii);
16
+ for (const style of flow.styles.styles.values()) {
17
+ add(style.runProperties.fontFamily?.ascii);
18
+ add(style.paragraphProperties.runProperties?.fontFamily?.ascii);
19
+ add(style.tableLayer?.runProperties?.fontFamily?.ascii);
20
+ for (const c of style.tableConditions ?? []) add(c.layer.runProperties?.fontFamily?.ascii);
21
+ }
22
+ for (const abstract of flow.numbering?.abstractNums.values() ?? []) for (const level of abstract.levels.values()) add(level.runProperties.fontFamily?.ascii);
23
+ const shapeText = (shape) => {
24
+ if (shape.text) visit(shape.text.content);
25
+ for (const child of shape.children ?? []) shapeText(child.shape);
26
+ };
27
+ const visit = (elements) => {
28
+ for (const el of elements) if (el.kind === "paragraph") {
29
+ add(el.paragraph.properties.runProperties?.fontFamily?.ascii);
30
+ for (const run of el.paragraph.runs) add(run.properties.fontFamily?.ascii);
31
+ } else if (el.kind === "table") for (const row of el.table.rows) for (const cell of row.cells) visit(cell.content);
32
+ else if (el.kind === "shape") shapeText(el.shape);
33
+ };
34
+ visit(flow.body);
35
+ for (const band of flow.headersFooters?.values() ?? []) visit(band);
36
+ for (const note of flow.footnotes?.values() ?? []) visit(note);
37
+ for (const note of flow.endnotes?.values() ?? []) visit(note);
38
+ for (const comment of flow.comments?.values() ?? []) visit(comment.content);
39
+ const keys = new Set(["arimo"]);
40
+ for (const name of names) keys.add(resolveFamilyKey(name));
41
+ return keys;
42
+ }
43
+ //#endregion
44
+ export { familiesInFlow };
@@ -1,2 +1,3 @@
1
- export { fetchFontSet, resolveFamilyKey, clearFontCache } from './remote-fonts.js';
2
- export type { FamilyKey, FetchFontSetOptions, FetchLike } from './remote-fonts.js';
1
+ export { fetchFontSet, fetchScriptFont, isScriptKey, resolveFamilyKey, resolveFamilyStyle, clearFontCache, } from './remote-fonts.js';
2
+ export { scriptForCodepoint, scriptsInFlow } from './scripts.js';
3
+ export type { FamilyKey, FamilyStyle, FetchFontSetOptions, FetchLike, ScriptKey, SubstituteKey, } from './remote-fonts.js';
@@ -1,11 +1,61 @@
1
1
  import { FontBytesByVariant } from '../font/index.js';
2
2
  /** A curated open substitute family (the Croscore + Carlito/Caladea set, like LibreOffice). */
3
3
  export type FamilyKey = 'arimo' | 'tinos' | 'cousine' | 'carlito' | 'caladea';
4
+ /**
5
+ * A WRITING SYSTEM the curated five cannot draw at all. They are Latin families
6
+ * — Greek, Cyrillic and Hebrew ride along, everything else is a notdef box —
7
+ * and 2145 characters of the corpus fall outside them: Han, Kana, Hangul,
8
+ * Arabic, the geometric symbols a form's checkbox is made of.
9
+ *
10
+ * These are fetched only when a document actually holds such a character, and
11
+ * only in the regular weight: Noto Sans SC is ten megabytes, and a bold run in
12
+ * it is better stroked (see `SyntheticFace`) than downloaded four times over.
13
+ */
14
+ export type ScriptKey = 'jp' | 'kr' | 'sc' | 'arabic' | 'hebrew' | 'thai' | 'symbols';
15
+ /** Either kind of substitute — a Latin family or a per-script face. */
16
+ export type SubstituteKey = FamilyKey | ScriptKey;
17
+ /** Whether a substitute key names a writing system rather than a Latin family. */
18
+ export declare function isScriptKey(key: SubstituteKey): key is ScriptKey;
19
+ /**
20
+ * Fetch the ONE face a writing system is drawn with.
21
+ *
22
+ * @param script The writing system.
23
+ * @param fetchImpl Injectable `fetch` (defaults to the global one).
24
+ * @returns The regular face, or `undefined` when the download fails.
25
+ */
26
+ export declare function fetchScriptFont(script: ScriptKey, fetchImpl?: FetchLike): Promise<FontBytesByVariant | undefined>;
27
+ /** A family name, read: which substitute it maps to and what it says about the face. */
28
+ export interface FamilyStyle {
29
+ readonly key: FamilyKey;
30
+ /** The NAME asks for a heavy face (`Arial Black`, `DIN-Bold`). */
31
+ readonly bold?: boolean;
32
+ /** …or a slanted one (`Frutiger Oblique`). */
33
+ readonly italic?: boolean;
34
+ /**
35
+ * …or a narrow one (`Arial Narrow`), as the fraction of the normal advance it
36
+ * sets at. No curated family ships a condensed cut, so the substitute is
37
+ * squeezed instead — Arial Narrow's own widths are 82 % of Arial's, which is
38
+ * also what LibreOffice's Liberation Sans Narrow reproduces.
39
+ */
40
+ readonly widthScale?: number;
41
+ }
4
42
  /**
5
43
  * Map a document-referenced font family to a curated open substitute: an exact
6
44
  * metric twin when one is known (e.g. Calibri → Carlito), otherwise a
7
45
  * serif/mono/sans style fallback.
8
46
  *
47
+ * The name may also carry the FACE — `Times New Roman Bold` is the Times
48
+ * family, and read whole it matches no twin at all and fell through to the
49
+ * generic sans, which is how a Times heading came out in a grotesque.
50
+ *
51
+ * @param name The referenced family name (case-insensitive); empty ⇒ Arimo.
52
+ * @returns The chosen family and what the name said about the face.
53
+ */
54
+ export declare function resolveFamilyStyle(name: string | undefined): FamilyStyle;
55
+ /**
56
+ * The curated substitute for a family name — {@link resolveFamilyStyle} without
57
+ * the face it also carries.
58
+ *
9
59
  * @param name The referenced family name (case-insensitive); empty ⇒ Arimo.
10
60
  * @returns The chosen {@link FamilyKey}.
11
61
  */
@@ -6,6 +6,48 @@ var VARIANT_SUFFIX = {
6
6
  italic: "400Regular_Italic",
7
7
  boldItalic: "700Bold_Italic"
8
8
  };
9
+ var SCRIPTS = {
10
+ jp: {
11
+ pkg: "noto-sans-jp",
12
+ file: "NotoSansJP"
13
+ },
14
+ kr: {
15
+ pkg: "noto-sans-kr",
16
+ file: "NotoSansKR"
17
+ },
18
+ sc: {
19
+ pkg: "noto-sans-sc",
20
+ file: "NotoSansSC"
21
+ },
22
+ arabic: {
23
+ pkg: "noto-sans-arabic",
24
+ file: "NotoSansArabic"
25
+ },
26
+ hebrew: {
27
+ pkg: "noto-sans-hebrew",
28
+ file: "NotoSansHebrew"
29
+ },
30
+ thai: {
31
+ pkg: "noto-sans-thai",
32
+ file: "NotoSansThai"
33
+ },
34
+ symbols: {
35
+ pkg: "noto-sans-symbols-2",
36
+ file: "NotoSansSymbols2"
37
+ }
38
+ };
39
+ /**
40
+ * Fetch the ONE face a writing system is drawn with.
41
+ *
42
+ * @param script The writing system.
43
+ * @param fetchImpl Injectable `fetch` (defaults to the global one).
44
+ * @returns The regular face, or `undefined` when the download fails.
45
+ */
46
+ async function fetchScriptFont(script, fetchImpl = globalThis.fetch.bind(globalThis)) {
47
+ const family = SCRIPTS[script];
48
+ const bytes = await fetchTtf(`${CDN_BASE}/${family.pkg}/400Regular/${family.file}_400Regular.ttf`, fetchImpl, false);
49
+ return bytes ? { regular: bytes } : void 0;
50
+ }
9
51
  var FAMILIES = {
10
52
  arimo: {
11
53
  pkg: "arimo",
@@ -31,6 +73,7 @@ var FAMILIES = {
31
73
  };
32
74
  var EXACT = {
33
75
  calibri: "carlito",
76
+ "calibri light": "carlito",
34
77
  cambria: "caladea",
35
78
  arial: "arimo",
36
79
  helvetica: "arimo",
@@ -59,22 +102,85 @@ var MONO = new Set([
59
102
  "dejavu sans mono",
60
103
  "monospace"
61
104
  ]);
105
+ var WEIGHT_WORDS = new Map([
106
+ ["black", "bold"],
107
+ ["heavy", "bold"],
108
+ ["bold", "bold"],
109
+ ["semibold", "bold"],
110
+ ["demibold", "bold"],
111
+ ["demi", "bold"],
112
+ ["italic", "italic"],
113
+ ["oblique", "italic"],
114
+ ["light", "none"],
115
+ ["thin", "none"],
116
+ ["medium", "none"],
117
+ ["regular", "none"],
118
+ ["book", "none"],
119
+ ["narrow", "narrow"],
120
+ ["condensed", "narrow"],
121
+ ["cond", "narrow"],
122
+ ["expanded", "none"],
123
+ ["extra", "none"],
124
+ ["ultra", "none"],
125
+ ["pro", "none"]
126
+ ]);
127
+ /** Arial Narrow's advance widths, as a fraction of Arial's. */
128
+ var NARROW_SCALE = .82;
62
129
  /**
63
130
  * Map a document-referenced font family to a curated open substitute: an exact
64
131
  * metric twin when one is known (e.g. Calibri → Carlito), otherwise a
65
132
  * serif/mono/sans style fallback.
66
133
  *
134
+ * The name may also carry the FACE — `Times New Roman Bold` is the Times
135
+ * family, and read whole it matches no twin at all and fell through to the
136
+ * generic sans, which is how a Times heading came out in a grotesque.
137
+ *
138
+ * @param name The referenced family name (case-insensitive); empty ⇒ Arimo.
139
+ * @returns The chosen family and what the name said about the face.
140
+ */
141
+ function resolveFamilyStyle(name) {
142
+ if (!name) return { key: "arimo" };
143
+ const words = name.trim().toLowerCase().split(/[\s\-_,]+/u).filter((w) => w !== "");
144
+ let bold = false;
145
+ let italic = false;
146
+ let narrow = false;
147
+ while (words.length > 1) {
148
+ const word = WEIGHT_WORDS.get(words[words.length - 1]);
149
+ if (word === void 0) break;
150
+ if (word === "bold") bold = true;
151
+ if (word === "italic") italic = true;
152
+ if (word === "narrow") narrow = true;
153
+ words.pop();
154
+ }
155
+ const tries = [
156
+ words.join(" "),
157
+ words[words.length - 1],
158
+ words[0]
159
+ ];
160
+ let key = "arimo";
161
+ for (const n of tries) {
162
+ const found = EXACT[n] ?? (MONO.has(n) ? "cousine" : SERIF.has(n) ? "tinos" : void 0);
163
+ if (found) {
164
+ key = found;
165
+ break;
166
+ }
167
+ }
168
+ return {
169
+ key,
170
+ ...bold ? { bold } : {},
171
+ ...italic ? { italic } : {},
172
+ ...narrow ? { widthScale: NARROW_SCALE } : {}
173
+ };
174
+ }
175
+ /**
176
+ * The curated substitute for a family name — {@link resolveFamilyStyle} without
177
+ * the face it also carries.
178
+ *
67
179
  * @param name The referenced family name (case-insensitive); empty ⇒ Arimo.
68
180
  * @returns The chosen {@link FamilyKey}.
69
181
  */
70
182
  function resolveFamilyKey(name) {
71
- if (!name) return "arimo";
72
- const n = name.trim().toLowerCase();
73
- const exact = EXACT[n];
74
- if (exact) return exact;
75
- if (MONO.has(n)) return "cousine";
76
- if (SERIF.has(n)) return "tinos";
77
- return "arimo";
183
+ return resolveFamilyStyle(name).key;
78
184
  }
79
185
  function fontUrl(family, variant) {
80
186
  const suffix = VARIANT_SUFFIX[variant];
@@ -128,4 +234,4 @@ async function fetchFontSet(options = {}) {
128
234
  };
129
235
  }
130
236
  //#endregion
131
- export { fetchFontSet, resolveFamilyKey };
237
+ export { fetchFontSet, fetchScriptFont, resolveFamilyKey, resolveFamilyStyle };
@@ -0,0 +1,26 @@
1
+ import { FlowDoc } from '../ir/flow.js';
2
+ import { ScriptKey } from './remote-fonts.js';
3
+ /**
4
+ * The writing system a character needs a face for, or `undefined` when a Latin
5
+ * family covers it.
6
+ *
7
+ * @param cp The code point.
8
+ * @param hanFace Which face draws unified Han in this document (see
9
+ * {@link scriptsInFlow}); defaults to Simplified.
10
+ */
11
+ export declare function scriptForCodepoint(cp: number, hanFace?: ScriptKey): ScriptKey | undefined;
12
+ /**
13
+ * The writing systems a document holds text in, beside the Latin one.
14
+ *
15
+ * Unified Han is written the same in Japanese, Korean and Chinese and drawn
16
+ * differently in each, and the character does not say which — the document
17
+ * does: Kana beside it means Japanese, Hangul means Korean. So the whole text
18
+ * is read once, and the answer applies to every Han character in it.
19
+ *
20
+ * @param flow The parsed document.
21
+ * @returns The scripts to fetch a face for, and the one Han is drawn with.
22
+ */
23
+ export declare function scriptsInFlow(flow: FlowDoc): {
24
+ scripts: Set<ScriptKey>;
25
+ hanFace: ScriptKey;
26
+ };