@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.
@@ -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 { RenderSvgOptions } from "./svg.js";
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 `renderSvg` options the document resolves
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
- * `renderSvg` throws for it. After `dispose()` it rejects with `font-registry-disposed`.
31
+ * `toSvg` throws for it. After `dispose()` it rejects with `font-registry-disposed`.
32
32
  */
33
- ensureLazyFonts(presentation: unknown, options?: { signal?: AbortSignal } & Partial<RenderSvgOptions>): Promise<LazyFont[]>;
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 `renderSvg` options the document resolves with (`catalogs`, ...)
37
- * over the loader's `renderOptions`. Throws what `renderSvg` throws for a document that does not resolve.
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<RenderSvgOptions>): LazyFont[];
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<RenderSvgOptions>): Promise<AutoScriptSelection & { loaded: string[]; uncovered: string[] }>;
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<RenderSvgOptions>): string[];
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 `renderSvg`, `renderSlideSvg`, `<opf-deck>` and the player, core `paginate` and `validate`, and the editor.
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 `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
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<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[];
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 `renderSvg` draws with this handle embeds each face cut to the characters the
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 `textAsPaths` 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).
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 `renderSvg` options the documents resolve with (`catalogs` above all), the default for `pendingLazyFonts`,
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<RenderSvgOptions>;
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
- * `renderSvg` options the document resolves with (`catalogs`, ...). A document that does not resolve throws what `renderSvg`
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<RenderSvgOptions>): Set<string>;
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 `renderSvg` options the document resolves with; a document that does not resolve throws what `renderSvg`
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<RenderSvgOptions>, registry?: { faces: Iterable<FaceRequest>; policy?: "none" | "metric" | "visual"; aliases?: ReadonlyMap<string, string>; fallbackFamily?: string }): FaceRequest[];
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<RenderSvgOptions> | undefined, context: { lazy: readonly LazyFont[]; held: Iterable<FaceRequest>; loaded?: ReadonlySet<string>; policy?: "none" | "metric" | "visual"; aliases?: ReadonlyMap<string, string>; fallbackFamily?: string }): LazyFont[];
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.
@@ -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 `renderSvg` options a document resolves with (`catalogs`, ...). `options.renderOptions` are the defaults; a call's own
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 `renderSvg` options the document resolves with
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 `renderSvg` options the document resolves with (`catalogs`, ...). */
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 `renderSvg` throws for it.
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 `renderSvg` options the document resolves with (`catalogs`, ...) over the
214
- * loader's `renderOptions`; a document that does not resolve rejects with what `renderSvg` throws for it.
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 `renderSvg` options the document resolves with (`catalogs`, ...). */
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
- * `renderSvg`, `renderSlideSvg`, `<opf-deck>` and the player, core `paginate` and `validate`, and the editor. `scripts`, `scriptBaseUrl`,
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 `renderSvg(deck, { fonts, textAsPaths: true })` draws text with, over this registry's faces.
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 `renderSvg` options the document resolves with (`catalogs`, ...) over the loader's `renderOptions`. A document that
333
- * does not resolve rejects with what `renderSvg` throws for it. After `dispose()` it rejects with `font-registry-disposed`.
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 `renderSvg` throws for a document that does not resolve. */
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
  };
@@ -1,5 +1,5 @@
1
1
  import type {FontRegistry,FontRegistryOptions,FontFaceInput,FontsHandle,EnsureResult,ScriptFontProfile} from "./fonts.js";
2
- import type {RenderSvgOptions} from "./svg.js";
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 `renderSvg` options the document resolves with (`catalogs`): a font scheme that exists only in the host's catalogs and names a script font selects that font's script (FF-41). */
20
- export declare function detectPresentationScripts(presentation: unknown, options?: { profile?: ScriptFontProfile } & Partial<RenderSvgOptions>): string[];
21
- export declare function autoScriptSelection(presentation: unknown, options?: { profile?: ScriptFontProfile } & Partial<RenderSvgOptions>): AutoScriptSelection;
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 `renderSvg` options the presentation resolves with (`catalogs`, ...), for the font schemes `scripts: "auto"` reads (FF-41). */
34
- renderOptions?: Partial<RenderSvgOptions>;
35
- /** Receives `script-font-unavailable` and `script-font-not-installed` for `scripts: "auto"`. */
36
- onDiagnostic?: (diagnostic: {code: string; message: string; script?: string; package?: string; scripts?: string[]}) => void;
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 `svgToPng` and `svgToPdf`; includes the script faces `ensure` loaded. */
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<RenderSvgOptions> }): Promise<EnsureResult>;
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<RenderSvgOptions>): string[];
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. */
@@ -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 { require.resolve(`${pkg.name}/package.json`); return false; } catch { return true; }
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 = fileURLToPath(new URL(`../${pkg.vendored}/`, import.meta.url));
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 = require.resolve(`${pkg.name}/package.json`);
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 `renderSvg`
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 renderSvg stays synchronous. A copy that cannot be loaded leaves the handle without `subsets`
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
- let shaperPromise;
264
- function nodeShaper() {
265
- shaperPromise ??= readFile(require.resolve("harfbuzzjs/dist/harfbuzz.wasm")).then(createShaper, () => null).catch(() => null);
266
- return shaperPromise;
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
- * `renderSvg`, `renderSlideSvg`, `svgToPng`, `svgToPdf`, core `paginate` and `validate`, and `toPptx`.
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([nodeSubsetter(), nodeShaper()]);
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 `renderSvg(deck, { fonts, textAsPaths: true })` draws text with, over this registry's faces.
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: `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
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 `renderSvg` 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). */
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 `textAsPaths: true` draws text with (RR-64); a `loadFonts` handle carries one over its registry's faces. */
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 `renderSvg`; hosts pass the handle and `textAsPaths: true`. */
60
- export interface TextOutlines { outlineSlideText(content: string[], options: object): { content: string[]; defs: string } }
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 `textAsPaths: true` (RR-64), over this registry's faces, including faces `ensure` loads later. */
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
- /** 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 } = {}) {
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 glyph in output order; later glyphs of the cluster carry none.
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 seen = new Set();
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 (seen.has(glyph.cluster)) { glyph.codePoints = []; continue; }
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
  }