@openpresentation/opf-render 0.17.0 → 0.18.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 +80 -64
- package/dist/deck-runtime.js +4 -4
- package/dist/drawn-faces.js +38 -0
- package/dist/element.d.ts +12 -9
- package/dist/element.js +33 -18
- package/dist/export-browser.d.ts +21 -11
- package/dist/export-browser.js +33 -20
- package/dist/font-fstype.js +70 -0
- package/dist/font-subset.js +173 -0
- package/dist/fonts-browser.d.ts +33 -22
- package/dist/fonts-browser.js +43 -15
- package/dist/fonts-node.d.ts +9 -9
- package/dist/fonts-node.js +26 -4
- package/dist/fonts.d.ts +10 -6
- package/dist/hb-shape.js +121 -0
- package/dist/index.d.ts +66 -33
- package/dist/index.js +1 -1
- package/dist/lazy-fonts.js +10 -10
- package/dist/pdf-fonts.js +1 -1
- package/dist/pdf-text.js +6 -3
- package/dist/pdf-vector.js +5 -2
- package/dist/pdf.d.ts +2 -2
- package/dist/pdf.js +3 -3
- package/dist/player.d.ts +3 -3
- package/dist/player.js +1 -1
- package/dist/png.d.ts +2 -2
- package/dist/png.js +2 -2
- package/dist/raster.js +33 -28
- package/dist/script-font-pack.js +5 -5
- package/dist/slide-sources.js +47 -0
- package/dist/svg.d.ts +39 -32
- package/dist/svg.js +74 -58
- package/dist/text-paths.js +82 -30
- package/package.json +3 -3
package/dist/fonts-browser.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
FontsHandle,
|
|
7
7
|
} from "./fonts.js";
|
|
8
8
|
import type { AutoScriptSelection, BundledFontPackage, LazyFont, ScriptSelection } from "./fonts-node.js";
|
|
9
|
-
import type {
|
|
9
|
+
import type { ToSvgOptions } from "./svg.js";
|
|
10
10
|
export type { LazyFont } from "./fonts-node.js";
|
|
11
11
|
export type { AutoScriptSelection } from "./fonts-node.js";
|
|
12
12
|
export { detectPresentationScripts, autoScriptSelection } from "./fonts-node.js";
|
|
@@ -26,17 +26,17 @@ export interface BrowserFontRegistry extends FontRegistry {
|
|
|
26
26
|
* family the policy would not resolve, nor for a weight or style nothing draws), once each, from `lazyFontsBaseUrl`.
|
|
27
27
|
* Fetches and verifies every file, then adds the faces to the document and the registry together; on failure nothing
|
|
28
28
|
* changes and the call can be retried. Call it before measuring a document, and again after edits: an edit that adds a
|
|
29
|
-
* bold or italic run loads just that face. Besides `signal`, the call takes the `
|
|
29
|
+
* bold or italic run loads just that face. Besides `signal`, the call takes the `toSvg` options the document resolves
|
|
30
30
|
* with (`catalogs`, ...) over the loader's `renderOptions`. A document that does not resolve rejects with the error
|
|
31
|
-
* `
|
|
31
|
+
* `toSvg` throws for it. After `dispose()` it rejects with `font-registry-disposed`.
|
|
32
32
|
*/
|
|
33
|
-
ensureLazyFonts(presentation: unknown, options?: { signal?: AbortSignal } & Partial<
|
|
33
|
+
ensureLazyFonts(presentation: unknown, options?: { signal?: AbortSignal } & Partial<ToSvgOptions>): Promise<LazyFont[]>;
|
|
34
34
|
/**
|
|
35
35
|
* Synchronous: the vendored faces the presentation draws under the registry's policy that are not loaded yet. Empty means
|
|
36
|
-
* `ensureLazyFonts` fetches nothing. `renderOptions` are the `
|
|
37
|
-
* over the loader's `renderOptions`. Throws what `
|
|
36
|
+
* `ensureLazyFonts` fetches nothing. `renderOptions` are the `toSvg` options the document resolves with (`catalogs`, ...)
|
|
37
|
+
* over the loader's `renderOptions`. Throws what `toSvg` throws for a document that does not resolve.
|
|
38
38
|
*/
|
|
39
|
-
pendingLazyFonts(presentation: unknown, renderOptions?: Partial<
|
|
39
|
+
pendingLazyFonts(presentation: unknown, renderOptions?: Partial<ToSvgOptions>): LazyFont[];
|
|
40
40
|
|
|
41
41
|
/** Load script faces for ISO 15924 codes (or "all") from `scriptBaseUrl`. Resolves with the newly loaded package names. */
|
|
42
42
|
loadScripts(scripts: ScriptSelection, options?: { signal?: AbortSignal }): Promise<string[]>;
|
|
@@ -49,15 +49,15 @@ export interface BrowserFontRegistry extends FontRegistry {
|
|
|
49
49
|
* 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
|
|
50
50
|
* `dispose()` it rejects with `font-registry-disposed`.
|
|
51
51
|
*/
|
|
52
|
-
ensureScripts(presentation: unknown, options?: { signal?: AbortSignal } & Partial<
|
|
52
|
+
ensureScripts(presentation: unknown, options?: { signal?: AbortSignal } & Partial<ToSvgOptions>): Promise<AutoScriptSelection & { loaded: string[]; uncovered: string[] }>;
|
|
53
53
|
/** 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). */
|
|
54
|
-
pendingScripts(presentation: unknown, renderOptions?: Partial<
|
|
54
|
+
pendingScripts(presentation: unknown, renderOptions?: Partial<ToSvgOptions>): string[];
|
|
55
55
|
/** Names of the script-pack packages loaded so far. */
|
|
56
56
|
readonly loadedScriptPackages: string[];
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
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 `
|
|
60
|
+
* draws. Pass it as `{ fonts }` to `toSvg`, `<opf-deck>` and the player, core `paginate` and `validate`, and the editor.
|
|
61
61
|
* The registry behind it keeps the lower-level loaders (`ensureLazyFonts`, `ensureScripts`, `loadScripts`, `pendingLazyFonts`, `pendingScripts`).
|
|
62
62
|
*/
|
|
63
63
|
export interface BrowserFontsHandle extends FontsHandle {
|
|
@@ -66,13 +66,13 @@ export interface BrowserFontsHandle extends FontsHandle {
|
|
|
66
66
|
dispose(): void;
|
|
67
67
|
/**
|
|
68
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 `
|
|
70
|
-
* (`catalogs`, ...) over the loader's `renderOptions`. A document that does not resolve rejects with what `
|
|
69
|
+
* is needed. Cheap when nothing is needed. Besides `signal`, the call takes the `toSvg` options the document resolves with
|
|
70
|
+
* (`catalogs`, ...) over the loader's `renderOptions`. A document that does not resolve rejects with what `toSvg` throws for it; after
|
|
71
71
|
* `dispose()` it rejects with `font-registry-disposed`.
|
|
72
72
|
*/
|
|
73
|
-
ensure(presentation: unknown, options?: { signal?: AbortSignal } & Partial<
|
|
74
|
-
/** Synchronous: the vendored files and script packages `ensure` would fetch. Empty means a render can start now. Throws what `
|
|
75
|
-
pending(presentation: unknown, renderOptions?: Partial<
|
|
73
|
+
ensure(presentation: unknown, options?: { signal?: AbortSignal } & Partial<ToSvgOptions>): Promise<EnsureResult>;
|
|
74
|
+
/** Synchronous: the vendored files and script packages `ensure` would fetch. Empty means a render can start now. Throws what `toSvg` throws for a document that does not resolve. */
|
|
75
|
+
pending(presentation: unknown, renderOptions?: Partial<ToSvgOptions>): string[];
|
|
76
76
|
}
|
|
77
77
|
export declare function loadFonts(
|
|
78
78
|
options?: FontRegistryOptions & {
|
|
@@ -81,6 +81,17 @@ export declare function loadFonts(
|
|
|
81
81
|
document?: Document;
|
|
82
82
|
fetch?: typeof fetch;
|
|
83
83
|
signal?: AbortSignal;
|
|
84
|
+
/**
|
|
85
|
+
* RR-65: harfbuzzjs's `harfbuzz-subset.wasm` (the URL the host serves it from, its bytes or a compiled WebAssembly.Module). With
|
|
86
|
+
* it the handle carries `subsets`, and every SVG `toSvg` draws with this handle embeds each face cut to the characters the
|
|
87
|
+
* slide draws. Without it a browser handle embeds whole faces (the Node handle always subsets).
|
|
88
|
+
*/
|
|
89
|
+
subsetWasm?: string | URL | ArrayBuffer | ArrayBufferView | WebAssembly.Module;
|
|
90
|
+
/**
|
|
91
|
+
* RR-64: harfbuzzjs's `harfbuzz.wasm` (its URL, bytes or a compiled WebAssembly.Module). With it `text: "paths"` shapes outlines with
|
|
92
|
+
* HarfBuzz, as the browser shapes text; without it a browser handle shapes them with fontkit (the Node handle always uses HarfBuzz).
|
|
93
|
+
*/
|
|
94
|
+
shapeWasm?: string | URL | ArrayBuffer | ArrayBufferView | WebAssembly.Module;
|
|
84
95
|
crypto?: { subtle: SubtleCrypto };
|
|
85
96
|
/** Script faces to load once the registry exists: "auto" (with `presentation`), "all" or ISO 15924 codes. Needs `scriptBaseUrl`. */
|
|
86
97
|
scripts?: ScriptSelection | "auto";
|
|
@@ -100,11 +111,11 @@ export declare function loadFonts(
|
|
|
100
111
|
*/
|
|
101
112
|
extraLazyFonts?: readonly ExtraLazyFont[];
|
|
102
113
|
/**
|
|
103
|
-
* FF-41: the `
|
|
114
|
+
* FF-41: the `toSvg` options the documents resolve with (`catalogs` above all), the default for `pendingLazyFonts`,
|
|
104
115
|
* `ensureLazyFonts`, `pendingScripts` and `ensureScripts`. A layout or font scheme id that only the host's catalogs have
|
|
105
116
|
* resolves the same way here as it does when the host renders. A call's own options win.
|
|
106
117
|
*/
|
|
107
|
-
renderOptions?: Partial<
|
|
118
|
+
renderOptions?: Partial<ToSvgOptions>;
|
|
108
119
|
},
|
|
109
120
|
): Promise<BrowserFontsHandle>;
|
|
110
121
|
export declare function scriptFontPackages(scripts: ScriptSelection): BundledFontPackage[];
|
|
@@ -127,10 +138,10 @@ export interface ExtraLazyFont { family: string; weight: number; italic?: boolea
|
|
|
127
138
|
export declare function splitStartupFaces<T extends { family: string; weight: number; italic?: boolean }>(faces: readonly T[], options?: { startup?: (face: T) => boolean }): { startup: T[]; rest: T[] };
|
|
128
139
|
/**
|
|
129
140
|
* The font families a presentation's slides resolve (heading, body and code roles of every slide). `options` are the
|
|
130
|
-
* `
|
|
141
|
+
* `toSvg` options the document resolves with (`catalogs`, ...). A document that does not resolve throws what `toSvg`
|
|
131
142
|
* throws for it (FF-41: it no longer returns an empty set). This is the role families, not what is drawn: see `presentationFaces`.
|
|
132
143
|
*/
|
|
133
|
-
export declare function presentationFamilies(presentation: unknown, options?: Partial<
|
|
144
|
+
export declare function presentationFamilies(presentation: unknown, options?: Partial<ToSvgOptions>): Set<string>;
|
|
134
145
|
/** A face request or a drawn face: a family, a weight and a style. */
|
|
135
146
|
export interface FaceRequest { family: string; weight: number; italic: boolean }
|
|
136
147
|
/**
|
|
@@ -140,16 +151,16 @@ export interface FaceRequest { family: string; weight: number; italic: boolean }
|
|
|
140
151
|
* faces are the styles as the document names them. With `registry` (`{faces, policy, aliases, fallbackFamily}`: the faces a
|
|
141
152
|
* registry holds or could hold and its substitution options) every style resolves as that registry resolves it, in the two
|
|
142
153
|
* steps drawing takes (role family, then weight within it), and the result is the faces the registry paints.
|
|
143
|
-
* `options` are the `
|
|
154
|
+
* `options` are the `toSvg` options the document resolves with; a document that does not resolve throws what `toSvg`
|
|
144
155
|
* throws for it.
|
|
145
156
|
*/
|
|
146
|
-
export declare function presentationFaces(presentation: unknown, options?: Partial<
|
|
157
|
+
export declare function presentationFaces(presentation: unknown, options?: Partial<ToSvgOptions>, registry?: { faces: Iterable<FaceRequest>; policy?: "none" | "metric" | "visual"; aliases?: ReadonlyMap<string, string>; fallbackFamily?: string }): FaceRequest[];
|
|
147
158
|
/**
|
|
148
159
|
* The vendored faces (of `lazy`) a presentation draws that the registry does not hold. `held` is `registry.describeFaces()`,
|
|
149
160
|
* `loaded` the lazy files already loaded; `policy`, `aliases` and `fallbackFamily` are the registry's. Face level: only the
|
|
150
161
|
* drawn faces, so an edit adding a bold run adds one face.
|
|
151
162
|
*/
|
|
152
|
-
export declare function lazyFacesNeeded(presentation: unknown, renderOptions: Partial<
|
|
163
|
+
export declare function lazyFacesNeeded(presentation: unknown, renderOptions: Partial<ToSvgOptions> | undefined, context: { lazy: readonly LazyFont[]; held: Iterable<FaceRequest>; loaded?: ReadonlySet<string>; policy?: "none" | "metric" | "visual"; aliases?: ReadonlyMap<string, string>; fallbackFamily?: string }): LazyFont[];
|
|
153
164
|
/**
|
|
154
165
|
* The vendored faces a list of requests needs that the registry does not hold, following the registry's own order and
|
|
155
166
|
* `policy` (default "visual"): the family itself, an alias target, the declared replacement, then its alternates.
|
package/dist/fonts-browser.js
CHANGED
|
@@ -3,6 +3,8 @@ import { lazyFontEntries, lazyFontList, normalizeExtraLazyFonts } from "./lazy-f
|
|
|
3
3
|
import { lazyFacesNeeded } from "./lazy-fonts.js";
|
|
4
4
|
import { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
5
5
|
import { textOutlines } from "./text-paths.js";
|
|
6
|
+
import { createSubsetter, fontSubsets } from "./font-subset.js";
|
|
7
|
+
import { instantiateShaper } from "./hb-shape.js";
|
|
6
8
|
import { analyzePresentationScripts, nextFallbackPackage, scriptFontPackages, scriptPackageEntries, scriptSelectionOf, uncoveredCjkCharacters } from "./script-font-pack.js";
|
|
7
9
|
export { autoScriptSelection, detectPresentationScripts, scriptFontEntries, scriptFontPackages } from "./script-font-pack.js";
|
|
8
10
|
export { lazyFontEntries, lazyFontList, splitStartupFaces } from "./lazy-font-list.js";
|
|
@@ -95,7 +97,7 @@ async function loadRegistry(entries, options = {}) {
|
|
|
95
97
|
// Script faces (FF-19) are loaded lazily, once a document needs them, and only for the scripts it uses.
|
|
96
98
|
const scriptPackages = new Set();
|
|
97
99
|
let queue = Promise.resolve(), disposed = false;
|
|
98
|
-
// FF-41: the `
|
|
100
|
+
// FF-41: the `toSvg` options a document resolves with (`catalogs`, ...). `options.renderOptions` are the defaults; a call's own
|
|
99
101
|
// options (everything but `signal`) are added over them, so a host that passes its canvas options gets the same resolution here.
|
|
100
102
|
const renderOptionsFor = (callOptions) => { const { signal: _signal, ...own } = callOptions ?? {}; return { ...options.renderOptions, ...own }; };
|
|
101
103
|
const gone = () => new OPFFontError("font-registry-disposed", "The font registry was disposed.");
|
|
@@ -149,7 +151,7 @@ async function loadRegistry(entries, options = {}) {
|
|
|
149
151
|
* fallback needs for characters the loaded faces lack. Cheap when nothing new is needed; call it after edits and
|
|
150
152
|
* render again afterwards. Resolves with the detected scripts, the packages loaded by this call, the scripts no
|
|
151
153
|
* pinned font serves and `uncovered`, drawn CJK characters no loaded face covers (the renderer reports
|
|
152
|
-
* `missing-glyph` for them). Besides `signal`, the call takes the `
|
|
154
|
+
* `missing-glyph` for them). Besides `signal`, the call takes the `toSvg` options the document resolves with
|
|
153
155
|
* (`catalogs`, ...) over the loader's `renderOptions`. Each package loads all or nothing. If some package fails, the others still load: the
|
|
154
156
|
* call then rejects with an OPFFontError (the first failure's code) whose `details` hold `loaded` (packages this
|
|
155
157
|
* call did load) and `failed` (`{package, code, message}` per failed package); nothing is lost and a later call
|
|
@@ -174,7 +176,7 @@ async function loadRegistry(entries, options = {}) {
|
|
|
174
176
|
}
|
|
175
177
|
return { ...selection, loaded: loadedNow, uncovered: uncoveredCjkCharacters(analysis, { covers }) };
|
|
176
178
|
},
|
|
177
|
-
/** Synchronous: script-pack packages the presentation needs that are not loaded yet. Empty means `ensureScripts` would fetch nothing. `renderOptions` are the `
|
|
179
|
+
/** Synchronous: script-pack packages the presentation needs that are not loaded yet. Empty means `ensureScripts` would fetch nothing. `renderOptions` are the `toSvg` options the document resolves with (`catalogs`, ...). */
|
|
178
180
|
pendingScripts(presentation, renderOptions) {
|
|
179
181
|
const analysis = analyzePresentationScripts(presentation, undefined, renderOptionsFor(renderOptions));
|
|
180
182
|
const primary = scriptFontPackages(scriptSelectionOf(analysis).scripts).map((item) => item.name).filter((name) => !scriptPackages.has(name));
|
|
@@ -192,7 +194,7 @@ async function loadRegistry(entries, options = {}) {
|
|
|
192
194
|
const policy = options.substitutionPolicy ?? "none";
|
|
193
195
|
const aliasTargets = new Map(Object.entries(options.aliases ?? {}).map(([from, to]) => [from.toLowerCase(), to]));
|
|
194
196
|
// Face level (FF-41): the faces the document draws, resolved as the registry resolves them with every vendored face loaded.
|
|
195
|
-
// A document that does not resolve throws what `
|
|
197
|
+
// A document that does not resolve throws what `toSvg` throws for it.
|
|
196
198
|
const neededLazy = (presentation, callOptions) => lazyFacesNeeded(presentation, renderOptionsFor(callOptions), { lazy, held: registry.describeFaces(), loaded: lazyLoaded, policy, aliases: aliasTargets, fallbackFamily: options.fallbackFamily });
|
|
197
199
|
const lazyBaseUrl = () => {
|
|
198
200
|
if (typeof options.lazyFontsBaseUrl !== "string" || !options.lazyFontsBaseUrl)
|
|
@@ -208,8 +210,8 @@ async function loadRegistry(entries, options = {}) {
|
|
|
208
210
|
* policy (nothing is downloaded for a family the policy would not resolve), once each. All or nothing: on any failure
|
|
209
211
|
* (fetch, hash, FontFace.load, registry) the document and registry are unchanged and the call can be retried. Cheap when
|
|
210
212
|
* nothing is needed. Call it before measuring a document, and again after edits that change fonts. Resolves with the faces
|
|
211
|
-
* added. Besides `signal`, the call takes the `
|
|
212
|
-
* loader's `renderOptions`; a document that does not resolve rejects with what `
|
|
213
|
+
* added. Besides `signal`, the call takes the `toSvg` options the document resolves with (`catalogs`, ...) over the
|
|
214
|
+
* loader's `renderOptions`; a document that does not resolve rejects with what `toSvg` throws for it.
|
|
213
215
|
*/
|
|
214
216
|
ensureLazyFonts(presentation, callOptions = {}) {
|
|
215
217
|
const result = queue.then(async () => {
|
|
@@ -245,7 +247,7 @@ async function loadRegistry(entries, options = {}) {
|
|
|
245
247
|
queue = result.catch(() => {});
|
|
246
248
|
return result;
|
|
247
249
|
},
|
|
248
|
-
/** 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 `
|
|
250
|
+
/** 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 `toSvg` options the document resolves with (`catalogs`, ...). */
|
|
249
251
|
pendingLazyFonts: (presentation, renderOptions) => neededLazy(presentation, renderOptions),
|
|
250
252
|
});
|
|
251
253
|
/** Names of the script-pack packages loaded so far. */
|
|
@@ -275,13 +277,37 @@ const MAX_ENSURE_ROUNDS = 4;
|
|
|
275
277
|
/**
|
|
276
278
|
* The fonts handle for a browser: the faces you list (`faces`, each `{ url | data, family, weight, italic, sha256 }`, fetched and
|
|
277
279
|
* hash-verified, then registered with the document) and, on demand, the script and vendored faces a deck draws. Pass it as `{ fonts }` to
|
|
278
|
-
* `
|
|
280
|
+
* `toSvg`, `<opf-deck>` and the player, core `paginate` and `validate`, and the editor. `scripts`, `scriptBaseUrl`,
|
|
279
281
|
* `lazyFontsBaseUrl`, `extraLazyFonts`, `renderOptions`, `signal`, `document`, `fetch`, `crypto` and the registry options
|
|
280
282
|
* (`substitutionPolicy`, `aliases`, `fallbackFamily`, `themeFonts`, `strictGlyphs`) are as for the registry (see `fonts-browser.d.ts`).
|
|
281
283
|
* `dispose()` removes every face from the document.
|
|
282
284
|
*/
|
|
283
|
-
|
|
284
|
-
|
|
285
|
+
// RR-65: the host's copy of harfbuzzjs's `harfbuzz-subset.wasm` (a URL it serves, the bytes or a compiled module), instantiated
|
|
286
|
+
// asynchronously here (a browser may refuse to compile a large module synchronously on the main thread).
|
|
287
|
+
async function browserSubsetter(source, fetchImpl, signal) {
|
|
288
|
+
if (source instanceof WebAssembly.Module) return createSubsetter(await WebAssembly.instantiate(source, {}));
|
|
289
|
+
if (source instanceof ArrayBuffer || ArrayBuffer.isView(source)) return createSubsetter((await WebAssembly.instantiate(source, {})).instance);
|
|
290
|
+
if (typeof source !== "string" && !(source instanceof URL)) throw new OPFFontError("invalid-font-source", "subsetWasm must be the URL of harfbuzz-subset.wasm, its bytes or a compiled WebAssembly.Module.");
|
|
291
|
+
const response = await (fetchImpl ?? globalThis.fetch)(String(source), { signal });
|
|
292
|
+
if (!response.ok) throw new OPFFontError("font-resource-unavailable", `harfbuzz-subset.wasm could not be fetched (${response.status}).`, { url: String(source) });
|
|
293
|
+
return createSubsetter((await WebAssembly.instantiate(await response.arrayBuffer(), {})).instance);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// RR-64 phase 2: the host's copy of harfbuzzjs's `harfbuzz.wasm`, instantiated asynchronously, so outlines are shaped by HarfBuzz.
|
|
297
|
+
async function browserShaper(source, fetchImpl, signal) {
|
|
298
|
+
return instantiateShaper(source instanceof WebAssembly.Module || source instanceof ArrayBuffer || ArrayBuffer.isView(source) ? source : await fetchWasm(source, fetchImpl, signal, "harfbuzz.wasm"));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function fetchWasm(source, fetchImpl, signal, name) {
|
|
302
|
+
if (typeof source !== "string" && !(source instanceof URL)) throw new OPFFontError("invalid-font-source", `${name} must be given as a URL, its bytes or a compiled WebAssembly.Module.`);
|
|
303
|
+
const response = await (fetchImpl ?? globalThis.fetch)(String(source), { signal });
|
|
304
|
+
if (!response.ok) throw new OPFFontError("font-resource-unavailable", `${name} could not be fetched (${response.status}).`, { url: String(source) });
|
|
305
|
+
return response.arrayBuffer();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function loadFonts({ faces = [], subsetWasm, shapeWasm, ...options } = {}) {
|
|
309
|
+
const [registry, subsetter, shaper] = await Promise.all([loadRegistry(faces, options), subsetWasm === undefined ? null : browserSubsetter(subsetWasm, options.fetch, options.signal),
|
|
310
|
+
shapeWasm === undefined ? null : browserShaper(shapeWasm, options.fetch, options.signal)]);
|
|
285
311
|
// The embedded list is base64 of every eager face: compute it once, and again only when faces were added.
|
|
286
312
|
let embedded, stale = true;
|
|
287
313
|
const addFaces = registry.addFaces;
|
|
@@ -294,15 +320,17 @@ export async function loadFonts({ faces = [], ...options } = {}) {
|
|
|
294
320
|
textMeasurement: registry.textMeasurement,
|
|
295
321
|
get embeddedFonts() { if (stale) { embedded = registry.embeddedFonts; stale = false; } return embedded; },
|
|
296
322
|
registry,
|
|
297
|
-
// RR-64: the outline engine `
|
|
298
|
-
outlines: textOutlines({ registry }),
|
|
323
|
+
// RR-64: the outline engine `toSvg(deck, { fonts, text: "paths" })` draws text with, over this registry's faces.
|
|
324
|
+
outlines: textOutlines({ registry, shaper }),
|
|
325
|
+
// RR-65: with `subsetWasm`, the subset engine that cuts each face an SVG embeds to the characters the slide draws.
|
|
326
|
+
...(subsetter ? { subsets: fontSubsets(registry, subsetter) } : {}),
|
|
299
327
|
manifest: BUNDLED_FONT_MANIFEST,
|
|
300
328
|
get substitutions() { return registry.substitutions; },
|
|
301
329
|
/**
|
|
302
330
|
* Load the vendored and script faces the presentation draws, each hash-verified, all or nothing, and repeat while loading
|
|
303
331
|
* changes what is needed (a script face can need a vendored one). Cheap when nothing is needed. Besides `signal`, the call
|
|
304
|
-
* takes the `
|
|
305
|
-
* does not resolve rejects with what `
|
|
332
|
+
* takes the `toSvg` options the document resolves with (`catalogs`, ...) over the loader's `renderOptions`. A document that
|
|
333
|
+
* does not resolve rejects with what `toSvg` throws for it. After `dispose()` it rejects with `font-registry-disposed`.
|
|
306
334
|
*/
|
|
307
335
|
async ensure(presentation, callOptions = {}) {
|
|
308
336
|
const lazy = [], scripts = [];
|
|
@@ -316,7 +344,7 @@ export async function loadFonts({ faces = [], ...options } = {}) {
|
|
|
316
344
|
}
|
|
317
345
|
return { scripts, lazy, uncovered };
|
|
318
346
|
},
|
|
319
|
-
/** Synchronous: the files and script packages `ensure` would fetch. Empty means a render can start now. Throws what `
|
|
347
|
+
/** Synchronous: the files and script packages `ensure` would fetch. Empty means a render can start now. Throws what `toSvg` throws for a document that does not resolve. */
|
|
320
348
|
pending,
|
|
321
349
|
dispose: () => registry.dispose(),
|
|
322
350
|
};
|
package/dist/fonts-node.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type {FontRegistry,FontRegistryOptions,FontFaceInput,FontsHandle,EnsureResult,ScriptFontProfile} from "./fonts.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type {ToSvgOptions} 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});
|
|
5
5
|
/** "all" or ISO 15924 codes (`Jpan`, `Hans`, `Hant`, `Kore`, `Arab`, `Hebr`, `Deva`, `Thai`, ...). */
|
|
@@ -16,9 +16,9 @@ export interface AutoScriptSelection {
|
|
|
16
16
|
/** Script-pack packages that `scripts: "auto"` loaded or found not installed. */
|
|
17
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[] }
|
|
18
18
|
/** Script keys whose faces a preview of the presentation needs: text decides, the document language only tells Han scripts apart. */
|
|
19
|
-
/** `options` may also carry the `
|
|
20
|
-
export declare function detectPresentationScripts(presentation: unknown, options?: { profile?: ScriptFontProfile } & Partial<
|
|
21
|
-
export declare function autoScriptSelection(presentation: unknown, options?: { profile?: ScriptFontProfile } & Partial<
|
|
19
|
+
/** `options` may also carry the `toSvg` 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<ToSvgOptions>): string[];
|
|
21
|
+
export declare function autoScriptSelection(presentation: unknown, options?: { profile?: ScriptFontProfile } & Partial<ToSvgOptions>): AutoScriptSelection;
|
|
22
22
|
export interface ScriptPackOptions {
|
|
23
23
|
/** Caller-supplied faces, loaded first so a licensed real font resolves as an exact face (FF-31). */
|
|
24
24
|
faces?: readonly CallerFontFace[];
|
|
@@ -30,8 +30,8 @@ export interface ScriptPackOptions {
|
|
|
30
30
|
scripts?: ScriptSelection | "auto";
|
|
31
31
|
/** The presentation whose text decides `scripts: "auto"`. */
|
|
32
32
|
presentation?: unknown;
|
|
33
|
-
/** The `
|
|
34
|
-
renderOptions?: Partial<
|
|
33
|
+
/** The `toSvg` options the presentation resolves with (`catalogs`, ...), for the font schemes `scripts: "auto"` reads (FF-41). */
|
|
34
|
+
renderOptions?: Partial<ToSvgOptions>;
|
|
35
35
|
/** Receives `script-font-unavailable` and `script-font-not-installed` for `scripts: "auto"`. */
|
|
36
36
|
onDiagnostic?: (diagnostic: {code: string; message: string; script?: string; package?: string; scripts?: string[]}) => void;
|
|
37
37
|
}
|
|
@@ -74,7 +74,7 @@ export declare function scriptFontPackages(scripts: ScriptSelection): BundledFon
|
|
|
74
74
|
export interface NodeFontsHandle extends FontsHandle {
|
|
75
75
|
readonly registry: NodeFontRegistry;
|
|
76
76
|
readonly manifest: BundledFontManifest;
|
|
77
|
-
/** Every font file the handle holds, for `
|
|
77
|
+
/** Every font file the handle holds, for `toPng` and `toPdf`; includes the script faces `ensure` loaded. */
|
|
78
78
|
readonly fontFiles: string[];
|
|
79
79
|
/** The files are already in `fontFiles`, so a conversion adds no bundled faces. */
|
|
80
80
|
readonly useBundledFonts: false;
|
|
@@ -86,9 +86,9 @@ export interface NodeFontsHandle extends FontsHandle {
|
|
|
86
86
|
* the new faces. A package that is not installed or a script no pinned font serves is reported once through `onDiagnostic`.
|
|
87
87
|
* Resolves with the packages this call added (`scripts`; `lazy` is empty: Node loads every vendored face with its pack).
|
|
88
88
|
*/
|
|
89
|
-
ensure(presentation: unknown, options?: { onDiagnostic?: ScriptPackOptions["onDiagnostic"]; renderOptions?: Partial<
|
|
89
|
+
ensure(presentation: unknown, options?: { onDiagnostic?: ScriptPackOptions["onDiagnostic"]; renderOptions?: Partial<ToSvgOptions> }): Promise<EnsureResult>;
|
|
90
90
|
/** Synchronous: the script packages the presentation needs that are not loaded yet. */
|
|
91
|
-
pending(presentation: unknown, renderOptions?: Partial<
|
|
91
|
+
pending(presentation: unknown, renderOptions?: Partial<ToSvgOptions>): string[];
|
|
92
92
|
}
|
|
93
93
|
export interface LoadFontsOptions extends FontRegistryOptions, ScriptPackOptions {
|
|
94
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. */
|
package/dist/fonts-node.js
CHANGED
|
@@ -7,6 +7,8 @@ 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
|
import { textOutlines } from "./text-paths.js";
|
|
10
|
+
import { createSubsetter, fontSubsets } from "./font-subset.js";
|
|
11
|
+
import { createShaper } from "./hb-shape.js";
|
|
10
12
|
export { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
11
13
|
import { analyzePresentationScripts, autoScriptSelection, nextFallbackPackage, scriptFontPackages, scriptSelectionOf, uncoveredCjkCharacters } from "./script-font-pack.js";
|
|
12
14
|
export { autoScriptSelection, detectPresentationScripts, scriptFontPackages } from "./script-font-pack.js";
|
|
@@ -93,7 +95,7 @@ const loadPack = pack => loadPackages(BUNDLED_FONT_MANIFEST.packages.filter(item
|
|
|
93
95
|
/**
|
|
94
96
|
* Adds the script pack. `scripts` is 'all', ISO 15924 codes, or 'auto' (FF-19): detect the scripts the
|
|
95
97
|
* `presentation` draws and load only their faces. Auto never fails on a missing optional package or a
|
|
96
|
-
* script no pinned font serves; it reports them through `onDiagnostic` and `selection`. `renderOptions` are the host's `
|
|
98
|
+
* script no pinned font serves; it reports them through `onDiagnostic` and `selection`. `renderOptions` are the host's `toSvg`
|
|
97
99
|
* options (see `analyzePresentationScripts`).
|
|
98
100
|
*/
|
|
99
101
|
async function withScripts(loaded, scripts, {presentation, onDiagnostic, renderOptions} = {}) {
|
|
@@ -247,9 +249,26 @@ async function ensureFonts(registry, loaded, presentation, {onDiagnostic, render
|
|
|
247
249
|
return {scripts: loaded.packages.slice(before), lazy: [], uncovered: selection.uncovered ? [...selection.uncovered] : []};
|
|
248
250
|
}
|
|
249
251
|
|
|
252
|
+
// RR-65: hb-subset from the pinned harfbuzzjs package (MIT), compiled once per process. The module has no imports, so it is
|
|
253
|
+
// instantiated synchronously and toSvg stays synchronous. A copy that cannot be loaded leaves the handle without `subsets`
|
|
254
|
+
// (whole faces, as before RR-65): a byte saving never stops fonts from loading.
|
|
255
|
+
let subsetterPromise;
|
|
256
|
+
function nodeSubsetter() {
|
|
257
|
+
subsetterPromise ??= readFile(require.resolve("harfbuzzjs/dist/harfbuzz-subset.wasm")).then(createSubsetter, () => null).catch(() => null);
|
|
258
|
+
return subsetterPromise;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// RR-64 phase 2: HarfBuzz's shaper (harfbuzz.wasm of the same package) for text drawn as outlines, so the outlines are shaped as
|
|
262
|
+
// a browser shapes the text. Without it outlines are shaped with fontkit.
|
|
263
|
+
let shaperPromise;
|
|
264
|
+
function nodeShaper() {
|
|
265
|
+
shaperPromise ??= readFile(require.resolve("harfbuzzjs/dist/harfbuzz.wasm")).then(createShaper, () => null).catch(() => null);
|
|
266
|
+
return shaperPromise;
|
|
267
|
+
}
|
|
268
|
+
|
|
250
269
|
/**
|
|
251
270
|
* The fonts handle: one set of verified font inputs for layout, SVG, editor, PPTX and Node raster export. Pass it as `{ fonts }` to
|
|
252
|
-
* `
|
|
271
|
+
* `toSvg`, `toPng`, `toPdf`, core `paginate` and `validate`, and `toPptx`.
|
|
253
272
|
*
|
|
254
273
|
* Bundled, openly licensed faces only: no system font discovery and no network requests. `pack`: `base` (default, Roboto),
|
|
255
274
|
* `office` (the Office substitutes and the open families) or `none` (only the `faces` you supply). Everything else
|
|
@@ -266,6 +285,7 @@ export async function loadFonts({pack = "base", embedScriptFonts = false, ...opt
|
|
|
266
285
|
// The embedded list is base64 of every face: computed on first use, and again after `ensure` added faces.
|
|
267
286
|
let embeddedFonts, fontFiles = [...registry.fontFiles];
|
|
268
287
|
const refresh = () => { embeddedFonts = undefined; fontFiles = [...registry.fontFiles]; };
|
|
288
|
+
const [subsetter, shaper] = await Promise.all([nodeSubsetter(), nodeShaper()]);
|
|
269
289
|
return {
|
|
270
290
|
textMeasurement: registry.textMeasurement,
|
|
271
291
|
get embeddedFonts() { return (embeddedFonts ??= selectEmbedded()); },
|
|
@@ -273,8 +293,10 @@ export async function loadFonts({pack = "base", embedScriptFonts = false, ...opt
|
|
|
273
293
|
useBundledFonts: false,
|
|
274
294
|
loadSystemFonts: false,
|
|
275
295
|
registry,
|
|
276
|
-
// RR-64: the outline engine `
|
|
277
|
-
outlines: textOutlines({ registry }),
|
|
296
|
+
// RR-64: the outline engine `toSvg(deck, { fonts, text: "paths" })` draws text with, over this registry's faces.
|
|
297
|
+
outlines: textOutlines({ registry, shaper }),
|
|
298
|
+
// RR-65: the subset engine that cuts each face an SVG embeds to the characters the slide draws.
|
|
299
|
+
...(subsetter ? { subsets: fontSubsets(registry, subsetter) } : {}),
|
|
278
300
|
manifest: BUNDLED_FONT_MANIFEST,
|
|
279
301
|
get substitutions() { return registry.substitutions; },
|
|
280
302
|
/** Load the script faces the presentation's text needs (see `ensureFonts`); `embeddedFonts` and `fontFiles` then include them. */
|
package/dist/fonts.d.ts
CHANGED
|
@@ -34,14 +34,14 @@ export interface FontRegistryOptions {
|
|
|
34
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}>[];
|
|
35
35
|
export declare const EXPERIMENTAL_FONT_CANDIDATES: readonly Readonly<{requestedFamily:string;substitute:string;source:string;note:string}>[];
|
|
36
36
|
/**
|
|
37
|
-
* What the deck-level functions read from their `fonts` option: `
|
|
38
|
-
* the faces to embed; `
|
|
37
|
+
* What the deck-level functions read from their `fonts` option: `toSvg` and `<opf-deck>` take the measurement and
|
|
38
|
+
* the faces to embed; `toPng` and `toPdf` (Node) take the font files; core `paginate`, `validate` and `resolveSlideContext` take the
|
|
39
39
|
* measurement. The object `loadFonts()` returns is a `FontsHandle`, which extends this; any object with these fields works.
|
|
40
40
|
*/
|
|
41
41
|
export interface RenderFonts {
|
|
42
42
|
/** Measures text with the real faces. Without it layout uses core's portable estimate. */
|
|
43
43
|
textMeasurement?: TextMeasurement;
|
|
44
|
-
/** Faces `
|
|
44
|
+
/** Faces `toSvg` may write into an SVG as @font-face data: each only into the SVGs whose text draws it (its family, weight and style), unless flagged `embed: "always"` (RR-61). */
|
|
45
45
|
embeddedFonts?: readonly EmbeddedFont[];
|
|
46
46
|
/** Font files the Node raster and PDF conversions draw with. */
|
|
47
47
|
fontFiles?: readonly string[];
|
|
@@ -49,10 +49,14 @@ export interface RenderFonts {
|
|
|
49
49
|
useBundledFonts?: boolean;
|
|
50
50
|
/** Whether the raster conversion loads system fonts. Default false (the output never depends on the machine). */
|
|
51
51
|
loadSystemFonts?: boolean;
|
|
52
|
-
/** The outline engine `
|
|
52
|
+
/** The outline engine `text: "paths"` draws text with (RR-64); a `loadFonts` handle carries one over its registry's faces. */
|
|
53
53
|
outlines?: TextOutlines;
|
|
54
|
+
/** The subset engine (RR-65): each face an SVG embeds is cut to the characters the slide draws. The Node `loadFonts` handle carries one; a browser handle with `subsetWasm`. */
|
|
55
|
+
subsets?: FontSubsets;
|
|
54
56
|
}
|
|
55
|
-
/**
|
|
57
|
+
/** Cuts an embedded face to code points (RR-65), with hb-subset; a face whose license, Reserved Font Name or fsType does not allow it keeps its own data URL. */
|
|
58
|
+
export interface FontSubsets { subsetDataUrl(font: EmbeddedFont, codePoints: Iterable<number>): string }
|
|
59
|
+
/** Draws a slide's text as glyph outlines (RR-64). Read by `toSvg`; hosts pass the handle and `text: "paths"`. */
|
|
56
60
|
export interface TextOutlines { outlineSlideText(content: string[], options: object): { content: string[]; defs: string } }
|
|
57
61
|
/** What `fonts.ensure(presentation)` loaded: the script packages and vendored faces that were missing, and drawn CJK characters no face covers. */
|
|
58
62
|
export interface EnsureResult { scripts: string[]; lazy: LazyFont[]; uncovered: string[] }
|
|
@@ -62,7 +66,7 @@ export interface FontsHandle extends RenderFonts {
|
|
|
62
66
|
readonly embeddedFonts: EmbeddedFont[];
|
|
63
67
|
/** The face registry behind the handle (shaping, resolution, lazy and script loading). */
|
|
64
68
|
readonly registry: FontRegistry;
|
|
65
|
-
/** The outline engine for `
|
|
69
|
+
/** The outline engine for `text: "paths"` (RR-64), over this registry's faces, including faces `ensure` loads later. */
|
|
66
70
|
readonly outlines: TextOutlines;
|
|
67
71
|
/** The pinned manifest of the bundled font packages. */
|
|
68
72
|
readonly manifest: { readonly version: number; readonly packages: readonly object[] };
|
package/dist/hb-shape.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// RR-64 phase 2: HarfBuzz shaping for text drawn as outlines. Browsers shape with HarfBuzz, and fontkit differs from it on some
|
|
2
|
+
// scripts (Thai tone marks, Bengali and Khmer conjunct forms, Myanmar medials, Telugu offsets, Hebrew points, Japanese combining
|
|
3
|
+
// sound marks), so outlines shaped by HarfBuzz are what the browser draws. This drives `harfbuzz.wasm` of the pinned harfbuzzjs
|
|
4
|
+
// package (MIT) through HarfBuzz's C API. The module needs only five small host functions, so it is instantiated synchronously
|
|
5
|
+
// from its bytes and the renderer stays synchronous. Glyphs come back in the shape `pdf-fonts.js` `shape()` returns: left to
|
|
6
|
+
// right, `{gid, codePoints, advance, xOffset, yOffset}` in font units.
|
|
7
|
+
|
|
8
|
+
const HB_MEMORY_MODE_READONLY = 1;
|
|
9
|
+
const HB_DIRECTION = { ltr: 4, rtl: 5 };
|
|
10
|
+
const INFO_SIZE = 20, POSITION_SIZE = 20, FEATURE_SIZE = 16;
|
|
11
|
+
|
|
12
|
+
// The five host functions the module imports, bound to the instance's memory once it exists.
|
|
13
|
+
function hostImports() {
|
|
14
|
+
const state = { memory: null };
|
|
15
|
+
const imports = {
|
|
16
|
+
env: {
|
|
17
|
+
// Emscripten grows the heap through the host: grow the memory to the requested size.
|
|
18
|
+
emscripten_resize_heap: (requested) => {
|
|
19
|
+
try {
|
|
20
|
+
const missing = (requested >>> 0) - state.memory.buffer.byteLength;
|
|
21
|
+
if (missing > 0) state.memory.grow(Math.ceil(missing / 65536));
|
|
22
|
+
return 1;
|
|
23
|
+
} catch { return 0; }
|
|
24
|
+
},
|
|
25
|
+
_abort_js: () => { throw new Error("HarfBuzz aborted."); },
|
|
26
|
+
_setitimer_js: () => 0,
|
|
27
|
+
_emscripten_runtime_keepalive_clear: () => {},
|
|
28
|
+
},
|
|
29
|
+
wasi_snapshot_preview1: { proc_exit: (code) => { throw new Error(`HarfBuzz exited (${code}).`); } },
|
|
30
|
+
};
|
|
31
|
+
return { imports, state };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A synchronous HarfBuzz shaper over `harfbuzz.wasm` (its bytes or a compiled WebAssembly.Module): Node. */
|
|
35
|
+
export function createShaper(wasm) {
|
|
36
|
+
const { imports, state } = hostImports();
|
|
37
|
+
return shaperOver(new WebAssembly.Instance(wasm instanceof WebAssembly.Module ? wasm : new WebAssembly.Module(wasm), imports), state);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The same shaper, instantiated asynchronously (a browser may refuse to compile a large module synchronously). */
|
|
41
|
+
export async function instantiateShaper(wasm) {
|
|
42
|
+
const { imports, state } = hostImports();
|
|
43
|
+
const result = await WebAssembly.instantiate(wasm, imports);
|
|
44
|
+
return shaperOver(result instanceof WebAssembly.Instance ? result : result.instance, state);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function shaperOver(instance, state) {
|
|
48
|
+
const hb = instance.exports;
|
|
49
|
+
const memory = state.memory = hb.memory;
|
|
50
|
+
hb.__wasm_call_ctors?.();
|
|
51
|
+
const fonts = new Map();
|
|
52
|
+
const utf8 = new TextEncoder();
|
|
53
|
+
const string = (text) => {
|
|
54
|
+
const bytes = utf8.encode(text), pointer = hb.malloc(bytes.length + 1);
|
|
55
|
+
const view = new Uint8Array(memory.buffer, pointer, bytes.length + 1);
|
|
56
|
+
view.set(bytes);
|
|
57
|
+
view[bytes.length] = 0;
|
|
58
|
+
return { pointer, length: bytes.length };
|
|
59
|
+
};
|
|
60
|
+
// One HarfBuzz font per face, kept for the shaper's lifetime (faces are few and shaped again and again).
|
|
61
|
+
const fontFor = (key, data) => {
|
|
62
|
+
let font = fonts.get(key);
|
|
63
|
+
if (font) return font;
|
|
64
|
+
const pointer = hb.malloc(data.length);
|
|
65
|
+
new Uint8Array(memory.buffer, pointer, data.length).set(data);
|
|
66
|
+
const blob = hb.hb_blob_create(pointer, data.length, HB_MEMORY_MODE_READONLY, 0, 0);
|
|
67
|
+
const face = hb.hb_face_create(blob, 0);
|
|
68
|
+
font = hb.hb_font_create(face);
|
|
69
|
+
fonts.set(key, font);
|
|
70
|
+
return font;
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
/** Shape `text` with the face (`key` names it, `data` is its bytes); `features` is an object of OpenType feature booleans. */
|
|
74
|
+
shape(key, data, text, { direction = "ltr", language, features } = {}) {
|
|
75
|
+
const font = fontFor(key, data);
|
|
76
|
+
const buffer = hb.hb_buffer_create();
|
|
77
|
+
const units = text.length, textPointer = hb.malloc(units * 2 + 2);
|
|
78
|
+
const view = new Uint16Array(memory.buffer, textPointer, units);
|
|
79
|
+
for (let index = 0; index < units; index++) view[index] = text.charCodeAt(index);
|
|
80
|
+
const allocated = [textPointer];
|
|
81
|
+
try {
|
|
82
|
+
hb.hb_buffer_add_utf16(buffer, textPointer, units, 0, units);
|
|
83
|
+
hb.hb_buffer_set_direction(buffer, HB_DIRECTION[direction] ?? HB_DIRECTION.ltr);
|
|
84
|
+
if (language) { const tag = string(language); allocated.push(tag.pointer); hb.hb_buffer_set_language(buffer, hb.hb_language_from_string(tag.pointer, tag.length)); }
|
|
85
|
+
hb.hb_buffer_guess_segment_properties(buffer);
|
|
86
|
+
const entries = Object.entries(features ?? {});
|
|
87
|
+
let featurePointer = 0;
|
|
88
|
+
if (entries.length) {
|
|
89
|
+
featurePointer = hb.malloc(entries.length * FEATURE_SIZE);
|
|
90
|
+
allocated.push(featurePointer);
|
|
91
|
+
entries.forEach(([tag, on], index) => { const spec = string(`${on ? "+" : "-"}${tag}`); allocated.push(spec.pointer); hb.hb_feature_from_string(spec.pointer, spec.length, featurePointer + index * FEATURE_SIZE); });
|
|
92
|
+
}
|
|
93
|
+
hb.hb_shape(font, buffer, featurePointer, entries.length);
|
|
94
|
+
const lengthPointer = hb.malloc(4);
|
|
95
|
+
allocated.push(lengthPointer);
|
|
96
|
+
const infos = hb.hb_buffer_get_glyph_infos(buffer, lengthPointer);
|
|
97
|
+
const count = new Uint32Array(memory.buffer, lengthPointer, 1)[0];
|
|
98
|
+
const positions = hb.hb_buffer_get_glyph_positions(buffer, 0);
|
|
99
|
+
const data32 = new DataView(memory.buffer);
|
|
100
|
+
const glyphs = [];
|
|
101
|
+
for (let index = 0; index < count; index++) {
|
|
102
|
+
const info = infos + index * INFO_SIZE, position = positions + index * POSITION_SIZE;
|
|
103
|
+
glyphs.push({ gid: data32.getUint32(info, true), cluster: data32.getUint32(info + 8, true), advance: data32.getInt32(position, true), xOffset: data32.getInt32(position + 8, true), yOffset: data32.getInt32(position + 12, true) });
|
|
104
|
+
}
|
|
105
|
+
// The characters of each cluster go to its first glyph in output order; later glyphs of the cluster carry none.
|
|
106
|
+
const starts = [...new Set(glyphs.map((glyph) => glyph.cluster))].sort((a, b) => a - b);
|
|
107
|
+
const seen = new Set();
|
|
108
|
+
for (const glyph of glyphs) {
|
|
109
|
+
if (seen.has(glyph.cluster)) { glyph.codePoints = []; continue; }
|
|
110
|
+
seen.add(glyph.cluster);
|
|
111
|
+
const end = starts[starts.indexOf(glyph.cluster) + 1] ?? units;
|
|
112
|
+
glyph.codePoints = [...text.slice(glyph.cluster, end)].map((character) => character.codePointAt(0));
|
|
113
|
+
}
|
|
114
|
+
return glyphs.map(({ gid, codePoints, advance, xOffset, yOffset }) => ({ gid, codePoints, advance, xOffset, yOffset }));
|
|
115
|
+
} finally {
|
|
116
|
+
hb.hb_buffer_destroy(buffer);
|
|
117
|
+
for (const pointer of allocated) hb.free(pointer);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|