@openpresentation/opf-render 0.9.0 → 0.11.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 (98) hide show
  1. package/README.md +115 -13
  2. package/dist/font-compatibility.js +72 -3
  3. package/dist/font-manifest.js +1746 -12
  4. package/dist/font-policy.js +2518 -0
  5. package/dist/fonts-browser.d.ts +65 -1
  6. package/dist/fonts-browser.js +229 -35
  7. package/dist/fonts-node.d.ts +68 -10
  8. package/dist/fonts-node.js +152 -16
  9. package/dist/fonts.d.ts +87 -5
  10. package/dist/fonts.js +147 -37
  11. package/dist/index.d.ts +31 -2
  12. package/dist/lazy-font-list.js +23 -0
  13. package/dist/lazy-fonts.js +52 -0
  14. package/dist/raster.js +3 -1
  15. package/dist/script-font-pack.js +179 -0
  16. package/dist/script-fonts.js +563 -0
  17. package/dist/svg.d.ts +29 -2
  18. package/dist/svg.js +446 -41
  19. package/fonts/bebas-neue/BebasNeue_400Regular.ttf +0 -0
  20. package/fonts/bebas-neue/OFL.txt +93 -0
  21. package/fonts/bebas-neue/PROVENANCE.json +23 -0
  22. package/fonts/carlito/Carlito-Bold.ttf +0 -0
  23. package/fonts/carlito/Carlito-BoldItalic.ttf +0 -0
  24. package/fonts/carlito/Carlito-Italic.ttf +0 -0
  25. package/fonts/carlito/Carlito-Regular.ttf +0 -0
  26. package/fonts/carlito/OFL.txt +94 -0
  27. package/fonts/intos/Intos-Bold.ttf +0 -0
  28. package/fonts/intos/Intos-BoldItalic.ttf +0 -0
  29. package/fonts/intos/Intos-Italic.ttf +0 -0
  30. package/fonts/intos/Intos-Regular.ttf +0 -0
  31. package/fonts/intos/IntosDisplay-Bold.ttf +0 -0
  32. package/fonts/intos/IntosDisplay-BoldItalic.ttf +0 -0
  33. package/fonts/intos/IntosDisplay-Italic.ttf +0 -0
  34. package/fonts/intos/IntosDisplay-Regular.ttf +0 -0
  35. package/fonts/intos/IntosNarrow-Bold.ttf +0 -0
  36. package/fonts/intos/IntosNarrow-BoldItalic.ttf +0 -0
  37. package/fonts/intos/IntosNarrow-Italic.ttf +0 -0
  38. package/fonts/intos/IntosNarrow-Regular.ttf +0 -0
  39. package/fonts/intos/IntosSerif-Bold.ttf +0 -0
  40. package/fonts/intos/IntosSerif-BoldItalic.ttf +0 -0
  41. package/fonts/intos/IntosSerif-Italic.ttf +0 -0
  42. package/fonts/intos/IntosSerif-Regular.ttf +0 -0
  43. package/fonts/intos/NOTICE.md +18 -0
  44. package/fonts/intos/OFL.txt +94 -0
  45. package/fonts/intos/PROVENANCE.json +144 -0
  46. package/fonts/lora/Lora-Bold.ttf +0 -0
  47. package/fonts/lora/Lora-BoldItalic.ttf +0 -0
  48. package/fonts/lora/Lora-Italic.ttf +0 -0
  49. package/fonts/lora/Lora-Regular.ttf +0 -0
  50. package/fonts/lora/OFL.txt +93 -0
  51. package/fonts/lora/PROVENANCE.json +48 -0
  52. package/fonts/merriweather-sans/MerriweatherSans-Bold.ttf +0 -0
  53. package/fonts/merriweather-sans/MerriweatherSans-BoldItalic.ttf +0 -0
  54. package/fonts/merriweather-sans/MerriweatherSans-Italic.ttf +0 -0
  55. package/fonts/merriweather-sans/MerriweatherSans-Regular.ttf +0 -0
  56. package/fonts/merriweather-sans/OFL.txt +96 -0
  57. package/fonts/merriweather-sans/PROVENANCE.json +48 -0
  58. package/fonts/montserrat/Montserrat_400Regular.ttf +0 -0
  59. package/fonts/montserrat/Montserrat_400Regular_Italic.ttf +0 -0
  60. package/fonts/montserrat/Montserrat_700Bold.ttf +0 -0
  61. package/fonts/montserrat/Montserrat_700Bold_Italic.ttf +0 -0
  62. package/fonts/montserrat/Montserrat_900Black.ttf +0 -0
  63. package/fonts/montserrat/OFL.txt +93 -0
  64. package/fonts/montserrat/PROVENANCE.json +55 -0
  65. package/fonts/open-sans/OFL.txt +92 -0
  66. package/fonts/open-sans/OpenSans_400Regular.ttf +0 -0
  67. package/fonts/open-sans/OpenSans_400Regular_Italic.ttf +0 -0
  68. package/fonts/open-sans/OpenSans_700Bold.ttf +0 -0
  69. package/fonts/open-sans/OpenSans_700Bold_Italic.ttf +0 -0
  70. package/fonts/open-sans/PROVENANCE.json +47 -0
  71. package/fonts/poppins/OFL.txt +93 -0
  72. package/fonts/poppins/PROVENANCE.json +47 -0
  73. package/fonts/poppins/Poppins_400Regular.ttf +0 -0
  74. package/fonts/poppins/Poppins_400Regular_Italic.ttf +0 -0
  75. package/fonts/poppins/Poppins_700Bold.ttf +0 -0
  76. package/fonts/poppins/Poppins_700Bold_Italic.ttf +0 -0
  77. package/fonts/pt-serif/OFL.txt +93 -0
  78. package/fonts/pt-serif/PROVENANCE.json +50 -0
  79. package/fonts/pt-serif/PT_Serif-Web-Bold.ttf +0 -0
  80. package/fonts/pt-serif/PT_Serif-Web-BoldItalic.ttf +0 -0
  81. package/fonts/pt-serif/PT_Serif-Web-Italic.ttf +0 -0
  82. package/fonts/pt-serif/PT_Serif-Web-Regular.ttf +0 -0
  83. package/fonts/red-hat-display/OFL.txt +93 -0
  84. package/fonts/red-hat-display/PROVENANCE.json +38 -0
  85. package/fonts/red-hat-display/RedHatDisplay-Bold.ttf +0 -0
  86. package/fonts/red-hat-display/RedHatDisplay-Light.ttf +0 -0
  87. package/fonts/red-hat-display/RedHatDisplay-Regular.ttf +0 -0
  88. package/fonts/red-hat-text/OFL.txt +93 -0
  89. package/fonts/red-hat-text/PROVENANCE.json +30 -0
  90. package/fonts/red-hat-text/RedHatText-Bold.ttf +0 -0
  91. package/fonts/red-hat-text/RedHatText-Regular.ttf +0 -0
  92. package/fonts/source-sans-3/OFL.txt +93 -0
  93. package/fonts/source-sans-3/PROVENANCE.json +49 -0
  94. package/fonts/source-sans-3/SourceSans3-Bold.ttf +0 -0
  95. package/fonts/source-sans-3/SourceSans3-BoldIt.ttf +0 -0
  96. package/fonts/source-sans-3/SourceSans3-It.ttf +0 -0
  97. package/fonts/source-sans-3/SourceSans3-Regular.ttf +0 -0
  98. package/package.json +167 -6
