@openpresentation/opf-render 0.11.3 → 0.11.5
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.
- package/README.md +8 -4
- package/dist/charts.js +367 -2
- package/dist/font-manifest.js +124 -0
- package/dist/fonts-browser.d.ts +56 -15
- package/dist/fonts-browser.js +24 -16
- package/dist/fonts-node.d.ts +6 -2
- package/dist/fonts-node.js +18 -14
- package/dist/fonts.js +84 -47
- package/dist/lazy-fonts.js +149 -21
- package/dist/script-font-pack.js +19 -7
- package/fonts/playfair-display/OFL.txt +93 -0
- package/fonts/playfair-display/PROVENANCE.json +48 -0
- package/fonts/playfair-display/PlayfairDisplay-Bold.ttf +0 -0
- package/fonts/playfair-display/PlayfairDisplay-BoldItalic.ttf +0 -0
- package/fonts/playfair-display/PlayfairDisplay-Italic.ttf +0 -0
- package/fonts/playfair-display/PlayfairDisplay-Regular.ttf +0 -0
- package/fonts/raleway/OFL.txt +93 -0
- package/fonts/raleway/PROVENANCE.json +48 -0
- package/fonts/raleway/Raleway-Bold.ttf +0 -0
- package/fonts/raleway/Raleway-BoldItalic.ttf +0 -0
- package/fonts/raleway/Raleway-Italic.ttf +0 -0
- package/fonts/raleway/Raleway-Regular.ttf +0 -0
- package/package.json +3 -3
package/dist/fonts-browser.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
FontRegistryOptions,
|
|
5
5
|
} from "./fonts.js";
|
|
6
6
|
import type { AutoScriptSelection, BundledFontPackage, LazyFont, ScriptSelection } from "./fonts-node.js";
|
|
7
|
+
import type { RenderSvgOptions } from "./svg.js";
|
|
7
8
|
export type { LazyFont } from "./fonts-node.js";
|
|
8
9
|
export type { AutoScriptSelection } from "./fonts-node.js";
|
|
9
10
|
export { detectPresentationScripts, autoScriptSelection } from "./fonts-node.js";
|
|
@@ -18,15 +19,22 @@ export interface BrowserFontRegistry extends FontRegistry {
|
|
|
18
19
|
/** Every vendored face a host can load on demand: family, style, package-relative file and pinned sha256. */
|
|
19
20
|
readonly lazyFonts: readonly LazyFont[];
|
|
20
21
|
/**
|
|
21
|
-
* Load the vendored faces (for example Intos for the default Aptos scheme) the
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* Load the vendored faces (for example Intos Display 700 and Intos 400 for a plain deck in the default Aptos scheme) the
|
|
23
|
+
* presentation draws, resolved as the registry resolves them under its `substitutionPolicy` (nothing is downloaded for a
|
|
24
|
+
* family the policy would not resolve, nor for a weight or style nothing draws), once each, from `lazyFontsBaseUrl`.
|
|
25
|
+
* Fetches and verifies every file, then adds the faces to the document and the registry together; on failure nothing
|
|
26
|
+
* changes and the call can be retried. Call it before measuring a document, and again after edits: an edit that adds a
|
|
27
|
+
* bold or italic run loads just that face. Besides `signal`, the call takes the `renderSvg` options the document resolves
|
|
28
|
+
* with (`catalogs`, ...) over the loader's `renderOptions`. A document that does not resolve rejects with the error
|
|
29
|
+
* `renderSvg` throws for it. After `dispose()` it rejects with `font-registry-disposed`.
|
|
30
|
+
*/
|
|
31
|
+
ensureLazyFonts(presentation: unknown, options?: { signal?: AbortSignal } & Partial<RenderSvgOptions>): Promise<LazyFont[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Synchronous: the vendored faces the presentation draws under the registry's policy that are not loaded yet. Empty means
|
|
34
|
+
* `ensureLazyFonts` fetches nothing. `renderOptions` are the `renderSvg` options the document resolves with (`catalogs`, ...)
|
|
35
|
+
* over the loader's `renderOptions`. Throws what `renderSvg` throws for a document that does not resolve.
|
|
26
36
|
*/
|
|
27
|
-
|
|
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[];
|
|
37
|
+
pendingLazyFonts(presentation: unknown, renderOptions?: Partial<RenderSvgOptions>): LazyFont[];
|
|
30
38
|
|
|
31
39
|
/** Load script faces for ISO 15924 codes (or "all") from `scriptBaseUrl`. Resolves with the newly loaded package names. */
|
|
32
40
|
loadScripts(scripts: ScriptSelection, options?: { signal?: AbortSignal }): Promise<string[]>;
|
|
@@ -39,9 +47,9 @@ export interface BrowserFontRegistry extends FontRegistry {
|
|
|
39
47
|
* 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
48
|
* `dispose()` it rejects with `font-registry-disposed`.
|
|
41
49
|
*/
|
|
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[];
|
|
50
|
+
ensureScripts(presentation: unknown, options?: { signal?: AbortSignal } & Partial<RenderSvgOptions>): Promise<AutoScriptSelection & { loaded: string[]; uncovered: string[] }>;
|
|
51
|
+
/** Synchronous: package names the presentation needs that are not loaded yet. Empty means `ensureScripts` fetches nothing, so a host can render immediately. Text decides, so a document is analyzed without resolving its layouts; `renderOptions.catalogs` matter for font schemes only the host's catalogs have (FF-41). */
|
|
52
|
+
pendingScripts(presentation: unknown, renderOptions?: Partial<RenderSvgOptions>): string[];
|
|
45
53
|
/** Names of the script-pack packages loaded so far. */
|
|
46
54
|
readonly loadedScriptPackages: string[];
|
|
47
55
|
}
|
|
@@ -60,6 +68,12 @@ export declare function loadBrowserFontRegistry(
|
|
|
60
68
|
scriptBaseUrl?: string;
|
|
61
69
|
/** 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
70
|
lazyFontsBaseUrl?: string;
|
|
71
|
+
/**
|
|
72
|
+
* FF-41: the `renderSvg` options the documents resolve with (`catalogs` above all), the default for `pendingLazyFonts`,
|
|
73
|
+
* `ensureLazyFonts`, `pendingScripts` and `ensureScripts`. A layout or font scheme id that only the host's catalogs have
|
|
74
|
+
* resolves the same way here as it does when the host renders. A call's own options win.
|
|
75
|
+
*/
|
|
76
|
+
renderOptions?: Partial<RenderSvgOptions>;
|
|
63
77
|
},
|
|
64
78
|
): Promise<BrowserFontRegistry>;
|
|
65
79
|
export declare function scriptFontPackages(scripts: ScriptSelection): BundledFontPackage[];
|
|
@@ -72,10 +86,37 @@ export declare function scriptFontEntries(
|
|
|
72
86
|
export declare function lazyFontList(): readonly LazyFont[];
|
|
73
87
|
/** Hash-pinned browser entries for the vendored faces, served by the host from `baseUrl` at their package-relative paths. */
|
|
74
88
|
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
89
|
/**
|
|
78
|
-
* The
|
|
90
|
+
* The font families a presentation's slides resolve (heading, body and code roles of every slide). `options` are the
|
|
91
|
+
* `renderSvg` options the document resolves with (`catalogs`, ...). A document that does not resolve throws what `renderSvg`
|
|
92
|
+
* throws for it (FF-41: it no longer returns an empty set). This is the role families, not what is drawn: see `presentationFaces`.
|
|
93
|
+
*/
|
|
94
|
+
export declare function presentationFamilies(presentation: unknown, options?: Partial<RenderSvgOptions>): Set<string>;
|
|
95
|
+
/** A face request or a drawn face: a family, a weight and a style. */
|
|
96
|
+
export interface FaceRequest { family: string; weight: number; italic: boolean }
|
|
97
|
+
/**
|
|
98
|
+
* The faces a preview of the presentation draws (FF-41), sorted by family, style and weight: the renderer's own layout and
|
|
99
|
+
* painting with a recording measurement, so heading and body roles, bold and italic runs, tables, furniture, chart labels and
|
|
100
|
+
* the code role (only where code is drawn), per-slide overrides and script slots are all followed. Without `registry` the
|
|
101
|
+
* faces are the styles as the document names them. With `registry` (`{faces, policy, aliases, fallbackFamily}`: the faces a
|
|
102
|
+
* registry holds or could hold and its substitution options) every style resolves as that registry resolves it, in the two
|
|
103
|
+
* steps drawing takes (role family, then weight within it), and the result is the faces the registry paints.
|
|
104
|
+
* `options` are the `renderSvg` options the document resolves with; a document that does not resolve throws what `renderSvg`
|
|
105
|
+
* throws for it.
|
|
106
|
+
*/
|
|
107
|
+
export declare function presentationFaces(presentation: unknown, options?: Partial<RenderSvgOptions>, registry?: { faces: Iterable<FaceRequest>; policy?: "none" | "metric" | "visual"; aliases?: ReadonlyMap<string, string>; fallbackFamily?: string }): FaceRequest[];
|
|
108
|
+
/**
|
|
109
|
+
* The vendored faces (of `lazy`) a presentation draws that the registry does not hold. `held` is `registry.describeFaces()`,
|
|
110
|
+
* `loaded` the lazy files already loaded; `policy`, `aliases` and `fallbackFamily` are the registry's. Face level: only the
|
|
111
|
+
* drawn faces, so an edit adding a bold run adds one face.
|
|
112
|
+
*/
|
|
113
|
+
export declare function lazyFacesNeeded(presentation: unknown, renderOptions: Partial<RenderSvgOptions> | undefined, context: { lazy: readonly LazyFont[]; held: Iterable<FaceRequest>; loaded?: ReadonlySet<string>; policy?: "none" | "metric" | "visual"; aliases?: ReadonlyMap<string, string>; fallbackFamily?: string }): LazyFont[];
|
|
114
|
+
/**
|
|
115
|
+
* The vendored faces a list of requests needs that the registry does not hold, following the registry's own order and
|
|
79
116
|
* `policy` (default "visual"): the family itself, an alias target, the declared replacement, then its alternates.
|
|
117
|
+
* Entries are face requests (`presentationFaces`), resolved as the registry resolves them with every vendored face loaded
|
|
118
|
+
* over `held` (`registry.describeFaces()`), so only the faces asked for are returned and a partially loaded family still
|
|
119
|
+
* loads its missing faces; or family names, which mean every face of the family. Without `held` a face request falls back to
|
|
120
|
+
* family level with `hasFamily`.
|
|
80
121
|
*/
|
|
81
|
-
export declare function lazyFontsFor(
|
|
122
|
+
export declare function lazyFontsFor(entries: Iterable<string | { family: string; weight?: number; italic?: boolean }>, context: { lazy: readonly LazyFont[]; held?: Iterable<FaceRequest>; hasFamily?: (family: string) => boolean; loaded?: ReadonlySet<string>; policy?: "none" | "metric" | "visual"; aliases?: ReadonlyMap<string, string>; fallbackFamily?: string }): LazyFont[];
|
package/dist/fonts-browser.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createFontRegistry, OPFFontError } from "./fonts.js";
|
|
2
2
|
import { lazyFontEntries, lazyFontList } from "./lazy-font-list.js";
|
|
3
|
-
import {
|
|
3
|
+
import { lazyFacesNeeded } from "./lazy-fonts.js";
|
|
4
4
|
import { analyzePresentationScripts, nextFallbackPackage, scriptFontPackages, scriptPackageEntries, scriptSelectionOf, uncoveredCjkCharacters } from "./script-font-pack.js";
|
|
5
5
|
export { autoScriptSelection, detectPresentationScripts, scriptFontEntries, scriptFontPackages } from "./script-font-pack.js";
|
|
6
6
|
export { lazyFontEntries, lazyFontList } from "./lazy-font-list.js";
|
|
7
|
-
export { lazyFontsFor, presentationFamilies } from "./lazy-fonts.js";
|
|
7
|
+
export { lazyFacesNeeded, lazyFontsFor, presentationFaces, presentationFamilies } from "./lazy-fonts.js";
|
|
8
8
|
|
|
9
9
|
async function verifyDigest(entry, data, subtle) {
|
|
10
10
|
if (entry.sha256 === undefined) return;
|
|
@@ -91,6 +91,9 @@ export async function loadBrowserFontRegistry(entries, options = {}) {
|
|
|
91
91
|
// Script faces (FF-19) are loaded lazily, once a document needs them, and only for the scripts it uses.
|
|
92
92
|
const scriptPackages = new Set();
|
|
93
93
|
let queue = Promise.resolve(), disposed = false;
|
|
94
|
+
// FF-41: the `renderSvg` options a document resolves with (`catalogs`, ...). `options.renderOptions` are the defaults; a call's own
|
|
95
|
+
// options (everything but `signal`) are added over them, so a host that passes its canvas options gets the same resolution here.
|
|
96
|
+
const renderOptionsFor = (callOptions) => { const { signal: _signal, ...own } = callOptions ?? {}; return { ...options.renderOptions, ...own }; };
|
|
94
97
|
const gone = () => new OPFFontError("font-registry-disposed", "The font registry was disposed.");
|
|
95
98
|
const baseUrl = () => {
|
|
96
99
|
if (typeof options.scriptBaseUrl !== "string" || !options.scriptBaseUrl)
|
|
@@ -142,14 +145,15 @@ export async function loadBrowserFontRegistry(entries, options = {}) {
|
|
|
142
145
|
* fallback needs for characters the loaded faces lack. Cheap when nothing new is needed; call it after edits and
|
|
143
146
|
* render again afterwards. Resolves with the detected scripts, the packages loaded by this call, the scripts no
|
|
144
147
|
* pinned font serves and `uncovered`, drawn CJK characters no loaded face covers (the renderer reports
|
|
145
|
-
* `missing-glyph` for them).
|
|
148
|
+
* `missing-glyph` for them). Besides `signal`, the call takes the `renderSvg` options the document resolves with
|
|
149
|
+
* (`catalogs`, ...) over the loader's `renderOptions`. Each package loads all or nothing. If some package fails, the others still load: the
|
|
146
150
|
* call then rejects with an OPFFontError (the first failure's code) whose `details` hold `loaded` (packages this
|
|
147
151
|
* call did load) and `failed` (`{package, code, message}` per failed package); nothing is lost and a later call
|
|
148
152
|
* retries only the failures. `signal` aborts this call's fetches.
|
|
149
153
|
*/
|
|
150
154
|
async ensureScripts(presentation, callOptions) {
|
|
151
155
|
if (disposed) throw gone();
|
|
152
|
-
const analysis = analyzePresentationScripts(presentation), selection = scriptSelectionOf(analysis);
|
|
156
|
+
const analysis = analyzePresentationScripts(presentation, undefined, renderOptionsFor(callOptions)), selection = scriptSelectionOf(analysis);
|
|
153
157
|
const loadedNow = [], failed = [];
|
|
154
158
|
const attempt = async (item) => {
|
|
155
159
|
try { loadedNow.push(...(await loadPackageList([item], callOptions))); }
|
|
@@ -166,9 +170,9 @@ export async function loadBrowserFontRegistry(entries, options = {}) {
|
|
|
166
170
|
}
|
|
167
171
|
return { ...selection, loaded: loadedNow, uncovered: uncoveredCjkCharacters(analysis, { covers }) };
|
|
168
172
|
},
|
|
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);
|
|
173
|
+
/** Synchronous: script-pack packages the presentation needs that are not loaded yet. Empty means `ensureScripts` would fetch nothing. `renderOptions` are the `renderSvg` options the document resolves with (`catalogs`, ...). */
|
|
174
|
+
pendingScripts(presentation, renderOptions) {
|
|
175
|
+
const analysis = analyzePresentationScripts(presentation, undefined, renderOptionsFor(renderOptions));
|
|
172
176
|
const primary = scriptFontPackages(scriptSelectionOf(analysis).scripts).map((item) => item.name).filter((name) => !scriptPackages.has(name));
|
|
173
177
|
if (primary.length) return primary;
|
|
174
178
|
const next = nextFallbackPackage(analysis, { covers, loaded: scriptPackages });
|
|
@@ -181,8 +185,9 @@ export async function loadBrowserFontRegistry(entries, options = {}) {
|
|
|
181
185
|
const lazy = lazyFontList(), lazyLoaded = new Set();
|
|
182
186
|
const policy = options.substitutionPolicy ?? "none";
|
|
183
187
|
const aliasTargets = new Map(Object.entries(options.aliases ?? {}).map(([from, to]) => [from.toLowerCase(), to]));
|
|
184
|
-
|
|
185
|
-
|
|
188
|
+
// Face level (FF-41): the faces the document draws, resolved as the registry resolves them with every vendored face loaded.
|
|
189
|
+
// A document that does not resolve throws what `renderSvg` throws for it.
|
|
190
|
+
const neededLazy = (presentation, callOptions) => lazyFacesNeeded(presentation, renderOptionsFor(callOptions), { lazy, held: registry.describeFaces(), loaded: lazyLoaded, policy, aliases: aliasTargets, fallbackFamily: options.fallbackFamily });
|
|
186
191
|
const lazyBaseUrl = () => {
|
|
187
192
|
if (typeof options.lazyFontsBaseUrl !== "string" || !options.lazyFontsBaseUrl)
|
|
188
193
|
throw new OPFFontError("invalid-font-source", "Loading vendored fonts requires lazyFontsBaseUrl, where the host serves the package's fonts directory.");
|
|
@@ -192,16 +197,19 @@ export async function loadBrowserFontRegistry(entries, options = {}) {
|
|
|
192
197
|
/** Every vendored face a host can load on demand: family, style, package-relative file and pinned sha256. */
|
|
193
198
|
lazyFonts: lazy,
|
|
194
199
|
/**
|
|
195
|
-
* Load the vendored faces the presentation
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
200
|
+
* Load the vendored faces the presentation draws (FF-41: only the faces, by family, weight and style, that the renderer
|
|
201
|
+
* draws; a bold or italic run added by an edit adds just that face) as the registry resolves them under its substitution
|
|
202
|
+
* policy (nothing is downloaded for a family the policy would not resolve), once each. All or nothing: on any failure
|
|
203
|
+
* (fetch, hash, FontFace.load, registry) the document and registry are unchanged and the call can be retried. Cheap when
|
|
204
|
+
* nothing is needed. Call it before measuring a document, and again after edits that change fonts. Resolves with the faces
|
|
205
|
+
* added. Besides `signal`, the call takes the `renderSvg` options the document resolves with (`catalogs`, ...) over the
|
|
206
|
+
* loader's `renderOptions`; a document that does not resolve rejects with what `renderSvg` throws for it.
|
|
199
207
|
*/
|
|
200
208
|
ensureLazyFonts(presentation, callOptions = {}) {
|
|
201
209
|
const result = queue.then(async () => {
|
|
202
210
|
if (disposed) throw gone();
|
|
203
211
|
callOptions.signal?.throwIfAborted?.();
|
|
204
|
-
const pending = neededLazy(presentation);
|
|
212
|
+
const pending = neededLazy(presentation, callOptions);
|
|
205
213
|
if (!pending.length) return [];
|
|
206
214
|
const fetched = (await fetchFaces(lazyFontEntries({ baseUrl: lazyBaseUrl() }, pending), callOptions.signal)).map((entry) => ({ ...entry, embed: "used" }));
|
|
207
215
|
let browserFaces;
|
|
@@ -229,8 +237,8 @@ export async function loadBrowserFontRegistry(entries, options = {}) {
|
|
|
229
237
|
queue = result.catch(() => {});
|
|
230
238
|
return result;
|
|
231
239
|
},
|
|
232
|
-
/** Synchronous: the vendored faces the presentation
|
|
233
|
-
pendingLazyFonts: (presentation) => neededLazy(presentation),
|
|
240
|
+
/** Synchronous: the vendored faces the presentation draws under the registry's policy that are not loaded yet. Empty means `ensureLazyFonts` fetches nothing. `renderOptions` are the `renderSvg` options the document resolves with (`catalogs`, ...). */
|
|
241
|
+
pendingLazyFonts: (presentation, renderOptions) => neededLazy(presentation, renderOptions),
|
|
234
242
|
});
|
|
235
243
|
/** Names of the script-pack packages loaded so far. */
|
|
236
244
|
Object.defineProperty(registry, "loadedScriptPackages", { get: () => [...scriptPackages], enumerable: true });
|
package/dist/fonts-node.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {FontRegistry,FontRegistryOptions,EmbeddedFont,FontFaceInput,ScriptFontProfile} from "./fonts.js";
|
|
2
|
+
import type {RenderSvgOptions} from "./svg.js";
|
|
2
3
|
/** A caller-supplied face (FF-31), for example a licensed copy of the real font. Node callers may pass a file path. */
|
|
3
4
|
export type CallerFontFace = FontFaceInput | (Omit<FontFaceInput, "data"> & {path: string});
|
|
4
5
|
/** "all" or ISO 15924 codes (`Jpan`, `Hans`, `Hant`, `Kore`, `Arab`, `Hebr`, `Deva`, `Thai`, ...). */
|
|
@@ -15,8 +16,9 @@ export interface AutoScriptSelection {
|
|
|
15
16
|
/** Script-pack packages that `scripts: "auto"` loaded or found not installed. */
|
|
16
17
|
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
18
|
/** Script keys whose faces a preview of the presentation needs: text decides, the document language only tells Han scripts apart. */
|
|
18
|
-
|
|
19
|
-
export declare function
|
|
19
|
+
/** `options` may also carry the `renderSvg` options the document resolves with (`catalogs`): a font scheme that exists only in the host's catalogs and names a script font selects that font's script (FF-41). */
|
|
20
|
+
export declare function detectPresentationScripts(presentation: unknown, options?: { profile?: ScriptFontProfile } & Partial<RenderSvgOptions>): string[];
|
|
21
|
+
export declare function autoScriptSelection(presentation: unknown, options?: { profile?: ScriptFontProfile } & Partial<RenderSvgOptions>): AutoScriptSelection;
|
|
20
22
|
export interface ScriptPackOptions {
|
|
21
23
|
/** Caller-supplied faces, loaded first so a licensed real font resolves as an exact face (FF-31). */
|
|
22
24
|
faces?: readonly CallerFontFace[];
|
|
@@ -28,6 +30,8 @@ export interface ScriptPackOptions {
|
|
|
28
30
|
scripts?: ScriptSelection | "auto";
|
|
29
31
|
/** The presentation whose text decides `scripts: "auto"`. */
|
|
30
32
|
presentation?: unknown;
|
|
33
|
+
/** The `renderSvg` options the presentation resolves with (`catalogs`, ...), for the font schemes `scripts: "auto"` reads (FF-41). */
|
|
34
|
+
renderOptions?: Partial<RenderSvgOptions>;
|
|
31
35
|
/** Receives `script-font-unavailable` and `script-font-not-installed` for `scripts: "auto"`. */
|
|
32
36
|
onDiagnostic?: (diagnostic: {code: string; message: string; script?: string; package?: string; scripts?: string[]}) => void;
|
|
33
37
|
}
|
package/dist/fonts-node.js
CHANGED
|
@@ -7,7 +7,7 @@ import { createFontRegistry, OPFFontError } from "./fonts.js";
|
|
|
7
7
|
import { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
8
8
|
import { lazyFontList } from "./lazy-font-list.js";
|
|
9
9
|
export { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
10
|
-
import { analyzePresentationScripts, nextFallbackPackage, scriptFontPackages, scriptSelectionOf, uncoveredCjkCharacters } from "./script-font-pack.js";
|
|
10
|
+
import { analyzePresentationScripts, autoScriptSelection, nextFallbackPackage, scriptFontPackages, scriptSelectionOf, uncoveredCjkCharacters } from "./script-font-pack.js";
|
|
11
11
|
export { autoScriptSelection, detectPresentationScripts, scriptFontPackages } from "./script-font-pack.js";
|
|
12
12
|
// Script faces are embedded in a standalone SVG only when the slide's text uses their family.
|
|
13
13
|
const embedUsed = entries => entries.map(entry => ({...entry, embed: "used"}));
|
|
@@ -78,9 +78,10 @@ const loadPack = pack => loadPackages(BUNDLED_FONT_MANIFEST.packages.filter(item
|
|
|
78
78
|
/**
|
|
79
79
|
* Adds the script pack. `scripts` is 'all', ISO 15924 codes, or 'auto' (FF-19): detect the scripts the
|
|
80
80
|
* `presentation` draws and load only their faces. Auto never fails on a missing optional package or a
|
|
81
|
-
* script no pinned font serves; it reports them through `onDiagnostic` and `selection`.
|
|
81
|
+
* script no pinned font serves; it reports them through `onDiagnostic` and `selection`. `renderOptions` are the host's `renderSvg`
|
|
82
|
+
* options (see `analyzePresentationScripts`).
|
|
82
83
|
*/
|
|
83
|
-
async function withScripts(loaded, scripts, {presentation, onDiagnostic} = {}) {
|
|
84
|
+
async function withScripts(loaded, scripts, {presentation, onDiagnostic, renderOptions} = {}) {
|
|
84
85
|
if (scripts === undefined || (Array.isArray(scripts) && !scripts.length)) return loaded;
|
|
85
86
|
// A package the registry already loaded (the default Noto Sans fallback) is never loaded twice.
|
|
86
87
|
const notLoaded = list => list.filter(pkg => !loaded.packages?.includes(pkg.name));
|
|
@@ -89,7 +90,7 @@ async function withScripts(loaded, scripts, {presentation, onDiagnostic} = {}) {
|
|
|
89
90
|
return {...loaded, entries:[...loaded.entries, ...embedUsed(extra.entries)], fontFiles:[...loaded.fontFiles, ...extra.fontFiles], packages:[...(loaded.packages ?? []), ...extra.packages]};
|
|
90
91
|
}
|
|
91
92
|
if (presentation === null || typeof presentation !== "object") throw new OPFFontError("invalid-font-scripts", "scripts: 'auto' needs the presentation whose text decides the scripts.", {scripts});
|
|
92
|
-
const selection = scriptSelectionOf(analyzePresentationScripts(presentation)), skipped = [];
|
|
93
|
+
const selection = scriptSelectionOf(analyzePresentationScripts(presentation, undefined, renderOptions)), skipped = [];
|
|
93
94
|
const extra = await loadPackages(notLoaded(scriptFontPackages(selection.scripts)), skipped);
|
|
94
95
|
for (const script of selection.unavailable) onDiagnostic?.({code: "script-font-unavailable", script, message: `No pinned open font serves script '${script}'; that text uses the design font.`});
|
|
95
96
|
for (const item of skipped) onDiagnostic?.({code: "script-font-not-installed", package: item.package, scripts: item.scripts, message: `Install ${item.package}@${item.version} to preview ${item.scripts.join(", ")} text with its designated open font.`});
|
|
@@ -102,12 +103,12 @@ async function withScripts(loaded, scripts, {presentation, onDiagnostic} = {}) {
|
|
|
102
103
|
* has (Japanese-only kanji in a Simplified Chinese run, hanja) needs the next CJK face along the fallback chain.
|
|
103
104
|
* Adds those packages to the registry and its fontFiles until every such character is covered or the chain ends.
|
|
104
105
|
*/
|
|
105
|
-
async function completeFallback(registry, presentation, loaded, onDiagnostic) {
|
|
106
|
+
async function completeFallback(registry, presentation, loaded, onDiagnostic, renderOptions) {
|
|
106
107
|
const selection = loaded.selection;
|
|
107
108
|
if (!selection) return;
|
|
108
109
|
const seen = new Set([...selection.packages, ...selection.notInstalled]);
|
|
109
110
|
for (;;) {
|
|
110
|
-
const analysis = analyzePresentationScripts(presentation), covers = character => registry.scriptFacesCover(character);
|
|
111
|
+
const analysis = analyzePresentationScripts(presentation, undefined, renderOptions), covers = character => registry.scriptFacesCover(character);
|
|
111
112
|
const next = nextFallbackPackage(analysis, {covers, loaded: seen});
|
|
112
113
|
if (!next) {
|
|
113
114
|
const uncovered = uncoveredCjkCharacters(analysis, {covers, limit: 8});
|
|
@@ -152,17 +153,17 @@ async function withFaces(loaded, faces) {
|
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
/** Bundled, openly licensed faces. No system font discovery or network requests. */
|
|
155
|
-
export async function loadBundledFontRegistry({scripts, faces, presentation, onDiagnostic, ...options} = {}) {
|
|
156
|
-
const {entries, fontFiles, selection} = await withFaces(await withScripts(await loadPack("base"), scripts, {presentation, onDiagnostic}), faces);
|
|
156
|
+
export async function loadBundledFontRegistry({scripts, faces, presentation, onDiagnostic, renderOptions, ...options} = {}) {
|
|
157
|
+
const {entries, fontFiles, selection} = await withFaces(await withScripts(await loadPack("base"), scripts, {presentation, onDiagnostic, renderOptions}), faces);
|
|
157
158
|
const registry = Object.assign(createFontRegistry(entries,options),{fontFiles, ...(selection ? {scriptSelection: selection} : {})});
|
|
158
159
|
registry.lazyFonts = lazyOf(registry);
|
|
159
|
-
await completeFallback(registry, presentation, {fontFiles, selection}, onDiagnostic);
|
|
160
|
+
await completeFallback(registry, presentation, {fontFiles, selection}, onDiagnostic, renderOptions);
|
|
160
161
|
return registry;
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
/** Office substitutes plus the open families that font schemes select (FF-31),
|
|
164
165
|
* optionally alongside the base Roboto pack. `includeOpenFonts: false` leaves the open families out. */
|
|
165
|
-
export async function loadOfficeFontRegistry({scripts, faces, presentation, onDiagnostic, ...options} = {}) {
|
|
166
|
+
export async function loadOfficeFontRegistry({scripts, faces, presentation, onDiagnostic, renderOptions, ...options} = {}) {
|
|
166
167
|
const {entries, fontFiles, packages} = await loadPack("office");
|
|
167
168
|
for (const [include, pack] of [[options.includeOpenFonts, "open"], [options.includeBaseFonts, "base"]]) {
|
|
168
169
|
if (include === false) continue;
|
|
@@ -174,17 +175,20 @@ export async function loadOfficeFontRegistry({scripts, faces, presentation, onDi
|
|
|
174
175
|
// Noto Sans (regular, bold, italic, bold italic) is the default Latin, Cyrillic and Greek fallback face
|
|
175
176
|
// (glyph fallback), so Georgia with Russian text previews without the scripts option. It is embedded in an
|
|
176
177
|
// SVG only when the slide's text draws it (embed "used"); raster output reads it from fontFiles. Unless the
|
|
177
|
-
// caller asked for it with scripts it is fallback-only, so no other family's preview changes.
|
|
178
|
-
|
|
178
|
+
// caller asked for it with scripts it is fallback-only, so no other family's preview changes. "auto" counts as asking for it when the
|
|
179
|
+
// presentation itself selects Latn (a Sylfaen scheme: Noto Sans is its designated replacement, which a fallback-only face never is), as
|
|
180
|
+
// an explicit list does; a presentation that does not select Latn keeps it fallback-only.
|
|
181
|
+
const requested = scripts === "auto" && presentation !== null && typeof presentation === "object" ? scriptFontPackages(autoScriptSelection(presentation, renderOptions).scripts).map(pkg => pkg.name)
|
|
182
|
+
: scripts === undefined || scripts === "auto" || (Array.isArray(scripts) && !scripts.length) ? [] : scriptFontPackages(scripts).map(pkg => pkg.name);
|
|
179
183
|
const fallback = await loadPackages(scriptFontPackages(["Latn"]).filter(pkg => !requested.includes(pkg.name)), undefined, {fallbackOnly:true});
|
|
180
184
|
fontFiles.push(...fallback.fontFiles);
|
|
181
185
|
entries.push(...fallback.entries);
|
|
182
186
|
packages.push(...fallback.packages);
|
|
183
|
-
const loaded = await withFaces(await withScripts({entries, fontFiles, packages}, scripts, {presentation, onDiagnostic}), faces);
|
|
187
|
+
const loaded = await withFaces(await withScripts({entries, fontFiles, packages}, scripts, {presentation, onDiagnostic, renderOptions}), faces);
|
|
184
188
|
const aliases = options.includeOpenFonts === false ? options.aliases : {...renamedAliases(), ...options.aliases};
|
|
185
189
|
const registry = Object.assign(createFontRegistry(loaded.entries,{substitutionPolicy:"metric",...options,...(aliases?{aliases}:{})}),{fontFiles:loaded.fontFiles, ...(loaded.selection ? {scriptSelection: loaded.selection} : {})});
|
|
186
190
|
registry.lazyFonts = lazyOf(registry);
|
|
187
|
-
await completeFallback(registry, presentation, loaded, onDiagnostic);
|
|
191
|
+
await completeFallback(registry, presentation, loaded, onDiagnostic, renderOptions);
|
|
188
192
|
return registry;
|
|
189
193
|
}
|
|
190
194
|
|
package/dist/fonts.js
CHANGED
|
@@ -45,6 +45,83 @@ function unavailableFontError(family, style, policy) {
|
|
|
45
45
|
return new OPFFontError("font-unavailable", `No local font face for '${family}'. ${owner} None of its replacements (${candidates.join(", ")}) is loaded${packs.length ? `; load the ${packs.map(pack=>"'"+pack+"'").join(" or ")} font pack` : ""}, or ${hook}. The PPTX names '${family}' either way.`, details);
|
|
46
46
|
}
|
|
47
47
|
/** Local font files only. The caller explicitly chooses aliases and fallback. */
|
|
48
|
+
// fontkit 2.0.4 cannot decode every OpenType lookup: its GSUB type 8 (reverse chaining contextual single substitution) struct lacks the
|
|
49
|
+
// backtrackGlyphCount field, so the lookup fails with "Not a fixed size" when a feature that references it is applied, and it has no processor
|
|
50
|
+
// for the type anyway. Noto Sans Mongolian 3.x has such a lookup under calt and rclt in every script, so every shaping of the face threw (FF-44).
|
|
51
|
+
// A lookup that cannot be decoded is treated as one that applies nothing (no subtables), so the rest of the feature still runs: advances equal
|
|
52
|
+
// a browser's for the face's Latin and Mongolian samples (test/mongolian-shaping.mjs). Applied only after shaping failed, once per font.
|
|
53
|
+
const lookupsGuarded = new WeakSet();
|
|
54
|
+
function skipUndecodableLookups(font) {
|
|
55
|
+
if (lookupsGuarded.has(font)) return false;
|
|
56
|
+
lookupsGuarded.add(font);
|
|
57
|
+
let guarded = false;
|
|
58
|
+
for (const tag of ["GSUB", "GPOS"]) {
|
|
59
|
+
let list;
|
|
60
|
+
try { list = font[tag]?.lookupList; } catch { continue; }
|
|
61
|
+
if (typeof list?.get !== "function") continue;
|
|
62
|
+
const get = list.get.bind(list);
|
|
63
|
+
list.get = index => { try { return get(index); } catch { return { lookupType: 1, flags: {}, subTables: [] }; } };
|
|
64
|
+
guarded = true;
|
|
65
|
+
}
|
|
66
|
+
return guarded;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The face a registry draws for `family` (a validated concrete family: theme tokens are already resolved) in `style`, chosen
|
|
71
|
+
* from `faces` ({family, familyGroup, weight, italic, fallbackOnly?, scripts?}) under the substitution policy, aliases and
|
|
72
|
+
* fallback family, or an OPFFontError when none applies. Shared by the registry's resolution and by the lazy loader's face
|
|
73
|
+
* planning (lazy-fonts.js), so the browser loads exactly the faces Node would draw with everything loaded.
|
|
74
|
+
*/
|
|
75
|
+
export function pickFace(faces, family, style, {policy = "none", aliases = new Map(), fallbackFamily} = {}) {
|
|
76
|
+
const findFamily = name => faces.filter(face=>face.family.toLowerCase()===name.toLowerCase() || face.familyGroup.toLowerCase()===name.toLowerCase());
|
|
77
|
+
const weight = style.fontWeight ?? 400;
|
|
78
|
+
let matching = findFamily(family), compatibility = "exact", rule, targetWeight = weight, styleFallback = false, via = "family";
|
|
79
|
+
const encodedSymbol = /^(wingdings(?: [23])?|webdings|symbol)$/i.test(family);
|
|
80
|
+
if (!matching.length && encodedSymbol) throw new OPFFontError("font-encoding-required", `Font '${family}' requires character mapping before substitution.`, {path:style.path,fontFamily:family});
|
|
81
|
+
if (!matching.length && aliases.has(family.toLowerCase())) {
|
|
82
|
+
matching = findFamily(aliases.get(family.toLowerCase())); compatibility = "visual"; via = "alias";
|
|
83
|
+
}
|
|
84
|
+
if (!matching.length && policy!=="none") {
|
|
85
|
+
const candidate = FONT_COMPATIBILITY.find(entry=>entry.requestedFamily.toLowerCase()===family.toLowerCase());
|
|
86
|
+
const tier = candidate?.compatibility==="metric" && !candidate.weights.includes(weight) ? "visual" : candidate?.compatibility;
|
|
87
|
+
// A declared visual replacement is used in visual mode, and in metric mode only when the
|
|
88
|
+
// policy keeps it as a metric-mode fallback (Cambria -> Caladea); it is still reported visual.
|
|
89
|
+
// In metric mode the fallback applies only where the pre-FF-31 metric rule did: the declared
|
|
90
|
+
// replacement at weights 400 and 700, normal or italic, with an exact face weight.
|
|
91
|
+
const metricFallback = policy!=="visual" && candidate?.metricModeFallback===true && [400,700].includes(weight);
|
|
92
|
+
const allowVisual = policy==="visual" || metricFallback;
|
|
93
|
+
if (candidate && (allowVisual || tier==="metric")) {
|
|
94
|
+
// A family that names its weight (Segoe UI Semibold, Arial Black) selects that weight in
|
|
95
|
+
// the replacement; its bold style link still selects bold (FF-31).
|
|
96
|
+
const wanted = candidate.weight ? (weight>=600 ? Math.max(candidate.weight,700) : candidate.weight) : weight;
|
|
97
|
+
for (const [index, substitute] of candidate.substitutes.entries()) {
|
|
98
|
+
// Only the declared replacement can carry the row's metric claim; an alternate is visual.
|
|
99
|
+
const substituteTier = index===0 ? tier : "visual";
|
|
100
|
+
if (substituteTier==="visual" && !(policy==="visual" || metricFallback && index===0)) continue;
|
|
101
|
+
const exactWeight = substituteTier==="metric" || policy!=="visual";
|
|
102
|
+
const pool = findFamily(substitute).filter(face=>!face.fallbackOnly && (!exactWeight || face.weight===weight));
|
|
103
|
+
let available = pool.filter(face=>face.italic===!!style.italic);
|
|
104
|
+
// Visual replacements without the requested style draw the other one and say so.
|
|
105
|
+
if (!available.length && substituteTier==="visual" && policy==="visual" && pool.length) { available = pool.filter(face=>!face.italic); styleFallback = available.length>0; }
|
|
106
|
+
if (available.length) { matching=available; compatibility=substituteTier; rule={...candidate, substituteIndex:index}; targetWeight=wanted; via="replacement"; break; }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Equations require an explicit math-aware choice; never fall through to body text.
|
|
111
|
+
if (!matching.length && /^(cambria math|stix two math|noto sans math)$/i.test(family)) throw new OPFFontError("math-font-required", `Supply '${family}' or an explicit math-font alias.`, {path:style.path});
|
|
112
|
+
if (!matching.length && fallbackFamily) { matching=findFamily(fallbackFamily); compatibility="generic"; via="fallback"; }
|
|
113
|
+
if (!matching.length) throw unavailableFontError(family, style, policy);
|
|
114
|
+
const styled = styleFallback ? matching : matching.filter(face=>face.italic===!!style.italic);
|
|
115
|
+
// Script replacement faces have no italics; use upright glyphs and advances.
|
|
116
|
+
if (!styled.length && style.italic && matching.length && matching.every(face=>face.scripts)) { if (compatibility!=="generic") compatibility="visual"; }
|
|
117
|
+
else matching = styled;
|
|
118
|
+
if (!matching.length) throw new OPFFontError("font-style-unavailable", `No ${style.italic ? "italic" : "upright"} face for '${family}'.`, {path:style.path});
|
|
119
|
+
matching.sort((a,b)=>Math.abs(a.weight-targetWeight)-Math.abs(b.weight-targetWeight) || a.weight-b.weight);
|
|
120
|
+
const face = matching[0];
|
|
121
|
+
if ((face.weight!==targetWeight || styleFallback) && compatibility!=="generic") compatibility="visual";
|
|
122
|
+
return {face, compatibility, rule, styleFallback, via};
|
|
123
|
+
}
|
|
124
|
+
|
|
48
125
|
export function createFontRegistry(entries, options = {}) {
|
|
49
126
|
if (!Array.isArray(entries) || !entries.length) throw new OPFFontError("empty-font-registry", "Supply at least one font file.");
|
|
50
127
|
const makeFace = entry => {
|
|
@@ -106,7 +183,6 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
106
183
|
const policy = options.substitutionPolicy ?? "none";
|
|
107
184
|
if (!["none","metric","visual"].includes(policy)) throw new OPFFontError("invalid-font-policy", "substitutionPolicy must be none, metric, or visual.");
|
|
108
185
|
if (options.fallbackFamily) validFamily(options.fallbackFamily);
|
|
109
|
-
const findFamily = family => faces.filter(face=>face.family.toLowerCase()===family.toLowerCase() || face.familyGroup.toLowerCase()===family.toLowerCase());
|
|
110
186
|
const resolve = style => {
|
|
111
187
|
const requested = validFamily(style?.fontFamily);
|
|
112
188
|
const weight = style.fontWeight ?? 400;
|
|
@@ -117,50 +193,7 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
117
193
|
const family = themeKey ? options.themeFonts?.[themeKey] : requested;
|
|
118
194
|
if (!family || family.startsWith("+")) throw new OPFFontError("unresolved-theme-font", `Supply a concrete theme family for '${requested}'.`, {path:style.path});
|
|
119
195
|
validFamily(family);
|
|
120
|
-
|
|
121
|
-
const encodedSymbol = /^(wingdings(?: [23])?|webdings|symbol)$/i.test(family);
|
|
122
|
-
if (!matching.length && encodedSymbol) throw new OPFFontError("font-encoding-required", `Font '${family}' requires character mapping before substitution.`, {path:style.path,fontFamily:family});
|
|
123
|
-
if (!matching.length && aliases.has(family.toLowerCase())) {
|
|
124
|
-
matching = findFamily(aliases.get(family.toLowerCase())); compatibility = "visual"; via = "alias";
|
|
125
|
-
}
|
|
126
|
-
if (!matching.length && policy!=="none") {
|
|
127
|
-
const candidate = FONT_COMPATIBILITY.find(entry=>entry.requestedFamily.toLowerCase()===family.toLowerCase());
|
|
128
|
-
const tier = candidate?.compatibility==="metric" && !candidate.weights.includes(weight) ? "visual" : candidate?.compatibility;
|
|
129
|
-
// A declared visual replacement is used in visual mode, and in metric mode only when the
|
|
130
|
-
// policy keeps it as a metric-mode fallback (Cambria -> Caladea); it is still reported visual.
|
|
131
|
-
// In metric mode the fallback applies only where the pre-FF-31 metric rule did: the declared
|
|
132
|
-
// replacement at weights 400 and 700, normal or italic, with an exact face weight.
|
|
133
|
-
const metricFallback = policy!=="visual" && candidate?.metricModeFallback===true && [400,700].includes(weight);
|
|
134
|
-
const allowVisual = policy==="visual" || metricFallback;
|
|
135
|
-
if (candidate && (allowVisual || tier==="metric")) {
|
|
136
|
-
// A family that names its weight (Segoe UI Semibold, Arial Black) selects that weight in
|
|
137
|
-
// the replacement; its bold style link still selects bold (FF-31).
|
|
138
|
-
const wanted = candidate.weight ? (weight>=600 ? Math.max(candidate.weight,700) : candidate.weight) : weight;
|
|
139
|
-
for (const [index, substitute] of candidate.substitutes.entries()) {
|
|
140
|
-
// Only the declared replacement can carry the row's metric claim; an alternate is visual.
|
|
141
|
-
const substituteTier = index===0 ? tier : "visual";
|
|
142
|
-
if (substituteTier==="visual" && !(policy==="visual" || metricFallback && index===0)) continue;
|
|
143
|
-
const exactWeight = substituteTier==="metric" || policy!=="visual";
|
|
144
|
-
const faces = findFamily(substitute).filter(face=>!face.fallbackOnly && (!exactWeight || face.weight===weight));
|
|
145
|
-
let available = faces.filter(face=>face.italic===!!style.italic);
|
|
146
|
-
// Visual replacements without the requested style draw the other one and say so.
|
|
147
|
-
if (!available.length && substituteTier==="visual" && policy==="visual" && faces.length) { available = faces.filter(face=>!face.italic); styleFallback = available.length>0; }
|
|
148
|
-
if (available.length) { matching=available; compatibility=substituteTier; rule={...candidate, substituteIndex:index}; targetWeight=wanted; via="replacement"; break; }
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
// Equations require an explicit math-aware choice; never fall through to body text.
|
|
153
|
-
if (!matching.length && /^(cambria math|stix two math|noto sans math)$/i.test(family)) throw new OPFFontError("math-font-required", `Supply '${family}' or an explicit math-font alias.`, {path:style.path});
|
|
154
|
-
if (!matching.length && options.fallbackFamily) { matching=findFamily(options.fallbackFamily); compatibility="generic"; via="fallback"; }
|
|
155
|
-
if (!matching.length) throw unavailableFontError(family, style, policy);
|
|
156
|
-
const styled = styleFallback ? matching : matching.filter(face=>face.italic===!!style.italic);
|
|
157
|
-
// Script replacement faces have no italics; use upright glyphs and advances.
|
|
158
|
-
if (!styled.length && style.italic && matching.length && matching.every(face=>face.scripts)) { if (compatibility!=="generic") compatibility="visual"; }
|
|
159
|
-
else matching = styled;
|
|
160
|
-
if (!matching.length) throw new OPFFontError("font-style-unavailable", `No ${style.italic ? "italic" : "upright"} face for '${family}'.`, {path:style.path});
|
|
161
|
-
matching.sort((a,b)=>Math.abs(a.weight-targetWeight)-Math.abs(b.weight-targetWeight) || a.weight-b.weight);
|
|
162
|
-
const face = matching[0];
|
|
163
|
-
if ((face.weight!==targetWeight || styleFallback) && compatibility!=="generic") compatibility="visual";
|
|
196
|
+
const {face, compatibility, rule, styleFallback, via} = pickFace(faces, family, style, {policy, aliases, fallbackFamily:options.fallbackFamily});
|
|
164
197
|
// FF-31: `substitute` is true whenever the face is not the chosen family itself (policy
|
|
165
198
|
// replacement, alias, script replacement or fallback). Exporters keep writing sourceFamily.
|
|
166
199
|
const substitute = via!=="family", policyRow = substitute ? fontPolicyFor(family) : undefined;
|
|
@@ -203,11 +236,15 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
203
236
|
let run;
|
|
204
237
|
try { run=shape(); }
|
|
205
238
|
catch (error) {
|
|
239
|
+
// A lookup fontkit cannot decode (GSUB type 8 in Noto Sans Mongolian) applies nothing; shape again with the rest (FF-44).
|
|
240
|
+
if (skipUndecodableLookups(face.font)) { try { run=shape(); } catch { /* the mark retry below */ } }
|
|
206
241
|
// fontkit rejects some mark-attachment lookups (null anchors, for example
|
|
207
242
|
// Gurmukhi tippi in Noto Sans Gurmukhi). Mark positioning moves marks
|
|
208
243
|
// without changing advances, so measure again without it (FF-19).
|
|
209
|
-
|
|
210
|
-
|
|
244
|
+
if (!run) {
|
|
245
|
+
try { run=shape({abvm:false,blwm:false,mark:false,mkmk:false}); }
|
|
246
|
+
catch { throw new OPFFontError("font-shaping-failed", `Font '${face.family}' cannot shape this text.`, {path:style.path,fontFamily:face.family,cause:error?.message}); }
|
|
247
|
+
}
|
|
211
248
|
}
|
|
212
249
|
value={width:run.positions.reduce((total,position)=>total+position.xAdvance,0)/face.font.unitsPerEm,...value};
|
|
213
250
|
if(includeOutline) {
|