@openpresentation/opf-render 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import type { BrowserFontRegistry } from "./fonts-browser.js";
1
+ import type { BrowserFontsHandle } from "./fonts-browser.js";
2
2
 
3
3
  /** One face of the self-hosted font root: where it sits under the root, and its pinned hash. */
4
4
  export interface PreviewBaseFace { family: string; weight: number; italic: boolean; file: string; sha256: string; license: string }
@@ -16,8 +16,8 @@ export declare function previewFontLayout(manifest?: unknown, lazyFiles?: readon
16
16
  export declare function previewBaseFaces(manifest?: unknown): PreviewBaseFace[];
17
17
 
18
18
  /**
19
- * A browser font registry over a self-hosted font root: `<root>/base/<package>/<file>`, `<root>/lazy/fonts/<family>/<file>` and
19
+ * A browser fonts handle (`loadFonts`) over a self-hosted font root: `<root>/base/<package>/<file>`, `<root>/lazy/fonts/<family>/<file>` and
20
20
  * `<root>/scripts/<package>/<file>`. It starts with Roboto Regular and loads other faces on demand, hash-verified, only when a
21
- * deck draws them. Registries are shared per root on a page.
21
+ * deck draws them. Handles are shared per root on a page.
22
22
  */
23
- export declare function loadPreviewFonts(root: string, options?: { document?: Document; fetch?: typeof fetch; signal?: AbortSignal; registryOptions?: Record<string, unknown> }): Promise<BrowserFontRegistry>;
23
+ export declare function loadPreviewFonts(root: string, options?: { document?: Document; fetch?: typeof fetch; signal?: AbortSignal; registryOptions?: Record<string, unknown> }): Promise<BrowserFontsHandle>;
@@ -8,7 +8,7 @@
8
8
  // `copyPreviewFonts` (the `/fonts-node` side of this layout, `preview-fonts-node.js`) fills it from the installed packages.
9
9
  // Every file is verified against its pinned SHA-256 by the registry before use, and only the faces a deck draws are fetched.
10
10
  import { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
11
- import { lazyFontList, loadBrowserFontRegistry, splitStartupFaces } from "./fonts-browser.js";
11
+ import { lazyFontList, loadFonts, splitStartupFaces } from "./fonts-browser.js";
12
12
 
13
13
  /** The directory a package's files sit in under its kind: a vendored family's own name, or the npm package name without its scope. */
14
14
  export function previewFontDirectory(pkg) {
@@ -47,9 +47,9 @@ const absolute = (root, document) => {
47
47
  const shared = new Map();
48
48
 
49
49
  /**
50
- * A browser font registry over a font root. It starts with Roboto Regular (the renderer's fallback family) and loads the rest
51
- * only when a deck draws it, face by face: vendored faces from `<root>/lazy/`, script faces from `<root>/scripts/`, the other
52
- * eager faces as the registry's extra lazy faces. One registry is shared by every deck on the page that names the same root.
50
+ * A browser fonts handle (`loadFonts`) over a font root. It starts with Roboto Regular (the renderer's fallback family) and loads
51
+ * the rest only when a deck draws it, face by face: vendored faces from `<root>/lazy/`, script faces from `<root>/scripts/`, the
52
+ * other eager faces as the registry's extra lazy faces. One handle is shared by every deck on the page that names the same root.
53
53
  */
54
54
  export function loadPreviewFonts(root, options = {}) {
55
55
  if (typeof root !== "string" || !root) return Promise.reject(new TypeError("loadPreviewFonts needs the URL of the font root."));
@@ -59,7 +59,8 @@ export function loadPreviewFonts(root, options = {}) {
59
59
  const faces = previewBaseFaces();
60
60
  const { startup, rest } = splitStartupFaces(faces, { startup: (face) => face.family === "Roboto" && face.weight === 400 && !face.italic });
61
61
  const entry = (face) => ({ url: base + face.file, family: face.family, weight: face.weight, italic: face.italic, license: face.license, sha256: face.sha256 });
62
- const promise = loadBrowserFontRegistry(startup.map(entry), {
62
+ const promise = loadFonts({
63
+ faces: startup.map(entry),
63
64
  substitutionPolicy: "visual",
64
65
  fallbackFamily: "Roboto",
65
66
  scriptBaseUrl: `${base}scripts/`,
package/dist/raster.js CHANGED
@@ -10,11 +10,24 @@ const DEFAULT_RASTER_BACKGROUND = "#FFFFFF";
10
10
  const DEFAULT_PDF_MODE = "vector";
11
11
  let bundledFontFilesCache=null;
12
12
 
13
+ // The faces a raster or PDF conversion draws with, from the fonts handle (`loadFonts()` from `/fonts-node`): its `fontFiles`, and
14
+ // whether the bundled base faces are added (`useBundledFonts`, default true; a Node handle already holds its own, so it says false) and
15
+ // whether system fonts load (`loadSystemFonts`, default false). Without a handle the bundled base faces draw.
16
+ function fontSettings(options) {
17
+ const fonts = options.fonts ?? {};
18
+ return { fontFiles: stringArray(fonts.fontFiles), useBundledFonts: fonts.useBundledFonts !== false, loadSystemFonts: fonts.loadSystemFonts === true };
19
+ }
20
+
21
+ /**
22
+ * Convert one SVG (the output of `renderSvg` or `renderSlideSvg`) to PNG bytes. `fonts` is the handle `loadFonts()` returns: the raster draws
23
+ * with its font files, so the preview and the PNG use the same faces. Without it the bundled base faces draw.
24
+ */
13
25
  export async function svgToPng(svg, options = {}) {
14
26
  const rendered = await rasterizeSvg(svg, options);
15
27
  return rendered.png;
16
28
  }
17
29
 
30
+ /** Convert SVG slides (the output of `renderSvg`) to a PDF, one page each. `fonts` is as for `svgToPng`; `mode` is `"vector"` (default) or `"raster"`. */
18
31
  export async function svgToPdf(svgs, options = {}) {
19
32
  const pdfInputs = normalizeSvgList(svgs);
20
33
  if (!pdfInputs.length) {
@@ -49,13 +62,14 @@ export async function svgToPdf(svgs, options = {}) {
49
62
  }
50
63
 
51
64
  async function vectorPdf(inputs, options) {
52
- if (options.loadSystemFonts === true) {
53
- throw new OPFRenderError("pdf-system-fonts-unsupported", 'Vector PDF output embeds only the font files you supply: loadSystemFonts is not supported with mode "vector". Pass fontFiles/fontDirs, or use mode "raster".', { option: "loadSystemFonts" });
65
+ const settings = fontSettings(options);
66
+ if (settings.loadSystemFonts) {
67
+ throw new OPFRenderError("pdf-system-fonts-unsupported", 'Vector PDF output embeds only the font files you supply: fonts.loadSystemFonts is not supported with mode "vector". Pass a fonts handle with fontFiles (and fontDirs), or use mode "raster".', { option: "fonts.loadSystemFonts" });
54
68
  }
55
69
  const scale = positiveNumber(options.rasterFallbackScale, "rasterFallbackScale", 2);
56
70
  const fontFiles = [
57
- ...(options.useBundledFonts === false ? [] : await bundledFontFiles()),
58
- ...stringArray(options.fontFiles)
71
+ ...(settings.useBundledFonts ? await bundledFontFiles() : []),
72
+ ...settings.fontFiles
59
73
  ];
60
74
  const svgs = [];
61
75
  for (const input of inputs) svgs.push(await prepareRasterImages(normalizeSvgInput(input)));
@@ -100,12 +114,13 @@ async function rasterizeSvg(svgInput, options) {
100
114
  const { Resvg } = await loadResvg();
101
115
  const scale = positiveNumber(options.scale, "scale", DEFAULT_RASTER_SCALE);
102
116
  // FF-45: resvg draws no COLRv1 or OT-SVG colour glyphs; the colour faces stay out of its font list (see color-fonts.js).
117
+ const settings = fontSettings(options);
103
118
  const fontFiles = rasterFontFiles([
104
- ...(options.useBundledFonts === false ? [] : await bundledFontFiles()),
105
- ...stringArray(options.fontFiles)
119
+ ...(settings.useBundledFonts ? await bundledFontFiles() : []),
120
+ ...settings.fontFiles
106
121
  ]);
107
122
  const font = {
108
- loadSystemFonts: options.loadSystemFonts === true,
123
+ loadSystemFonts: settings.loadSystemFonts,
109
124
  fontFiles,
110
125
  fontDirs: stringArray(options.fontDirs),
111
126
  defaultFontFamily: options.defaultFontFamily ?? "Roboto",
@@ -178,8 +193,8 @@ async function bundledFontFiles() {
178
193
  }
179
194
 
180
195
  async function resolveBundledFontFiles() {
181
- const {loadBundledFontRegistry} = await import('./fonts-node.js');
182
- return (await loadBundledFontRegistry()).fontFiles;
196
+ const {loadFonts} = await import('./fonts-node.js');
197
+ return (await loadFonts()).fontFiles;
183
198
  }
184
199
 
185
200
  function normalizeSvgList(value) {
@@ -2,11 +2,9 @@
2
2
  // stand in for proprietary script fonts in previews. The packages are optional
3
3
  // peers, so the renderer install stays small; hosts install only what they use.
4
4
  import { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
5
- import { OPFFontError } from "./fonts.js";
6
- import * as scriptFontModule from "./script-fonts.js";
7
- import { scriptOfCharacter, scriptsOfText } from "./script-fonts.js";
8
- // Optional core exports are read from the namespace so an older published core still loads.
9
- import * as opfCore from "@openpresentation/opf";
5
+ import { OPFFontError } from "./font-registry.js";
6
+ import { SCRIPT_FONT_REPLACEMENTS, glyphFallbackFamilies, scriptOfCharacter, scriptsOfText } from "./script-fonts.js";
7
+ import { resolveScriptFonts } from "@openpresentation/opf/composition";
10
8
  import { presentationFamilies } from "./lazy-fonts.js";
11
9
  import { SYMBOL_SCRIPT, isSymbolEncodedFamily } from "./symbol-fonts.js";
12
10
 
@@ -33,7 +31,7 @@ export function scriptFontPackages(scripts) {
33
31
  * Browser entries for the script pack. `baseUrl` is where the host serves the
34
32
  * installed `@expo-google-fonts/*` packages (for example a copy of
35
33
  * `node_modules/@expo-google-fonts`). Each entry carries the reviewed SHA-256,
36
- * which `loadBrowserFontRegistry` verifies before use.
34
+ * which `loadFonts` (browser) verifies before use.
37
35
  */
38
36
  export function scriptFontEntries(scripts, {baseUrl}) {
39
37
  return scriptPackageEntries(scriptFontPackages(scripts), {baseUrl});
@@ -55,17 +53,17 @@ export function scriptPackageEntries(packages, {baseUrl}) {
55
53
  // Fields a preview never draws (checked against what the renderer and core composition draw).
56
54
  // Everywhere: the schema URL, embedded assets, catalog records, speaker notes, ids, image alt text and sources,
57
55
  // links, metadata and typed `extensions` passthrough. On the presentation: name (aria-label fallback), description,
58
- // filename, author, speaker, audience, purpose, tone, takeaway, duration, tags, narrative, language and version.
59
- // On a slide: beat. The organization (name) and a slide's section are drawn only as generated header/footer
60
- // furniture (`organization: true` / `section: true` in a design), so they count only when a design asks for
61
- // them. Everything else is drawn text: titles, text blocks, list items, runs, table cells, chart labels,
56
+ // filename, author, audience, purpose, tone, takeaway, duration, tags, narrative, language and version.
57
+ // On a slide: beat. The organization (name), the speaker (name and title) and a slide's section are drawn only as
58
+ // generated header/footer furniture (`organization: true` / `speaker: true` / `section: true` in a design), so they count
59
+ // only when a design asks for them; built-in variables copy their values into slide strings before this runs. Everything else is drawn text: titles, text blocks, list items, runs, table cells, chart labels,
62
60
  // categories and series names, code, quotes, metrics, timelines, furniture text.
63
61
  const UNDRAWN_KEYS = Object.freeze(["$schema", "assets", "catalogs", "notes", "id", "alt", "altText", "src", "url", "href", "link", "metadata", "extensions"]);
64
- const UNDRAWN_ROOT_KEYS = Object.freeze(["name", "description", "filename", "author", "authors", "creator", "speaker", "audience", "purpose", "tone", "takeaway", "duration", "tags", "narrative", "language", "license", "version", "keywords"]);
62
+ const UNDRAWN_ROOT_KEYS = Object.freeze(["name", "description", "filename", "author", "authors", "creator", "audience", "purpose", "tone", "takeaway", "duration", "tags", "narrative", "language", "license", "version", "keywords"]);
65
63
  const UNDRAWN_SLIDE_KEYS = Object.freeze(["beat"]);
66
64
  const UNDRAWN_VALUE = /^(?:https?|data|blob|pkg|file|mailto):\S*$/i;
67
65
 
68
- /** True when a design in the presentation turns on generated `field` furniture (`organization` or `section`). */
66
+ /** True when a design in the presentation turns on generated `field` furniture (`organization`, `speaker` or `section`). */
69
67
  function furnitureUses(presentation, field) {
70
68
  const designs = [presentation?.design, ...(Array.isArray(presentation?.slides) ? presentation.slides.map(slide => slide?.design) : [])];
71
69
  const pattern = new RegExp(`"${field}"\\s*:\\s*true`);
@@ -76,6 +74,7 @@ function furnitureUses(presentation, field) {
76
74
  export function* drawnStrings(presentation) {
77
75
  const undrawn = new Set(UNDRAWN_KEYS), undrawnRoot = new Set(UNDRAWN_ROOT_KEYS), undrawnSlide = new Set(UNDRAWN_SLIDE_KEYS);
78
76
  if (!furnitureUses(presentation, "organization")) undrawnRoot.add("organization");
77
+ if (!furnitureUses(presentation, "speaker")) undrawnRoot.add("speaker");
79
78
  if (!furnitureUses(presentation, "section")) undrawnSlide.add("section");
80
79
  function* visit(item, level) {
81
80
  if (typeof item === "string") { if (!UNDRAWN_VALUE.test(item)) yield item; }
@@ -92,21 +91,17 @@ export function* drawnStrings(presentation) {
92
91
 
93
92
  /**
94
93
  * The script profile of a presentation: core `resolveScriptFonts` output, exactly what the renderer plans
95
- * with. When the installed core has none (published core 0.11.0 and earlier) or it throws, the renderer
96
- * ignores the document language, so the profile is empty and Han text is Simplified Chinese, as drawn.
94
+ * with. When it throws, the renderer ignores the document language, so the profile is empty and Han text is
95
+ * Simplified Chinese, as drawn. `renderOptions.catalogs` (the host's registered catalogs) resolve as they do for the renderer.
97
96
  */
98
- export function presentationScriptProfile(presentation) {
99
- if (typeof opfCore.resolveScriptFonts === "function") {
100
- try { return opfCore.resolveScriptFonts(presentation); } catch { /* the renderer falls back the same way */ }
101
- }
97
+ export function presentationScriptProfile(presentation, renderOptions) {
98
+ try { return resolveScriptFonts(presentation, renderOptions?.catalogs !== undefined ? { catalogs: renderOptions.catalogs } : {}); } catch { /* the renderer falls back the same way */ }
102
99
  return {};
103
100
  }
104
101
 
105
102
  const servedScripts = () => new Set(BUNDLED_FONT_MANIFEST.packages.filter(item => item.pack === "scripts").flatMap(item => item.scripts));
106
103
  const cjkKeys = ["Jpan", "Hans", "Hant", "Kore"];
107
104
  const cjkCharacters = new Set(["Hani", "Hira", "Kana", "Hang", "Bopo"]);
108
- /** True when the renderer has per-character glyph fallback (FF-19 glyphFallbackFamilies). */
109
- const hasGlyphFallback = () => typeof scriptFontModule.glyphFallbackFamilies === "function";
110
105
 
111
106
  /**
112
107
  * ISO 15924 script whose pinned face a family resolves to: a proprietary script font (Yu Gothic: Jpan), a pinned face itself
@@ -116,7 +111,7 @@ function scriptOfFamily(family) {
116
111
  const name = String(family ?? "").toLowerCase();
117
112
  if (!name) return undefined;
118
113
  if (isSymbolEncodedFamily(name)) return SYMBOL_SCRIPT;
119
- const rule = scriptFontModule.SCRIPT_FONT_REPLACEMENTS.find(item => item.requestedFamily.toLowerCase() === name);
114
+ const rule = SCRIPT_FONT_REPLACEMENTS.find(item => item.requestedFamily.toLowerCase() === name);
120
115
  if (rule) return rule.script;
121
116
  return BUNDLED_FONT_MANIFEST.packages.find(item => item.pack === "scripts" && item.faces.some(face => face.family.toLowerCase() === name))?.scripts[0];
122
117
  }
@@ -128,9 +123,9 @@ function scriptOfFamily(family) {
128
123
  * scheme (`slide.design`), so each such slide is resolved too. `han` is the CJK script the scheme names, which tells
129
124
  * Han-only text apart when the language does not (a Yu Gothic deck whose language is English).
130
125
  *
131
- * `renderOptions` are the host's `renderSvg` options. Core resolves a font scheme id from the document and its bundled catalogs
132
- * only, while the renderer also reads `renderOptions.catalogs`; when a host passes catalogs the families the renderer resolves
133
- * per slide count too, so a scheme that exists only in the host's catalogs and names a script font still loads that font.
126
+ * `renderOptions` are the host's `renderSvg` options: their `catalogs` (core `Catalog[]`) go to core's `resolveScriptFonts`
127
+ * unchanged, and the families the renderer resolves per slide count too, so a scheme that exists only in the host's catalogs
128
+ * and names a script font still loads that font.
134
129
  */
135
130
  function designScripts(presentation, profile, renderOptions) {
136
131
  const scripts = new Set();
@@ -144,10 +139,10 @@ function designScripts(presentation, profile, renderOptions) {
144
139
  }
145
140
  };
146
141
  note(profile);
147
- if (typeof opfCore.resolveScriptFonts === "function" && Array.isArray(presentation?.slides)) {
142
+ if (Array.isArray(presentation?.slides)) {
148
143
  presentation.slides.forEach((slide, slideIndex) => {
149
144
  if (!slide || typeof slide !== "object" || !slide.design || typeof slide.design !== "object") return;
150
- try { note(opfCore.resolveScriptFonts(presentation, { slideIndex })); } catch { /* the renderer reports it when it draws the slide */ }
145
+ try { note(resolveScriptFonts(presentation, { slideIndex, ...(renderOptions?.catalogs !== undefined ? { catalogs: renderOptions.catalogs } : {}) })); } catch { /* the renderer reports it when it draws the slide */ }
151
146
  });
152
147
  }
153
148
  if (renderOptions?.catalogs) {
@@ -177,7 +172,8 @@ function* namedFontFamilies(value, key) {
177
172
  * `renderSvg` options) matter only for font schemes the host supplies in `renderOptions.catalogs`, which then resolve
178
173
  * like the renderer resolves them; a document that then does not resolve throws what `renderSvg` throws for it.
179
174
  */
180
- export function analyzePresentationScripts(presentation, profile = presentationScriptProfile(presentation), renderOptions) {
175
+ export function analyzePresentationScripts(presentation, profile, renderOptions) {
176
+ profile ??= presentationScriptProfile(presentation, renderOptions);
181
177
  const scripts = new Set(), cjk = new Set();
182
178
  const design = designScripts(presentation, profile, renderOptions);
183
179
  // Han-only text in a deck whose language is not CJK draws with the scheme's own CJK face when the scheme names one.
@@ -230,13 +226,12 @@ export function autoScriptSelection(presentation, options) {
230
226
  * renderer reports `missing-glyph` for them.
231
227
  */
232
228
  export function nextFallbackPackage(analysis, { covers, loaded }) {
233
- if (!hasGlyphFallback()) return undefined;
234
229
  const packages = BUNDLED_FONT_MANIFEST.packages.filter(item => item.pack === "scripts" && item.scripts.some(script => cjkKeys.includes(script)));
235
230
  const primary = new Set([...analysis.detected, ...(analysis.design ?? [])].filter(script => cjkKeys.includes(script))).size;
236
231
  if (packages.filter(item => loaded.has(item.name)).length >= primary + 1) return undefined;
237
232
  for (const character of analysis.cjk) {
238
233
  if (covers(character)) continue;
239
- for (const family of scriptFontModule.glyphFallbackFamilies(character, analysis.profile, analysis.profile?.serif === true)) {
234
+ for (const family of glyphFallbackFamilies(character, analysis.profile, analysis.profile?.serif === true)) {
240
235
  const pkg = packages.find(item => !loaded.has(item.name) && item.faces.some(face => face.family === family));
241
236
  if (pkg) return pkg;
242
237
  }
@@ -244,8 +239,7 @@ export function nextFallbackPackage(analysis, { covers, loaded }) {
244
239
  return undefined;
245
240
  }
246
241
 
247
- /** Drawn Han, kana or Hangul characters (at most `limit`) that no loaded script face covers. Empty without glyph fallback (nothing else would draw them). */
242
+ /** Drawn Han, kana or Hangul characters (at most `limit`) that no loaded script face covers. */
248
243
  export function uncoveredCjkCharacters(analysis, { covers, limit = 16 }) {
249
- if (!hasGlyphFallback()) return [];
250
244
  return [...analysis.cjk].filter(character => !covers(character)).slice(0, limit);
251
245
  }
@@ -16,8 +16,10 @@
16
16
  // is reported as a note. `glyphFallback: "none"` (strict mode) keeps the exact
17
17
  // faces and lets a missing glyph raise `missing-glyph`.
18
18
 
19
- import { fontPolicyFor } from "./font-policy.js";
20
- import { SYMBOL_PLACEHOLDER, SYMBOL_SCRIPT, mapSymbolText, symbolEncodingFor, symbolPreviewFaces } from "./symbol-fonts.js";
19
+ import { fontPolicyFor } from "@openpresentation/opf/font-policy";
20
+ import { scriptFontRole } from "@openpresentation/opf/composition";
21
+ import { SYMBOL_PLACEHOLDER, SYMBOL_SCRIPT, mapSymbolAdvances, symbolEncodingFamily, symbolPreviewFaces } from "./symbol-fonts.js";
22
+ export { scriptFontRole };
21
23
 
22
24
  const freeze = value => { if (value && typeof value === "object") { for (const child of Object.values(value)) freeze(child); Object.freeze(value); } return value; };
23
25
 
@@ -171,23 +173,10 @@ export function adjustedFontSize(size, adjust) {
171
173
  export const EMOJI_FONT_FAMILIES = SCRIPT_FONT_REPLACEMENTS.find(rule => rule.script === "Zsye").substitutes;
172
174
  const emojiFaces = new Set(EMOJI_FONT_FAMILIES);
173
175
 
174
- const eastAsianScripts = new Set(["Hani", "Hans", "Hant", "Hanb", "Jpan", "Kore", "Hang", "Jamo", "Hira", "Kana", "Hrkt", "Bopo", "Yiii"]);
175
- const complexScripts = new Set([
176
- "Arab", "Hebr", "Syrc", "Thaa", "Nkoo", "Adlm", "Rohg", "Mand", "Samr",
177
- "Deva", "Beng", "Guru", "Gujr", "Orya", "Taml", "Telu", "Knda", "Mlym", "Sinh",
178
- "Thai", "Laoo", "Tibt", "Mymr", "Khmr", "Mong", "Bali", "Java", "Lana", "Tale", "Talu", "Cakm", "Olck",
179
- ]);
180
176
  const rtlScripts = new Set(["Arab", "Hebr", "Syrc", "Thaa", "Nkoo", "Adlm", "Rohg", "Mand", "Samr"]);
181
177
  /** Scripts the latin slot covers directly; never split from the surrounding Latin run. */
182
178
  const latinGroup = new Set(["Latn", "Cyrl", "Grek", "Zyyy", "Zinh", "Zzzz"]);
183
179
 
184
- /** OOXML script slot for an ISO 15924 script code (the same table as core `scriptFontRole`). */
185
- export function scriptFontRole(script) {
186
- if (eastAsianScripts.has(script)) return "eastAsian";
187
- if (complexScripts.has(script)) return "complexScript";
188
- return "latin";
189
- }
190
-
191
180
  const detected = ["Latn", "Cyrl", "Grek", "Hani", "Hira", "Kana", "Hang", "Bopo", "Yiii", "Arab", "Hebr", "Syrc", "Thaa", "Nkoo", "Adlm", "Rohg", "Mand", "Samr",
192
181
  "Deva", "Beng", "Guru", "Gujr", "Orya", "Taml", "Telu", "Knda", "Mlym", "Sinh", "Thai", "Laoo", "Tibt", "Mymr", "Khmr", "Mong", "Bali", "Java", "Lana",
193
182
  "Tale", "Talu", "Cakm", "Olck", "Armn", "Geor", "Ethi", "Cher", "Tfng", "Vaii", "Zinh", "Zyyy"].map(code => [code, new RegExp(String.raw`^\p{Script=${code}}$`, "u")]);
@@ -564,7 +553,7 @@ export function createScriptFonts(profile = {}, measurement, options = {}) {
564
553
  const hex = item.code.toString(16).toUpperCase().padStart(2, "0");
565
554
  if (!entry.codes.includes(hex) && entry.codes.length < 32) { entry.codes.push(hex); if (entry.characters.length < 16) entry.characters.push(item.source); }
566
555
  };
567
- for (const item of mapSymbolText(encoding, text)) {
556
+ for (const item of mapSymbolAdvances(encoding, text)) {
568
557
  if (item.code === null) { foreign += item.source; continue; }
569
558
  flushForeign();
570
559
  let drawn = item.unicode, family = null, placeholder = false;
@@ -600,7 +589,7 @@ export function createScriptFonts(profile = {}, measurement, options = {}) {
600
589
  text = String(text ?? "");
601
590
  // A symbol-encoded family (resolved styles carry `symbolEncoding`; an unresolved style names the family itself) is mapped code by
602
591
  // code; `symbolEncoding: null` is the planner's own request for ordinary runs (characters that are not codes).
603
- const encoding = style.symbolEncoding === null ? undefined : style.symbolEncoding ?? symbolEncodingFor(style.fontFamily)?.family;
592
+ const encoding = style.symbolEncoding === null ? undefined : style.symbolEncoding ?? symbolEncodingFamily(style.fontFamily);
604
593
  if (encoding) return text ? planSymbols(text, style, encoding) : [{text, family: style.fontFamily, own: true}];
605
594
  // An emoji face (Noto Color Emoji for a Segoe UI Emoji run) draws emoji-presentation clusters only: its Latin text and digits
606
595
  // are planned per cluster below, where they take a text face (FF-45).
package/dist/svg.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { EmbeddedFont, ScriptFonts } from "./fonts.js";
1
+ import type { RenderFonts, ScriptFonts } from "./fonts.js";
2
+ import type { Catalog, Finding, UnresolvedReferenceDiagnostic } from "@openpresentation/opf";
2
3
  import type { SlideComposition, LayoutDiagnostic, TextMeasurement } from "@openpresentation/opf/composition";
3
4
  export declare const packageName = "@openpresentation/opf-render";
4
5
 
@@ -16,19 +17,12 @@ export declare const runtimePolicy: Readonly<{
16
17
  deterministicLocalExecution: true;
17
18
  }>;
18
19
 
20
+ /** What draws when nothing resolves: core's engine defaults (OPF 0.15 ships no catalog records), and the chart type a chart without `type` previews as. */
19
21
  export declare const engineDefaults: Readonly<{
20
- catalogs: Readonly<Record<string, Readonly<{ source: string }>>>;
21
- theme: "minimal";
22
- colorScheme: "cool-horizon";
23
- language: "english";
24
- narrative: "classic-story";
25
- tone: "formal";
26
- audience: "executives";
27
- fontScheme: Readonly<{
28
- pptx: Readonly<{ latin: "aptos"; ea: "microsoft-yahei"; cs: "nirmala-ui" }>;
29
- google: Readonly<{ latin: "roboto"; ea: "noto-sans-sc"; cs: "noto-sans" }>;
30
- }>;
31
- chartTypes: readonly ["stacked-column-3x", "stacked-area-3x", "line-with-markers-3x"];
22
+ theme: typeof import("@openpresentation/opf/composition").ENGINE_DEFAULT_THEME;
23
+ colorScheme: typeof import("@openpresentation/opf/composition").ENGINE_DEFAULT_COLOR_SCHEME;
24
+ fontScheme: typeof import("@openpresentation/opf/composition").ENGINE_DEFAULT_FONT_SCHEME;
25
+ chartType: string;
32
26
  }>;
33
27
 
34
28
  export type RenderDiagnostic = LayoutDiagnostic | {
@@ -41,17 +35,10 @@ export type RenderDiagnostic = LayoutDiagnostic | {
41
35
  scripts: string[];
42
36
  characters: string[];
43
37
  } | {
44
- code: "unsupported-pattern" | "variable-example-used" | "date-needs-value" | "language-preview-unavailable" | "language-preview-unresolved" | "paragraph-direction-unavailable";
38
+ code: "unsupported-pattern" | "variable-example-used" | "variable-builtin-missing" | "date-needs-value" | "language-preview-unresolved";
45
39
  path: string;
46
40
  message: string;
47
- } | {
48
- /** A font-scheme id matched no record; the default font scheme (`aptos`) was used as the base. */
49
- code: "unresolved-font-scheme";
50
- path: string;
51
- message: string;
52
- id: string;
53
- fallback: string;
54
- } | {
41
+ } | UnresolvedReferenceDiagnostic | {
55
42
  code: "unresolved-asset";
56
43
  path: string;
57
44
  message: string;
@@ -63,12 +50,18 @@ export type RenderDiagnostic = LayoutDiagnostic | {
63
50
  };
64
51
 
65
52
  export interface RenderSvgOptions {
66
- textMeasurement?: TextMeasurement;
53
+ /** Leave out slides marked `hidden: true`, as the player does. Default false: one SVG per slide. With true the result holds only the visible slides, in order, so an index no longer names the slide at that index. Read by the deck-level `renderSvg`; `renderSlideSvg` draws the slide it is given. */
54
+ skipHidden?: boolean;
55
+ /**
56
+ * The fonts the deck is laid out and drawn with: the handle `loadFonts()` returns (from `@openpresentation/opf-render/fonts-node` or
57
+ * `/fonts-browser`), or any object with a `textMeasurement` and, to embed faces in each SVG, an `embeddedFonts` list. Without it layout uses
58
+ * core's portable width estimate and the SVG names the design fonts without embedding them.
59
+ */
60
+ fonts?: RenderFonts;
67
61
  /** `"chain"` (default): a character the resolved face lacks is drawn with the first bundled face that has it, and reported as `font-glyph-fallback`. `"none"`: exact faces, a missing glyph raises `missing-glyph`. */
68
62
  glyphFallback?: "chain" | "none";
69
63
  /** Unscaled reference-pixel clearance around supplied vector text outlines; default 1. */
70
64
  textRasterPadding?: number;
71
- embeddedFonts?: EmbeddedFont[];
72
65
  strictAssets?: boolean;
73
66
  imageResolver?: (src: string | undefined, context: { asset: unknown; path: string }) => string | null | undefined;
74
67
  onDiagnostic?: (diagnostic: RenderDiagnostic) => void;
@@ -77,34 +70,38 @@ export interface RenderSvgOptions {
77
70
  /**
78
71
  * Values for the deck's template variables, keyed by variable id (core `resolveVariables`). A deck that uses
79
72
  * content variables, or is marked `template: true`, is resolved to a concrete deck first. A template previews with
80
- * each unfilled variable's example (reported as `variable-example-used`); a normal deck with an unfilled required
73
+ * each unfilled variable's example (reported as `variable-example-used`; a built-in such as `{{speaker.name}}` with no source value is reported as `variable-builtin-missing`); a normal deck with an unfilled required
81
74
  * variable throws `unfilled-variables`. `false` draws the document as authored, with `{{id}}` tokens and
82
75
  * `var:id` references visible and nothing resolved: the view an editor uses while the template itself is edited.
83
76
  */
84
77
  variables?: Record<string, unknown> | false;
85
- slideIndex?: number;
86
78
  /**
87
79
  * Today's calendar date (ISO YYYY-MM-DD) for `date: true` header/footer furniture. The renderer
88
80
  * never reads a clock; without it a current date is reported as unresolved content.
89
81
  */
90
82
  date?: string;
91
83
  trace?: boolean;
92
- catalogs?: Record<string, { records?: unknown[] } | unknown[]>;
93
84
  /**
94
- * Records for catalog sources the host has already fetched, keyed by the source string a document's
95
- * `catalogs.<kind>.source` names (a single source or an ordered array; first match wins, the bundled
96
- * default catalog is appended). The renderer never fetches a source itself.
85
+ * Catalogs the host registered (core `Catalog[]`), passed unchanged to core resolution: a reference the document does not
86
+ * embed resolves against the catalog whose `source` its group names, and the first entry is the default catalog for bare ids
87
+ * when the document omits `catalogs.default`. The renderer bundles and fetches no catalog; register the gallery snapshot with
88
+ * `import { defaultCatalog } from "@openpresentation/opf/catalog"` and `catalogs: [defaultCatalog]`. Omitted: only embedded
89
+ * records resolve, and anything else draws with core's engine defaults (reported as `unresolved-reference`).
90
+ * A malformed value throws core's `OPFCatalogsOptionError` (`code: "invalid-catalogs"`); a reference with an undeclared prefix
91
+ * (`foo:id` without `catalogs.foo`) fails the boundary check (`invalid-opf`, finding `opf/undeclared-catalog`).
97
92
  */
98
- catalogSources?: Record<string, { records?: unknown[] } | unknown[]>;
93
+ catalogs?: readonly Catalog[];
94
+ /** Fail with `unresolved-reference` (an OPFRenderError whose `details.diagnostics` list the references) instead of falling back when a reference resolves nowhere. Default false. */
95
+ strictReferences?: boolean;
99
96
  }
100
97
 
101
98
  export interface SvgToPngOptions {
99
+ /** The fonts the conversion draws with (Node): see `SvgToPngOptions` in the main entry. */
100
+ fonts?: RenderFonts;
102
101
  scale?: number;
103
102
  background?: string;
104
103
  dpi?: number;
105
- useBundledFonts?: boolean;
106
- loadSystemFonts?: boolean;
107
- fontFiles?: string[];
104
+ /** Extra directories of font files to draw with. */
108
105
  fontDirs?: string[];
109
106
  defaultFontFamily?: string;
110
107
  sansSerifFamily?: string;
@@ -128,7 +125,8 @@ export interface ResolvedSlide {
128
125
  index: number;
129
126
  path: string;
130
127
  slide: unknown;
131
- layout: unknown;
128
+ /** The layout record the slide names; absent when it names none or a reference that resolves nowhere (the slide then composes automatically). */
129
+ layout?: unknown;
132
130
  design: {
133
131
  theme: unknown;
134
132
  colorScheme: unknown;
@@ -157,15 +155,18 @@ export interface RenderBinding {
157
155
  export declare class OPFRenderError extends Error {
158
156
  readonly code: string;
159
157
  readonly details: Record<string, unknown>;
160
- readonly issues?: unknown[];
158
+ /** `invalid-opf`: the error findings of core `validate(presentation, { only: ["format"] })` (also `details.findings`; `details.report` is the whole report). */
159
+ readonly findings?: Finding[];
161
160
  readonly path?: string;
162
161
  constructor(code: string, message: string, details?: Record<string, unknown>);
163
162
  }
164
163
 
165
164
  export declare function resolvePresentation(input: unknown, options?: RenderSvgOptions): ResolvedPresentation;
166
165
 
167
- export declare function renderSvg(input: unknown, options?: RenderSvgOptions): string;
166
+ /** The SVG of every slide of the deck, in order. */
167
+ export declare function renderSvg(input: unknown, options?: RenderSvgOptions): string[];
168
168
 
169
- export declare function renderSvgDeck(input: unknown, options?: RenderSvgOptions): string[];
169
+ /** The SVG of one slide of the deck (`index` is zero-based; out of range throws `slide-index-out-of-range`). */
170
+ export declare function renderSlideSvg(input: unknown, index: number, options?: RenderSvgOptions): string;
170
171
 
171
172