@@ -3,9 +3,47 @@ import type {
3
3
  FontRegistry,
4
4
  FontRegistryOptions,
5
5
  } from "./fonts.js";
6
+ import type { AutoScriptSelection, BundledFontPackage, LazyFont, ScriptSelection } from "./fonts-node.js";
7
+ export type { LazyFont } from "./fonts-node.js";
8
+ export type { AutoScriptSelection } from "./fonts-node.js";
9
+ export { detectPresentationScripts, autoScriptSelection } from "./fonts-node.js";
6
10
  export interface BrowserFontInput extends Omit<FontFaceInput, "data"> {
7
11
  data?: Uint8Array;
8
12
  url?: string;
13
+ /** Reviewed SHA-256 (hex); verified with Web Crypto before the face is used. */
14
+ sha256?: string;
15
+ }
16
+ export interface BrowserFontRegistry extends FontRegistry {
17
+ dispose(): void;
18
+ /** Every vendored face a host can load on demand: family, style, package-relative file and pinned sha256. */
19
+ readonly lazyFonts: readonly LazyFont[];
20
+ /**
21
+ * Load the vendored faces (for example Intos for the default Aptos scheme) the presentation's font families resolve to
22
+ * under the registry's `substitutionPolicy` (nothing is downloaded for a family the policy would not resolve), once
23
+ * each, from `lazyFontsBaseUrl`. Fetches and verifies every file, then adds the faces to the document and the registry
24
+ * together; on failure nothing changes and the call can be retried. Call it before measuring a document. After
25
+ * `dispose()` it rejects with `font-registry-disposed`.
26
+ */
27
+ ensureLazyFonts(presentation: unknown, options?: { signal?: AbortSignal }): Promise<LazyFont[]>;
28
+ /** Synchronous: the vendored faces the presentation needs under the registry's policy that are not loaded yet. Empty means `ensureLazyFonts` fetches nothing. */
29
+ pendingLazyFonts(presentation: unknown): LazyFont[];
30
+
31
+ /** Load script faces for ISO 15924 codes (or "all") from `scriptBaseUrl`. Resolves with the newly loaded package names. */
32
+ loadScripts(scripts: ScriptSelection, options?: { signal?: AbortSignal }): Promise<string[]>;
33
+ /**
34
+ * Load the script faces the presentation's text needs, once each (FF-19). Cheap when nothing new is
35
+ * needed. Also loads the CJK face a glyph fallback needs for drawn characters the loaded faces lack. Call it after
36
+ * edits and render again afterwards: measurements planned earlier do not know the new faces. `signal` belongs to
37
+ * this call only. `uncovered` lists drawn CJK characters no loaded face covers. At most one CJK face beyond the text's own
38
+ * scripts is loaded for glyph fallback. If a package fails the others still load and the call rejects with an
39
+ * OPFFontError whose `details` are `{ loaded: string[]; failed: {package, code, message}[] }`. A failed call (fetch, hash, FontFace load) leaves nothing loaded and can be retried; after
40
+ * `dispose()` it rejects with `font-registry-disposed`.
41
+ */
42
+ ensureScripts(presentation: unknown, options?: { signal?: AbortSignal }): Promise<AutoScriptSelection & { loaded: string[]; uncovered: string[] }>;
43
+ /** Synchronous: package names the presentation needs that are not loaded yet. Empty means `ensureScripts` fetches nothing, so a host can render immediately. */
44
+ pendingScripts(presentation: unknown): string[];
45
+ /** Names of the script-pack packages loaded so far. */
46
+ readonly loadedScriptPackages: string[];
9
47
  }
