@openpresentation/opf-render 0.13.1 → 0.15.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.
- package/README.md +123 -86
- package/dist/chart-options.js +26 -21
- package/dist/charts.js +206 -123
- package/dist/deck-runtime.js +21 -34
- package/dist/element.d.ts +11 -6
- package/dist/element.js +28 -18
- package/dist/export-browser.d.ts +7 -1
- package/dist/export-browser.js +7 -6
- package/dist/font-compatibility.js +4 -4
- package/dist/font-registry.js +324 -0
- package/dist/fonts-browser.d.ts +25 -3
- package/dist/fonts-browser.js +54 -3
- package/dist/fonts-node.d.ts +32 -16
- package/dist/fonts-node.js +110 -50
- package/dist/fonts.d.ts +48 -27
- package/dist/fonts.js +7 -320
- package/dist/index.d.ts +45 -40
- package/dist/lazy-font-list.js +1 -1
- package/dist/lazy-fonts.js +7 -7
- package/dist/pdf-fonts.js +1 -1
- package/dist/player.d.ts +4 -4
- package/dist/player.js +5 -5
- package/dist/preview-fonts.d.ts +4 -4
- package/dist/preview-fonts.js +6 -5
- package/dist/raster.js +24 -9
- package/dist/script-font-pack.js +25 -31
- package/dist/script-fonts.js +6 -17
- package/dist/svg.d.ts +39 -38
- package/dist/svg.js +394 -376
- package/dist/symbol-fonts.js +22 -55
- package/package.json +2 -2
- package/dist/font-policy.js +0 -2734
- package/dist/symbol-encodings.js +0 -7
package/dist/fonts-browser.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
EnsureResult,
|
|
2
3
|
FontFaceInput,
|
|
3
4
|
FontRegistry,
|
|
4
5
|
FontRegistryOptions,
|
|
6
|
+
FontsHandle,
|
|
5
7
|
} from "./fonts.js";
|
|
6
8
|
import type { AutoScriptSelection, BundledFontPackage, LazyFont, ScriptSelection } from "./fonts-node.js";
|
|
7
9
|
import type { RenderSvgOptions } from "./svg.js";
|
|
@@ -53,9 +55,29 @@ export interface BrowserFontRegistry extends FontRegistry {
|
|
|
53
55
|
/** Names of the script-pack packages loaded so far. */
|
|
54
56
|
readonly loadedScriptPackages: string[];
|
|
55
57
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
/**
|
|
59
|
+
* The fonts handle of a browser: the faces you list, registered with the document, and on demand the script and vendored faces a deck
|
|
60
|
+
* draws. Pass it as `{ fonts }` to `renderSvg`, `renderSlideSvg`, `<opf-deck>` and the player, core `paginate` and `validate`, and the editor.
|
|
61
|
+
* The registry behind it keeps the lower-level loaders (`ensureLazyFonts`, `ensureScripts`, `loadScripts`, `pendingLazyFonts`, `pendingScripts`).
|
|
62
|
+
*/
|
|
63
|
+
export interface BrowserFontsHandle extends FontsHandle {
|
|
64
|
+
readonly registry: BrowserFontRegistry;
|
|
65
|
+
/** Remove every face this handle added from the document. */
|
|
66
|
+
dispose(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Load the vendored and script faces the presentation draws (each hash-verified, all or nothing), repeating while loading changes what
|
|
69
|
+
* is needed. Cheap when nothing is needed. Besides `signal`, the call takes the `renderSvg` options the document resolves with
|
|
70
|
+
* (`catalogs`, ...) over the loader's `renderOptions`. A document that does not resolve rejects with what `renderSvg` throws for it; after
|
|
71
|
+
* `dispose()` it rejects with `font-registry-disposed`.
|
|
72
|
+
*/
|
|
73
|
+
ensure(presentation: unknown, options?: { signal?: AbortSignal } & Partial<RenderSvgOptions>): Promise<EnsureResult>;
|
|
74
|
+
/** Synchronous: the vendored files and script packages `ensure` would fetch. Empty means a render can start now. Throws what `renderSvg` throws for a document that does not resolve. */
|
|
75
|
+
pending(presentation: unknown, renderOptions?: Partial<RenderSvgOptions>): string[];
|
|
76
|
+
}
|
|
77
|
+
export declare function loadFonts(
|
|
58
78
|
options?: FontRegistryOptions & {
|
|
79
|
+
/** The faces to register at startup (the host's startup set; the rest can be `extraLazyFonts`). Each is fetched from `url` or taken from `data`, and verified when it has a `sha256`. */
|
|
80
|
+
faces?: BrowserFontInput[];
|
|
59
81
|
document?: Document;
|
|
60
82
|
fetch?: typeof fetch;
|
|
61
83
|
signal?: AbortSignal;
|
|
@@ -84,7 +106,7 @@ export declare function loadBrowserFontRegistry(
|
|
|
84
106
|
*/
|
|
85
107
|
renderOptions?: Partial<RenderSvgOptions>;
|
|
86
108
|
},
|
|
87
|
-
): Promise<
|
|
109
|
+
): Promise<BrowserFontsHandle>;
|
|
88
110
|
export declare function scriptFontPackages(scripts: ScriptSelection): BundledFontPackage[];
|
|
89
111
|
/** Hash-pinned browser entries for the script pack, served by the host from `baseUrl`. */
|
|
90
112
|
export declare function scriptFontEntries(
|
package/dist/fonts-browser.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { createFontRegistry, OPFFontError } from "./
|
|
1
|
+
import { createFontRegistry, OPFFontError } from "./font-registry.js";
|
|
2
2
|
import { lazyFontEntries, lazyFontList, normalizeExtraLazyFonts } from "./lazy-font-list.js";
|
|
3
3
|
import { lazyFacesNeeded } from "./lazy-fonts.js";
|
|
4
|
+
import { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
4
5
|
import { analyzePresentationScripts, nextFallbackPackage, scriptFontPackages, scriptPackageEntries, scriptSelectionOf, uncoveredCjkCharacters } from "./script-font-pack.js";
|
|
5
6
|
export { autoScriptSelection, detectPresentationScripts, scriptFontEntries, scriptFontPackages } from "./script-font-pack.js";
|
|
6
7
|
export { lazyFontEntries, lazyFontList, splitStartupFaces } from "./lazy-font-list.js";
|
|
@@ -20,8 +21,8 @@ async function verifyDigest(entry, data, subtle) {
|
|
|
20
21
|
);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
// The registry behind `loadFonts`: fetch only host-selected font files, then use those exact bytes for shaping and CSS.
|
|
25
|
+
async function loadRegistry(entries, options = {}) {
|
|
25
26
|
// Validate the host's extra faces first: a bad list must not leave startup faces registered in the document with no registry to dispose.
|
|
26
27
|
const extraLazy = normalizeExtraLazyFonts(options.extraLazyFonts);
|
|
27
28
|
const documentRef = options.document ?? globalThis.document;
|
|
@@ -267,3 +268,53 @@ export async function loadBrowserFontRegistry(entries, options = {}) {
|
|
|
267
268
|
},
|
|
268
269
|
});
|
|
269
270
|
}
|
|
271
|
+
|
|
272
|
+
const MAX_ENSURE_ROUNDS = 4;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The fonts handle for a browser: the faces you list (`faces`, each `{ url | data, family, weight, italic, sha256 }`, fetched and
|
|
276
|
+
* hash-verified, then registered with the document) and, on demand, the script and vendored faces a deck draws. Pass it as `{ fonts }` to
|
|
277
|
+
* `renderSvg`, `renderSlideSvg`, `<opf-deck>` and the player, core `paginate` and `validate`, and the editor. `scripts`, `scriptBaseUrl`,
|
|
278
|
+
* `lazyFontsBaseUrl`, `extraLazyFonts`, `renderOptions`, `signal`, `document`, `fetch`, `crypto` and the registry options
|
|
279
|
+
* (`substitutionPolicy`, `aliases`, `fallbackFamily`, `themeFonts`, `strictGlyphs`) are as for the registry (see `fonts-browser.d.ts`).
|
|
280
|
+
* `dispose()` removes every face from the document.
|
|
281
|
+
*/
|
|
282
|
+
export async function loadFonts({ faces = [], ...options } = {}) {
|
|
283
|
+
const registry = await loadRegistry(faces, options);
|
|
284
|
+
// The embedded list is base64 of every eager face: compute it once, and again only when faces were added.
|
|
285
|
+
let embedded, stale = true;
|
|
286
|
+
const addFaces = registry.addFaces;
|
|
287
|
+
registry.addFaces = (added) => { const result = addFaces(added); stale = true; return result; };
|
|
288
|
+
const pending = (presentation, renderOptions) => [
|
|
289
|
+
...registry.pendingLazyFonts(presentation, renderOptions).map((face) => face.file),
|
|
290
|
+
...registry.pendingScripts(presentation, renderOptions),
|
|
291
|
+
];
|
|
292
|
+
return {
|
|
293
|
+
textMeasurement: registry.textMeasurement,
|
|
294
|
+
get embeddedFonts() { if (stale) { embedded = registry.embeddedFonts; stale = false; } return embedded; },
|
|
295
|
+
registry,
|
|
296
|
+
manifest: BUNDLED_FONT_MANIFEST,
|
|
297
|
+
get substitutions() { return registry.substitutions; },
|
|
298
|
+
/**
|
|
299
|
+
* Load the vendored and script faces the presentation draws, each hash-verified, all or nothing, and repeat while loading
|
|
300
|
+
* changes what is needed (a script face can need a vendored one). Cheap when nothing is needed. Besides `signal`, the call
|
|
301
|
+
* takes the `renderSvg` options the document resolves with (`catalogs`, ...) over the loader's `renderOptions`. A document that
|
|
302
|
+
* does not resolve rejects with what `renderSvg` throws for it. After `dispose()` it rejects with `font-registry-disposed`.
|
|
303
|
+
*/
|
|
304
|
+
async ensure(presentation, callOptions = {}) {
|
|
305
|
+
const lazy = [], scripts = [];
|
|
306
|
+
let uncovered = [];
|
|
307
|
+
for (let round = 0; round < MAX_ENSURE_ROUNDS; round++) {
|
|
308
|
+
const needLazy = registry.pendingLazyFonts(presentation, callOptions).length > 0, needScripts = registry.pendingScripts(presentation, callOptions).length > 0;
|
|
309
|
+
if (!needLazy && !needScripts) break;
|
|
310
|
+
callOptions.signal?.throwIfAborted?.();
|
|
311
|
+
if (needLazy) lazy.push(...(await registry.ensureLazyFonts(presentation, callOptions)));
|
|
312
|
+
if (needScripts) { const result = await registry.ensureScripts(presentation, callOptions); scripts.push(...result.loaded); uncovered = result.uncovered; }
|
|
313
|
+
}
|
|
314
|
+
return { scripts, lazy, uncovered };
|
|
315
|
+
},
|
|
316
|
+
/** Synchronous: the files and script packages `ensure` would fetch. Empty means a render can start now. Throws what `renderSvg` throws for a document that does not resolve. */
|
|
317
|
+
pending,
|
|
318
|
+
dispose: () => registry.dispose(),
|
|
319
|
+
};
|
|
320
|
+
}
|
package/dist/fonts-node.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {FontRegistry,FontRegistryOptions,
|
|
1
|
+
import type {FontRegistry,FontRegistryOptions,FontFaceInput,FontsHandle,EnsureResult,ScriptFontProfile} from "./fonts.js";
|
|
2
2
|
import type {RenderSvgOptions} from "./svg.js";
|
|
3
3
|
/** A caller-supplied face (FF-31), for example a licensed copy of the real font. Node callers may pass a file path. */
|
|
4
4
|
export type CallerFontFace = FontFaceInput | (Omit<FontFaceInput, "data"> & {path: string});
|
|
@@ -39,10 +39,6 @@ export interface ScriptPackOptions {
|
|
|
39
39
|
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; /** Set on a host's extra lazy face (`extraLazyFonts`, `package: "host"`): where the host serves the file from. */ readonly url?: string }
|
|
40
40
|
export type ScriptFontRegistry = FontRegistry & {fontFiles:string[]; /** Set for `scripts: "auto"`. */ scriptSelection?: AppliedScriptSelection};
|
|
41
41
|
export type NodeFontRegistry = ScriptFontRegistry & {/** The embed "used" faces this registry holds, with the files a host copies to serve them itself. */ lazyFonts: readonly LazyFont[]};
|
|
42
|
-
export declare function loadBundledFontRegistry(options?: FontRegistryOptions & ScriptPackOptions): Promise<NodeFontRegistry>;
|
|
43
|
-
|
|
44
|
-
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>;
|
|
45
|
-
|
|
46
42
|
export type BundledFontPackage = Readonly<{
|
|
47
43
|
/** An npm package name, or a plain id for a vendored entry of a git upstream. */
|
|
48
44
|
name:string;
|
|
@@ -74,15 +70,35 @@ export interface BundledFontManifest {
|
|
|
74
70
|
}
|
|
75
71
|
export declare const BUNDLED_FONT_MANIFEST: BundledFontManifest;
|
|
76
72
|
export declare function scriptFontPackages(scripts: ScriptSelection): BundledFontPackage[];
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
73
|
+
/** The fonts handle of Node: the verified bundled faces (and any you supply) for layout, SVG, editor, PPTX and Node raster export. */
|
|
74
|
+
export interface NodeFontsHandle extends FontsHandle {
|
|
75
|
+
readonly registry: NodeFontRegistry;
|
|
76
|
+
readonly manifest: BundledFontManifest;
|
|
77
|
+
/** Every font file the handle holds, for `svgToPng` and `svgToPdf`; includes the script faces `ensure` loaded. */
|
|
78
|
+
readonly fontFiles: string[];
|
|
79
|
+
/** The files are already in `fontFiles`, so a conversion adds no bundled faces. */
|
|
80
|
+
readonly useBundledFonts: false;
|
|
81
|
+
/** System fonts never load: the output does not depend on the machine. */
|
|
82
|
+
readonly loadSystemFonts: false;
|
|
83
|
+
/**
|
|
84
|
+
* Load the script faces the presentation's text needs that this handle does not hold (what `scripts: "auto"` does at load, for another
|
|
85
|
+
* presentation), including the CJK face a glyph fallback needs. Cheap when nothing is missing; `embeddedFonts` and `fontFiles` then include
|
|
86
|
+
* the new faces. A package that is not installed or a script no pinned font serves is reported once through `onDiagnostic`.
|
|
87
|
+
* Resolves with the packages this call added (`scripts`; `lazy` is empty: Node loads every vendored face with its pack).
|
|
88
|
+
*/
|
|
89
|
+
ensure(presentation: unknown, options?: { onDiagnostic?: ScriptPackOptions["onDiagnostic"]; renderOptions?: Partial<RenderSvgOptions> }): Promise<EnsureResult>;
|
|
90
|
+
/** Synchronous: the script packages the presentation needs that are not loaded yet. */
|
|
91
|
+
pending(presentation: unknown, renderOptions?: Partial<RenderSvgOptions>): string[];
|
|
92
|
+
}
|
|
93
|
+
export interface LoadFontsOptions extends FontRegistryOptions, ScriptPackOptions {
|
|
94
|
+
/** `base` (default): the bundled Roboto faces. `office`: the metric and visual substitutes for the Office families plus the open families font schemes select. `none`: only the `faces` you supply. */
|
|
95
|
+
pack?: "base" | "office" | "none";
|
|
96
|
+
/** With pack "office": add the base Roboto faces too; default true. */
|
|
97
|
+
includeBaseFonts?: boolean;
|
|
98
|
+
/** With pack "office": leave out the open families (FF-31); default true. */
|
|
99
|
+
includeOpenFonts?: boolean;
|
|
100
|
+
/** Embed script faces in SVG (large); default false. */
|
|
101
|
+
embedScriptFonts?: boolean;
|
|
87
102
|
}
|
|
88
|
-
|
|
103
|
+
/** Load verified, bundled, openly licensed fonts: no system font discovery, no network. Pass the handle as `{ fonts }` to every deck-level function. */
|
|
104
|
+
export declare function loadFonts(options?: LoadFontsOptions): Promise<NodeFontsHandle>;
|
package/dist/fonts-node.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
-
import { createFontRegistry, OPFFontError } from "./
|
|
6
|
+
import { createFontRegistry, OPFFontError } from "./font-registry.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";
|
|
@@ -106,7 +106,7 @@ async function withScripts(loaded, scripts, {presentation, onDiagnostic, renderO
|
|
|
106
106
|
async function completeFallback(registry, presentation, loaded, onDiagnostic, renderOptions) {
|
|
107
107
|
const selection = loaded.selection;
|
|
108
108
|
if (!selection) return;
|
|
109
|
-
const seen = new Set([...selection.packages, ...selection.notInstalled]);
|
|
109
|
+
const seen = new Set([...(loaded.packages ?? []), ...selection.packages, ...selection.notInstalled]);
|
|
110
110
|
for (;;) {
|
|
111
111
|
const analysis = analyzePresentationScripts(presentation, undefined, renderOptions), covers = character => registry.scriptFacesCover(character);
|
|
112
112
|
const next = nextFallbackPackage(analysis, {covers, loaded: seen});
|
|
@@ -124,6 +124,7 @@ async function completeFallback(registry, presentation, loaded, onDiagnostic, re
|
|
|
124
124
|
}
|
|
125
125
|
registry.addFaces(embedUsed(extra.entries));
|
|
126
126
|
loaded.fontFiles.push(...extra.fontFiles);
|
|
127
|
+
loaded.packages?.push(next.name);
|
|
127
128
|
selection.packages.push(next.name);
|
|
128
129
|
}
|
|
129
130
|
}
|
|
@@ -152,63 +153,122 @@ async function withFaces(loaded, faces) {
|
|
|
152
153
|
return {...loaded, entries:[...own.entries, ...loaded.entries], fontFiles:[...own.fontFiles, ...loaded.fontFiles]};
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
/**
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Build the registry of a pack: `base` (the bundled Roboto faces), `office` (the metric and visual substitutes for the Office
|
|
158
|
+
* families plus the open families font schemes select, FF-31, optionally alongside the base) or `none` (only the caller's
|
|
159
|
+
* `faces`). Returns the live registry and what was loaded (`entries`, `fontFiles`, `packages`, `selection`), which `ensure` extends.
|
|
160
|
+
*/
|
|
161
|
+
async function buildRegistry(pack, {scripts, faces, presentation, onDiagnostic, renderOptions, ...options}) {
|
|
162
|
+
let loaded, registryOptions = options;
|
|
163
|
+
if (pack === "office") {
|
|
164
|
+
const {entries, fontFiles, packages} = await loadPack("office");
|
|
165
|
+
for (const [include, name] of [[options.includeOpenFonts, "open"], [options.includeBaseFonts, "base"]]) {
|
|
166
|
+
if (include === false) continue;
|
|
167
|
+
const extra = await loadPack(name);
|
|
168
|
+
fontFiles.push(...extra.fontFiles);
|
|
169
|
+
entries.push(...extra.entries);
|
|
170
|
+
packages.push(...extra.packages);
|
|
171
|
+
}
|
|
172
|
+
// Noto Sans (regular, bold, italic, bold italic) is the default Latin, Cyrillic and Greek fallback face
|
|
173
|
+
// (glyph fallback), so Georgia with Russian text previews without the scripts option. It is embedded in an
|
|
174
|
+
// SVG only when the slide's text draws it (embed "used"); raster output reads it from fontFiles. Unless the
|
|
175
|
+
// 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
|
|
176
|
+
// presentation itself selects Latn (a Sylfaen scheme: Noto Sans is its designated replacement, which a fallback-only face never is), as
|
|
177
|
+
// an explicit list does; a presentation that does not select Latn keeps it fallback-only.
|
|
178
|
+
const requested = scripts === "auto" && presentation !== null && typeof presentation === "object" ? scriptFontPackages(autoScriptSelection(presentation, renderOptions).scripts).map(pkg => pkg.name)
|
|
179
|
+
: scripts === undefined || scripts === "auto" || (Array.isArray(scripts) && !scripts.length) ? [] : scriptFontPackages(scripts).map(pkg => pkg.name);
|
|
180
|
+
const fallback = await loadPackages(scriptFontPackages(["Latn"]).filter(pkg => !requested.includes(pkg.name)), undefined, {fallbackOnly:true});
|
|
181
|
+
fontFiles.push(...fallback.fontFiles);
|
|
182
|
+
entries.push(...fallback.entries);
|
|
183
|
+
packages.push(...fallback.packages);
|
|
184
|
+
loaded = await withFaces(await withScripts({entries, fontFiles, packages}, scripts, {presentation, onDiagnostic, renderOptions}), faces);
|
|
185
|
+
// A package that is the renamed successor of a family answers to the old name through a built-in alias.
|
|
186
|
+
const aliases = options.includeOpenFonts === false ? options.aliases : {...renamedAliases(), ...options.aliases};
|
|
187
|
+
registryOptions = {substitutionPolicy:"metric", ...options, ...(aliases ? {aliases} : {})};
|
|
188
|
+
} else {
|
|
189
|
+
loaded = await withFaces(await withScripts(pack === "none" ? {entries:[], fontFiles:[], packages:[]} : await loadPack("base"), scripts, {presentation, onDiagnostic, renderOptions}), faces);
|
|
190
|
+
}
|
|
191
|
+
loaded.packages ??= [];
|
|
192
|
+
const registry = Object.assign(createFontRegistry(loaded.entries, registryOptions), {fontFiles: loaded.fontFiles, ...(loaded.selection ? {scriptSelection: loaded.selection} : {})});
|
|
159
193
|
registry.lazyFonts = lazyOf(registry);
|
|
160
|
-
await completeFallback(registry, presentation,
|
|
161
|
-
return registry;
|
|
194
|
+
await completeFallback(registry, presentation, loaded, onDiagnostic, renderOptions);
|
|
195
|
+
return {registry, loaded};
|
|
162
196
|
}
|
|
163
197
|
|
|
164
|
-
/**
|
|
165
|
-
*
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Load the faces a text needs that this handle does not hold yet: what `scripts: "auto"` does at load, run again for another
|
|
200
|
+
* presentation (an edit that adds a script, another deck on the same handle). Cheap when nothing is needed. A package that is not
|
|
201
|
+
* installed, or a script no pinned font serves, is reported once through `onDiagnostic`. Resolves with the script packages this
|
|
202
|
+
* call added (`scripts`; `lazy` is always empty here: Node loads every vendored face with its pack) and the drawn CJK
|
|
203
|
+
* characters no loaded face covers.
|
|
204
|
+
*/
|
|
205
|
+
async function ensureFonts(registry, loaded, presentation, {onDiagnostic, renderOptions} = {}) {
|
|
206
|
+
if (presentation === null || typeof presentation !== "object") throw new OPFFontError("invalid-font-scripts", "ensure needs the presentation whose text decides the scripts.");
|
|
207
|
+
const selection = (loaded.selection ??= {detected: [], scripts: [], unavailable: [], packages: [], notInstalled: []});
|
|
208
|
+
registry.scriptSelection = selection;
|
|
209
|
+
const before = loaded.packages.length;
|
|
210
|
+
const wanted = scriptSelectionOf(analyzePresentationScripts(presentation, undefined, renderOptions));
|
|
211
|
+
const held = new Set([...loaded.packages, ...selection.notInstalled]);
|
|
212
|
+
const skipped = [];
|
|
213
|
+
const extra = await loadPackages(scriptFontPackages(wanted.scripts).filter(pkg => !held.has(pkg.name)), skipped);
|
|
214
|
+
if (extra.entries.length) registry.addFaces(embedUsed(extra.entries));
|
|
215
|
+
loaded.fontFiles.push(...extra.fontFiles);
|
|
216
|
+
loaded.packages.push(...extra.packages);
|
|
217
|
+
selection.packages.push(...extra.packages);
|
|
218
|
+
for (const item of skipped) {
|
|
219
|
+
selection.notInstalled.push(item.package);
|
|
220
|
+
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.`});
|
|
221
|
+
}
|
|
222
|
+
for (const script of wanted.unavailable) if (!selection.unavailable.includes(script)) {
|
|
223
|
+
selection.unavailable.push(script);
|
|
224
|
+
onDiagnostic?.({code: "script-font-unavailable", script, message: `No pinned open font serves script '${script}'; that text uses the design font.`});
|
|
174
225
|
}
|
|
175
|
-
// Noto Sans (regular, bold, italic, bold italic) is the default Latin, Cyrillic and Greek fallback face
|
|
176
|
-
// (glyph fallback), so Georgia with Russian text previews without the scripts option. It is embedded in an
|
|
177
|
-
// SVG only when the slide's text draws it (embed "used"); raster output reads it from fontFiles. Unless the
|
|
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);
|
|
183
|
-
const fallback = await loadPackages(scriptFontPackages(["Latn"]).filter(pkg => !requested.includes(pkg.name)), undefined, {fallbackOnly:true});
|
|
184
|
-
fontFiles.push(...fallback.fontFiles);
|
|
185
|
-
entries.push(...fallback.entries);
|
|
186
|
-
packages.push(...fallback.packages);
|
|
187
|
-
const loaded = await withFaces(await withScripts({entries, fontFiles, packages}, scripts, {presentation, onDiagnostic, renderOptions}), faces);
|
|
188
|
-
const aliases = options.includeOpenFonts === false ? options.aliases : {...renamedAliases(), ...options.aliases};
|
|
189
|
-
const registry = Object.assign(createFontRegistry(loaded.entries,{substitutionPolicy:"metric",...options,...(aliases?{aliases}:{})}),{fontFiles:loaded.fontFiles, ...(loaded.selection ? {scriptSelection: loaded.selection} : {})});
|
|
190
|
-
registry.lazyFonts = lazyOf(registry);
|
|
191
226
|
await completeFallback(registry, presentation, loaded, onDiagnostic, renderOptions);
|
|
192
|
-
return
|
|
227
|
+
return {scripts: loaded.packages.slice(before), lazy: [], uncovered: selection.uncovered ? [...selection.uncovered] : []};
|
|
193
228
|
}
|
|
194
229
|
|
|
195
|
-
/**
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
230
|
+
/**
|
|
231
|
+
* The fonts handle: one set of verified font inputs for layout, SVG, editor, PPTX and Node raster export. Pass it as `{ fonts }` to
|
|
232
|
+
* `renderSvg`, `renderSlideSvg`, `svgToPng`, `svgToPdf`, core `paginate` and `validate`, and `toPptx`.
|
|
233
|
+
*
|
|
234
|
+
* Bundled, openly licensed faces only: no system font discovery and no network requests. `pack`: `base` (default, Roboto),
|
|
235
|
+
* `office` (the Office substitutes and the open families) or `none` (only the `faces` you supply). Everything else
|
|
236
|
+
* (`substitutionPolicy`, `aliases`, `fallbackFamily`, `themeFonts`, `strictGlyphs`, `includeBaseFonts`, `includeOpenFonts`,
|
|
237
|
+
* `faces`, `scripts`, `presentation`, `onDiagnostic`, `renderOptions`) goes to the registry. `embedScriptFonts` embeds the large script
|
|
238
|
+
* faces in standalone SVG (default false: raster output reads them from `fontFiles`).
|
|
239
|
+
*/
|
|
240
|
+
export async function loadFonts({pack = "base", embedScriptFonts = false, ...options} = {}) {
|
|
241
|
+
if (pack !== "base" && pack !== "office" && pack !== "none") throw new OPFFontError("invalid-font-pack", "Choose the base, office or none font pack.", {pack});
|
|
242
|
+
const {registry, loaded} = await buildRegistry(pack, options);
|
|
243
|
+
// Script faces are large (CJK faces are 5-10 MB each). Raster output reads them from fontFiles; embed them in standalone SVG only
|
|
244
|
+
// on request. The default Noto Sans fallback is always offered (embed "used"), so an SVG carries it exactly when its text draws it.
|
|
245
|
+
const selectEmbedded = () => registry.selectEmbeddedFonts(face => embedScriptFonts || !face.scripts || face.fallbackOnly);
|
|
246
|
+
// The embedded list is base64 of every face: computed on first use, and again after `ensure` added faces.
|
|
247
|
+
let embeddedFonts, fontFiles = [...registry.fontFiles];
|
|
248
|
+
const refresh = () => { embeddedFonts = undefined; fontFiles = [...registry.fontFiles]; };
|
|
203
249
|
return {
|
|
250
|
+
textMeasurement: registry.textMeasurement,
|
|
251
|
+
get embeddedFonts() { return (embeddedFonts ??= selectEmbedded()); },
|
|
252
|
+
get fontFiles() { return fontFiles; },
|
|
253
|
+
useBundledFonts: false,
|
|
254
|
+
loadSystemFonts: false,
|
|
204
255
|
registry,
|
|
205
|
-
manifest:BUNDLED_FONT_MANIFEST,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
256
|
+
manifest: BUNDLED_FONT_MANIFEST,
|
|
257
|
+
get substitutions() { return registry.substitutions; },
|
|
258
|
+
/** Load the script faces the presentation's text needs (see `ensureFonts`); `embeddedFonts` and `fontFiles` then include them. */
|
|
259
|
+
async ensure(presentation, callOptions = {}) {
|
|
260
|
+
const result = await ensureFonts(registry, loaded, presentation, {onDiagnostic: callOptions.onDiagnostic ?? options.onDiagnostic, renderOptions: {...options.renderOptions, ...callOptions.renderOptions}});
|
|
261
|
+
refresh();
|
|
262
|
+
return result;
|
|
263
|
+
},
|
|
264
|
+
/** Synchronous: the script packages the presentation needs that are not loaded yet. Empty means `ensure` loads nothing. */
|
|
265
|
+
pending(presentation, renderOptions) {
|
|
266
|
+
const held = new Set([...loaded.packages, ...(loaded.selection?.notInstalled ?? [])]);
|
|
267
|
+
const analysis = analyzePresentationScripts(presentation, undefined, {...options.renderOptions, ...renderOptions});
|
|
268
|
+
const primary = scriptFontPackages(scriptSelectionOf(analysis).scripts).map(pkg => pkg.name).filter(name => !held.has(name));
|
|
269
|
+
if (primary.length) return primary;
|
|
270
|
+
const next = nextFallbackPackage(analysis, {covers: character => registry.scriptFacesCover(character), loaded: held});
|
|
271
|
+
return next ? [next.name] : [];
|
|
212
272
|
},
|
|
213
273
|
};
|
|
214
274
|
}
|
package/dist/fonts.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import type { TextMeasurement, TextStyle } from "@openpresentation/opf/composition";
|
|
1
|
+
import type { TextMeasurement, TextStyle, ScriptRole } from "@openpresentation/opf/composition";
|
|
2
|
+
import type { FontPolicyEntry } from "@openpresentation/opf/font-policy";
|
|
3
|
+
import type { LazyFont } from "./fonts-node.js";
|
|
4
|
+
// Core owns these: the font policy table and lookup, the symbol-font code rules and the script slot of a script. They are core's own
|
|
5
|
+
// exports (the same objects), not copies.
|
|
6
|
+
export { FONT_POLICY, fontPolicyFor } from "@openpresentation/opf/font-policy";
|
|
7
|
+
export type { FontPolicyEntry } from "@openpresentation/opf/font-policy";
|
|
8
|
+
export { isSymbolEncodedFamily, mapSymbolText, symbolCodeOf } from "@openpresentation/opf/symbol-font-encodings";
|
|
9
|
+
export { scriptFontRole } from "@openpresentation/opf/composition";
|
|
10
|
+
export type { ScriptRole } from "@openpresentation/opf/composition";
|
|
2
11
|
export interface FontFaceInput { data: Uint8Array; family?: string; weight?: number; italic?: boolean; postscriptName?: string; license?: string; /** Serves glyph fallback and requests by its own family only; never a replacement for another family. */ fallbackOnly?: boolean; /** "used" embeds the face in an SVG only when the slide's text names its family (the open pack, FF-31). Default "always". */ embed?: "always" | "used"; /** ISO 15924 scripts a designated script replacement face serves (FF-19). */ scripts?: string[] }
|
|
3
12
|
export interface EmbeddedFont { family: string; weight: number; italic?: boolean; dataUrl: string; license?: string; embed?: "used" }
|
|
4
13
|
export type FontCompatibility = "exact" | "metric" | "visual" | "generic";
|
|
@@ -17,18 +26,6 @@ export interface FontResolution {
|
|
|
17
26
|
measured?:FontReplacementMeasurement;
|
|
18
27
|
licenseClass?:FontPolicyEntry['licenseClass']; availability?:FontPolicyEntry['availability'];
|
|
19
28
|
}
|
|
20
|
-
/** One OPF font policy row (snapshot of opf spec/reference/font-policy.json, decisions applied). */
|
|
21
|
-
export interface FontPolicyEntry {
|
|
22
|
-
family:string; licenseClass:"open"|"proprietary-standard"|"proprietary-nonstandard"; license:string;
|
|
23
|
-
availability:("windows"|"windows-optional"|"macos"|"office"|"office-cloud")[]; embeddableByOpf:boolean;
|
|
24
|
-
replacement:{family:string; compatibility:"metric"|"visual"; decision?:string; metricModeFallback?:true; weight?:number; measured:FontReplacementMeasurement|null; source?:string}|null;
|
|
25
|
-
alternates?:string[];
|
|
26
|
-
}
|
|
27
|
-
export declare const FONT_POLICY: readonly Readonly<FontPolicyEntry>[];
|
|
28
|
-
export declare const FONT_POLICY_SOURCE: Readonly<{version:number; path:string; sha256:string}>;
|
|
29
|
-
/** Provisional owner decisions applied to the snapshot (owner may revise). */
|
|
30
|
-
export declare const FONT_POLICY_DECISIONS: Readonly<Record<string, unknown>>;
|
|
31
|
-
export declare function fontPolicyFor(family:string): Readonly<FontPolicyEntry>|undefined;
|
|
32
29
|
export interface FontRegistryOptions {
|
|
33
30
|
aliases?: Record<string,string>; fallbackFamily?: string; strictGlyphs?: boolean;
|
|
34
31
|
substitutionPolicy?: "none" | "metric" | "visual";
|
|
@@ -36,6 +33,42 @@ export interface FontRegistryOptions {
|
|
|
36
33
|
}
|
|
37
34
|
export declare const FONT_COMPATIBILITY: readonly Readonly<{requestedFamily:string;substitutes:readonly string[];compatibility:"metric"|"visual";weights?:readonly number[];weight?:number;source?:string;measured?:FontReplacementMeasurement;decision?:string;metricModeFallback?:true;licenseClass?:FontPolicyEntry['licenseClass'];note:string}>[];
|
|
38
35
|
export declare const EXPERIMENTAL_FONT_CANDIDATES: readonly Readonly<{requestedFamily:string;substitute:string;source:string;note:string}>[];
|
|
36
|
+
/**
|
|
37
|
+
* What the deck-level functions read from their `fonts` option: `renderSvg`, `renderSlideSvg` and `<opf-deck>` take the measurement and
|
|
38
|
+
* the faces to embed; `svgToPng` and `svgToPdf` (Node) take the font files; core `paginate`, `validate` and `resolveSlideContext` take the
|
|
39
|
+
* measurement. The object `loadFonts()` returns is a `FontsHandle`, which extends this; any object with these fields works.
|
|
40
|
+
*/
|
|
41
|
+
export interface RenderFonts {
|
|
42
|
+
/** Measures text with the real faces. Without it layout uses core's portable estimate. */
|
|
43
|
+
textMeasurement?: TextMeasurement;
|
|
44
|
+
/** Faces `renderSvg` writes into each SVG as @font-face data; a face flagged `embed: "used"` only when a slide draws its family. */
|
|
45
|
+
embeddedFonts?: readonly EmbeddedFont[];
|
|
46
|
+
/** Font files the Node raster and PDF conversions draw with. */
|
|
47
|
+
fontFiles?: readonly string[];
|
|
48
|
+
/** Whether a conversion adds the bundled base faces to `fontFiles`. Default true; a handle that already holds them says false. */
|
|
49
|
+
useBundledFonts?: boolean;
|
|
50
|
+
/** Whether the raster conversion loads system fonts. Default false (the output never depends on the machine). */
|
|
51
|
+
loadSystemFonts?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** What `fonts.ensure(presentation)` loaded: the script packages and vendored faces that were missing, and drawn CJK characters no face covers. */
|
|
54
|
+
export interface EnsureResult { scripts: string[]; lazy: LazyFont[]; uncovered: string[] }
|
|
55
|
+
/** The fonts handle `loadFonts()` returns from `/fonts-node` and `/fonts-browser`: pass it as `{ fonts }` to every deck-level function. */
|
|
56
|
+
export interface FontsHandle extends RenderFonts {
|
|
57
|
+
readonly textMeasurement: FontRegistry["textMeasurement"];
|
|
58
|
+
readonly embeddedFonts: EmbeddedFont[];
|
|
59
|
+
/** The face registry behind the handle (shaping, resolution, lazy and script loading). */
|
|
60
|
+
readonly registry: FontRegistry;
|
|
61
|
+
/** The pinned manifest of the bundled font packages. */
|
|
62
|
+
readonly manifest: { readonly version: number; readonly packages: readonly object[] };
|
|
63
|
+
/** The substitutions made so far (a requested family drawn with another face), as `registry.substitutions`. */
|
|
64
|
+
readonly substitutions: FontResolution[];
|
|
65
|
+
/** Load the script (and, in a browser, vendored) faces the presentation's text needs. Cheap when nothing is missing. */
|
|
66
|
+
ensure(presentation: unknown, options?: object): Promise<EnsureResult>;
|
|
67
|
+
/** Synchronous: the files and script packages `ensure` would load. Empty means a render can start now. */
|
|
68
|
+
pending(presentation: unknown, renderOptions?: object): string[];
|
|
69
|
+
/** Remove the faces this handle added to the document (a browser handle). */
|
|
70
|
+
dispose?(): void;
|
|
71
|
+
}
|
|
39
72
|
export interface FaceDescription { family:string; weight:number; italic:boolean; scripts?:string[] }
|
|
40
73
|
export interface FontRegistry {
|
|
41
74
|
textMeasurement: TextMeasurement & {outlineBounds: NonNullable<TextMeasurement['outlineBounds']>; resolveFont(style: TextStyle): FontResolution};
|
|
@@ -46,6 +79,8 @@ export interface FontRegistry {
|
|
|
46
79
|
selectEmbeddedFonts(predicate: (face: FaceDescription) => boolean): EmbeddedFont[];
|
|
47
80
|
/** Parsed face metadata in entry order, without encoding font bytes. */
|
|
48
81
|
describeFaces(): FaceDescription[];
|
|
82
|
+
/** Every face as `{ family, data }`, the bytes the registry measures with. */
|
|
83
|
+
exportFaces(): { family: string; data: Uint8Array }[];
|
|
49
84
|
readonly substitutions: FontResolution[];
|
|
50
85
|
/** True when a loaded script-pack face has a glyph for the character. */
|
|
51
86
|
scriptFacesCover(character: string): boolean;
|
|
@@ -53,10 +88,7 @@ export interface FontRegistry {
|
|
|
53
88
|
addFaces(entries: FontFaceInput[]): { family: string; weight: number; italic: boolean; scripts?: string[] }[];
|
|
54
89
|
}
|
|
55
90
|
export declare class OPFFontError extends Error { code:string; details:Record<string,unknown>; constructor(code:string,message:string,details?:Record<string,unknown>) }
|
|
56
|
-
export declare function createFontRegistry(entries: FontFaceInput[], options?: FontRegistryOptions): FontRegistry;
|
|
57
91
|
|
|
58
|
-
/** OOXML script slot. */
|
|
59
|
-
export type ScriptRole = "latin" | "eastAsian" | "complexScript";
|
|
60
92
|
export interface ScriptFontSlots { latin: string; eastAsian: string; complexScript: string }
|
|
61
93
|
/** Core `resolveScriptFonts` output, or the same shape. `serif` prefers serif replacements. */
|
|
62
94
|
export interface ScriptFontProfile {
|
|
@@ -81,7 +113,6 @@ export interface ScriptFonts {
|
|
|
81
113
|
export declare const SCRIPT_FONT_FAMILIES: Readonly<Record<string, Readonly<{ sans?: string; serif?: string }>>>;
|
|
82
114
|
export declare const SCRIPT_FONT_REPLACEMENTS: readonly Readonly<{requestedFamily:string;script:string;substitutes:readonly string[];compatibility:"visual";note:string}>[];
|
|
83
115
|
export declare function scriptOfCharacter(character: string): string;
|
|
84
|
-
export declare function scriptFontRole(script: string): ScriptRole;
|
|
85
116
|
export declare function itemizeScripts(text: string, profile?: ScriptFontProfile): ScriptRun[];
|
|
86
117
|
/** Script keys in the strings of a JSON value. The profile's own script counts unless `includeLanguage` is false; `ignoreKeys` are not visited. */
|
|
87
118
|
export declare function detectScripts(value: unknown, profile?: ScriptFontProfile, options?: { includeLanguage?: boolean; ignoreKeys?: readonly string[] }): string[];
|
|
@@ -107,17 +138,7 @@ export declare const SYMBOL_PLACEHOLDER: string;
|
|
|
107
138
|
export declare const SYMBOL_PREVIEW_FACES: Readonly<Record<SymbolFontFamily, readonly string[]>>;
|
|
108
139
|
/** Every open face a symbol preview may draw with. */
|
|
109
140
|
export declare const SYMBOL_FACE_FAMILIES: readonly string[];
|
|
110
|
-
/** Per family: the verified font version and, for codes 0x20..0xFF in order, `[unicode, advance, reason?]` (hex code points joined with '+', font units). */
|
|
111
|
-
export declare const SYMBOL_ENCODINGS: readonly Readonly<{ family: SymbolFontFamily; version: string; unitsPerEm: number; mapped: number; codes: readonly (readonly [string | null, number | null, string?])[] }>[];
|
|
112
|
-
/** Which core table (spec/reference/symbol-font-encodings.json) the snapshot came from. */
|
|
113
|
-
export declare const SYMBOL_ENCODINGS_SOURCE: Readonly<{ version: number; path: string; sha256: string }>;
|
|
114
|
-
export declare function isSymbolEncodedFamily(family: string): boolean;
|
|
115
|
-
export declare function symbolEncodingFor(family: string): (typeof SYMBOL_ENCODINGS)[number] | undefined;
|
|
116
141
|
export declare function symbolPreviewFaces(family: string): readonly string[];
|
|
117
|
-
/** The symbol code (0x20..0xFF) of one character: a private-use U+F0xx, an ASCII or Latin-1 character, or a Windows-1252 character at 0x80..0x9F; null for anything else. */
|
|
118
|
-
export declare function symbolCodeOf(character: string): number | null;
|
|
119
|
-
/** Normalise text in a symbol-encoded family: per character its code (null when not a code), the Unicode equivalent to draw (or null with a reason) and the verified font's advance in em. */
|
|
120
|
-
export declare function mapSymbolText(family: string, text: string): { source: string; code: number | null; unicode: string | null; advance: number | null; reason?: string }[];
|
|
121
142
|
/** Wrap a measurement so pagination, the renderer and the editor itemize script runs identically. */
|
|
122
143
|
export declare function createScriptTextMeasurement(measurement: TextMeasurement, profile: ScriptFontProfile, options?: ScriptFontsOptions): TextMeasurement;
|
|
123
144
|
export declare function openTypeLanguage(tag: string | undefined): string | undefined;
|