@openpresentation/opf-render 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -22
- package/dist/annotations.js +1 -1
- package/dist/charts.js +8 -12
- package/dist/converters.js +41 -0
- package/dist/deck-runtime.js +6 -3
- package/dist/element.d.ts +6 -0
- package/dist/element.js +31 -8
- package/dist/export-browser.d.ts +6 -0
- package/dist/export-browser.js +16 -2
- package/dist/font-registry.js +3 -3
- package/dist/fonts-node.js +24 -5
- package/dist/fonts.d.ts +3 -3
- package/dist/index.d.ts +21 -34
- package/dist/pdf-vector.js +2 -0
- package/dist/pdf.d.ts +2 -0
- package/dist/pdf.js +4 -0
- package/dist/player.d.ts +2 -1
- package/dist/png.d.ts +2 -0
- package/dist/png.js +4 -0
- package/dist/preview-fonts-node.js +2 -2
- package/dist/raster-images.js +3 -1
- package/dist/raster.js +4 -20
- package/dist/script-font-pack.js +9 -8
- package/dist/svg.d.ts +21 -34
- package/dist/svg.js +215 -179
- package/package.json +70 -17
package/dist/element.js
CHANGED
|
@@ -168,19 +168,22 @@ function createElementClass() {
|
|
|
168
168
|
/** Open the full-screen slideshow (needs a user gesture to enter full screen). Loads the player on first use. */
|
|
169
169
|
async present(options = {}) {
|
|
170
170
|
if (this.#session && !this.#session.closed) return this.#session;
|
|
171
|
+
// Capture before loading the player: activeElement in the document is the shadow host, not its invoking button.
|
|
172
|
+
let returnFocus = options.returnFocus ?? this.ownerDocument.activeElement;
|
|
173
|
+
if (!options.returnFocus) while (returnFocus?.shadowRoot?.activeElement) returnFocus = returnFocus.shadowRoot.activeElement;
|
|
171
174
|
await this.#readyPromise;
|
|
172
175
|
if (!this.#store || !this.#cursor) throw new DeckError("not-ready", "The deck is not loaded yet.");
|
|
173
176
|
const { present } = await import("./player.js");
|
|
174
177
|
const session = await present(this.#deck, {
|
|
175
178
|
store: this.#store, fonts: this.#fonts ?? this.#resolvedFonts, renderOptions: this.#renderOptions,
|
|
176
|
-
includeHidden: this.includeHidden, startSlide: this.#cursor.number,
|
|
179
|
+
includeHidden: this.includeHidden, startSlide: this.#cursor.number, ...options, returnFocus,
|
|
177
180
|
});
|
|
178
181
|
this.#session = session;
|
|
179
182
|
session.addEventListener("slidechange", (event) => { if (this.#cursor && event.detail?.index !== undefined && this.#cursor.gotoIndex(event.detail.index)) { this.#show({ announce: false }); this.#emitChange(); } });
|
|
180
183
|
session.addEventListener("close", () => {
|
|
181
184
|
if (this.#session === session) this.#session = undefined;
|
|
182
185
|
this.#show({ announce: false });
|
|
183
|
-
this.#parts.viewport?.focus({ preventScroll: true });
|
|
186
|
+
if (!returnFocus?.isConnected) this.#parts.viewport?.focus({ preventScroll: true });
|
|
184
187
|
this.dispatchEvent(new CustomEvent("presentend", { detail: { slide: this.#cursor?.number } }));
|
|
185
188
|
});
|
|
186
189
|
this.dispatchEvent(new CustomEvent("presentstart", { detail: { slide: this.#cursor?.number } }));
|
|
@@ -253,9 +256,15 @@ function createElementClass() {
|
|
|
253
256
|
if (this.#documentValue !== undefined) return parseDeckDocument(this.#documentValue);
|
|
254
257
|
const src = this.getAttribute("src");
|
|
255
258
|
if (src) {
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
+
const source = new URL(src, this.ownerDocument.baseURI).href;
|
|
260
|
+
const response = await fetch(source, { signal, credentials: "same-origin" });
|
|
261
|
+
const details = {source, status: response.status, contentType: response.headers.get("content-type") ?? ""};
|
|
262
|
+
if (!response.ok) throw new DeckError("fetch-failed", `The deck could not be loaded (${response.status}) from ${source}.`, details);
|
|
263
|
+
const text = await response.text();
|
|
264
|
+
try { return parseDeckDocument(text); } catch (cause) {
|
|
265
|
+
const hint = /^\s*(?:<!doctype\s+html|<html\b|<head\b|<body\b)/i.test(text) ? " The response is HTML; check the deck URL and the host's SPA fallback route." : "";
|
|
266
|
+
throw new DeckError("invalid-document", `Invalid OPF from ${source} (${details.contentType || "unknown content type"}): ${cause.message}${hint}`, {...details, cause});
|
|
267
|
+
}
|
|
259
268
|
}
|
|
260
269
|
const inline = this.querySelector(':scope > script[type="application/opf+json"], :scope > script[type="application/json"]');
|
|
261
270
|
if (inline) return parseDeckDocument(inline.textContent ?? "");
|
|
@@ -263,7 +272,8 @@ function createElementClass() {
|
|
|
263
272
|
}
|
|
264
273
|
|
|
265
274
|
#report(error, fatal) {
|
|
266
|
-
|
|
275
|
+
const {source, status, contentType} = error.details ?? {};
|
|
276
|
+
this.dispatchEvent(new CustomEvent("error", { detail: { code: error.code, message: error.message, fatal, ...(source ? {source, status, contentType} : {}) } }));
|
|
267
277
|
}
|
|
268
278
|
|
|
269
279
|
#prefetch() {
|
|
@@ -504,6 +514,8 @@ const escapeHtml = (value) => String(value).replace(/&/g, "&").replace(/</g,
|
|
|
504
514
|
*
|
|
505
515
|
* `slides`: `"first"` (default: the first slide plus a list of titles), `"all"` or a list of 1-based slide numbers.
|
|
506
516
|
* `embed` puts the document in an `application/opf+json` script child, so the element upgrades without a request.
|
|
517
|
+
* `fontMode`: `"standalone"` (default, embeds per slide), `"shared"` (one set of font rules in the returned tag), or
|
|
518
|
+
* `"external"` (no font rules; the host must supply CSS with the exact pinned faces the handle measures).
|
|
507
519
|
*/
|
|
508
520
|
export function renderDeckHtml(input, options = {}) {
|
|
509
521
|
const deck = parseDeckDocument(input);
|
|
@@ -512,13 +524,23 @@ export function renderDeckHtml(input, options = {}) {
|
|
|
512
524
|
const wanted = options.slides ?? "first";
|
|
513
525
|
const shown = wanted === "all" ? sequence.map((_, position) => position) : wanted === "first" ? [0] : wanted.map((number) => number - 1).filter((position) => position >= 0 && position < sequence.length);
|
|
514
526
|
const handle = options.fonts !== null && typeof options.fonts === "object" ? options.fonts : undefined;
|
|
527
|
+
const fontMode = options.fontMode ?? "standalone";
|
|
528
|
+
if (!["standalone", "shared", "external"].includes(fontMode)) throw new DeckError("invalid-font-mode", "Choose standalone, shared or external SSR fonts.");
|
|
529
|
+
// Every mode measures with the same handle; external writes no @font-face data (RR-61 `embedFonts: false`).
|
|
530
|
+
const renderFonts = handle ?? options.renderOptions?.fonts;
|
|
531
|
+
const sharedRules = new Set();
|
|
515
532
|
const attributes = { src: options.src, slide: options.slide, fonts: handle ? undefined : options.fonts, label: options.label, ...options.attributes };
|
|
516
533
|
const attributeText = Object.entries(attributes).filter(([, value]) => value !== undefined && value !== false).map(([name, value]) => (value === true ? ` ${name}` : ` ${name}="${escapeHtml(value)}"`)).join("")
|
|
517
534
|
+ (options.thumbnails ? " thumbnails" : "") + (options.present ? " present" : "") + (options.includeHidden ? " include-hidden" : "");
|
|
518
535
|
const date = options.renderOptions?.date ?? options.date;
|
|
519
536
|
const figures = shown.map((position) => {
|
|
520
537
|
const index = sequence[position];
|
|
521
|
-
|
|
538
|
+
let svg = renderSlideSvg(deck, index, { ...options.renderOptions, fonts: renderFonts, ...(fontMode === "external" ? { embedFonts: false } : {}), ...(date ? { date } : {}) });
|
|
539
|
+
if (fontMode === "shared") svg = svg.replace(/<style>(@font-face[\s\S]*?)<\/style>/g, (_, css) => {
|
|
540
|
+
// These rules are produced by renderSlideSvg after its used-face selection and validation.
|
|
541
|
+
for (const rule of css.split("\n")) sharedRules.add(rule);
|
|
542
|
+
return "";
|
|
543
|
+
});
|
|
522
544
|
const { width, height } = svgDimensions(svg);
|
|
523
545
|
const prepared = prepareSlideSvg(svg).replace(/^<svg\b/, `<svg style="display:block;width:100%;height:auto;aspect-ratio:${width} / ${height}"`);
|
|
524
546
|
const label = slideLabel(slideInfo(deck, index), position + 1, sequence.length);
|
|
@@ -527,5 +549,6 @@ export function renderDeckHtml(input, options = {}) {
|
|
|
527
549
|
const titles = wanted === "first" && sequence.length > 1
|
|
528
550
|
? `<nav aria-label="Slides"><ol>${sequence.map((index) => `<li>${escapeHtml(slideInfo(deck, index).title)}</li>`).join("")}</ol></nav>` : "";
|
|
529
551
|
const embedded = options.embed ? `<script type="application/opf+json">${JSON.stringify(deck).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")}</script>` : "";
|
|
530
|
-
|
|
552
|
+
const shared = sharedRules.size ? `<style>${[...sharedRules].join("\n")}</style>` : "";
|
|
553
|
+
return `<${tag}${attributeText}>${shared}${figures.join("")}${titles}${embedded}</${tag}>`;
|
|
531
554
|
}
|
package/dist/export-browser.d.ts
CHANGED
|
@@ -13,6 +13,12 @@ export interface BrowserSvgToPngOptions {
|
|
|
13
13
|
export interface BrowserSvgToPdfOptions extends Pick<SvgToPdfOptions, "mode" | "background" | "serifFamily" | "metadata" | "tagged" | "compress" | "strict" | "rasterFallbackScale" | "defaultFontFamily" | "sansSerifFamily" | "monospaceFamily" | "onDiagnostic"> {
|
|
14
14
|
/** Raster mode: pixel density of each page image (default 2). */
|
|
15
15
|
scale?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Raster mode only: the pdf-lib module (`import * as pdfLib from "pdf-lib"`), an optional peer of the renderer that this entry never imports itself, so a
|
|
18
|
+
* bundle of it holds no PDF library. Without it raster mode tries to import "pdf-lib" by name and rejects with `OPFRenderError` `converter-missing`
|
|
19
|
+
* when the page cannot. The default vector mode needs no option.
|
|
20
|
+
*/
|
|
21
|
+
pdfLib?: { PDFDocument: { create(options?: { updateMetadata?: boolean }): Promise<any> } };
|
|
16
22
|
/**
|
|
17
23
|
* The browser fonts handle (`loadFonts` from `/fonts-browser`): every face it holds can be embedded in the PDF, including script faces
|
|
18
24
|
* a standalone SVG does not carry. Faces an SVG embeds as `@font-face` data (the `embeddedFonts` of the handle `renderSvg` was given) need no entry.
|
package/dist/export-browser.js
CHANGED
|
@@ -8,6 +8,9 @@ import { OPFRenderError, packageName } from "./svg.js";
|
|
|
8
8
|
// to hand one over) and nothing is fetched: faces come from the SVG's own @font-face data (the `embeddedFonts` of the fonts handle
|
|
9
9
|
// `renderSvg` was given), from the faces the `fonts` handle holds (`loadFonts` from `/fonts-browser`, so script faces the SVG does not
|
|
10
10
|
// embed reach the PDF too), or from `fontData`.
|
|
11
|
+
//
|
|
12
|
+
// RR-63: this entry imports no converter. The one optional piece is pdf-lib, for `mode: "raster"` only (the default vector PDF
|
|
13
|
+
// needs nothing): the host imports it and passes it as `options.pdfLib`, so a bundler never has to resolve it.
|
|
11
14
|
|
|
12
15
|
const DEFAULT_DIMENSIONS = { width: 1280, height: 720 };
|
|
13
16
|
const MAX_PIXELS = 40_000_000;
|
|
@@ -32,7 +35,7 @@ export async function svgToPng(svg, options = {}) {
|
|
|
32
35
|
* Convert SVG slides to a PDF, one slide per page (1 SVG pixel is 1 PDF point, as in Node). `mode: "vector"` (default) writes
|
|
33
36
|
* real text, paths, gradients and images; `"raster"` draws each slide as an image (`scale`, default 2). Takes the options of
|
|
34
37
|
* the Node `svgToPdf` that make sense here (`metadata`, `tagged`, `strict`, `compress`, `onDiagnostic`, the generic family
|
|
35
|
-
* names, `rasterFallbackScale`), `fonts` (the handle `loadFonts()` returns: every face it holds can be embedded), `fontData`
|
|
38
|
+
* names, `rasterFallbackScale`), `pdfLib` (raster mode only: `import * as pdfLib from "pdf-lib"`), `fonts` (the handle `loadFonts()` returns: every face it holds can be embedded), `fontData`
|
|
36
39
|
* (`[{ data, family? }]`, extra face bytes without a handle), `signal` (an AbortSignal, checked between pages) and
|
|
37
40
|
* `onProgress({ page, pages })`.
|
|
38
41
|
*/
|
|
@@ -70,7 +73,7 @@ export async function svgToPdf(svgs, options = {}) {
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
async function rasterPdf(inputs, options) {
|
|
73
|
-
const { PDFDocument } = await
|
|
76
|
+
const { PDFDocument } = options.pdfLib ?? await importPdfLib();
|
|
74
77
|
const pdf = await PDFDocument.create({ updateMetadata: false });
|
|
75
78
|
pdf.setCreator(packageName);
|
|
76
79
|
pdf.setProducer(packageName);
|
|
@@ -87,6 +90,17 @@ async function rasterPdf(inputs, options) {
|
|
|
87
90
|
return pdf.save({ addDefaultPage: false, useObjectStreams: false });
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
// pdf-lib for raster mode when the host did not pass it. The specifier is not a literal, so a browser bundler leaves it alone (and
|
|
94
|
+
// never fails for a host that does not install pdf-lib); it resolves where the page can import it by name, for example with an import map.
|
|
95
|
+
async function importPdfLib() {
|
|
96
|
+
const name = "pdf-lib";
|
|
97
|
+
try {
|
|
98
|
+
return await import(/* webpackIgnore: true */ /* @vite-ignore */ name);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
throw new OPFRenderError("converter-missing", 'Raster-mode PDF output needs pdf-lib, an optional peer dependency of @openpresentation/opf-render: run `npm install pdf-lib@^1.17.1` and pass it as the pdfLib option (import * as pdfLib from "pdf-lib"). The default vector mode needs nothing.', { package: "pdf-lib", range: "^1.17.1", install: "npm install pdf-lib@^1.17.1", cause: error instanceof Error ? error.message : String(error) });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
90
104
|
// ---------------------------------------------------------------------------------------------------------------------------
|
|
91
105
|
// Canvas
|
|
92
106
|
|
package/dist/font-registry.js
CHANGED
|
@@ -295,8 +295,8 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
295
295
|
resolveFont(style) { return resolve(style).resolution; },
|
|
296
296
|
clearSubstitutions() { substitutions.clear(); },
|
|
297
297
|
get substitutions() { return [...substitutions.values()]; },
|
|
298
|
-
// Faces flagged embed:"used" (the open pack, FF-31) are not in this eager list:
|
|
299
|
-
//
|
|
298
|
+
// Faces flagged embed:"used" (the open pack, FF-31) are not in this eager list: they come with selectEmbeddedFonts (or the
|
|
299
|
+
// `loadFonts` handle's embeddedFonts). Whichever list it is given, the SVG writer embeds a face only where its text draws it (RR-61).
|
|
300
300
|
get embeddedFonts() { return embedded(face=>face.embed!=="used"); },
|
|
301
301
|
/** Parsed face metadata in entry order, without encoding font bytes. */
|
|
302
302
|
describeFaces: ()=>faces.map(face=>({family:face.family,weight:face.weight,italic:face.italic,...(face.scripts?{scripts:[...face.scripts]}:{}),...(face.fallbackOnly?{fallbackOnly:true}:{})})),
|
|
@@ -320,5 +320,5 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
320
320
|
return made.map(face=>({family:face.family,weight:face.weight,italic:face.italic,...(face.scripts?{scripts:[...face.scripts]}:{})}));
|
|
321
321
|
},
|
|
322
322
|
};
|
|
323
|
-
function embedded(predicate) { return faces.filter(face=>predicate({family:face.family,weight:face.weight,italic:face.italic,scripts:face.scripts,embed:face.embed,fallbackOnly:face.fallbackOnly})).map(face=>({family:face.family,weight:face.weight,italic:face.italic,...(face.license ? {license:face.license} : {}),...(face.embed
|
|
323
|
+
function embedded(predicate) { return faces.filter(face=>predicate({family:face.family,weight:face.weight,italic:face.italic,scripts:face.scripts,embed:face.embed,fallbackOnly:face.fallbackOnly})).map(face=>({family:face.family,weight:face.weight,italic:face.italic,...(face.license ? {license:face.license} : {}),...(face.embed ? {embed:face.embed} : {}),dataUrl:`data:font/${face.format};base64,${base64(face.data)}`})); }
|
|
324
324
|
}
|
package/dist/fonts-node.js
CHANGED
|
@@ -25,10 +25,24 @@ async function verifiedFile(file, expected, details) {
|
|
|
25
25
|
// The default Latin, Cyrillic and Greek glyph-fallback face (the office pack always loads it, fallback-only).
|
|
26
26
|
const FALLBACK_PACKAGE = "@expo-google-fonts/noto-sans";
|
|
27
27
|
|
|
28
|
+
// RR-63: every `@expo-google-fonts/*` package is an optional peer dependency, so a host installs only the faces it uses. A request that
|
|
29
|
+
// needs packages that are not installed fails once, naming all of them and the install command, instead of one package at a time.
|
|
30
|
+
function requireInstalled(packages, what) {
|
|
31
|
+
const missing = packages.filter(pkg => {
|
|
32
|
+
if (pkg.vendored) return false;
|
|
33
|
+
try { require.resolve(`${pkg.name}/package.json`); return false; } catch { return true; }
|
|
34
|
+
});
|
|
35
|
+
if (!missing.length) return;
|
|
36
|
+
const install = `npm install ${missing.map(pkg => `${pkg.name}@${pkg.version}`).join(" ")}`;
|
|
37
|
+
throw new OPFFontError("font-resource-unavailable", `${what} needs font packages that are not installed (optional peer dependencies of @openpresentation/opf-render): run \`${install}\`.`, {packages: missing.map(pkg => pkg.name), install});
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
// `skipped`: an optional npm package that is not installed is recorded there instead of failing (scripts: 'auto').
|
|
29
41
|
// `fallbackOnly`: the faces serve glyph fallback and requests by their own name, never a replacement for another family.
|
|
30
|
-
|
|
42
|
+
// `what`: how the request is named in the error for a package that is not installed, for example "The 'office' font pack".
|
|
43
|
+
async function loadPackages(packages, skipped, {fallbackOnly = false, what = "This font request"} = {}) {
|
|
31
44
|
const entries = [], fontFiles = [];
|
|
45
|
+
if (!skipped) requireInstalled(packages, what);
|
|
32
46
|
for (const pkg of packages) {
|
|
33
47
|
if (pkg.vendored) {
|
|
34
48
|
// FF-31: vendored faces ship inside this package (pkg.vendored, for example fonts/carlito), hash-pinned like the npm packs. The open pack is
|
|
@@ -53,7 +67,7 @@ async function loadPackages(packages, skipped, {fallbackOnly = false} = {}) {
|
|
|
53
67
|
installed = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
54
68
|
} catch (error) {
|
|
55
69
|
if (skipped) { skipped.push({package: pkg.name, version: pkg.version, scripts: [...pkg.scripts]}); continue; }
|
|
56
|
-
throw new OPFFontError("font-resource-unavailable", `Install ${pkg.name}@${pkg.version} to use
|
|
70
|
+
throw new OPFFontError("font-resource-unavailable", `Install ${pkg.name}@${pkg.version} to use ${what}.`, {package:pkg.name, cause:error.code});
|
|
57
71
|
}
|
|
58
72
|
if (installed.version !== pkg.version) throw new OPFFontError("font-version-mismatch", `Expected ${pkg.name}@${pkg.version}; reinstall the pinned package.`, {package:pkg.name, expected:pkg.version, actual:installed.version});
|
|
59
73
|
const directory = path.dirname(manifestPath);
|
|
@@ -73,7 +87,7 @@ async function loadPackages(packages, skipped, {fallbackOnly = false} = {}) {
|
|
|
73
87
|
const renamedAliases = () => Object.fromEntries(BUNDLED_FONT_MANIFEST.packages.filter(item => item.pack === "open" && item.renamedFrom).map(item => [item.renamedFrom, item.faces[0].family]));
|
|
74
88
|
// The vendored faces this registry holds that are embed "used": a host that serves fonts itself lists them to copy their files.
|
|
75
89
|
const lazyOf = registry => lazyFontList().filter(face => registry.describeFaces().some(held => held.family === face.family && held.weight === face.weight && held.italic === face.italic));
|
|
76
|
-
const loadPack = pack => loadPackages(BUNDLED_FONT_MANIFEST.packages.filter(item => item.pack === pack));
|
|
90
|
+
const loadPack = pack => loadPackages(BUNDLED_FONT_MANIFEST.packages.filter(item => item.pack === pack), undefined, {what: `The '${pack}' font pack`});
|
|
77
91
|
|
|
78
92
|
/**
|
|
79
93
|
* Adds the script pack. `scripts` is 'all', ISO 15924 codes, or 'auto' (FF-19): detect the scripts the
|
|
@@ -86,7 +100,7 @@ async function withScripts(loaded, scripts, {presentation, onDiagnostic, renderO
|
|
|
86
100
|
// A package the registry already loaded (the default Noto Sans fallback) is never loaded twice.
|
|
87
101
|
const notLoaded = list => list.filter(pkg => !loaded.packages?.includes(pkg.name));
|
|
88
102
|
if (scripts !== "auto") {
|
|
89
|
-
const extra = await loadPackages(notLoaded(scriptFontPackages(scripts)));
|
|
103
|
+
const extra = await loadPackages(notLoaded(scriptFontPackages(scripts)), undefined, {what: `The requested script fonts (${Array.isArray(scripts) ? scripts.join(", ") : scripts})`});
|
|
90
104
|
return {...loaded, entries:[...loaded.entries, ...embedUsed(extra.entries)], fontFiles:[...loaded.fontFiles, ...extra.fontFiles], packages:[...(loaded.packages ?? []), ...extra.packages]};
|
|
91
105
|
}
|
|
92
106
|
if (presentation === null || typeof presentation !== "object") throw new OPFFontError("invalid-font-scripts", "scripts: 'auto' needs the presentation whose text decides the scripts.", {scripts});
|
|
@@ -160,6 +174,11 @@ async function withFaces(loaded, faces) {
|
|
|
160
174
|
*/
|
|
161
175
|
async function buildRegistry(pack, {scripts, faces, presentation, onDiagnostic, renderOptions, ...options}) {
|
|
162
176
|
let loaded, registryOptions = options;
|
|
177
|
+
// RR-63: name every npm font package the pack needs at once, before loading any of them.
|
|
178
|
+
if (pack === "base" || pack === "office") {
|
|
179
|
+
const packs = pack === "base" ? ["base"] : ["office", ...(options.includeBaseFonts === false ? [] : ["base"])];
|
|
180
|
+
requireInstalled([...BUNDLED_FONT_MANIFEST.packages.filter(item => packs.includes(item.pack)), ...(pack === "office" ? scriptFontPackages(["Latn"]) : [])], `The '${pack}' font pack`);
|
|
181
|
+
}
|
|
163
182
|
if (pack === "office") {
|
|
164
183
|
const {entries, fontFiles, packages} = await loadPack("office");
|
|
165
184
|
for (const [include, name] of [[options.includeOpenFonts, "open"], [options.includeBaseFonts, "base"]]) {
|
|
@@ -177,7 +196,7 @@ async function buildRegistry(pack, {scripts, faces, presentation, onDiagnostic,
|
|
|
177
196
|
// an explicit list does; a presentation that does not select Latn keeps it fallback-only.
|
|
178
197
|
const requested = scripts === "auto" && presentation !== null && typeof presentation === "object" ? scriptFontPackages(autoScriptSelection(presentation, renderOptions).scripts).map(pkg => pkg.name)
|
|
179
198
|
: scripts === undefined || scripts === "auto" || (Array.isArray(scripts) && !scripts.length) ? [] : scriptFontPackages(scripts).map(pkg => pkg.name);
|
|
180
|
-
const fallback = await loadPackages(scriptFontPackages(["Latn"]).filter(pkg => !requested.includes(pkg.name)), undefined, {fallbackOnly:true});
|
|
199
|
+
const fallback = await loadPackages(scriptFontPackages(["Latn"]).filter(pkg => !requested.includes(pkg.name)), undefined, {fallbackOnly:true, what: "The 'office' font pack (its default glyph-fallback face)"});
|
|
181
200
|
fontFiles.push(...fallback.fontFiles);
|
|
182
201
|
entries.push(...fallback.entries);
|
|
183
202
|
packages.push(...fallback.packages);
|
package/dist/fonts.d.ts
CHANGED
|
@@ -8,8 +8,8 @@ export type { FontPolicyEntry } from "@openpresentation/opf/font-policy";
|
|
|
8
8
|
export { isSymbolEncodedFamily, mapSymbolText, symbolCodeOf } from "@openpresentation/opf/symbol-font-encodings";
|
|
9
9
|
export { scriptFontRole } from "@openpresentation/opf/composition";
|
|
10
10
|
export type { ScriptRole } from "@openpresentation/opf/composition";
|
|
11
|
-
export interface FontFaceInput { data: Uint8Array; family?: string; weight?: number; italic?: boolean; postscriptName?: string; license?: string; /** Serves glyph fallback and requests by its own family only; never a replacement for another family. */ fallbackOnly?: boolean; /**
|
|
12
|
-
export interface EmbeddedFont { family: string; weight: number; italic?: boolean; dataUrl: string; license?: string; embed?: "used" }
|
|
11
|
+
export interface FontFaceInput { data: Uint8Array; family?: string; weight?: number; italic?: boolean; postscriptName?: string; license?: string; /** Serves glyph fallback and requests by its own family only; never a replacement for another family. */ fallbackOnly?: boolean; /** Every face is embedded in an SVG only when the slide's text draws it (RR-61). "used" also leaves the face out of the eager `registry.embeddedFonts` list (the open pack, FF-31); "always" embeds it in every SVG. */ embed?: "always" | "used"; /** ISO 15924 scripts a designated script replacement face serves (FF-19). */ scripts?: string[] }
|
|
12
|
+
export interface EmbeddedFont { family: string; weight: number; italic?: boolean; dataUrl: string; license?: string; /** "always": written into every SVG; otherwise only into an SVG whose text draws the face (RR-61). */ embed?: "always" | "used" }
|
|
13
13
|
export type FontCompatibility = "exact" | "metric" | "visual" | "generic";
|
|
14
14
|
export interface FontReplacementMeasurement { replacement:string; meanAbsWidthDelta:number; meanWidthDelta:number; maxAbsWidthDelta:number; styles:number; reference:string }
|
|
15
15
|
export interface FontResolution {
|
|
@@ -41,7 +41,7 @@ export declare const EXPERIMENTAL_FONT_CANDIDATES: readonly Readonly<{requestedF
|
|
|
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`
|
|
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). */
|
|
45
45
|
embeddedFonts?: readonly EmbeddedFont[];
|
|
46
46
|
/** Font files the Node raster and PDF conversions draw with. */
|
|
47
47
|
fontFiles?: readonly string[];
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RenderFonts, ScriptFonts } from "./fonts.js";
|
|
2
|
-
import type { Finding } from "@openpresentation/opf";
|
|
2
|
+
import type { Catalog, Finding, UnresolvedReferenceDiagnostic } from "@openpresentation/opf";
|
|
3
3
|
import type { SlideComposition, LayoutDiagnostic, TextMeasurement } from "@openpresentation/opf/composition";
|
|
4
4
|
export declare const packageName = "@openpresentation/opf-render";
|
|
5
5
|
|
|
@@ -17,19 +17,12 @@ export declare const runtimePolicy: Readonly<{
|
|
|
17
17
|
deterministicLocalExecution: true;
|
|
18
18
|
}>;
|
|
19
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. */
|
|
20
21
|
export declare const engineDefaults: Readonly<{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
narrative: "classic-story";
|
|
26
|
-
tone: "formal";
|
|
27
|
-
audience: "executives";
|
|
28
|
-
fontScheme: Readonly<{
|
|
29
|
-
pptx: Readonly<{ latin: "aptos"; ea: "microsoft-yahei"; cs: "nirmala-ui" }>;
|
|
30
|
-
google: Readonly<{ latin: "roboto"; ea: "noto-sans-sc"; cs: "noto-sans" }>;
|
|
31
|
-
}>;
|
|
32
|
-
chartTypes: readonly ["stacked-column", "stacked-area", "line-with-markers"];
|
|
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;
|
|
33
26
|
}>;
|
|
34
27
|
|
|
35
28
|
export type RenderDiagnostic = LayoutDiagnostic | {
|
|
@@ -45,20 +38,7 @@ export type RenderDiagnostic = LayoutDiagnostic | {
|
|
|
45
38
|
code: "unsupported-pattern" | "variable-example-used" | "variable-builtin-missing" | "date-needs-value" | "language-preview-unresolved";
|
|
46
39
|
path: string;
|
|
47
40
|
message: string;
|
|
48
|
-
} | {
|
|
49
|
-
/** A font-scheme id matched no record; the default font scheme (`aptos`) was used as the base. */
|
|
50
|
-
code: "unresolved-font-scheme";
|
|
51
|
-
path: string;
|
|
52
|
-
message: string;
|
|
53
|
-
id: string;
|
|
54
|
-
fallback: string;
|
|
55
|
-
} | {
|
|
56
|
-
/** A layout, theme or colour-scheme id matched no record: the slide composed with no layout record, the `minimal` theme or the `cool-horizon` colour scheme. Never an error. */
|
|
57
|
-
code: "unresolved-layout" | "unresolved-theme" | "unresolved-color-scheme";
|
|
58
|
-
path: string;
|
|
59
|
-
message: string;
|
|
60
|
-
id: string;
|
|
61
|
-
} | {
|
|
41
|
+
} | UnresolvedReferenceDiagnostic | {
|
|
62
42
|
code: "unresolved-asset";
|
|
63
43
|
path: string;
|
|
64
44
|
message: string;
|
|
@@ -74,10 +54,12 @@ export interface RenderSvgOptions {
|
|
|
74
54
|
skipHidden?: boolean;
|
|
75
55
|
/**
|
|
76
56
|
* The fonts the deck is laid out and drawn with: the handle `loadFonts()` returns (from `@openpresentation/opf-render/fonts-node` or
|
|
77
|
-
* `/fonts-browser`), or any object with a `textMeasurement` and, to embed faces
|
|
57
|
+
* `/fonts-browser`), or any object with a `textMeasurement` and, to embed faces, an `embeddedFonts` list (each SVG carries only the faces its text draws). Without it layout uses
|
|
78
58
|
* core's portable width estimate and the SVG names the design fonts without embedding them.
|
|
79
59
|
*/
|
|
80
60
|
fonts?: RenderFonts;
|
|
61
|
+
/** `false` writes no `@font-face` data into the SVG (RR-61): the fonts handle still measures and the SVG still names the families, for a host whose page already has the faces (a browser `loadFonts` handle adds them to the document). Default true: each SVG embeds the faces of `fonts.embeddedFonts` its own text draws. */
|
|
62
|
+
embedFonts?: boolean;
|
|
81
63
|
/** `"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`. */
|
|
82
64
|
glyphFallback?: "chain" | "none";
|
|
83
65
|
/** Unscaled reference-pixel clearance around supplied vector text outlines; default 1. */
|
|
@@ -100,13 +82,18 @@ export interface RenderSvgOptions {
|
|
|
100
82
|
*/
|
|
101
83
|
date?: string;
|
|
102
84
|
trace?: boolean;
|
|
103
|
-
catalogs?: Record<string, { records?: unknown[] } | unknown[]>;
|
|
104
85
|
/**
|
|
105
|
-
*
|
|
106
|
-
* `
|
|
107
|
-
*
|
|
86
|
+
* Catalogs the host registered (core `Catalog[]`), passed unchanged to core resolution: a reference the document does not
|
|
87
|
+
* embed resolves against the catalog whose `source` its group names, and the first entry is the default catalog for bare ids
|
|
88
|
+
* when the document omits `catalogs.default`. The renderer bundles and fetches no catalog; register the gallery snapshot with
|
|
89
|
+
* `import { defaultCatalog } from "@openpresentation/opf/catalog"` and `catalogs: [defaultCatalog]`. Omitted: only embedded
|
|
90
|
+
* records resolve, and anything else draws with core's engine defaults (reported as `unresolved-reference`).
|
|
91
|
+
* A malformed value throws core's `OPFCatalogsOptionError` (`code: "invalid-catalogs"`); a reference with an undeclared prefix
|
|
92
|
+
* (`foo:id` without `catalogs.foo`) fails the boundary check (`invalid-opf`, finding `opf/undeclared-catalog`).
|
|
108
93
|
*/
|
|
109
|
-
|
|
94
|
+
catalogs?: readonly Catalog[];
|
|
95
|
+
/** Fail with `unresolved-reference` (an OPFRenderError whose `details.diagnostics` list the references) instead of falling back when a reference resolves nowhere. Default false. */
|
|
96
|
+
strictReferences?: boolean;
|
|
110
97
|
}
|
|
111
98
|
|
|
112
99
|
export interface SvgToPngOptions {
|
|
@@ -233,7 +220,7 @@ export interface ResolvedSlide {
|
|
|
233
220
|
index: number;
|
|
234
221
|
path: string;
|
|
235
222
|
slide: unknown;
|
|
236
|
-
/** The layout record the slide names; absent when it names none or
|
|
223
|
+
/** The layout record the slide names; absent when it names none or a reference that resolves nowhere (the slide then composes automatically). */
|
|
237
224
|
layout?: unknown;
|
|
238
225
|
design: {
|
|
239
226
|
theme: unknown;
|
package/dist/pdf-vector.js
CHANGED
|
@@ -724,6 +724,8 @@ class Converter {
|
|
|
724
724
|
result = { resource, object, width: info.width, height: info.height };
|
|
725
725
|
}
|
|
726
726
|
} catch (error) {
|
|
727
|
+
// A missing converter is the host's install, not a bad picture: say so instead of writing a PDF without its pictures.
|
|
728
|
+
if (error?.code === "converter-missing") throw error;
|
|
727
729
|
this.unsupported("pdf-image-skipped", `An embedded image could not be decoded and is not drawn: ${error instanceof Error ? error.message : String(error)}`, { path: tracePath });
|
|
728
730
|
}
|
|
729
731
|
this.images.set(digest, result);
|
package/dist/pdf.d.ts
ADDED
package/dist/pdf.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// RR-63: the PDF format entry (Node). The default vector `svgToPdf` needs no converter for text and shapes; it loads the optional peer
|
|
2
|
+
// `sharp` for pictures and `@resvg/resvg-js` for the rare element it rasterizes, and `mode: "raster"` also loads `pdf-lib`. A converter
|
|
3
|
+
// that is not installed rejects with `OPFRenderError` code `converter-missing`. For a page use `@openpresentation/opf-render/export-browser`.
|
|
4
|
+
export { svgToPdf } from "./raster.js";
|
package/dist/player.d.ts
CHANGED
|
@@ -7,7 +7,8 @@ import type { RenderSvgOptions } from "./svg.js";
|
|
|
7
7
|
*/
|
|
8
8
|
export declare class DeckError extends Error {
|
|
9
9
|
readonly code: string;
|
|
10
|
-
|
|
10
|
+
readonly details?: {source: string; status?: number; contentType?: string};
|
|
11
|
+
constructor(code: string, message: string, options?: { cause?: unknown; source?: string; status?: number; contentType?: string });
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/** The slide that is showing, in the sequence that plays (hidden slides are not counted). `notes` is plain text. */
|
package/dist/png.d.ts
ADDED
package/dist/png.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// RR-63: the PNG format entry (Node). `svgToPng` loads the optional peers `@resvg/resvg-js` (and `sharp` for WebP and rotated JPEG
|
|
2
|
+
// pictures) on first use and rejects with `OPFRenderError` code `converter-missing` when they are not installed. For a page use
|
|
3
|
+
// `@openpresentation/opf-render/export-browser`.
|
|
4
|
+
export { svgToPng } from "./raster.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// RR-28: fill a self-hosted font root (the layout of `preview-fonts.js`) from the installed packages, for hosts that serve
|
|
2
2
|
// `<opf-deck fonts="/opf-fonts/">`. Node only. Every file is copied unmodified and checked against its pinned SHA-256 and
|
|
3
3
|
// every license notice travels with it (LICENSES.txt). Nothing is downloaded: the faces come from this package's `fonts`
|
|
4
|
-
// directory and the installed `@expo-google-fonts/*` packages.
|
|
4
|
+
// directory and the installed `@expo-google-fonts/*` packages (optional peer dependencies of this package: install the ones you copy).
|
|
5
5
|
import crypto from "node:crypto";
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
@@ -50,7 +50,7 @@ export function copyPreviewFonts({ outDir, scripts = false, cwd, manifest = BUND
|
|
|
50
50
|
const sourceDirectory = pkg.vendored ? within(rendererRoot, pkg.vendored) : resolvePackageDirectory(pkg.name, cwd);
|
|
51
51
|
if (!sourceDirectory || !fs.existsSync(sourceDirectory)) {
|
|
52
52
|
if (pkg.pack === "scripts") { missing.push(pkg.name); continue; }
|
|
53
|
-
throw new Error(`${pkg.name} is not installed:
|
|
53
|
+
throw new Error(`${pkg.name} is not installed: it is an optional peer dependency of @openpresentation/opf-render, so run "npm install ${pkg.name}@${pkg.version}".`);
|
|
54
54
|
}
|
|
55
55
|
if (!pkg.vendored) {
|
|
56
56
|
const installed = JSON.parse(fs.readFileSync(path.join(sourceDirectory, "package.json"), "utf8"));
|
package/dist/raster-images.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {OPFRenderError} from './svg.js';
|
|
2
|
+
import {isConverterMissing,loadConverter} from './converters.js';
|
|
2
3
|
|
|
3
4
|
// Resvg does not decode WebP or apply JPEG EXIF orientation, and draws no text of an SVG used as an image (its nested
|
|
4
5
|
// document has no fonts). Replace only affected embedded image hrefs in the private
|
|
@@ -27,6 +28,7 @@ export async function prepareRasterImages(svg, {nestedSvg} = {}) {
|
|
|
27
28
|
const replacement = href[0].slice(0, href[0].indexOf(href[1]) + 1) + png + href[1];
|
|
28
29
|
tag = tag.slice(0, href.index) + replacement + tag.slice(href.index + href[0].length);
|
|
29
30
|
} catch (error) {
|
|
31
|
+
if (isConverterMissing(error)) throw error;
|
|
30
32
|
throw new OPFRenderError('image-conversion-failed', 'Embedded raster image could not be prepared for PNG/PDF output.', {path, cause:error instanceof Error ? error.message : String(error)});
|
|
31
33
|
}
|
|
32
34
|
output += svg.slice(end, match.index) + tag;
|
|
@@ -74,7 +76,7 @@ async function prepareRasterImage(uri) {
|
|
|
74
76
|
} else if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8 || bytes[2] !== 0xff) {
|
|
75
77
|
throw new Error('Embedded data is not a JPEG image.');
|
|
76
78
|
}
|
|
77
|
-
const {default:sharp}=await
|
|
79
|
+
const {default:sharp}=await loadConverter('sharp');
|
|
78
80
|
const decoder=sharp(bytes,{limitInputPixels:40_000_000,animated:false,failOn:'warning'});
|
|
79
81
|
if (!webp) {
|
|
80
82
|
const {orientation}=await decoder.metadata();
|
package/dist/raster.js
CHANGED
|
@@ -3,6 +3,7 @@ import {OPFRenderError,packageName} from './svg.js';
|
|
|
3
3
|
import {separateLigatures} from './font-compatibility.js';
|
|
4
4
|
import {monochromeColorFonts,rasterFontFiles} from './color-fonts.js';
|
|
5
5
|
import {pinScriptClusters} from './raster-text.js';
|
|
6
|
+
import {loadConverter} from './converters.js';
|
|
6
7
|
const DEFAULT_DIMENSIONS = { width: 1280, height: 720 };
|
|
7
8
|
const DEFAULT_RASTER_SCALE = 1;
|
|
8
9
|
const DEFAULT_RASTER_BACKGROUND = "#FFFFFF";
|
|
@@ -100,7 +101,7 @@ async function vectorPdf(inputs, options) {
|
|
|
100
101
|
// The picture decoder of the Node vector export (the browser entry passes a canvas one). sharp loads on first use.
|
|
101
102
|
function sharpImageCodec() {
|
|
102
103
|
let loaded;
|
|
103
|
-
const sharp = () => (loaded ??=
|
|
104
|
+
const sharp = () => (loaded ??= loadConverter("sharp").then((module) => module.default));
|
|
104
105
|
return {
|
|
105
106
|
metadata: async (bytes) => (await sharp())(bytes, { limitInputPixels: 40_000_000, animated: false }).metadata(),
|
|
106
107
|
rgba: async (bytes, { orient }) => {
|
|
@@ -162,25 +163,8 @@ async function rasterizeSvg(svgInput, options) {
|
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return await import("@resvg/resvg-js");
|
|
168
|
-
} catch (error) {
|
|
169
|
-
throw new OPFRenderError("png-renderer-unavailable", "PNG/PDF conversion requires the Node runtime dependency @resvg/resvg-js.", {
|
|
170
|
-
cause: error instanceof Error ? error.message : String(error)
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async function loadPdfLib() {
|
|
176
|
-
try {
|
|
177
|
-
return await import("pdf-lib");
|
|
178
|
-
} catch (error) {
|
|
179
|
-
throw new OPFRenderError("pdf-writer-unavailable", "PDF conversion requires the runtime dependency pdf-lib.", {
|
|
180
|
-
cause: error instanceof Error ? error.message : String(error)
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
}
|
|
166
|
+
const loadResvg = () => loadConverter("@resvg/resvg-js");
|
|
167
|
+
const loadPdfLib = () => loadConverter("pdf-lib");
|
|
184
168
|
|
|
185
169
|
async function bundledFontFiles() {
|
|
186
170
|
if (!bundledFontFilesCache) {
|
package/dist/script-font-pack.js
CHANGED
|
@@ -92,10 +92,10 @@ export function* drawnStrings(presentation) {
|
|
|
92
92
|
/**
|
|
93
93
|
* The script profile of a presentation: core `resolveScriptFonts` output, exactly what the renderer plans
|
|
94
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.
|
|
95
|
+
* Simplified Chinese, as drawn. `renderOptions.catalogs` (the host's registered catalogs) resolve as they do for the renderer.
|
|
96
96
|
*/
|
|
97
|
-
export function presentationScriptProfile(presentation) {
|
|
98
|
-
try { return resolveScriptFonts(presentation); } catch { /* the renderer falls back the same way */ }
|
|
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 */ }
|
|
99
99
|
return {};
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -123,9 +123,9 @@ function scriptOfFamily(family) {
|
|
|
123
123
|
* scheme (`slide.design`), so each such slide is resolved too. `han` is the CJK script the scheme names, which tells
|
|
124
124
|
* Han-only text apart when the language does not (a Yu Gothic deck whose language is English).
|
|
125
125
|
*
|
|
126
|
-
* `renderOptions` are the host's `renderSvg` options
|
|
127
|
-
*
|
|
128
|
-
*
|
|
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.
|
|
129
129
|
*/
|
|
130
130
|
function designScripts(presentation, profile, renderOptions) {
|
|
131
131
|
const scripts = new Set();
|
|
@@ -142,7 +142,7 @@ function designScripts(presentation, profile, renderOptions) {
|
|
|
142
142
|
if (Array.isArray(presentation?.slides)) {
|
|
143
143
|
presentation.slides.forEach((slide, slideIndex) => {
|
|
144
144
|
if (!slide || typeof slide !== "object" || !slide.design || typeof slide.design !== "object") return;
|
|
145
|
-
try { note(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 */ }
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
148
|
if (renderOptions?.catalogs) {
|
|
@@ -172,7 +172,8 @@ function* namedFontFamilies(value, key) {
|
|
|
172
172
|
* `renderSvg` options) matter only for font schemes the host supplies in `renderOptions.catalogs`, which then resolve
|
|
173
173
|
* like the renderer resolves them; a document that then does not resolve throws what `renderSvg` throws for it.
|
|
174
174
|
*/
|
|
175
|
-
export function analyzePresentationScripts(presentation, profile
|
|
175
|
+
export function analyzePresentationScripts(presentation, profile, renderOptions) {
|
|
176
|
+
profile ??= presentationScriptProfile(presentation, renderOptions);
|
|
176
177
|
const scripts = new Set(), cjk = new Set();
|
|
177
178
|
const design = designScripts(presentation, profile, renderOptions);
|
|
178
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.
|