@openpresentation/opf-render 0.9.0 → 0.11.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 +115 -13
- package/dist/font-compatibility.js +72 -3
- package/dist/font-manifest.js +1746 -12
- package/dist/font-policy.js +2518 -0
- package/dist/fonts-browser.d.ts +65 -1
- package/dist/fonts-browser.js +229 -35
- package/dist/fonts-node.d.ts +68 -10
- package/dist/fonts-node.js +152 -16
- package/dist/fonts.d.ts +87 -5
- package/dist/fonts.js +147 -37
- package/dist/index.d.ts +31 -2
- package/dist/lazy-font-list.js +23 -0
- package/dist/lazy-fonts.js +52 -0
- package/dist/raster.js +3 -1
- package/dist/script-font-pack.js +179 -0
- package/dist/script-fonts.js +563 -0
- package/dist/svg.d.ts +29 -2
- package/dist/svg.js +446 -41
- package/fonts/bebas-neue/BebasNeue_400Regular.ttf +0 -0
- package/fonts/bebas-neue/OFL.txt +93 -0
- package/fonts/bebas-neue/PROVENANCE.json +23 -0
- package/fonts/carlito/Carlito-Bold.ttf +0 -0
- package/fonts/carlito/Carlito-BoldItalic.ttf +0 -0
- package/fonts/carlito/Carlito-Italic.ttf +0 -0
- package/fonts/carlito/Carlito-Regular.ttf +0 -0
- package/fonts/carlito/OFL.txt +94 -0
- package/fonts/intos/Intos-Bold.ttf +0 -0
- package/fonts/intos/Intos-BoldItalic.ttf +0 -0
- package/fonts/intos/Intos-Italic.ttf +0 -0
- package/fonts/intos/Intos-Regular.ttf +0 -0
- package/fonts/intos/IntosDisplay-Bold.ttf +0 -0
- package/fonts/intos/IntosDisplay-BoldItalic.ttf +0 -0
- package/fonts/intos/IntosDisplay-Italic.ttf +0 -0
- package/fonts/intos/IntosDisplay-Regular.ttf +0 -0
- package/fonts/intos/IntosNarrow-Bold.ttf +0 -0
- package/fonts/intos/IntosNarrow-BoldItalic.ttf +0 -0
- package/fonts/intos/IntosNarrow-Italic.ttf +0 -0
- package/fonts/intos/IntosNarrow-Regular.ttf +0 -0
- package/fonts/intos/IntosSerif-Bold.ttf +0 -0
- package/fonts/intos/IntosSerif-BoldItalic.ttf +0 -0
- package/fonts/intos/IntosSerif-Italic.ttf +0 -0
- package/fonts/intos/IntosSerif-Regular.ttf +0 -0
- package/fonts/intos/NOTICE.md +18 -0
- package/fonts/intos/OFL.txt +94 -0
- package/fonts/intos/PROVENANCE.json +144 -0
- package/fonts/lora/Lora-Bold.ttf +0 -0
- package/fonts/lora/Lora-BoldItalic.ttf +0 -0
- package/fonts/lora/Lora-Italic.ttf +0 -0
- package/fonts/lora/Lora-Regular.ttf +0 -0
- package/fonts/lora/OFL.txt +93 -0
- package/fonts/lora/PROVENANCE.json +48 -0
- package/fonts/merriweather-sans/MerriweatherSans-Bold.ttf +0 -0
- package/fonts/merriweather-sans/MerriweatherSans-BoldItalic.ttf +0 -0
- package/fonts/merriweather-sans/MerriweatherSans-Italic.ttf +0 -0
- package/fonts/merriweather-sans/MerriweatherSans-Regular.ttf +0 -0
- package/fonts/merriweather-sans/OFL.txt +96 -0
- package/fonts/merriweather-sans/PROVENANCE.json +48 -0
- package/fonts/montserrat/Montserrat_400Regular.ttf +0 -0
- package/fonts/montserrat/Montserrat_400Regular_Italic.ttf +0 -0
- package/fonts/montserrat/Montserrat_700Bold.ttf +0 -0
- package/fonts/montserrat/Montserrat_700Bold_Italic.ttf +0 -0
- package/fonts/montserrat/Montserrat_900Black.ttf +0 -0
- package/fonts/montserrat/OFL.txt +93 -0
- package/fonts/montserrat/PROVENANCE.json +55 -0
- package/fonts/open-sans/OFL.txt +92 -0
- package/fonts/open-sans/OpenSans_400Regular.ttf +0 -0
- package/fonts/open-sans/OpenSans_400Regular_Italic.ttf +0 -0
- package/fonts/open-sans/OpenSans_700Bold.ttf +0 -0
- package/fonts/open-sans/OpenSans_700Bold_Italic.ttf +0 -0
- package/fonts/open-sans/PROVENANCE.json +47 -0
- package/fonts/poppins/OFL.txt +93 -0
- package/fonts/poppins/PROVENANCE.json +47 -0
- package/fonts/poppins/Poppins_400Regular.ttf +0 -0
- package/fonts/poppins/Poppins_400Regular_Italic.ttf +0 -0
- package/fonts/poppins/Poppins_700Bold.ttf +0 -0
- package/fonts/poppins/Poppins_700Bold_Italic.ttf +0 -0
- package/fonts/pt-serif/OFL.txt +93 -0
- package/fonts/pt-serif/PROVENANCE.json +50 -0
- package/fonts/pt-serif/PT_Serif-Web-Bold.ttf +0 -0
- package/fonts/pt-serif/PT_Serif-Web-BoldItalic.ttf +0 -0
- package/fonts/pt-serif/PT_Serif-Web-Italic.ttf +0 -0
- package/fonts/pt-serif/PT_Serif-Web-Regular.ttf +0 -0
- package/fonts/red-hat-display/OFL.txt +93 -0
- package/fonts/red-hat-display/PROVENANCE.json +38 -0
- package/fonts/red-hat-display/RedHatDisplay-Bold.ttf +0 -0
- package/fonts/red-hat-display/RedHatDisplay-Light.ttf +0 -0
- package/fonts/red-hat-display/RedHatDisplay-Regular.ttf +0 -0
- package/fonts/red-hat-text/OFL.txt +93 -0
- package/fonts/red-hat-text/PROVENANCE.json +30 -0
- package/fonts/red-hat-text/RedHatText-Bold.ttf +0 -0
- package/fonts/red-hat-text/RedHatText-Regular.ttf +0 -0
- package/fonts/source-sans-3/OFL.txt +93 -0
- package/fonts/source-sans-3/PROVENANCE.json +49 -0
- package/fonts/source-sans-3/SourceSans3-Bold.ttf +0 -0
- package/fonts/source-sans-3/SourceSans3-BoldIt.ttf +0 -0
- package/fonts/source-sans-3/SourceSans3-It.ttf +0 -0
- package/fonts/source-sans-3/SourceSans3-Regular.ttf +0 -0
- package/package.json +167 -6
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EmbeddedFont } from "./fonts.js";
|
|
1
|
+
import type { EmbeddedFont, ScriptFonts } from "./fonts.js";
|
|
2
2
|
import type { SlideComposition, LayoutDiagnostic, TextMeasurement } from "@openpresentation/opf/composition";
|
|
3
3
|
export declare const packageName = "@openpresentation/opf-render";
|
|
4
4
|
|
|
@@ -32,9 +32,25 @@ export declare const engineDefaults: Readonly<{
|
|
|
32
32
|
}>;
|
|
33
33
|
|
|
34
34
|
export type RenderDiagnostic = LayoutDiagnostic | {
|
|
35
|
-
|
|
35
|
+
/** A face lacked glyphs for the text; a bundled fallback face draws them in the preview. A note, not an error. */
|
|
36
|
+
code: "font-glyph-fallback";
|
|
37
|
+
path?: string;
|
|
38
|
+
message: string;
|
|
39
|
+
fontFamily: string;
|
|
40
|
+
fallbackFamily: string;
|
|
41
|
+
scripts: string[];
|
|
42
|
+
characters: string[];
|
|
43
|
+
} | {
|
|
44
|
+
code: "unsupported-pattern" | "date-needs-value" | "language-preview-unavailable" | "language-preview-unresolved" | "paragraph-direction-unavailable";
|
|
45
|
+
path: string;
|
|
46
|
+
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";
|
|
36
50
|
path: string;
|
|
37
51
|
message: string;
|
|
52
|
+
id: string;
|
|
53
|
+
fallback: string;
|
|
38
54
|
} | {
|
|
39
55
|
code: "unresolved-asset";
|
|
40
56
|
path: string;
|
|
@@ -48,12 +64,21 @@ export type RenderDiagnostic = LayoutDiagnostic | {
|
|
|
48
64
|
|
|
49
65
|
export interface RenderSvgOptions {
|
|
50
66
|
textMeasurement?: TextMeasurement;
|
|
67
|
+
/** `"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
|
+
glyphFallback?: "chain" | "none";
|
|
69
|
+
/** Unscaled reference-pixel clearance around supplied vector text outlines; default 1. */
|
|
70
|
+
textRasterPadding?: number;
|
|
51
71
|
embeddedFonts?: EmbeddedFont[];
|
|
52
72
|
strictAssets?: boolean;
|
|
53
73
|
imageResolver?: (src: string | undefined, context: { asset: unknown; path: string }) => string | null | undefined;
|
|
54
74
|
onDiagnostic?: (diagnostic: RenderDiagnostic) => void;
|
|
55
75
|
validate?: boolean;
|
|
56
76
|
slideIndex?: number;
|
|
77
|
+
/**
|
|
78
|
+
* Today's calendar date (ISO YYYY-MM-DD) for `date: true` header/footer furniture. The renderer
|
|
79
|
+
* never reads a clock; without it a current date is reported as unresolved content.
|
|
80
|
+
*/
|
|
81
|
+
date?: string;
|
|
57
82
|
trace?: boolean;
|
|
58
83
|
catalogs?: Record<string, { records?: unknown[] } | unknown[]>;
|
|
59
84
|
catalogSources?: Record<string, { records?: unknown[] } | unknown[]>;
|
|
@@ -82,6 +107,10 @@ export interface ResolvedPresentation {
|
|
|
82
107
|
|
|
83
108
|
export interface ResolvedSlide {
|
|
84
109
|
geometry: SlideComposition;
|
|
110
|
+
/** Script font plan for this slide (FF-19): slots, language tags and direction. */
|
|
111
|
+
scriptFonts: ScriptFonts;
|
|
112
|
+
/** The script-aware measurement composition used; undefined for estimated layout. */
|
|
113
|
+
textMeasurement?: TextMeasurement;
|
|
85
114
|
index: number;
|
|
86
115
|
path: string;
|
|
87
116
|
slide: unknown;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
2
|
+
import { OPFFontError } from "./fonts.js";
|
|
3
|
+
|
|
4
|
+
// FF-31: the vendored faces (the open families and Intos) are not in a registry's eager list. Browser hosts load them
|
|
5
|
+
// on demand from separate hash-pinned files; Node hosts and build scripts list them to copy the files.
|
|
6
|
+
|
|
7
|
+
/** Every vendored face: family, style, its file relative to the package root, its pinned sha256 and its package. */
|
|
8
|
+
export function lazyFontList(manifest = BUNDLED_FONT_MANIFEST) {
|
|
9
|
+
return Object.freeze(manifest.packages.filter(pkg => pkg.vendored && (pkg.pack === "open" || pkg.embed === "used")).flatMap(pkg => pkg.faces.map(face => Object.freeze({
|
|
10
|
+
package: pkg.name, family: face.family, weight: face.weight, italic: face.italic,
|
|
11
|
+
file: `${pkg.vendored}/${face.file}`, sha256: face.sha256, license: pkg.license,
|
|
12
|
+
...(pkg.renamedFrom ? { renamedFrom: pkg.renamedFrom } : {}),
|
|
13
|
+
}))));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Hash-pinned browser entries for the vendored faces, served by the host from `baseUrl` at their package-relative paths. */
|
|
17
|
+
export function lazyFontEntries(options, list = lazyFontList()) {
|
|
18
|
+
const baseUrl = options?.baseUrl;
|
|
19
|
+
if (typeof baseUrl !== "string" || !baseUrl) throw new OPFFontError("invalid-font-source", "lazyFontEntries needs a baseUrl, where the host serves the package's fonts directory.");
|
|
20
|
+
const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
21
|
+
return list.map(face => ({ url: `${base}${face.file}`, family: face.family, weight: face.weight, italic: face.italic, sha256: face.sha256, package: face.package }));
|
|
22
|
+
}
|
|
23
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { fontPolicyFor } from "./font-policy.js";
|
|
2
|
+
import { resolvePresentation } from "./svg.js";
|
|
3
|
+
export { lazyFontEntries, lazyFontList } from "./lazy-font-list.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* FF-31: the vendored faces (the open families and Intos) are not in a registry's eager list. Browser hosts load them
|
|
7
|
+
* on demand, from separate hash-pinned files, when a document's font families resolve to them. This module holds the
|
|
8
|
+
* pure parts (which faces exist, which a presentation needs); fonts-browser.js does the fetching.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const lc = value => String(value).trim().toLowerCase();
|
|
12
|
+
|
|
13
|
+
/** The font families a presentation's slides resolve (heading, body and code roles), or an empty set for a document that does not resolve. */
|
|
14
|
+
export function presentationFamilies(presentation) {
|
|
15
|
+
const families = new Set();
|
|
16
|
+
let resolved;
|
|
17
|
+
try { resolved = resolvePresentation(presentation, {}); } catch { return families; }
|
|
18
|
+
for (const slide of resolved.slides) for (const family of Object.values(slide.design?.fonts ?? {})) if (typeof family === "string" && family) families.add(family);
|
|
19
|
+
return families;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The vendored faces a document needs that the registry does not hold, following the registry's own order and substitution
|
|
24
|
+
* policy: the family itself, an alias target, then (unless the policy is "none") the declared replacement and, under
|
|
25
|
+
* "visual", its alternates. The first candidate the registry already has ends the search (it will resolve to that); the
|
|
26
|
+
* first vendored candidate is needed, whole family, so the browser resolves exactly what Node resolves with everything
|
|
27
|
+
* loaded, and nothing is downloaded for a family the policy would not resolve.
|
|
28
|
+
* @param {Iterable<string>} families families the document resolves
|
|
29
|
+
* @param {{lazy: readonly object[], hasFamily: (family: string) => boolean, loaded?: ReadonlySet<string>, policy?: "none"|"metric"|"visual", aliases?: ReadonlyMap<string, string>}} context
|
|
30
|
+
*/
|
|
31
|
+
export function lazyFontsFor(families, { lazy, hasFamily, loaded = new Set(), policy = "visual", aliases = new Map() }) {
|
|
32
|
+
const byFamily = new Map();
|
|
33
|
+
for (const face of lazy) { const list = byFamily.get(lc(face.family)) ?? []; list.push(face); byFamily.set(lc(face.family), list); }
|
|
34
|
+
const needed = new Set();
|
|
35
|
+
for (const family of families) {
|
|
36
|
+
if (hasFamily(family)) continue;
|
|
37
|
+
if (byFamily.has(lc(family))) { needed.add(lc(family)); continue; }
|
|
38
|
+
const target = aliases.get(lc(family));
|
|
39
|
+
if (target && (hasFamily(target) || byFamily.has(lc(target)))) { if (!hasFamily(target)) needed.add(lc(target)); continue; }
|
|
40
|
+
if (policy === "none") continue;
|
|
41
|
+
const row = fontPolicyFor(family), replacement = row?.replacement;
|
|
42
|
+
const route = [];
|
|
43
|
+
// Metric mode uses a metric replacement, or a visual one its policy row keeps as a metric-mode fallback; alternates are visual only.
|
|
44
|
+
if (replacement && (policy === "visual" || replacement.compatibility === "metric" || replacement.metricModeFallback)) route.push(replacement.family);
|
|
45
|
+
if (policy === "visual") route.push(...(row?.alternates ?? []));
|
|
46
|
+
for (const candidate of route) {
|
|
47
|
+
if (hasFamily(candidate)) break;
|
|
48
|
+
if (byFamily.has(lc(candidate))) { needed.add(lc(candidate)); break; }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return [...needed].flatMap(family => byFamily.get(family)).filter(face => !loaded.has(face.file));
|
|
52
|
+
}
|
package/dist/raster.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {prepareRasterImages} from './raster-images.js';
|
|
2
2
|
import {OPFRenderError,packageName} from './svg.js';
|
|
3
|
+
import {separateLigatures} from './font-compatibility.js';
|
|
3
4
|
const DEFAULT_DIMENSIONS = { width: 1280, height: 720 };
|
|
4
5
|
const DEFAULT_RASTER_SCALE = 1;
|
|
5
6
|
const DEFAULT_RASTER_BACKGROUND = "#FFFFFF";
|
|
@@ -40,7 +41,8 @@ export async function svgToPdf(svgs, options = {}) {
|
|
|
40
41
|
|
|
41
42
|
async function rasterizeSvg(svgInput, options) {
|
|
42
43
|
const { Resvg } = await loadResvg();
|
|
43
|
-
|
|
44
|
+
// FF-31: resvg ignores the SVG's ligature properties, so separate the letters a Gelasio ligature would join.
|
|
45
|
+
const svg = separateLigatures(await prepareRasterImages(normalizeSvgInput(svgInput)));
|
|
44
46
|
const scale = positiveNumber(options.scale, "scale", DEFAULT_RASTER_SCALE);
|
|
45
47
|
const fontFiles = [
|
|
46
48
|
...(options.useBundledFonts === false ? [] : await bundledFontFiles()),
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Optional script font pack (FF-19): pinned, hash-verified OFL Noto faces that
|
|
2
|
+
// stand in for proprietary script fonts in previews. The packages are optional
|
|
3
|
+
// peers, so the renderer install stays small; hosts install only what they use.
|
|
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";
|
|
10
|
+
|
|
11
|
+
const SCRIPT_ALIASES = Object.freeze({Hira: "Jpan", Kana: "Jpan", Hrkt: "Jpan", Hang: "Kore", Hani: "Hans", Zyyy: "Latn"});
|
|
12
|
+
|
|
13
|
+
/** Script-pack manifest packages serving `scripts` ("all" or ISO 15924 codes such as `Jpan`, `Arab`). */
|
|
14
|
+
export function scriptFontPackages(scripts) {
|
|
15
|
+
const available = BUNDLED_FONT_MANIFEST.packages.filter(item => item.pack === "scripts");
|
|
16
|
+
if (scripts === "all") return available;
|
|
17
|
+
if (!Array.isArray(scripts)) throw new OPFFontError("invalid-font-scripts", "scripts must be 'all' or an array of ISO 15924 script codes.", {scripts});
|
|
18
|
+
const wanted = new Set();
|
|
19
|
+
for (const value of scripts) {
|
|
20
|
+
const code = typeof value === "string" && /^[A-Za-z]{4}$/.test(value) ? value[0].toUpperCase() + value.slice(1).toLowerCase() : undefined;
|
|
21
|
+
const script = SCRIPT_ALIASES[code] ?? code;
|
|
22
|
+
if (!script || !available.some(item => item.scripts.includes(script))) {
|
|
23
|
+
throw new OPFFontError("font-script-unavailable", `No pinned open font serves script '${value}'.`, {script: value, available: [...new Set(available.flatMap(item => item.scripts))].sort()});
|
|
24
|
+
}
|
|
25
|
+
wanted.add(script);
|
|
26
|
+
}
|
|
27
|
+
return available.filter(item => item.scripts.some(script => wanted.has(script)));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Browser entries for the script pack. `baseUrl` is where the host serves the
|
|
32
|
+
* installed `@expo-google-fonts/*` packages (for example a copy of
|
|
33
|
+
* `node_modules/@expo-google-fonts`). Each entry carries the reviewed SHA-256,
|
|
34
|
+
* which `loadBrowserFontRegistry` verifies before use.
|
|
35
|
+
*/
|
|
36
|
+
export function scriptFontEntries(scripts, {baseUrl}) {
|
|
37
|
+
return scriptPackageEntries(scriptFontPackages(scripts), {baseUrl});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Browser entries for whole manifest packages (see `scriptFontEntries`). */
|
|
41
|
+
export function scriptPackageEntries(packages, {baseUrl}) {
|
|
42
|
+
if (typeof baseUrl !== "string" || !baseUrl) throw new OPFFontError("invalid-font-source", "scriptFontEntries requires the baseUrl that serves the installed font packages.");
|
|
43
|
+
const root = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
44
|
+
return packages.flatMap(pkg => pkg.faces.map(face => ({
|
|
45
|
+
url: `${root}${pkg.name.split("/").pop()}/${face.file}`,
|
|
46
|
+
family: face.family, weight: face.weight, italic: face.italic, sha256: face.sha256,
|
|
47
|
+
scripts: [...pkg.scripts], package: `${pkg.name}@${pkg.version}`, license: pkg.license,
|
|
48
|
+
// A script face is embedded in a standalone SVG only when the slide's text uses its family.
|
|
49
|
+
embed: "used",
|
|
50
|
+
})));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Fields a preview never draws (checked against what the renderer and core composition draw).
|
|
54
|
+
// Everywhere: the schema URL, embedded assets, catalog records, speaker notes, ids, image alt text and sources,
|
|
55
|
+
// links, metadata and typed `extensions` passthrough. On the presentation: name (aria-label fallback), description,
|
|
56
|
+
// filename, author, speaker, audience, purpose, tone, takeaway, duration, tags, narrative, language and version.
|
|
57
|
+
// On a slide: beat. The organization (name) and a slide's section are drawn only as generated header/footer
|
|
58
|
+
// furniture (`organization: true` / `section: true` in a design), so they count only when a design asks for
|
|
59
|
+
// them. Everything else is drawn text: titles, text blocks, list items, runs, table cells, chart labels,
|
|
60
|
+
// categories and series names, code, quotes, metrics, timelines, furniture text.
|
|
61
|
+
const UNDRAWN_KEYS = Object.freeze(["$schema", "assets", "catalogs", "notes", "id", "alt", "altText", "src", "url", "href", "link", "metadata", "extensions"]);
|
|
62
|
+
const UNDRAWN_ROOT_KEYS = Object.freeze(["name", "description", "filename", "author", "authors", "creator", "speaker", "audience", "purpose", "tone", "takeaway", "duration", "tags", "narrative", "language", "license", "version", "keywords"]);
|
|
63
|
+
const UNDRAWN_SLIDE_KEYS = Object.freeze(["beat"]);
|
|
64
|
+
const UNDRAWN_VALUE = /^(?:https?|data|blob|pkg|file|mailto):\S*$/i;
|
|
65
|
+
|
|
66
|
+
/** True when a design in the presentation turns on generated `field` furniture (`organization` or `section`). */
|
|
67
|
+
function furnitureUses(presentation, field) {
|
|
68
|
+
const designs = [presentation?.design, ...(Array.isArray(presentation?.slides) ? presentation.slides.map(slide => slide?.design) : [])];
|
|
69
|
+
const pattern = new RegExp(`"${field}"\\s*:\\s*true`);
|
|
70
|
+
return designs.some(design => design && pattern.test(JSON.stringify(design)));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The strings a preview of this presentation draws. */
|
|
74
|
+
export function* drawnStrings(presentation) {
|
|
75
|
+
const undrawn = new Set(UNDRAWN_KEYS), undrawnRoot = new Set(UNDRAWN_ROOT_KEYS), undrawnSlide = new Set(UNDRAWN_SLIDE_KEYS);
|
|
76
|
+
if (!furnitureUses(presentation, "organization")) undrawnRoot.add("organization");
|
|
77
|
+
if (!furnitureUses(presentation, "section")) undrawnSlide.add("section");
|
|
78
|
+
function* visit(item, level) {
|
|
79
|
+
if (typeof item === "string") { if (!UNDRAWN_VALUE.test(item)) yield item; }
|
|
80
|
+
else if (Array.isArray(item)) for (const child of item) yield* visit(child, level === "slides" ? "slide" : level === "root" ? "deep" : level);
|
|
81
|
+
else if (item && typeof item === "object") {
|
|
82
|
+
for (const [key, child] of Object.entries(item)) {
|
|
83
|
+
if (undrawn.has(key) || (level === "root" && undrawnRoot.has(key)) || (level === "slide" && undrawnSlide.has(key))) continue;
|
|
84
|
+
yield* visit(child, level === "root" && key === "slides" ? "slides" : "deep");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
yield* visit(presentation, "root");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The script profile of a presentation: core `resolveScriptFonts` output, exactly what the renderer plans
|
|
93
|
+
* with. When the installed core has none (published core 0.11.0 and earlier) or it throws, the renderer
|
|
94
|
+
* ignores the document language, so the profile is empty and Han text is Simplified Chinese, as drawn.
|
|
95
|
+
*/
|
|
96
|
+
export function presentationScriptProfile(presentation) {
|
|
97
|
+
if (typeof opfCore.resolveScriptFonts === "function") {
|
|
98
|
+
try { return opfCore.resolveScriptFonts(presentation); } catch { /* the renderer falls back the same way */ }
|
|
99
|
+
}
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const servedScripts = () => new Set(BUNDLED_FONT_MANIFEST.packages.filter(item => item.pack === "scripts").flatMap(item => item.scripts));
|
|
104
|
+
const cjkKeys = ["Jpan", "Hans", "Hant", "Kore"];
|
|
105
|
+
const cjkCharacters = new Set(["Hani", "Hira", "Kana", "Hang", "Bopo"]);
|
|
106
|
+
/** True when the renderer has per-character glyph fallback (FF-19 glyphFallbackFamilies). */
|
|
107
|
+
const hasGlyphFallback = () => typeof scriptFontModule.glyphFallbackFamilies === "function";
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* What a preview of the presentation draws: its script profile, the scripts of its text (the renderer's own
|
|
111
|
+
* itemization, so language-dependent punctuation counts), and the CJK characters that a glyph fallback might
|
|
112
|
+
* have to draw with another CJK face.
|
|
113
|
+
*/
|
|
114
|
+
export function analyzePresentationScripts(presentation, profile = presentationScriptProfile(presentation)) {
|
|
115
|
+
const scripts = new Set(), cjk = new Set();
|
|
116
|
+
for (const text of drawnStrings(presentation)) {
|
|
117
|
+
for (const script of scriptsOfText(text, profile)) scripts.add(script);
|
|
118
|
+
for (const character of text) if (character.codePointAt(0) > 0x2E7F && cjkCharacters.has(scriptOfCharacter(character))) cjk.add(character);
|
|
119
|
+
}
|
|
120
|
+
return { profile, detected: [...scripts].sort(), cjk };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Script keys (ISO 15924: `Jpan`, `Hans`, `Arab`, ...) whose faces a preview of this presentation needs.
|
|
125
|
+
* Text decides, itemized exactly as drawing does: the document language only tells Han text apart
|
|
126
|
+
* (Japanese, Korean, Simplified or Traditional Chinese) and makes curly quotes, dashes and the ellipsis
|
|
127
|
+
* East Asian. Text the preview never draws (ids, alt text, sources, assets, catalogs, notes) is ignored.
|
|
128
|
+
* Greek and Cyrillic add nothing: the office registry always carries Noto Sans (the glyph-fallback face) and Roboto covers them.
|
|
129
|
+
* Pass `profile` to reuse a resolved profile.
|
|
130
|
+
*/
|
|
131
|
+
export function detectPresentationScripts(presentation, { profile } = {}) {
|
|
132
|
+
return analyzePresentationScripts(presentation, profile).detected;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The scripts of an analysis that a pinned pack serves (`scripts`, the input for `scriptFontPackages`) and those it does not. */
|
|
136
|
+
export function scriptSelectionOf(analysis) {
|
|
137
|
+
const served = servedScripts();
|
|
138
|
+
return { detected: analysis.detected, scripts: analysis.detected.filter(script => served.has(script)), unavailable: analysis.detected.filter(script => !served.has(script)) };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* What `scripts: 'auto'` loads for a presentation: the `detected` scripts, those a pinned pack serves
|
|
143
|
+
* (`scripts`, the input for `scriptFontPackages`) and those it does not (`unavailable`, for example
|
|
144
|
+
* Cherokee or Tifinagh, which draw with whatever the design font covers).
|
|
145
|
+
*/
|
|
146
|
+
export function autoScriptSelection(presentation, options) {
|
|
147
|
+
return scriptSelectionOf(analyzePresentationScripts(presentation, options?.profile));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The next CJK package a glyph fallback needs (FF-19): a drawn Han, kana or Hangul character that no loaded
|
|
152
|
+
* script face has (Japanese-only kanji in a Simplified Chinese run, Simplified-only hanzi beside kana, hanja)
|
|
153
|
+
* takes the first unloaded CJK face along the renderer's fallback chain. `covers(character)` says whether a
|
|
154
|
+
* loaded script face has the glyph; `loaded` holds the loaded package names. Undefined when nothing is needed,
|
|
155
|
+
* the renderer has no glyph fallback, or the cap is reached: a character no CJK face covers must not pull in
|
|
156
|
+
* every CJK package (10 to 20 MiB each), so at most ONE fallback package is loaded beyond the packages the
|
|
157
|
+
* text's own scripts need. Characters still uncovered then are reported (`uncoveredCjkCharacters`) and the
|
|
158
|
+
* renderer reports `missing-glyph` for them.
|
|
159
|
+
*/
|
|
160
|
+
export function nextFallbackPackage(analysis, { covers, loaded }) {
|
|
161
|
+
if (!hasGlyphFallback()) return undefined;
|
|
162
|
+
const packages = BUNDLED_FONT_MANIFEST.packages.filter(item => item.pack === "scripts" && item.scripts.some(script => cjkKeys.includes(script)));
|
|
163
|
+
const primary = analysis.detected.filter(script => cjkKeys.includes(script)).length;
|
|
164
|
+
if (packages.filter(item => loaded.has(item.name)).length >= primary + 1) return undefined;
|
|
165
|
+
for (const character of analysis.cjk) {
|
|
166
|
+
if (covers(character)) continue;
|
|
167
|
+
for (const family of scriptFontModule.glyphFallbackFamilies(character, analysis.profile, analysis.profile?.serif === true)) {
|
|
168
|
+
const pkg = packages.find(item => !loaded.has(item.name) && item.faces.some(face => face.family === family));
|
|
169
|
+
if (pkg) return pkg;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Drawn Han, kana or Hangul characters (at most `limit`) that no loaded script face covers. Empty without glyph fallback (nothing else would draw them). */
|
|
176
|
+
export function uncoveredCjkCharacters(analysis, { covers, limit = 16 }) {
|
|
177
|
+
if (!hasGlyphFallback()) return [];
|
|
178
|
+
return [...analysis.cjk].filter(character => !covers(character)).slice(0, limit);
|
|
179
|
+
}
|