10
48
  export declare function loadBrowserFontRegistry(
11
49
  entries: BrowserFontInput[],
@@ -13,5 +51,31 @@ export declare function loadBrowserFontRegistry(
13
51
  document?: Document;
14
52
  fetch?: typeof fetch;
15
53
  signal?: AbortSignal;
54
+ crypto?: { subtle: SubtleCrypto };
55
+ /** Script faces to load once the registry exists: "auto" (with `presentation`), "all" or ISO 15924 codes. Needs `scriptBaseUrl`. */
56
+ scripts?: ScriptSelection | "auto";
57
+ /** The presentation whose text decides `scripts: "auto"`. */
58
+ presentation?: unknown;
59
+ /** Where the host serves the installed `@expo-google-fonts/*` packages; script faces are fetched from here, hash-verified. */
60
+ scriptBaseUrl?: string;
61
+ /** Where the host serves this package's `fonts` directory, so vendored faces (Intos, the open families) load on demand from `<base>/<package-relative file>` (fonts/intos/..., fonts/<family>/...). */
62
+ lazyFontsBaseUrl?: string;
16
63
  },
17
- ): Promise<FontRegistry & { dispose(): void }>;
64
+ ): Promise<BrowserFontRegistry>;
65
+ export declare function scriptFontPackages(scripts: ScriptSelection): BundledFontPackage[];
66
+ /** Hash-pinned browser entries for the script pack, served by the host from `baseUrl`. */
67
+ export declare function scriptFontEntries(
68
+ scripts: ScriptSelection,
69
+ options: { baseUrl: string },
70
+ ): (BrowserFontInput & { url: string; family: string; weight: number; italic: boolean; sha256: string; scripts: string[]; package: string; license: string })[];
71
+ /** Every vendored face of this package: family, style, package-relative file, sha256. */
72
+ export declare function lazyFontList(): readonly LazyFont[];
73
+ /** Hash-pinned browser entries for the vendored faces, served by the host from `baseUrl` at their package-relative paths. */
74
+ export declare function lazyFontEntries(options: { baseUrl: string }, list?: readonly LazyFont[]): (BrowserFontInput & { url: string; family: string; weight: number; italic: boolean; sha256: string; package: string })[];
75
+ /** The font families a presentation's slides resolve (heading, body and code roles); empty for a document that does not resolve. */
76
+ export declare function presentationFamilies(presentation: unknown): Set<string>;
77
+ /**
78
+ * The vendored faces a set of resolved families needs that the registry does not hold, following the registry's own order and
79
+ * `policy` (default "visual"): the family itself, an alias target, the declared replacement, then its alternates.
80
+ */
81
+ export declare function lazyFontsFor(families: Iterable<string>, context: { lazy: readonly LazyFont[]; hasFamily(family: string): boolean; loaded?: ReadonlySet<string>; policy?: "none" | "metric" | "visual"; aliases?: ReadonlyMap<string, string> }): LazyFont[];
@@ -1,4 +1,25 @@
1
1
  import { createFontRegistry, OPFFontError } from "./fonts.js";
