@openpresentation/opf-render 0.17.1 → 0.18.1
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 +157 -70
- package/dist/deck-runtime.js +4 -4
- package/dist/element.d.ts +12 -9
- package/dist/element.js +28 -19
- package/dist/export-browser.d.ts +21 -11
- package/dist/export-browser.js +35 -20
- package/dist/font-manifest.js +17 -17
- package/dist/font-registry.js +31 -0
- package/dist/font-subset.js +1 -1
- package/dist/fonts-browser.d.ts +26 -25
- package/dist/fonts-browser.js +12 -12
- package/dist/fonts-node.d.ts +15 -11
- package/dist/fonts-node.js +24 -21
- package/dist/fonts.d.ts +14 -7
- package/dist/hb-shape.js +16 -6
- package/dist/index.d.ts +51 -30
- package/dist/index.js +1 -1
- package/dist/lazy-fonts.js +10 -10
- package/dist/node-resolve.js +24 -0
- package/dist/pdf-fonts.js +1 -1
- package/dist/pdf-text.js +4 -4
- package/dist/pdf-vector.js +15 -5
- package/dist/pdf-writer.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 +9 -3
- package/dist/png.d.ts +2 -2
- package/dist/png.js +2 -2
- package/dist/preview-fonts-node.js +3 -6
- package/dist/raster-text.js +33 -2
- package/dist/raster.js +47 -38
- package/dist/script-font-pack.js +5 -5
- package/dist/script-fonts.js +4 -2
- package/dist/slide-sources.js +47 -0
- package/dist/svg.d.ts +24 -29
- package/dist/svg.js +50 -31
- package/dist/text-paths.js +16 -4
- package/package.json +23 -20
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 & {
|
|
@@ -83,13 +83,14 @@ export declare function loadFonts(
|
|
|
83
83
|
signal?: AbortSignal;
|
|
84
84
|
/**
|
|
85
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 `
|
|
86
|
+
* it the handle carries `subsets`, and every SVG `toSvg` draws with this handle embeds each face cut to the characters the
|
|
87
87
|
* slide draws. Without it a browser handle embeds whole faces (the Node handle always subsets).
|
|
88
88
|
*/
|
|
89
89
|
subsetWasm?: string | URL | ArrayBuffer | ArrayBufferView | WebAssembly.Module;
|
|
90
90
|
/**
|
|
91
|
-
* RR-64: harfbuzzjs's `harfbuzz.wasm` (its URL, bytes or a compiled WebAssembly.Module). With it `
|
|
92
|
-
* HarfBuzz, as the browser shapes text; without it a browser handle shapes them with fontkit (the
|
|
91
|
+
* RR-64: harfbuzzjs's `harfbuzz.wasm` (its URL, bytes or a compiled WebAssembly.Module). With it `text: "paths"` shapes outlines, and
|
|
92
|
+
* `toPdf` the vector PDF's text, with HarfBuzz, as the browser shapes text; without it a browser handle shapes them with fontkit (the
|
|
93
|
+
* Node handle always uses HarfBuzz).
|
|
93
94
|
*/
|
|
94
95
|
shapeWasm?: string | URL | ArrayBuffer | ArrayBufferView | WebAssembly.Module;
|
|
95
96
|
crypto?: { subtle: SubtleCrypto };
|
|
@@ -111,11 +112,11 @@ export declare function loadFonts(
|
|
|
111
112
|
*/
|
|
112
113
|
extraLazyFonts?: readonly ExtraLazyFont[];
|
|
113
114
|
/**
|
|
114
|
-
* FF-41: the `
|
|
115
|
+
* FF-41: the `toSvg` options the documents resolve with (`catalogs` above all), the default for `pendingLazyFonts`,
|
|
115
116
|
* `ensureLazyFonts`, `pendingScripts` and `ensureScripts`. A layout or font scheme id that only the host's catalogs have
|
|
116
117
|
* resolves the same way here as it does when the host renders. A call's own options win.
|
|
117
118
|
*/
|
|
118
|
-
renderOptions?: Partial<
|
|
119
|
+
renderOptions?: Partial<ToSvgOptions>;
|
|
119
120
|
},
|
|
120
121
|
): Promise<BrowserFontsHandle>;
|
|
121
122
|
export declare function scriptFontPackages(scripts: ScriptSelection): BundledFontPackage[];
|
|
@@ -138,10 +139,10 @@ export interface ExtraLazyFont { family: string; weight: number; italic?: boolea
|
|
|
138
139
|
export declare function splitStartupFaces<T extends { family: string; weight: number; italic?: boolean }>(faces: readonly T[], options?: { startup?: (face: T) => boolean }): { startup: T[]; rest: T[] };
|
|
139
140
|
/**
|
|
140
141
|
* The font families a presentation's slides resolve (heading, body and code roles of every slide). `options` are the
|
|
141
|
-
* `
|
|
142
|
+
* `toSvg` options the document resolves with (`catalogs`, ...). A document that does not resolve throws what `toSvg`
|
|
142
143
|
* throws for it (FF-41: it no longer returns an empty set). This is the role families, not what is drawn: see `presentationFaces`.
|
|
143
144
|
*/
|
|
144
|
-
export declare function presentationFamilies(presentation: unknown, options?: Partial<
|
|
145
|
+
export declare function presentationFamilies(presentation: unknown, options?: Partial<ToSvgOptions>): Set<string>;
|
|
145
146
|
/** A face request or a drawn face: a family, a weight and a style. */
|
|
146
147
|
export interface FaceRequest { family: string; weight: number; italic: boolean }
|
|
147
148
|
/**
|
|
@@ -151,16 +152,16 @@ export interface FaceRequest { family: string; weight: number; italic: boolean }
|
|
|
151
152
|
* faces are the styles as the document names them. With `registry` (`{faces, policy, aliases, fallbackFamily}`: the faces a
|
|
152
153
|
* registry holds or could hold and its substitution options) every style resolves as that registry resolves it, in the two
|
|
153
154
|
* steps drawing takes (role family, then weight within it), and the result is the faces the registry paints.
|
|
154
|
-
* `options` are the `
|
|
155
|
+
* `options` are the `toSvg` options the document resolves with; a document that does not resolve throws what `toSvg`
|
|
155
156
|
* throws for it.
|
|
156
157
|
*/
|
|
157
|
-
export declare function presentationFaces(presentation: unknown, options?: Partial<
|
|
158
|
+
export declare function presentationFaces(presentation: unknown, options?: Partial<ToSvgOptions>, registry?: { faces: Iterable<FaceRequest>; policy?: "none" | "metric" | "visual"; aliases?: ReadonlyMap<string, string>; fallbackFamily?: string }): FaceRequest[];
|
|
158
159
|
/**
|
|
159
160
|
* The vendored faces (of `lazy`) a presentation draws that the registry does not hold. `held` is `registry.describeFaces()`,
|
|
160
161
|
* `loaded` the lazy files already loaded; `policy`, `aliases` and `fallbackFamily` are the registry's. Face level: only the
|
|
161
162
|
* drawn faces, so an edit adding a bold run adds one face.
|
|
162
163
|
*/
|
|
163
|
-
export declare function lazyFacesNeeded(presentation: unknown, renderOptions: Partial<
|
|
164
|
+
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[];
|
|
164
165
|
/**
|
|
165
166
|
* The vendored faces a list of requests needs that the registry does not hold, following the registry's own order and
|
|
166
167
|
* `policy` (default "visual"): the family itself, an alias target, the declared replacement, then its alternates.
|
package/dist/fonts-browser.js
CHANGED
|
@@ -97,7 +97,7 @@ async function loadRegistry(entries, options = {}) {
|
|
|
97
97
|
// Script faces (FF-19) are loaded lazily, once a document needs them, and only for the scripts it uses.
|
|
98
98
|
const scriptPackages = new Set();
|
|
99
99
|
let queue = Promise.resolve(), disposed = false;
|
|
100
|
-
// FF-41: the `
|
|
100
|
+
// FF-41: the `toSvg` options a document resolves with (`catalogs`, ...). `options.renderOptions` are the defaults; a call's own
|
|
101
101
|
// options (everything but `signal`) are added over them, so a host that passes its canvas options gets the same resolution here.
|
|
102
102
|
const renderOptionsFor = (callOptions) => { const { signal: _signal, ...own } = callOptions ?? {}; return { ...options.renderOptions, ...own }; };
|
|
103
103
|
const gone = () => new OPFFontError("font-registry-disposed", "The font registry was disposed.");
|
|
@@ -151,7 +151,7 @@ async function loadRegistry(entries, options = {}) {
|
|
|
151
151
|
* fallback needs for characters the loaded faces lack. Cheap when nothing new is needed; call it after edits and
|
|
152
152
|
* render again afterwards. Resolves with the detected scripts, the packages loaded by this call, the scripts no
|
|
153
153
|
* pinned font serves and `uncovered`, drawn CJK characters no loaded face covers (the renderer reports
|
|
154
|
-
* `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
|
|
155
155
|
* (`catalogs`, ...) over the loader's `renderOptions`. Each package loads all or nothing. If some package fails, the others still load: the
|
|
156
156
|
* call then rejects with an OPFFontError (the first failure's code) whose `details` hold `loaded` (packages this
|
|
157
157
|
* call did load) and `failed` (`{package, code, message}` per failed package); nothing is lost and a later call
|
|
@@ -176,7 +176,7 @@ async function loadRegistry(entries, options = {}) {
|
|
|
176
176
|
}
|
|
177
177
|
return { ...selection, loaded: loadedNow, uncovered: uncoveredCjkCharacters(analysis, { covers }) };
|
|
178
178
|
},
|
|
179
|
-
/** 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`, ...). */
|
|
180
180
|
pendingScripts(presentation, renderOptions) {
|
|
181
181
|
const analysis = analyzePresentationScripts(presentation, undefined, renderOptionsFor(renderOptions));
|
|
182
182
|
const primary = scriptFontPackages(scriptSelectionOf(analysis).scripts).map((item) => item.name).filter((name) => !scriptPackages.has(name));
|
|
@@ -194,7 +194,7 @@ async function loadRegistry(entries, options = {}) {
|
|
|
194
194
|
const policy = options.substitutionPolicy ?? "none";
|
|
195
195
|
const aliasTargets = new Map(Object.entries(options.aliases ?? {}).map(([from, to]) => [from.toLowerCase(), to]));
|
|
196
196
|
// Face level (FF-41): the faces the document draws, resolved as the registry resolves them with every vendored face loaded.
|
|
197
|
-
// A document that does not resolve throws what `
|
|
197
|
+
// A document that does not resolve throws what `toSvg` throws for it.
|
|
198
198
|
const neededLazy = (presentation, callOptions) => lazyFacesNeeded(presentation, renderOptionsFor(callOptions), { lazy, held: registry.describeFaces(), loaded: lazyLoaded, policy, aliases: aliasTargets, fallbackFamily: options.fallbackFamily });
|
|
199
199
|
const lazyBaseUrl = () => {
|
|
200
200
|
if (typeof options.lazyFontsBaseUrl !== "string" || !options.lazyFontsBaseUrl)
|
|
@@ -210,8 +210,8 @@ async function loadRegistry(entries, options = {}) {
|
|
|
210
210
|
* policy (nothing is downloaded for a family the policy would not resolve), once each. All or nothing: on any failure
|
|
211
211
|
* (fetch, hash, FontFace.load, registry) the document and registry are unchanged and the call can be retried. Cheap when
|
|
212
212
|
* nothing is needed. Call it before measuring a document, and again after edits that change fonts. Resolves with the faces
|
|
213
|
-
* added. Besides `signal`, the call takes the `
|
|
214
|
-
* 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.
|
|
215
215
|
*/
|
|
216
216
|
ensureLazyFonts(presentation, callOptions = {}) {
|
|
217
217
|
const result = queue.then(async () => {
|
|
@@ -247,7 +247,7 @@ async function loadRegistry(entries, options = {}) {
|
|
|
247
247
|
queue = result.catch(() => {});
|
|
248
248
|
return result;
|
|
249
249
|
},
|
|
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 `
|
|
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`, ...). */
|
|
251
251
|
pendingLazyFonts: (presentation, renderOptions) => neededLazy(presentation, renderOptions),
|
|
252
252
|
});
|
|
253
253
|
/** Names of the script-pack packages loaded so far. */
|
|
@@ -277,7 +277,7 @@ const MAX_ENSURE_ROUNDS = 4;
|
|
|
277
277
|
/**
|
|
278
278
|
* The fonts handle for a browser: the faces you list (`faces`, each `{ url | data, family, weight, italic, sha256 }`, fetched and
|
|
279
279
|
* hash-verified, then registered with the document) and, on demand, the script and vendored faces a deck draws. Pass it as `{ fonts }` to
|
|
280
|
-
* `
|
|
280
|
+
* `toSvg`, `<opf-deck>` and the player, core `paginate` and `validate`, and the editor. `scripts`, `scriptBaseUrl`,
|
|
281
281
|
* `lazyFontsBaseUrl`, `extraLazyFonts`, `renderOptions`, `signal`, `document`, `fetch`, `crypto` and the registry options
|
|
282
282
|
* (`substitutionPolicy`, `aliases`, `fallbackFamily`, `themeFonts`, `strictGlyphs`) are as for the registry (see `fonts-browser.d.ts`).
|
|
283
283
|
* `dispose()` removes every face from the document.
|
|
@@ -320,7 +320,7 @@ export async function loadFonts({ faces = [], subsetWasm, shapeWasm, ...options
|
|
|
320
320
|
textMeasurement: registry.textMeasurement,
|
|
321
321
|
get embeddedFonts() { if (stale) { embedded = registry.embeddedFonts; stale = false; } return embedded; },
|
|
322
322
|
registry,
|
|
323
|
-
// RR-64: the outline engine `
|
|
323
|
+
// RR-64: the outline engine `toSvg(deck, { fonts, text: "paths" })` draws text with, over this registry's faces.
|
|
324
324
|
outlines: textOutlines({ registry, shaper }),
|
|
325
325
|
// RR-65: with `subsetWasm`, the subset engine that cuts each face an SVG embeds to the characters the slide draws.
|
|
326
326
|
...(subsetter ? { subsets: fontSubsets(registry, subsetter) } : {}),
|
|
@@ -329,8 +329,8 @@ export async function loadFonts({ faces = [], subsetWasm, shapeWasm, ...options
|
|
|
329
329
|
/**
|
|
330
330
|
* Load the vendored and script faces the presentation draws, each hash-verified, all or nothing, and repeat while loading
|
|
331
331
|
* changes what is needed (a script face can need a vendored one). Cheap when nothing is needed. Besides `signal`, the call
|
|
332
|
-
* takes the `
|
|
333
|
-
* 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`.
|
|
334
334
|
*/
|
|
335
335
|
async ensure(presentation, callOptions = {}) {
|
|
336
336
|
const lazy = [], scripts = [];
|
|
@@ -344,7 +344,7 @@ export async function loadFonts({ faces = [], subsetWasm, shapeWasm, ...options
|
|
|
344
344
|
}
|
|
345
345
|
return { scripts, lazy, uncovered };
|
|
346
346
|
},
|
|
347
|
-
/** 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. */
|
|
348
348
|
pending,
|
|
349
349
|
dispose: () => registry.dispose(),
|
|
350
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,10 +30,14 @@ 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<
|
|
35
|
-
/**
|
|
36
|
-
|
|
33
|
+
/** The `toSvg` options the presentation resolves with (`catalogs`, ...), for the font schemes `scripts: "auto"` reads (FF-41). */
|
|
34
|
+
renderOptions?: Partial<ToSvgOptions>;
|
|
35
|
+
/**
|
|
36
|
+
* Receives `script-font-unavailable` and `script-font-not-installed` for `scripts: "auto"`, and from `loadFonts`
|
|
37
|
+
* `harfbuzz-unavailable` when a harfbuzzjs .wasm (`file`) cannot be loaded, for example in a bundled server that did not ship it:
|
|
38
|
+
* the handle then has no `subsets`, or shapes outlines with fontkit.
|
|
39
|
+
*/
|
|
40
|
+
onDiagnostic?: (diagnostic: {code: string; message: string; script?: string; package?: string; scripts?: string[]; file?: string}) => void;
|
|
37
41
|
}
|
|
38
42
|
/** A vendored face that an SVG embeds only when its text names the family (the open families and Intos), and that browser hosts load on demand from a separate hash-pinned file. */
|
|
39
43
|
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 }
|
|
@@ -74,7 +78,7 @@ export declare function scriptFontPackages(scripts: ScriptSelection): BundledFon
|
|
|
74
78
|
export interface NodeFontsHandle extends FontsHandle {
|
|
75
79
|
readonly registry: NodeFontRegistry;
|
|
76
80
|
readonly manifest: BundledFontManifest;
|
|
77
|
-
/** Every font file the handle holds, for `
|
|
81
|
+
/** Every font file the handle holds, for `toPng` and `toPdf`; includes the script faces `ensure` loaded. */
|
|
78
82
|
readonly fontFiles: string[];
|
|
79
83
|
/** The files are already in `fontFiles`, so a conversion adds no bundled faces. */
|
|
80
84
|
readonly useBundledFonts: false;
|
|
@@ -86,9 +90,9 @@ export interface NodeFontsHandle extends FontsHandle {
|
|
|
86
90
|
* the new faces. A package that is not installed or a script no pinned font serves is reported once through `onDiagnostic`.
|
|
87
91
|
* Resolves with the packages this call added (`scripts`; `lazy` is empty: Node loads every vendored face with its pack).
|
|
88
92
|
*/
|
|
89
|
-
ensure(presentation: unknown, options?: { onDiagnostic?: ScriptPackOptions["onDiagnostic"]; renderOptions?: Partial<
|
|
93
|
+
ensure(presentation: unknown, options?: { onDiagnostic?: ScriptPackOptions["onDiagnostic"]; renderOptions?: Partial<ToSvgOptions> }): Promise<EnsureResult>;
|
|
90
94
|
/** Synchronous: the script packages the presentation needs that are not loaded yet. */
|
|
91
|
-
pending(presentation: unknown, renderOptions?: Partial<
|
|
95
|
+
pending(presentation: unknown, renderOptions?: Partial<ToSvgOptions>): string[];
|
|
92
96
|
}
|
|
93
97
|
export interface LoadFontsOptions extends FontRegistryOptions, ScriptPackOptions {
|
|
94
98
|
/** `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
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
2
|
import path from "node:path";
|
|
5
3
|
import { createHash } from "node:crypto";
|
|
6
4
|
import { createFontRegistry, OPFFontError } from "./font-registry.js";
|
|
@@ -9,12 +7,12 @@ import { lazyFontList } from "./lazy-font-list.js";
|
|
|
9
7
|
import { textOutlines } from "./text-paths.js";
|
|
10
8
|
import { createSubsetter, fontSubsets } from "./font-subset.js";
|
|
11
9
|
import { createShaper } from "./hb-shape.js";
|
|
10
|
+
import { packageRoot, resolveInstalled } from "./node-resolve.js";
|
|
12
11
|
export { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
13
12
|
import { analyzePresentationScripts, autoScriptSelection, nextFallbackPackage, scriptFontPackages, scriptSelectionOf, uncoveredCjkCharacters } from "./script-font-pack.js";
|
|
14
13
|
export { autoScriptSelection, detectPresentationScripts, scriptFontPackages } from "./script-font-pack.js";
|
|
15
14
|
// Script faces are embedded in a standalone SVG only when the slide's text uses their family.
|
|
16
15
|
const embedUsed = entries => entries.map(entry => ({...entry, embed: "used"}));
|
|
17
|
-
const require = createRequire(import.meta.url);
|
|
18
16
|
|
|
19
17
|
async function verifiedFile(file, expected, details) {
|
|
20
18
|
let bytes;
|
|
@@ -33,7 +31,7 @@ const FALLBACK_PACKAGE = "@expo-google-fonts/noto-sans";
|
|
|
33
31
|
function requireInstalled(packages, what) {
|
|
34
32
|
const missing = packages.filter(pkg => {
|
|
35
33
|
if (pkg.vendored) return false;
|
|
36
|
-
try {
|
|
34
|
+
try { resolveInstalled(`${pkg.name}/package.json`); return false; } catch { return true; }
|
|
37
35
|
});
|
|
38
36
|
if (!missing.length) return;
|
|
39
37
|
const install = `npm install ${missing.map(pkg => `${pkg.name}@${pkg.version}`).join(" ")}`;
|
|
@@ -50,7 +48,7 @@ async function loadPackages(packages, skipped, {fallbackOnly = false, what = "Th
|
|
|
50
48
|
if (pkg.vendored) {
|
|
51
49
|
// FF-31: vendored faces ship inside this package (pkg.vendored, for example fonts/carlito), hash-pinned like the npm packs. The open pack is
|
|
52
50
|
// embedded in an SVG only when the slide's text names the family (embed "used"); raster output reads the files.
|
|
53
|
-
const directory =
|
|
51
|
+
const directory = path.join(packageRoot, pkg.vendored);
|
|
54
52
|
let license = (await verifiedFile(path.join(directory, pkg.licenseFile), pkg.licenseSha256, {package:pkg.name, file:pkg.licenseFile})).toString("utf8");
|
|
55
53
|
// A notice file carries provenance and upstream copyright lines that the upstream license file omits.
|
|
56
54
|
if (pkg.noticeFile) license += "\n\n" + (await verifiedFile(path.join(directory, pkg.noticeFile), pkg.noticeSha256, {package:pkg.name, file:pkg.noticeFile})).toString("utf8");
|
|
@@ -66,7 +64,7 @@ async function loadPackages(packages, skipped, {fallbackOnly = false, what = "Th
|
|
|
66
64
|
}
|
|
67
65
|
let manifestPath, installed;
|
|
68
66
|
try {
|
|
69
|
-
manifestPath =
|
|
67
|
+
manifestPath = resolveInstalled(`${pkg.name}/package.json`);
|
|
70
68
|
installed = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
71
69
|
} catch (error) {
|
|
72
70
|
if (skipped) { skipped.push({package: pkg.name, version: pkg.version, scripts: [...pkg.scripts]}); continue; }
|
|
@@ -95,7 +93,7 @@ const loadPack = pack => loadPackages(BUNDLED_FONT_MANIFEST.packages.filter(item
|
|
|
95
93
|
/**
|
|
96
94
|
* Adds the script pack. `scripts` is 'all', ISO 15924 codes, or 'auto' (FF-19): detect the scripts the
|
|
97
95
|
* `presentation` draws and load only their faces. Auto never fails on a missing optional package or a
|
|
98
|
-
* script no pinned font serves; it reports them through `onDiagnostic` and `selection`. `renderOptions` are the host's `
|
|
96
|
+
* script no pinned font serves; it reports them through `onDiagnostic` and `selection`. `renderOptions` are the host's `toSvg`
|
|
99
97
|
* options (see `analyzePresentationScripts`).
|
|
100
98
|
*/
|
|
101
99
|
async function withScripts(loaded, scripts, {presentation, onDiagnostic, renderOptions} = {}) {
|
|
@@ -250,25 +248,26 @@ async function ensureFonts(registry, loaded, presentation, {onDiagnostic, render
|
|
|
250
248
|
}
|
|
251
249
|
|
|
252
250
|
// 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
|
|
251
|
+
// instantiated synchronously and toSvg stays synchronous. A copy that cannot be loaded leaves the handle without `subsets`
|
|
254
252
|
// (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
253
|
// 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
254
|
// a browser shapes the text. Without it outlines are shaped with fontkit.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
255
|
+
// render#199: a file that cannot be found (a bundled server that did not ship it) loads as null too, and `loadFonts` reports it
|
|
256
|
+
// through `onDiagnostic` (`harfbuzz-unavailable`).
|
|
257
|
+
const HARFBUZZ = {
|
|
258
|
+
subsets: {file: "harfbuzzjs/dist/harfbuzz-subset.wasm", create: createSubsetter, without: "standalone SVG embeds whole faces instead of subsets"},
|
|
259
|
+
shaper: {file: "harfbuzzjs/dist/harfbuzz.wasm", create: createShaper, without: "text drawn as outlines is shaped with fontkit instead of HarfBuzz"},
|
|
260
|
+
};
|
|
261
|
+
const harfbuzzEngines = {};
|
|
262
|
+
function nodeHarfbuzz(kind) {
|
|
263
|
+
const {file, create} = HARFBUZZ[kind];
|
|
264
|
+
harfbuzzEngines[kind] ??= Promise.resolve().then(() => readFile(resolveInstalled(file))).then(create).catch(() => null);
|
|
265
|
+
return harfbuzzEngines[kind];
|
|
267
266
|
}
|
|
268
267
|
|
|
269
268
|
/**
|
|
270
269
|
* The fonts handle: one set of verified font inputs for layout, SVG, editor, PPTX and Node raster export. Pass it as `{ fonts }` to
|
|
271
|
-
* `
|
|
270
|
+
* `toSvg`, `toPng`, `toPdf`, core `paginate` and `validate`, and `toPptx`.
|
|
272
271
|
*
|
|
273
272
|
* Bundled, openly licensed faces only: no system font discovery and no network requests. `pack`: `base` (default, Roboto),
|
|
274
273
|
* `office` (the Office substitutes and the open families) or `none` (only the `faces` you supply). Everything else
|
|
@@ -285,7 +284,11 @@ export async function loadFonts({pack = "base", embedScriptFonts = false, ...opt
|
|
|
285
284
|
// The embedded list is base64 of every face: computed on first use, and again after `ensure` added faces.
|
|
286
285
|
let embeddedFonts, fontFiles = [...registry.fontFiles];
|
|
287
286
|
const refresh = () => { embeddedFonts = undefined; fontFiles = [...registry.fontFiles]; };
|
|
288
|
-
const [subsetter, shaper] = await Promise.all([
|
|
287
|
+
const [subsetter, shaper] = await Promise.all([nodeHarfbuzz("subsets"), nodeHarfbuzz("shaper")]);
|
|
288
|
+
for (const [kind, engine] of [["subsets", subsetter], ["shaper", shaper]]) if (!engine) {
|
|
289
|
+
const {file, without} = HARFBUZZ[kind];
|
|
290
|
+
options.onDiagnostic?.({code: "harfbuzz-unavailable", package: "harfbuzzjs", file, message: `${file} could not be loaded, so ${without}. Ship the harfbuzzjs package with the server.`});
|
|
291
|
+
}
|
|
289
292
|
return {
|
|
290
293
|
textMeasurement: registry.textMeasurement,
|
|
291
294
|
get embeddedFonts() { return (embeddedFonts ??= selectEmbedded()); },
|
|
@@ -293,7 +296,7 @@ export async function loadFonts({pack = "base", embedScriptFonts = false, ...opt
|
|
|
293
296
|
useBundledFonts: false,
|
|
294
297
|
loadSystemFonts: false,
|
|
295
298
|
registry,
|
|
296
|
-
// RR-64: the outline engine `
|
|
299
|
+
// RR-64: the outline engine `toSvg(deck, { fonts, text: "paths" })` draws text with, over this registry's faces.
|
|
297
300
|
outlines: textOutlines({ registry, shaper }),
|
|
298
301
|
// RR-65: the subset engine that cuts each face an SVG embeds to the characters the slide draws.
|
|
299
302
|
...(subsetter ? { subsets: fontSubsets(registry, subsetter) } : {}),
|
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,15 +49,22 @@ 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
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
55
|
subsets?: FontSubsets;
|
|
56
56
|
}
|
|
57
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
58
|
export interface FontSubsets { subsetDataUrl(font: EmbeddedFont, codePoints: Iterable<number>): string }
|
|
59
|
-
/** Draws a slide's text as glyph outlines (RR-64). Read by `
|
|
60
|
-
export interface TextOutlines {
|
|
59
|
+
/** Draws a slide's text as glyph outlines (RR-64). Read by `toSvg`; hosts pass the handle and `text: "paths"`. */
|
|
60
|
+
export interface TextOutlines {
|
|
61
|
+
outlineSlideText(content: string[], options: object): { content: string[]; defs: string };
|
|
62
|
+
/**
|
|
63
|
+
* HarfBuzz shaping of one run of a face (opf-render#188), present when the handle has the shaper (the Node handle always, a browser
|
|
64
|
+
* handle with `shapeWasm`). Read by `toPdf`, which then shapes the vector PDF's text as the browser shapes it; fontkit otherwise.
|
|
65
|
+
*/
|
|
66
|
+
shape?(face: { hash: string; data: Uint8Array }, text: string, options?: { direction?: "ltr" | "rtl"; lang?: string; features?: Record<string, boolean> }): { gid: number; codePoints: number[]; advance: number; xOffset: number; yOffset: number }[];
|
|
67
|
+
}
|
|
61
68
|
/** What `fonts.ensure(presentation)` loaded: the script packages and vendored faces that were missing, and drawn CJK characters no face covers. */
|
|
62
69
|
export interface EnsureResult { scripts: string[]; lazy: LazyFont[]; uncovered: string[] }
|
|
63
70
|
/** The fonts handle `loadFonts()` returns from `/fonts-node` and `/fonts-browser`: pass it as `{ fonts }` to every deck-level function. */
|
|
@@ -66,7 +73,7 @@ export interface FontsHandle extends RenderFonts {
|
|
|
66
73
|
readonly embeddedFonts: EmbeddedFont[];
|
|
67
74
|
/** The face registry behind the handle (shaping, resolution, lazy and script loading). */
|
|
68
75
|
readonly registry: FontRegistry;
|
|
69
|
-
/** The outline engine for `
|
|
76
|
+
/** The outline engine for `text: "paths"` (RR-64), over this registry's faces, including faces `ensure` loads later. */
|
|
70
77
|
readonly outlines: TextOutlines;
|
|
71
78
|
/** The pinned manifest of the bundled font packages. */
|
|
72
79
|
readonly manifest: { readonly version: number; readonly packages: readonly object[] };
|
package/dist/hb-shape.js
CHANGED
|
@@ -70,8 +70,13 @@ function shaperOver(instance, state) {
|
|
|
70
70
|
return font;
|
|
71
71
|
};
|
|
72
72
|
return {
|
|
73
|
-
/**
|
|
74
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Shape `text` with the face (`key` names it, `data` is its bytes); `features` is an object of OpenType feature booleans.
|
|
75
|
+
* `clusterLevel` is HarfBuzz's cluster level (0 by default: a mark joins its base's cluster and a reordered syllable is one
|
|
76
|
+
* cluster; 2 keeps each character with the glyph it became). It changes only which glyph carries which characters, never the
|
|
77
|
+
* glyphs or their positions.
|
|
78
|
+
*/
|
|
79
|
+
shape(key, data, text, { direction = "ltr", language, features, clusterLevel } = {}) {
|
|
75
80
|
const font = fontFor(key, data);
|
|
76
81
|
const buffer = hb.hb_buffer_create();
|
|
77
82
|
const units = text.length, textPointer = hb.malloc(units * 2 + 2);
|
|
@@ -81,6 +86,7 @@ function shaperOver(instance, state) {
|
|
|
81
86
|
try {
|
|
82
87
|
hb.hb_buffer_add_utf16(buffer, textPointer, units, 0, units);
|
|
83
88
|
hb.hb_buffer_set_direction(buffer, HB_DIRECTION[direction] ?? HB_DIRECTION.ltr);
|
|
89
|
+
if (clusterLevel) hb.hb_buffer_set_cluster_level(buffer, clusterLevel);
|
|
84
90
|
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
91
|
hb.hb_buffer_guess_segment_properties(buffer);
|
|
86
92
|
const entries = Object.entries(features ?? {});
|
|
@@ -102,12 +108,16 @@ function shaperOver(instance, state) {
|
|
|
102
108
|
const info = infos + index * INFO_SIZE, position = positions + index * POSITION_SIZE;
|
|
103
109
|
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
110
|
}
|
|
105
|
-
// The characters of each cluster go to its first
|
|
111
|
+
// The characters of each cluster go to one of its glyphs: the first in logical order (output order, reversed right to left)
|
|
112
|
+
// that advances the pen, else the first; its other glyphs carry none (a mark or dot the font split off a letter).
|
|
106
113
|
const starts = [...new Set(glyphs.map((glyph) => glyph.cluster))].sort((a, b) => a - b);
|
|
107
|
-
const
|
|
114
|
+
const carriers = new Map();
|
|
115
|
+
for (const glyph of direction === "rtl" ? [...glyphs].reverse() : glyphs) {
|
|
116
|
+
const carrier = carriers.get(glyph.cluster);
|
|
117
|
+
if (!carrier || (carrier.advance === 0 && glyph.advance !== 0)) carriers.set(glyph.cluster, glyph);
|
|
118
|
+
}
|
|
108
119
|
for (const glyph of glyphs) {
|
|
109
|
-
if (
|
|
110
|
-
seen.add(glyph.cluster);
|
|
120
|
+
if (carriers.get(glyph.cluster) !== glyph) { glyph.codePoints = []; continue; }
|
|
111
121
|
const end = starts[starts.indexOf(glyph.cluster) + 1] ?? units;
|
|
112
122
|
glyph.codePoints = [...text.slice(glyph.cluster, end)].map((character) => character.codePointAt(0));
|
|
113
123
|
}
|