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
@@ -143,6 +143,22 @@ export declare class Ream {
143
143
  * @returns The font bytes and, for docx, optional per-family registries.
144
144
  */
145
145
  private resolveFonts;
146
+ /**
147
+ * Fetch one face per writing system the document holds text in — the curated
148
+ * families are Latin, and Han, Kana, Hangul, Arabic and the geometric symbols
149
+ * are a notdef box in every one of them.
150
+ *
151
+ * Only the regular weight is fetched: Noto Sans SC is ten megabytes, and a
152
+ * bold run in it is better stroked (see `SyntheticFace`) than downloaded four
153
+ * times over. A face that fails to arrive is a recorded loss, not a throw —
154
+ * the rest of the document still renders.
155
+ *
156
+ * @param into The registry map the run resolver looks in.
157
+ * @param flow The parsed document.
158
+ * @param options The convert options (for the injectable `fetch`).
159
+ * @param losses Where a face that could not be fetched records itself.
160
+ */
161
+ private addScriptFonts;
146
162
  /**
147
163
  * In strict mode, throw {@link ConversionLossError} for the first recorded loss.
148
164
  *
@@ -1,4 +1,5 @@
1
- import { fetchFontSet } from "../fonts/remote-fonts.js";
1
+ import { fetchFontSet, fetchScriptFont, resolveFamilyKey } from "../fonts/remote-fonts.js";
2
+ import { scriptsInFlow } from "../fonts/scripts.js";
2
3
  import { FontRegistry } from "../font/font-registry.js";
3
4
  import { createFontMeasure } from "../font/measure.js";
4
5
  import { ConversionLossError } from "../ir/loss.js";
@@ -12,8 +13,8 @@ import { writeDocx } from "../../word/docx-writer.js";
12
13
  import { writeXlsx } from "../../excel/xlsx-writer.js";
13
14
  import { writeHtml } from "../../html/html-writer.js";
14
15
  import { writeSvg } from "../../svg/svg-writer.js";
15
- import { resolveDocxAutoFonts } from "../../word/docx-to-pdf.js";
16
16
  import { DEFAULT_READERS, resolveFontsViaChain, toFlowDoc } from "./facade.js";
17
+ import { familiesInFlow } from "../fonts/families.js";
17
18
  import { decryptPackage, isEncryptedPackage } from "../crypto/offcrypto.js";
18
19
  //#region src/core/converter/ream.ts
19
20
  /**
@@ -150,7 +151,7 @@ var Ream = class Ream {
150
151
  losses
151
152
  };
152
153
  }
153
- const { fonts, registriesByFamily } = await this.resolveFonts(options, losses);
154
+ const { fonts, registriesByFamily } = await this.resolveFonts(options, losses, flow);
154
155
  const registry = FontRegistry.fromBytes(fonts);
155
156
  const paginated = this.sheet ? projectSheetDoc(this.sheet, {
156
157
  ...options.now ? { now: options.now } : {},
@@ -209,7 +210,7 @@ var Ream = class Ream {
209
210
  * @param losses The mutable loss list a substitution is appended to.
210
211
  * @returns The font bytes and, for docx, optional per-family registries.
211
212
  */
212
- async resolveFonts(options, losses) {
213
+ async resolveFonts(options, losses, flow) {
213
214
  const explicit = options.fonts ?? (options.fontBytes ? { regular: options.fontBytes } : void 0);
214
215
  if (explicit) return { fonts: explicit };
215
216
  if (options.fontProviders && options.fontProviders.length > 0) {
@@ -217,11 +218,60 @@ var Ream = class Ream {
217
218
  if (loss) losses.push(loss);
218
219
  if (fonts) return { fonts };
219
220
  }
220
- if (this.readerId === "docx") return resolveDocxAutoFonts(this.source, options);
221
- return { fonts: await fetchFontSet({
222
- ...options.fontFamily ? { family: options.fontFamily } : {},
223
- ...options.fontFetch ? { fetch: options.fontFetch } : {}
224
- }) };
221
+ const fetchOpt = options.fontFetch ? { fetch: options.fontFetch } : {};
222
+ const keys = options.fontFamily ? new Set([resolveFamilyKey(options.fontFamily)]) : familiesInFlow(flow);
223
+ if (keys.size <= 1 && scriptsInFlow(flow).scripts.size === 0) {
224
+ const family = keys.values().next().value;
225
+ return { fonts: await fetchFontSet({
226
+ ...family ? { family } : {},
227
+ ...fetchOpt
228
+ }) };
229
+ }
230
+ const registriesByFamily = /* @__PURE__ */ new Map();
231
+ let baseBytes;
232
+ for (const key of keys) {
233
+ const bytes = await fetchFontSet({
234
+ family: key,
235
+ ...fetchOpt
236
+ });
237
+ registriesByFamily.set(key, FontRegistry.fromBytes(bytes));
238
+ if (key === "arimo" || !baseBytes) baseBytes = bytes;
239
+ }
240
+ await this.addScriptFonts(registriesByFamily, flow, options, losses);
241
+ return {
242
+ fonts: baseBytes,
243
+ registriesByFamily
244
+ };
245
+ }
246
+ /**
247
+ * Fetch one face per writing system the document holds text in — the curated
248
+ * families are Latin, and Han, Kana, Hangul, Arabic and the geometric symbols
249
+ * are a notdef box in every one of them.
250
+ *
251
+ * Only the regular weight is fetched: Noto Sans SC is ten megabytes, and a
252
+ * bold run in it is better stroked (see `SyntheticFace`) than downloaded four
253
+ * times over. A face that fails to arrive is a recorded loss, not a throw —
254
+ * the rest of the document still renders.
255
+ *
256
+ * @param into The registry map the run resolver looks in.
257
+ * @param flow The parsed document.
258
+ * @param options The convert options (for the injectable `fetch`).
259
+ * @param losses Where a face that could not be fetched records itself.
260
+ */
261
+ async addScriptFonts(into, flow, options, losses) {
262
+ const { scripts } = scriptsInFlow(flow);
263
+ for (const script of scripts) {
264
+ const bytes = await fetchScriptFont(script, options.fontFetch);
265
+ if (!bytes) {
266
+ losses.push({
267
+ feature: "font",
268
+ detail: `no face could be fetched for the ${script} script; its text draws as boxes`,
269
+ severity: "substituted"
270
+ });
271
+ continue;
272
+ }
273
+ into.set(script, FontRegistry.fromBytes(bytes));
274
+ }
225
275
  }
226
276
  /**
227
277
  * In strict mode, throw {@link ConversionLossError} for the first recorded loss.
@@ -1025,6 +1025,24 @@ export interface ShapeFill {
1025
1025
  * text box: stretched, such a fill is a blur where it should be a pattern.
1026
1026
  */
1027
1027
  readonly tiled?: boolean;
1028
+ /**
1029
+ * The size ONE copy of a tiled fill occupies, when the shape states it —
1030
+ * MS-ODRAW §2.3.7.11/.12 `fillWidth` / `fillHeight`. Absent, a tile is the
1031
+ * picture at its own resolution, which is the default both formats give.
1032
+ */
1033
+ readonly tileSizePt?: {
1034
+ readonly widthPt: number;
1035
+ readonly heightPt: number;
1036
+ };
1037
+ /**
1038
+ * §20.1.8.58 `a:tile @sx @sy` — how far the picture is scaled BEFORE it is
1039
+ * repeated, as fractions of its own size (1 = 100 %). A texture halved tiles
1040
+ * four times as often, and read as a plain repeat it tiles once.
1041
+ */
1042
+ readonly tileScale?: {
1043
+ readonly sx: number;
1044
+ readonly sy: number;
1045
+ };
1028
1046
  /**
1029
1047
  * §20.1.8.14 `a:blipFill` — the picture painted across the shape's box. A
1030
1048
  * DrawingML picture IS a shape with one of these, which is how a `pic:pic`
@@ -46,7 +46,7 @@ function niceScale(dataMin, dataMax, maxTicks = 6) {
46
46
  const lo = Math.min(dataMin, dataMax);
47
47
  let hi = Math.max(dataMin, dataMax);
48
48
  if (lo === hi) hi = lo + 1;
49
- const step = niceNum(niceNum(hi - lo, false) / Math.max(1, maxTicks - 1), true);
49
+ const step = niceNum((hi - lo) / Math.max(1, maxTicks - 1), true);
50
50
  const headroom = hi > 0 ? hi * 1.05 : hi;
51
51
  return {
52
52
  min: Math.floor(lo / step) * step,
@@ -712,11 +712,17 @@ function autoRange(vals) {
712
712
  return [lo > 0 && lo < 5 / 6 * hi ? 0 : lo, hi];
713
713
  }
714
714
  /**
715
- * Ticks that fit along an axis `extentPt` long (mirrors {@link axisScale}).
716
- * A tick every 32pt is what both references draw: at 24 a 250pt plot carried
717
- * eleven of them (0, 0.5, 1 … 5.5) where both label 0…6 in whole numbers.
715
+ * How many ticks an axis `extentPt` long carries (mirrors {@link axisScale}).
716
+ *
717
+ * A tall axis takes more of them — a fixed six drew 0/100/200/300 down a plot
718
+ * where both references fit 0/50/…/300 — but the appetite is milder than it
719
+ * looks: asked for one every 32pt, a 427pt chart wanted ten, and Excel labels
720
+ * that chart's 0…5 data 0/1/…/6 where we drew half-steps (chart-texture-bg
721
+ * .pptx). One per ~48pt and never more than eight satisfies both, now that
722
+ * {@link niceScale} measures its step against the range the data spans rather
723
+ * than against Heckbert's rounded-up one.
718
724
  */
719
- var tickBudget = (extentPt) => Math.min(10, Math.max(4, Math.round(extentPt / 32)));
725
+ var tickBudget = (extentPt) => Math.min(8, Math.max(4, Math.round(extentPt / 48)));
720
726
  /** Side (points) of the square stamped for a series that names no symbol. */
721
727
  var DEFAULT_MARKER_PT = 4;
722
728
  /**
@@ -3,10 +3,12 @@ import { PoNode } from '../po-helpers.js';
3
3
  * A colour transform child (§20.1.2.3): `lumMod`/`lumOff` modulate luminance,
4
4
  * `shade` darkens toward black, `tint` lightens toward white. `val` is normalised
5
5
  * to 0..1 (the XML stores thousandths of a percent). `alpha` composites over white
6
- * (solid fills emit no transparency).
6
+ * (solid fills emit no transparency). `gamma`/`invGamma` carry no value: they
7
+ * shift the colour into the other light and back, which changes what the
8
+ * transforms between them mean.
7
9
  */
8
10
  export interface ColorMod {
9
- readonly kind: 'lumMod' | 'lumOff' | 'shade' | 'tint' | 'alpha' | 'satMod' | 'hueMod' | 'hueOff' | 'satOff';
11
+ readonly kind: 'lumMod' | 'lumOff' | 'shade' | 'tint' | 'alpha' | 'satMod' | 'hueMod' | 'hueOff' | 'satOff' | 'gamma' | 'invGamma';
10
12
  readonly val: number;
11
13
  }
12
14
  /**
@@ -99,6 +99,13 @@ function applyColorMods(hex, mods) {
99
99
  r = toSrgb(toLinear(r) * m.val + (1 - m.val));
100
100
  g = toSrgb(toLinear(g) * m.val + (1 - m.val));
101
101
  b = toSrgb(toLinear(b) * m.val + (1 - m.val));
102
+ } else if (m.kind === "gamma" || m.kind === "invGamma") {
103
+ const shift = m.kind === "gamma" ? toSrgb : toLinear;
104
+ [r, g, b] = [
105
+ shift(r),
106
+ shift(g),
107
+ shift(b)
108
+ ];
102
109
  } else if (m.kind === "lumMod" || m.kind === "lumOff") {
103
110
  const [h, s, l] = rgbToHsl(r, g, b);
104
111
  const l2 = m.kind === "lumMod" ? l * m.val : clamp01(l + m.val);
@@ -230,6 +237,14 @@ function readColorMods(colorNode) {
230
237
  val: v / 1e5
231
238
  });
232
239
  }
240
+ if (poIs(c, "a:gamma")) mods.push({
241
+ kind: "gamma",
242
+ val: 0
243
+ });
244
+ if (poIs(c, "a:invGamma")) mods.push({
245
+ kind: "invGamma",
246
+ val: 0
247
+ });
233
248
  if (poIs(c, "a:hueOff")) {
234
249
  const v = poIntAttr(c, "val");
235
250
  if (v !== void 0) mods.push({
@@ -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
+ };