2
+ import { lazyFontEntries, lazyFontList } from "./lazy-font-list.js";
3
+ import { lazyFontsFor, presentationFamilies } from "./lazy-fonts.js";
4
+ import { analyzePresentationScripts, nextFallbackPackage, scriptFontPackages, scriptPackageEntries, scriptSelectionOf, uncoveredCjkCharacters } from "./script-font-pack.js";
5
+ export { autoScriptSelection, detectPresentationScripts, scriptFontEntries, scriptFontPackages } from "./script-font-pack.js";
6
+ export { lazyFontEntries, lazyFontList } from "./lazy-font-list.js";
7
+ export { lazyFontsFor, presentationFamilies } from "./lazy-fonts.js";
8
+
9
+ async function verifyDigest(entry, data, subtle) {
10
+ if (entry.sha256 === undefined) return;
11
+ if (!subtle?.digest)
12
+ throw new OPFFontError("font-api-unavailable", "Verifying a font SHA-256 requires Web Crypto.");
13
+ const digest = new Uint8Array(await subtle.digest("SHA-256", data));
14
+ const actual = [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
15
+ if (actual !== String(entry.sha256).toLowerCase())
16
+ throw new OPFFontError(
17
+ "font-integrity-mismatch",
18
+ "The font file differs from its reviewed SHA-256; serve the pinned package.",
19
+ { url: entry.url, expected: entry.sha256, actual },
20
+ );
21
+ }
22
+
2
23
  /** Fetch only host-selected font files, then use those exact bytes for shaping and CSS. */
3
24
  export async function loadBrowserFontRegistry(entries, options = {}) {
4
25
  const documentRef = options.document ?? globalThis.document;
@@ -9,52 +30,225 @@ export async function loadBrowserFontRegistry(entries, options = {}) {
9
30
  "The browser Font Loading API is required.",
10
31
  );
11
32
  const fetcher = options.fetch ?? globalThis.fetch;
12
- const faces = await Promise.all(
13
- entries.map(async (entry) => {
14
- if (entry.data) return entry;
15
- if (!entry.url)
16
- throw new OPFFontError(
17
- "invalid-font-source",
18
- "Supply font bytes or a font-file URL.",
19
- );
20
- const response = await fetcher(entry.url, { signal: options.signal });
21
- if (!response.ok)
22
- throw new OPFFontError(
23
- "font-fetch-failed",
24
- `Could not load font (${response.status}).`,
25
- { url: entry.url },
26
- );
27
- return { ...entry, data: new Uint8Array(await response.arrayBuffer()) };
33
+ const subtle = options.crypto?.subtle ?? globalThis.crypto?.subtle;
34
+ const fetchFaces = (list, signal) => Promise.all(
35
+ list.map(async (entry) => {
36
+ let loaded = entry;
37
+ if (!entry.data) {
38
+ if (!entry.url)
39
+ throw new OPFFontError(
40
+ "invalid-font-source",
41
+ "Supply font bytes or a font-file URL.",
42
+ );
43
+ const response = await fetcher(entry.url, { signal });
44
+ if (!response.ok)
45
+ throw new OPFFontError(
46
+ "font-fetch-failed",
47
+ `Could not load font (${response.status}).`,
48
+ { url: entry.url },
49
+ );
50
+ loaded = { ...entry, data: new Uint8Array(await response.arrayBuffer()) };
51
+ }
52
+ // Script-pack entries carry the reviewed hash (FF-19); verify before use.
53
+ await verifyDigest(entry, loaded.data, subtle);
54
+ return loaded;
28
55
  }),
29
56
  );
57
+ const faces = await fetchFaces(entries, options.signal);
30
58
  const registry = createFontRegistry(faces, options),
31
59
  loaded = [];
32
60
  // Normalize family names using the registry's parsed font metadata.
33
- const embedded = registry.embeddedFonts;
61
+ const described = registry.describeFaces();
62
+ // Registers browser FontFaces for faces already parsed by the registry; on failure nothing stays registered.
63
+ const registerBrowserFaces = async (list, descriptors) => {
64
+ const added = [];
65
+ try {
66
+ await Promise.all(
67
+ list.map(async (entry, index) => {
68
+ const descriptor = descriptors[index];
69
+ const face = new FontFaceRef(
70
+ descriptor.family,
71
+ entry.data.slice().buffer,
72
+ {
73
+ weight: String(descriptor.weight),
74
+ style: descriptor.italic ? "italic" : "normal",
75
+ },
76
+ );
77
+ await face.load();
78
+ added.push(face);
79
+ }),
80
+ );
81
+ for (const face of added) documentRef.fonts.add(face);
82
+ await documentRef.fonts.ready;
83
+ } catch (error) {
84
+ for (const face of added) documentRef.fonts.delete(face);
85
+ throw new OPFFontError("font-load-failed", error.message);
86
+ }
87
+ loaded.push(...added);
88
+ };
89
+ await registerBrowserFaces(faces, described);
90
+
91
+ // Script faces (FF-19) are loaded lazily, once a document needs them, and only for the scripts it uses.
92
+ const scriptPackages = new Set();
93
+ let queue = Promise.resolve(), disposed = false;
94
+ const gone = () => new OPFFontError("font-registry-disposed", "The font registry was disposed.");
95
+ const baseUrl = () => {
96
+ if (typeof options.scriptBaseUrl !== "string" || !options.scriptBaseUrl)
97
+ throw new OPFFontError("invalid-font-source", "Loading script fonts requires scriptBaseUrl, where the host serves the installed @expo-google-fonts packages.");
98
+ return options.scriptBaseUrl;
99
+ };
100
+ // Loads whole packages. Order matters: fetch and verify, load every FontFace (not yet visible to the document),
101
+ // then register them with the document and add them to the registry atomically. Nothing is half loaded: on any
102
+ // failure (fetch, hash, FontFace.load, registry) the document and registry are unchanged and the packages stay
103
+ // pending, so the next call retries. `signal` belongs to this call only.
104
+ const loadPackageList = (packages, { signal } = {}) => {
105
+ const result = queue.then(async () => {
106
+ if (disposed) throw gone();
107
+ signal?.throwIfAborted?.();
108
+ const pending = packages.filter((item) => !scriptPackages.has(item.name));
109
+ if (!pending.length) return [];
110
+ const fetched = await fetchFaces(scriptPackageEntries(pending, { baseUrl: baseUrl() }), signal);
111
+ let browserFaces;
112
+ try {
113
+ browserFaces = await Promise.all(fetched.map(async (entry) => {
114
+ const face = new FontFaceRef(entry.family, entry.data.slice().buffer, { weight: String(entry.weight ?? 400), style: entry.italic ? "italic" : "normal" });
115
+ await face.load();
116
+ return face;
117
+ }));
118
+ } catch (error) { throw new OPFFontError("font-load-failed", error.message); }
119
+ if (disposed) throw gone();
120
+ const shown = [];
121
+ try {
122
+ for (const face of browserFaces) { documentRef.fonts.add(face); shown.push(face); }
123
+ registry.addFaces(fetched);
124
+ } catch (error) {
125
+ for (const face of shown) documentRef.fonts.delete(face);
126
+ throw error;
127
+ }
128
+ loaded.push(...browserFaces);
129
+ for (const item of pending) scriptPackages.add(item.name);
130
+ try { await documentRef.fonts.ready; } catch { /* the faces are loaded and registered */ }
131
+ return pending.map((item) => item.name);
132
+ });
133
+ queue = result.catch(() => {});
134
+ return result;
135
+ };
136
+ const covers = (character) => registry.scriptFacesCover(character);
137
+ Object.assign(registry, {
138
+ /** Load the script faces for explicit ISO 15924 codes (or 'all'). Resolves with the newly loaded package names. `signal` aborts this call's fetches. */
139
+ loadScripts: (scripts, callOptions) => loadPackageList(scriptFontPackages(scripts), callOptions),
140
+ /**
141
+ * Load the script faces the presentation's text needs, once each (FF-19), then the (at most one) CJK face a glyph
142
+ * fallback needs for characters the loaded faces lack. Cheap when nothing new is needed; call it after edits and
143
+ * render again afterwards. Resolves with the detected scripts, the packages loaded by this call, the scripts no
144
+ * pinned font serves and `uncovered`, drawn CJK characters no loaded face covers (the renderer reports
145
+ * `missing-glyph` for them). Each package loads all or nothing. If some package fails, the others still load: the
146
+ * call then rejects with an OPFFontError (the first failure's code) whose `details` hold `loaded` (packages this
147
+ * call did load) and `failed` (`{package, code, message}` per failed package); nothing is lost and a later call
148
+ * retries only the failures. `signal` aborts this call's fetches.
149
+ */
150
+ async ensureScripts(presentation, callOptions) {
151
+ if (disposed) throw gone();
152
+ const analysis = analyzePresentationScripts(presentation), selection = scriptSelectionOf(analysis);
153
+ const loadedNow = [], failed = [];
154
+ const attempt = async (item) => {
155
+ try { loadedNow.push(...(await loadPackageList([item], callOptions))); }
156
+ catch (error) { failed.push({ package: item.name, code: error?.code ?? "font-load-failed", message: error?.message ?? String(error), error }); }
157
+ };
158
+ for (const item of scriptFontPackages(selection.scripts)) await attempt(item);
159
+ // Fallback faces only follow a clean primary load: a failed package would otherwise be judged uncovered.
160
+ if (!failed.length) for (let next = nextFallbackPackage(analysis, { covers, loaded: scriptPackages }); next && !failed.length; next = nextFallbackPackage(analysis, { covers, loaded: scriptPackages })) await attempt(next);
161
+ // An aborted call rejects with the signal's reason; packages loaded before the abort stay loaded.
162
+ if (callOptions?.signal?.aborted) throw callOptions.signal.reason ?? failed[0]?.error;
163
+ if (failed.length) {
164
+ const first = failed[0];
165
+ throw new OPFFontError(first.code, first.message, { loaded: loadedNow, failed: failed.map(({ package: name, code, message }) => ({ package: name, code, message })), cause: first.error });
166
+ }
167
+ return { ...selection, loaded: loadedNow, uncovered: uncoveredCjkCharacters(analysis, { covers }) };
168
+ },
169
+ /** Synchronous: script-pack packages the presentation needs that are not loaded yet. Empty means `ensureScripts` would fetch nothing. */
170
+ pendingScripts(presentation) {
171
+ const analysis = analyzePresentationScripts(presentation);
172
+ const primary = scriptFontPackages(scriptSelectionOf(analysis).scripts).map((item) => item.name).filter((name) => !scriptPackages.has(name));
173
+ if (primary.length) return primary;
174
+ const next = nextFallbackPackage(analysis, { covers, loaded: scriptPackages });
175
+ return next ? [next.name] : [];
176
+ },
177
+ });
178
+ // FF-31: vendored faces (the open families and Intos) load on demand, once a document needs them. They share the
179
+ // script loader's queue and disposed state, and the same all-or-nothing order: fetch and verify, load every FontFace,
180
+ // then add them to the document and the registry together.
181
+ const lazy = lazyFontList(), lazyLoaded = new Set();
182
+ const policy = options.substitutionPolicy ?? "none";
183
+ const aliasTargets = new Map(Object.entries(options.aliases ?? {}).map(([from, to]) => [from.toLowerCase(), to]));
184
+ const hasFamily = (family) => registry.describeFaces().some((face) => face.family.toLowerCase() === String(family).toLowerCase());
185
+ const neededLazy = (presentation) => lazyFontsFor(presentationFamilies(presentation), { lazy, hasFamily, loaded: lazyLoaded, policy, aliases: aliasTargets });
186
+ const lazyBaseUrl = () => {
187
+ if (typeof options.lazyFontsBaseUrl !== "string" || !options.lazyFontsBaseUrl)
188
+ throw new OPFFontError("invalid-font-source", "Loading vendored fonts requires lazyFontsBaseUrl, where the host serves the package's fonts directory.");
189
+ return options.lazyFontsBaseUrl;
190
+ };
191
+ Object.assign(registry, {
192
+ /** Every vendored face a host can load on demand: family, style, package-relative file and pinned sha256. */
193
+ lazyFonts: lazy,
194
+ /**
195
+ * Load the vendored faces the presentation's font families resolve to under the registry's substitution policy (nothing
196
+ * is downloaded for a family the policy would not resolve), once each. All or nothing: on any failure (fetch, hash,
197
+ * FontFace.load, registry) the document and registry are unchanged and the call can be retried. Cheap when nothing is
198
+ * needed. Call it before measuring a document, and again after edits that change fonts. Resolves with the faces added.
199
+ */
200
+ ensureLazyFonts(presentation, callOptions = {}) {
201
+ const result = queue.then(async () => {
202
+ if (disposed) throw gone();
203
+ callOptions.signal?.throwIfAborted?.();
204
+ const pending = neededLazy(presentation);
205
+ if (!pending.length) return [];
206
+ const fetched = (await fetchFaces(lazyFontEntries({ baseUrl: lazyBaseUrl() }, pending), callOptions.signal)).map((entry) => ({ ...entry, embed: "used" }));
207
+ let browserFaces;
208
+ try {
209
+ browserFaces = await Promise.all(fetched.map(async (entry) => {
210
+ const face = new FontFaceRef(entry.family, entry.data.slice().buffer, { weight: String(entry.weight), style: entry.italic ? "italic" : "normal" });
211
+ await face.load();
212
+ return face;
213
+ }));
214
+ } catch (error) { throw new OPFFontError("font-load-failed", error.message); }
215
+ if (disposed) throw gone();
216
+ const shown = [];
217
+ try {
218
+ for (const face of browserFaces) { documentRef.fonts.add(face); shown.push(face); }
219
+ registry.addFaces(fetched);
220
+ } catch (error) {
221
+ for (const face of shown) documentRef.fonts.delete(face);
222
+ throw error;
223
+ }
224
+ loaded.push(...browserFaces);
225
+ for (const item of pending) lazyLoaded.add(item.file);
226
+ try { await documentRef.fonts.ready; } catch { /* the faces are loaded and registered */ }
227
+ return pending;
228
+ });
229
+ queue = result.catch(() => {});
230
+ return result;
231
+ },
232
+ /** Synchronous: the vendored faces the presentation needs under the registry's policy that are not loaded yet. Empty means `ensureLazyFonts` fetches nothing. */
233
+ pendingLazyFonts: (presentation) => neededLazy(presentation),
234
+ });
235
+ /** Names of the script-pack packages loaded so far. */
236
+ Object.defineProperty(registry, "loadedScriptPackages", { get: () => [...scriptPackages], enumerable: true });
34
237
  try {
35
- await Promise.all(
36
- faces.map(async (entry, index) => {
37
- const descriptor = embedded[index];
38
- const face = new FontFaceRef(
39
- descriptor.family,
40
- entry.data.slice().buffer,
41
- {
42
- weight: String(descriptor.weight),
43
- style: descriptor.italic ? "italic" : "normal",
44
- },
45
- );
46
- await face.load();
47
- loaded.push(face);
48
- }),
49
- );
50
- for (const face of loaded) documentRef.fonts.add(face);
51
- await documentRef.fonts.ready;
238
+ if (options.scripts === "auto") {
239
+ if (options.presentation === null || typeof options.presentation !== "object")
240
+ throw new OPFFontError("invalid-font-scripts", "scripts: 'auto' needs the presentation whose text decides the scripts.", { scripts: options.scripts });
241
+ await registry.ensureScripts(options.presentation, { signal: options.signal });
242
+ } else if (options.scripts !== undefined && !(Array.isArray(options.scripts) && !options.scripts.length)) {
243
+ await registry.loadScripts(options.scripts, { signal: options.signal });
244
+ }
52
245
  } catch (error) {
53
246
  for (const face of loaded) documentRef.fonts.delete(face);
54
- throw new OPFFontError("font-load-failed", error.message);
247
+ throw error;
55
248
  }
56
249
  return Object.assign(registry, {
57
250
  dispose() {
251
+ disposed = true;
58
252
  for (const face of loaded) documentRef.fonts.delete(face);
59
253
  },
60
254
  });
@@ -1,19 +1,77 @@
1
- import type {FontRegistry,FontRegistryOptions,EmbeddedFont} from "./fonts.js";
2
- export declare function loadBundledFontRegistry(options?: FontRegistryOptions): Promise<FontRegistry & {fontFiles:string[]}>;
1
+ import type {FontRegistry,FontRegistryOptions,EmbeddedFont,FontFaceInput,ScriptFontProfile} from "./fonts.js";
2
+ /** A caller-supplied face (FF-31), for example a licensed copy of the real font. Node callers may pass a file path. */
3
+ export type CallerFontFace = FontFaceInput | (Omit<FontFaceInput, "data"> & {path: string});
4
+ /** "all" or ISO 15924 codes (`Jpan`, `Hans`, `Hant`, `Kore`, `Arab`, `Hebr`, `Deva`, `Thai`, ...). */
5
+ export type ScriptSelection = "all" | string[];
6
+ /** What `scripts: "auto"` chose for a presentation (FF-19). */
7
+ export interface AutoScriptSelection {
8
+ /** Scripts the presentation's drawn text uses, other than Latin, Greek and Cyrillic. */
9
+ detected: string[];
10
+ /** The detected scripts a pinned package serves; the input for `scriptFontPackages`. */
11
+ scripts: string[];
12
+ /** Detected scripts no pinned open font serves; that text uses the design font. */
13
+ unavailable: string[];
14
+ }
15
+ /** Script-pack packages that `scripts: "auto"` loaded or found not installed. */
16
+ export interface AppliedScriptSelection extends AutoScriptSelection { packages: string[]; notInstalled: string[]; /** Drawn CJK characters no loaded face covers (glyph fallback stopped at its cap). */ uncovered?: string[] }
17
+ /** Script keys whose faces a preview of the presentation needs: text decides, the document language only tells Han scripts apart. */
18
+ export declare function detectPresentationScripts(presentation: unknown, options?: { profile?: ScriptFontProfile }): string[];
19
+ export declare function autoScriptSelection(presentation: unknown, options?: { profile?: ScriptFontProfile }): AutoScriptSelection;
20
+ export interface ScriptPackOptions {
21
+ /** Caller-supplied faces, loaded first so a licensed real font resolves as an exact face (FF-31). */
22
+ faces?: readonly CallerFontFace[];
23
+ /**
24
+ * Also load the optional, hash-pinned OFL Noto script pack for these scripts (FF-19).
25
+ * `"auto"` loads only the scripts `presentation` draws (and needs it); a script package that is not installed
26
+ * is reported through `onDiagnostic` instead of failing.
27
+ */
28
+ scripts?: ScriptSelection | "auto";
29
+ /** The presentation whose text decides `scripts: "auto"`. */
30
+ presentation?: unknown;
31
+ /** Receives `script-font-unavailable` and `script-font-not-installed` for `scripts: "auto"`. */
32
+ onDiagnostic?: (diagnostic: {code: string; message: string; script?: string; package?: string; scripts?: string[]}) => void;
33
+ }
34
+ /** A vendored face that an SVG embeds only when its text names the family (the open families and Intos), and that browser hosts load on demand from a separate hash-pinned file. */
35
+ export interface LazyFont { readonly package: string; readonly family: string; readonly weight: number; readonly italic: boolean; /** Path relative to the package root, for example fonts/intos/Intos-Regular.ttf. */ readonly file: string; readonly sha256: string; readonly license: string; readonly renamedFrom?: string }
36
+ export type ScriptFontRegistry = FontRegistry & {fontFiles:string[]; /** Set for `scripts: "auto"`. */ scriptSelection?: AppliedScriptSelection};
37
+ export type NodeFontRegistry = ScriptFontRegistry & {/** The embed "used" faces this registry holds, with the files a host copies to serve them itself. */ lazyFonts: readonly LazyFont[]};
38
+ export declare function loadBundledFontRegistry(options?: FontRegistryOptions & ScriptPackOptions): Promise<NodeFontRegistry>;
3
39
 
4
- export declare function loadOfficeFontRegistry(options?: FontRegistryOptions & {includeBaseFonts?:boolean}): Promise<FontRegistry & {fontFiles:string[]}>;
40
+ export declare function loadOfficeFontRegistry(options?: FontRegistryOptions & ScriptPackOptions & {includeBaseFonts?:boolean; /** Leave out the open families font schemes select (FF-31); default true. */ includeOpenFonts?:boolean}): Promise<NodeFontRegistry>;
5
41
 
42
+ export type BundledFontPackage = Readonly<{
43
+ /** An npm package name, or a plain id for a vendored entry of a git upstream. */
44
+ name:string;
45
+ /** The exact npm version, or for a git-upstream vendored entry the pinned upstream commit. */
46
+ version:string; pack:"base"|"office"|"open"|"scripts"; source:string;
47
+ /** FF-31: directory inside this package (for example `fonts/carlito`) that holds the vendored faces and notice (`file` and `licenseFile` are relative to it); absent for npm packages. */
48
+ vendored?: string;
49
+ /** "used": the package's faces are embedded in an SVG only when its text names the family, and browsers load them on demand (the open pack always does). */
50
+ embed?: "used";
51
+ /** An authored provenance and copyright notice in `vendored`, hash-pinned beside the license and appended to it. */
52
+ noticeFile?: string; noticeSha256?: string;
53
+ /** ISO 15924 scripts served by a script-pack package. */
54
+ scripts?: readonly string[];
55
+ license:string; licenseFile:string; licenseSha256:string;
56
+ /** Vendored from a git upstream: the commit-pinned URL of the notice. Vendored from npm: the notice's path in the npm package. */
57
+ upstreamLicenseUrl?:string; npmLicenseFile?:string;
58
+ /** The family this package is the renamed successor of; requests for the old name draw these faces, reported visual. */
59
+ renamedFrom?:string;
60
+ reservedFontNames:readonly string[]; upstream:string; copyright:string;
61
+ faces:readonly Readonly<{file:string; family:string; weight:number; italic:boolean; sha256:string;
62
+ /** FF-31: the byte-identical upstream release file this face is (sha256 equals the face's own). */
63
+ upstreamFile?: Readonly<{url:string; sha256:string}>;
64
+ /** FF-31: for a vendored npm-derived (instanced) face, its path in the npm package it was copied from. */
65
+ npmFile?: string}>[];
66
+ }>;
6
67
  export interface BundledFontManifest {
7
68
  readonly version: number;
8
- readonly packages: readonly Readonly<{
9
- name:string; version:string; pack:"base"|"office"; source:string;
10
- license:string; licenseFile:string; licenseSha256:string;
11
- faces:readonly Readonly<{file:string; family:string; weight:number; italic:boolean; sha256:string}>[];
12
- }>[];
69
+ readonly packages: readonly BundledFontPackage[];
13
70
  }
14
71
  export declare const BUNDLED_FONT_MANIFEST: BundledFontManifest;
72
+ export declare function scriptFontPackages(scripts: ScriptSelection): BundledFontPackage[];
15
73
  export interface PreparedNodeFonts {
16
- registry: FontRegistry & {fontFiles:string[]};
74
+ registry: NodeFontRegistry;
17
75
  manifest: BundledFontManifest;
18
76
  options: {
19
77
  textMeasurement: FontRegistry["textMeasurement"];
@@ -23,4 +81,4 @@ export interface PreparedNodeFonts {
23
81
  loadSystemFonts: false;
24
82
  };
25
83
  }
26
- export declare function prepareNodeFonts(options?: FontRegistryOptions & {pack?:"base"|"office"; includeBaseFonts?:boolean}): Promise<PreparedNodeFonts>;
84
+ export declare function prepareNodeFonts(options?: FontRegistryOptions & ScriptPackOptions & {pack?:"base"|"office"; includeBaseFonts?:boolean; /** With pack "office": leave out the open families (FF-31); default true. */ includeOpenFonts?:boolean; /** Embed script faces in SVG (large); default false. */ embedScriptFonts?:boolean;}): Promise<PreparedNodeFonts>;