@openpresentation/opf-render 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -7
- package/dist/font-compatibility.js +40 -1
- package/dist/font-manifest.js +1285 -12
- package/dist/font-policy.js +91 -36
- package/dist/fonts-browser.d.ts +56 -2
- package/dist/fonts-browser.js +194 -22
- package/dist/fonts-node.d.ts +53 -9
- package/dist/fonts-node.js +121 -23
- package/dist/fonts.d.ts +20 -5
- package/dist/fonts.js +63 -27
- package/dist/index.d.ts +11 -0
- 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 +140 -1
- package/dist/script-fonts.js +161 -24
- package/dist/svg.d.ts +11 -0
- package/dist/svg.js +97 -5
- package/fonts/anton/Anton-Regular.ttf +0 -0
- package/fonts/anton/OFL.txt +93 -0
- package/fonts/anton/PROVENANCE.json +21 -0
- package/fonts/archivo-narrow/ArchivoNarrow_400Regular.ttf +0 -0
- package/fonts/archivo-narrow/ArchivoNarrow_400Regular_Italic.ttf +0 -0
- package/fonts/archivo-narrow/ArchivoNarrow_700Bold.ttf +0 -0
- package/fonts/archivo-narrow/ArchivoNarrow_700Bold_Italic.ttf +0 -0
- package/fonts/archivo-narrow/OFL.txt +93 -0
- package/fonts/archivo-narrow/PROVENANCE.json +47 -0
- package/fonts/barlow/Barlow-Bold.ttf +0 -0
- package/fonts/barlow/Barlow-BoldItalic.ttf +0 -0
- package/fonts/barlow/Barlow-Italic.ttf +0 -0
- package/fonts/barlow/Barlow-Regular.ttf +0 -0
- package/fonts/barlow/OFL.txt +93 -0
- package/fonts/barlow/PROVENANCE.json +45 -0
- 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/bitter/Bitter_400Regular.ttf +0 -0
- package/fonts/bitter/Bitter_400Regular_Italic.ttf +0 -0
- package/fonts/bitter/Bitter_700Bold.ttf +0 -0
- package/fonts/bitter/Bitter_700Bold_Italic.ttf +0 -0
- package/fonts/bitter/OFL.txt +94 -0
- package/fonts/bitter/PROVENANCE.json +49 -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/eb-garamond/EBGaramond_400Regular.ttf +0 -0
- package/fonts/eb-garamond/EBGaramond_400Regular_Italic.ttf +0 -0
- package/fonts/eb-garamond/EBGaramond_700Bold.ttf +0 -0
- package/fonts/eb-garamond/EBGaramond_700Bold_Italic.ttf +0 -0
- package/fonts/eb-garamond/OFL.txt +93 -0
- package/fonts/eb-garamond/PROVENANCE.json +47 -0
- package/fonts/figtree/Figtree_400Regular.ttf +0 -0
- package/fonts/figtree/Figtree_400Regular_Italic.ttf +0 -0
- package/fonts/figtree/Figtree_700Bold.ttf +0 -0
- package/fonts/figtree/Figtree_700Bold_Italic.ttf +0 -0
- package/fonts/figtree/OFL.txt +93 -0
- package/fonts/figtree/PROVENANCE.json +47 -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/libre-caslon-text/LibreCaslonText_400Regular.ttf +0 -0
- package/fonts/libre-caslon-text/LibreCaslonText_400Regular_Italic.ttf +0 -0
- package/fonts/libre-caslon-text/LibreCaslonText_700Bold.ttf +0 -0
- package/fonts/libre-caslon-text/OFL.txt +93 -0
- package/fonts/libre-caslon-text/PROVENANCE.json +39 -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 +79 -0
- package/fonts/red-hat-display/RedHatDisplay_300Light.ttf +0 -0
- package/fonts/red-hat-display/RedHatDisplay_300Light_Italic.ttf +0 -0
- package/fonts/red-hat-display/RedHatDisplay_400Regular.ttf +0 -0
- package/fonts/red-hat-display/RedHatDisplay_400Regular_Italic.ttf +0 -0
- package/fonts/red-hat-display/RedHatDisplay_600SemiBold.ttf +0 -0
- package/fonts/red-hat-display/RedHatDisplay_600SemiBold_Italic.ttf +0 -0
- package/fonts/red-hat-display/RedHatDisplay_700Bold.ttf +0 -0
- package/fonts/red-hat-display/RedHatDisplay_700Bold_Italic.ttf +0 -0
- package/fonts/red-hat-text/OFL.txt +93 -0
- package/fonts/red-hat-text/PROVENANCE.json +47 -0
- package/fonts/red-hat-text/RedHatText_400Regular.ttf +0 -0
- package/fonts/red-hat-text/RedHatText_400Regular_Italic.ttf +0 -0
- package/fonts/red-hat-text/RedHatText_700Bold.ttf +0 -0
- package/fonts/red-hat-text/RedHatText_700Bold_Italic.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/fonts/work-sans/OFL.txt +93 -0
- package/fonts/work-sans/PROVENANCE.json +47 -0
- package/fonts/work-sans/WorkSans_400Regular.ttf +0 -0
- package/fonts/work-sans/WorkSans_400Regular_Italic.ttf +0 -0
- package/fonts/work-sans/WorkSans_700Bold.ttf +0 -0
- package/fonts/work-sans/WorkSans_700Bold_Italic.ttf +0 -0
- package/package.json +11 -10
package/dist/fonts-node.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { createHash } from "node:crypto";
|
|
5
6
|
import { createFontRegistry, OPFFontError } from "./fonts.js";
|
|
6
7
|
import { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
8
|
+
import { lazyFontList } from "./lazy-font-list.js";
|
|
7
9
|
export { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
8
|
-
import { scriptFontPackages } from "./script-font-pack.js";
|
|
9
|
-
export { scriptFontPackages } from "./script-font-pack.js";
|
|
10
|
+
import { analyzePresentationScripts, nextFallbackPackage, scriptFontPackages, scriptSelectionOf, uncoveredCjkCharacters } from "./script-font-pack.js";
|
|
11
|
+
export { autoScriptSelection, detectPresentationScripts, scriptFontPackages } from "./script-font-pack.js";
|
|
12
|
+
// Script faces are embedded in a standalone SVG only when the slide's text uses their family.
|
|
13
|
+
const embedUsed = entries => entries.map(entry => ({...entry, embed: "used"}));
|
|
10
14
|
const require = createRequire(import.meta.url);
|
|
11
15
|
|
|
12
16
|
async function verifiedFile(file, expected, details) {
|
|
@@ -18,14 +22,37 @@ async function verifiedFile(file, expected, details) {
|
|
|
18
22
|
return bytes;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
// The default Latin, Cyrillic and Greek glyph-fallback face (the office pack always loads it, fallback-only).
|
|
26
|
+
const FALLBACK_PACKAGE = "@expo-google-fonts/noto-sans";
|
|
27
|
+
|
|
28
|
+
// `skipped`: an optional npm package that is not installed is recorded there instead of failing (scripts: 'auto').
|
|
29
|
+
// `fallbackOnly`: the faces serve glyph fallback and requests by their own name, never a replacement for another family.
|
|
30
|
+
async function loadPackages(packages, skipped, {fallbackOnly = false} = {}) {
|
|
22
31
|
const entries = [], fontFiles = [];
|
|
23
32
|
for (const pkg of packages) {
|
|
33
|
+
if (pkg.vendored) {
|
|
34
|
+
// FF-31: vendored faces ship inside this package (pkg.vendored, for example fonts/carlito), hash-pinned like the npm packs. The open pack is
|
|
35
|
+
// embedded in an SVG only when the slide's text names the family (embed "used"); raster output reads the files.
|
|
36
|
+
const directory = fileURLToPath(new URL(`../${pkg.vendored}/`, import.meta.url));
|
|
37
|
+
let license = (await verifiedFile(path.join(directory, pkg.licenseFile), pkg.licenseSha256, {package:pkg.name, file:pkg.licenseFile})).toString("utf8");
|
|
38
|
+
// A notice file carries provenance and upstream copyright lines that the upstream license file omits.
|
|
39
|
+
if (pkg.noticeFile) license += "\n\n" + (await verifiedFile(path.join(directory, pkg.noticeFile), pkg.noticeSha256, {package:pkg.name, file:pkg.noticeFile})).toString("utf8");
|
|
40
|
+
// One rule for the vendored faces the SVG embeds only when its text names them: the open pack and any package flagged embed "used" (Intos).
|
|
41
|
+
const lazy = pkg.pack === "open" || pkg.embed === "used";
|
|
42
|
+
for (const face of pkg.faces) {
|
|
43
|
+
const file = path.join(directory, face.file);
|
|
44
|
+
const data = await verifiedFile(file, face.sha256, {package:pkg.name, file:face.file});
|
|
45
|
+
fontFiles.push(file);
|
|
46
|
+
entries.push({data:new Uint8Array(data), family:face.family, weight:face.weight, italic:face.italic, license, ...(lazy ? {embed:"used"} : {})});
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
24
50
|
let manifestPath, installed;
|
|
25
51
|
try {
|
|
26
52
|
manifestPath = require.resolve(`${pkg.name}/package.json`);
|
|
27
53
|
installed = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
28
54
|
} catch (error) {
|
|
55
|
+
if (skipped) { skipped.push({package: pkg.name, version: pkg.version, scripts: [...pkg.scripts]}); continue; }
|
|
29
56
|
throw new OPFFontError("font-resource-unavailable", `Install ${pkg.name}@${pkg.version} to use this offline font pack.`, {package:pkg.name, cause:error.code});
|
|
30
57
|
}
|
|
31
58
|
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});
|
|
@@ -35,18 +62,69 @@ async function loadPackages(packages) {
|
|
|
35
62
|
const file = path.join(directory, face.file);
|
|
36
63
|
const data = await verifiedFile(file, face.sha256, {package:pkg.name, file:face.file});
|
|
37
64
|
fontFiles.push(file);
|
|
38
|
-
entries.push({data:new Uint8Array(data), family:face.family, weight:face.weight, italic:face.italic, license, ...(pkg.scripts ? {scripts:[...pkg.scripts]} : {})});
|
|
65
|
+
entries.push({data:new Uint8Array(data), family:face.family, weight:face.weight, italic:face.italic, license, ...(pkg.scripts ? {scripts:[...pkg.scripts]} : {}), ...(fallbackOnly ? {fallbackOnly:true, embed:"used"} : {})});
|
|
39
66
|
}
|
|
40
67
|
}
|
|
41
|
-
return {entries, fontFiles};
|
|
68
|
+
return {entries, fontFiles, packages:packages.map(pkg => pkg.name)};
|
|
42
69
|
}
|
|
43
70
|
|
|
71
|
+
// A package that is the renamed successor of a family (Source Sans 3, formerly Source Sans Pro) answers to the
|
|
72
|
+
// old name through a built-in alias, reported visual like other aliases; it is not a claim of the old face.
|
|
73
|
+
const renamedAliases = () => Object.fromEntries(BUNDLED_FONT_MANIFEST.packages.filter(item => item.pack === "open" && item.renamedFrom).map(item => [item.renamedFrom, item.faces[0].family]));
|
|
74
|
+
// The vendored faces this registry holds that are embed "used": a host that serves fonts itself lists them to copy their files.
|
|
75
|
+
const lazyOf = registry => lazyFontList().filter(face => registry.describeFaces().some(held => held.family === face.family && held.weight === face.weight && held.italic === face.italic));
|
|
44
76
|
const loadPack = pack => loadPackages(BUNDLED_FONT_MANIFEST.packages.filter(item => item.pack === pack));
|
|
45
77
|
|
|
46
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Adds the script pack. `scripts` is 'all', ISO 15924 codes, or 'auto' (FF-19): detect the scripts the
|
|
80
|
+
* `presentation` draws and load only their faces. Auto never fails on a missing optional package or a
|
|
81
|
+
* script no pinned font serves; it reports them through `onDiagnostic` and `selection`.
|
|
82
|
+
*/
|
|
83
|
+
async function withScripts(loaded, scripts, {presentation, onDiagnostic} = {}) {
|
|
47
84
|
if (scripts === undefined || (Array.isArray(scripts) && !scripts.length)) return loaded;
|
|
48
|
-
|
|
49
|
-
|
|
85
|
+
// A package the registry already loaded (the default Noto Sans fallback) is never loaded twice.
|
|
86
|
+
const notLoaded = list => list.filter(pkg => !loaded.packages?.includes(pkg.name));
|
|
87
|
+
if (scripts !== "auto") {
|
|
88
|
+
const extra = await loadPackages(notLoaded(scriptFontPackages(scripts)));
|
|
89
|
+
return {...loaded, entries:[...loaded.entries, ...embedUsed(extra.entries)], fontFiles:[...loaded.fontFiles, ...extra.fontFiles], packages:[...(loaded.packages ?? []), ...extra.packages]};
|
|
90
|
+
}
|
|
91
|
+
if (presentation === null || typeof presentation !== "object") throw new OPFFontError("invalid-font-scripts", "scripts: 'auto' needs the presentation whose text decides the scripts.", {scripts});
|
|
92
|
+
const selection = scriptSelectionOf(analyzePresentationScripts(presentation)), skipped = [];
|
|
93
|
+
const extra = await loadPackages(notLoaded(scriptFontPackages(selection.scripts)), skipped);
|
|
94
|
+
for (const script of selection.unavailable) onDiagnostic?.({code: "script-font-unavailable", script, message: `No pinned open font serves script '${script}'; that text uses the design font.`});
|
|
95
|
+
for (const item of skipped) onDiagnostic?.({code: "script-font-not-installed", package: item.package, scripts: item.scripts, message: `Install ${item.package}@${item.version} to preview ${item.scripts.join(", ")} text with its designated open font.`});
|
|
96
|
+
return {entries:[...loaded.entries, ...embedUsed(extra.entries)], fontFiles:[...loaded.fontFiles, ...extra.fontFiles], packages:[...(loaded.packages ?? []), ...extra.packages],
|
|
97
|
+
selection:{...selection, packages: scriptFontPackages(selection.scripts).map(item => item.name).filter(name => !skipped.some(item => item.package === name)), notInstalled: skipped.map(item => item.package)}};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* scripts: 'auto' with glyph fallback (FF-19): a drawn Han, kana or Hangul character that no loaded script face
|
|
102
|
+
* has (Japanese-only kanji in a Simplified Chinese run, hanja) needs the next CJK face along the fallback chain.
|
|
103
|
+
* Adds those packages to the registry and its fontFiles until every such character is covered or the chain ends.
|
|
104
|
+
*/
|
|
105
|
+
async function completeFallback(registry, presentation, loaded, onDiagnostic) {
|
|
106
|
+
const selection = loaded.selection;
|
|
107
|
+
if (!selection) return;
|
|
108
|
+
const seen = new Set([...selection.packages, ...selection.notInstalled]);
|
|
109
|
+
for (;;) {
|
|
110
|
+
const analysis = analyzePresentationScripts(presentation), covers = character => registry.scriptFacesCover(character);
|
|
111
|
+
const next = nextFallbackPackage(analysis, {covers, loaded: seen});
|
|
112
|
+
if (!next) {
|
|
113
|
+
const uncovered = uncoveredCjkCharacters(analysis, {covers, limit: 8});
|
|
114
|
+
if (uncovered.length) { selection.uncovered = uncovered; onDiagnostic?.({code: "script-glyph-uncovered", characters: uncovered, message: `No loaded script face has ${uncovered.map(character => "U+" + character.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")).join(", ")}; the preview reports missing-glyph for them.`}); }
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
seen.add(next.name);
|
|
118
|
+
const skipped = [], extra = await loadPackages([next], skipped);
|
|
119
|
+
if (skipped.length) {
|
|
120
|
+
selection.notInstalled.push(next.name);
|
|
121
|
+
onDiagnostic?.({code: "script-font-not-installed", package: next.name, scripts: [...next.scripts], message: `Install ${next.name}@${next.version} for glyphs the loaded CJK faces lack.`});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
registry.addFaces(embedUsed(extra.entries));
|
|
125
|
+
loaded.fontFiles.push(...extra.fontFiles);
|
|
126
|
+
selection.packages.push(next.name);
|
|
127
|
+
}
|
|
50
128
|
}
|
|
51
129
|
|
|
52
130
|
/** FF-31: caller-supplied faces, for example the caller's own licensed Aptos files. Plain
|
|
@@ -70,25 +148,44 @@ async function callerFaces(faces = []) {
|
|
|
70
148
|
async function withFaces(loaded, faces) {
|
|
71
149
|
if (!faces?.length) return loaded;
|
|
72
150
|
const own = await callerFaces(faces);
|
|
73
|
-
return {entries:[...own.entries, ...loaded.entries], fontFiles:[...own.fontFiles, ...loaded.fontFiles]};
|
|
151
|
+
return {...loaded, entries:[...own.entries, ...loaded.entries], fontFiles:[...own.fontFiles, ...loaded.fontFiles]};
|
|
74
152
|
}
|
|
75
153
|
|
|
76
154
|
/** Bundled, openly licensed faces. No system font discovery or network requests. */
|
|
77
|
-
export async function loadBundledFontRegistry({scripts, faces, ...options} = {}) {
|
|
78
|
-
const {entries, fontFiles} = await withFaces(await withScripts(await loadPack("base"), scripts), faces);
|
|
79
|
-
|
|
155
|
+
export async function loadBundledFontRegistry({scripts, faces, presentation, onDiagnostic, ...options} = {}) {
|
|
156
|
+
const {entries, fontFiles, selection} = await withFaces(await withScripts(await loadPack("base"), scripts, {presentation, onDiagnostic}), faces);
|
|
157
|
+
const registry = Object.assign(createFontRegistry(entries,options),{fontFiles, ...(selection ? {scriptSelection: selection} : {})});
|
|
158
|
+
registry.lazyFonts = lazyOf(registry);
|
|
159
|
+
await completeFallback(registry, presentation, {fontFiles, selection}, onDiagnostic);
|
|
160
|
+
return registry;
|
|
80
161
|
}
|
|
81
162
|
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
163
|
+
/** Office substitutes plus the open families that font schemes select (FF-31),
|
|
164
|
+
* optionally alongside the base Roboto pack. `includeOpenFonts: false` leaves the open families out. */
|
|
165
|
+
export async function loadOfficeFontRegistry({scripts, faces, presentation, onDiagnostic, ...options} = {}) {
|
|
166
|
+
const {entries, fontFiles, packages} = await loadPack("office");
|
|
167
|
+
for (const [include, pack] of [[options.includeOpenFonts, "open"], [options.includeBaseFonts, "base"]]) {
|
|
168
|
+
if (include === false) continue;
|
|
169
|
+
const extra = await loadPack(pack);
|
|
170
|
+
fontFiles.push(...extra.fontFiles);
|
|
171
|
+
entries.push(...extra.entries);
|
|
172
|
+
packages.push(...extra.packages);
|
|
89
173
|
}
|
|
90
|
-
|
|
91
|
-
|
|
174
|
+
// Noto Sans (regular, bold, italic, bold italic) is the default Latin, Cyrillic and Greek fallback face
|
|
175
|
+
// (glyph fallback), so Georgia with Russian text previews without the scripts option. It is embedded in an
|
|
176
|
+
// SVG only when the slide's text draws it (embed "used"); raster output reads it from fontFiles. Unless the
|
|
177
|
+
// caller asked for it with scripts it is fallback-only, so no other family's preview changes.
|
|
178
|
+
const requested = scripts === undefined || scripts === "auto" || (Array.isArray(scripts) && !scripts.length) ? [] : scriptFontPackages(scripts).map(pkg => pkg.name);
|
|
179
|
+
const fallback = await loadPackages(scriptFontPackages(["Latn"]).filter(pkg => !requested.includes(pkg.name)), undefined, {fallbackOnly:true});
|
|
180
|
+
fontFiles.push(...fallback.fontFiles);
|
|
181
|
+
entries.push(...fallback.entries);
|
|
182
|
+
packages.push(...fallback.packages);
|
|
183
|
+
const loaded = await withFaces(await withScripts({entries, fontFiles, packages}, scripts, {presentation, onDiagnostic}), faces);
|
|
184
|
+
const aliases = options.includeOpenFonts === false ? options.aliases : {...renamedAliases(), ...options.aliases};
|
|
185
|
+
const registry = Object.assign(createFontRegistry(loaded.entries,{substitutionPolicy:"metric",...options,...(aliases?{aliases}:{})}),{fontFiles:loaded.fontFiles, ...(loaded.selection ? {scriptSelection: loaded.selection} : {})});
|
|
186
|
+
registry.lazyFonts = lazyOf(registry);
|
|
187
|
+
await completeFallback(registry, presentation, loaded, onDiagnostic);
|
|
188
|
+
return registry;
|
|
92
189
|
}
|
|
93
190
|
|
|
94
191
|
/** One set of verified font inputs for layout, SVG, editor, PPTX, and Node raster export. */
|
|
@@ -96,8 +193,9 @@ export async function prepareNodeFonts({pack = "base", embedScriptFonts = false,
|
|
|
96
193
|
if (pack !== "base" && pack !== "office") throw new OPFFontError("invalid-font-pack", "Choose the base or office font pack.", {pack});
|
|
97
194
|
const registry = await (pack === "base" ? loadBundledFontRegistry(options) : loadOfficeFontRegistry(options));
|
|
98
195
|
// Script faces are large (CJK faces are 5-10 MB each). Raster output reads them
|
|
99
|
-
// from fontFiles; embed them in standalone SVG only on request.
|
|
100
|
-
|
|
196
|
+
// from fontFiles; embed them in standalone SVG only on request. The default Noto Sans fallback is always offered
|
|
197
|
+
// (embed "used"), so an SVG carries it exactly when its text draws it.
|
|
198
|
+
const embeddedFonts = registry.selectEmbeddedFonts(face => embedScriptFonts || !face.scripts || face.fallbackOnly);
|
|
101
199
|
return {
|
|
102
200
|
registry,
|
|
103
201
|
manifest:BUNDLED_FONT_MANIFEST,
|
package/dist/fonts.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TextMeasurement, TextStyle } from "@openpresentation/opf/composition";
|
|
2
|
-
export interface FontFaceInput { data: Uint8Array; family?: string; weight?: number; italic?: boolean; postscriptName?: string; license?: string; /** ISO 15924 scripts a designated script replacement face serves (FF-19). */ scripts?: string[] }
|
|
3
|
-
export interface EmbeddedFont { family: string; weight: number; italic?: boolean; dataUrl: string; license?: string }
|
|
2
|
+
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; /** "used" embeds the face in an SVG only when the slide's text names its family (the open pack, FF-31). Default "always". */ embed?: "always" | "used"; /** ISO 15924 scripts a designated script replacement face serves (FF-19). */ scripts?: string[] }
|
|
3
|
+
export interface EmbeddedFont { family: string; weight: number; italic?: boolean; dataUrl: string; license?: string; embed?: "used" }
|
|
4
4
|
export type FontCompatibility = "exact" | "metric" | "visual" | "generic";
|
|
5
5
|
export interface FontReplacementMeasurement { replacement:string; meanAbsWidthDelta:number; meanWidthDelta:number; maxAbsWidthDelta:number; styles:number; reference:string }
|
|
6
6
|
export interface FontResolution {
|
|
@@ -47,6 +47,10 @@ export interface FontRegistry {
|
|
|
47
47
|
/** Parsed face metadata in entry order, without encoding font bytes. */
|
|
48
48
|
describeFaces(): FaceDescription[];
|
|
49
49
|
readonly substitutions: FontResolution[];
|
|
50
|
+
/** True when a loaded script-pack face has a glyph for the character. */
|
|
51
|
+
scriptFacesCover(character: string): boolean;
|
|
52
|
+
/** Register more faces (FF-19 script faces once a document needs them). Atomic; returns the added faces' metadata. */
|
|
53
|
+
addFaces(entries: FontFaceInput[]): { family: string; weight: number; italic: boolean; scripts?: string[] }[];
|
|
50
54
|
}
|
|
51
55
|
export declare class OPFFontError extends Error { code:string; details:Record<string,unknown>; constructor(code:string,message:string,details?:Record<string,unknown>) }
|
|
52
56
|
export declare function createFontRegistry(entries: FontFaceInput[], options?: FontRegistryOptions): FontRegistry;
|
|
@@ -66,6 +70,8 @@ export interface PlannedRun { text: string; family: string; own: boolean; stack?
|
|
|
66
70
|
export interface ScriptFonts {
|
|
67
71
|
readonly profile: ScriptFontProfile;
|
|
68
72
|
readonly rtl: boolean;
|
|
73
|
+
/** Glyph fallback notes recorded so far. */
|
|
74
|
+
readonly fallbacks: readonly GlyphFallbackNote[];
|
|
69
75
|
/** Font runs for text drawn in a resolved latin style. */
|
|
70
76
|
plan(text: string, style: TextStyle): PlannedRun[];
|
|
71
77
|
runWidths(runs: PlannedRun[], size: number, style: TextStyle): number[] | undefined;
|
|
@@ -77,12 +83,21 @@ export declare const SCRIPT_FONT_REPLACEMENTS: readonly Readonly<{requestedFamil
|
|
|
77
83
|
export declare function scriptOfCharacter(character: string): string;
|
|
78
84
|
export declare function scriptFontRole(script: string): ScriptRole;
|
|
79
85
|
export declare function itemizeScripts(text: string, profile?: ScriptFontProfile): ScriptRun[];
|
|
80
|
-
|
|
86
|
+
/** Script keys in the strings of a JSON value. The profile's own script counts unless `includeLanguage` is false; `ignoreKeys` are not visited. */
|
|
87
|
+
export declare function detectScripts(value: unknown, profile?: ScriptFontProfile, options?: { includeLanguage?: boolean; ignoreKeys?: readonly string[] }): string[];
|
|
81
88
|
export declare function designatedFamilies(script: string, serif?: boolean): string[];
|
|
82
89
|
export declare function scriptFontAliases(families: Iterable<string>): Record<string, string>;
|
|
83
|
-
export
|
|
90
|
+
export interface GlyphFallbackNote { fontFamily: string; fallbackFamily: string; scripts: string[]; characters: string[]; path?: string }
|
|
91
|
+
export interface ScriptFontsOptions {
|
|
92
|
+
/** `"chain"` (default) draws and measures a character the face lacks with the first bundled face that has it; `"none"` keeps exact faces and a missing glyph raises `missing-glyph`. */
|
|
93
|
+
glyphFallback?: "chain" | "none";
|
|
94
|
+
onFallback?: (note: GlyphFallbackNote) => void;
|
|
95
|
+
}
|
|
96
|
+
export declare function createScriptFonts(profile?: ScriptFontProfile, measurement?: TextMeasurement, options?: ScriptFontsOptions): ScriptFonts;
|
|
97
|
+
/** Designated open families a preview tries, in order, for a character its face lacks (own script, deck script, Noto Sans, other CJK, other Noto scripts). */
|
|
98
|
+
export declare function glyphFallbackFamilies(character: string, profile?: ScriptFontProfile, serif?: boolean): string[];
|
|
84
99
|
/** Wrap a measurement so pagination, the renderer and the editor itemize script runs identically. */
|
|
85
|
-
export declare function createScriptTextMeasurement(measurement: TextMeasurement, profile: ScriptFontProfile): TextMeasurement;
|
|
100
|
+
export declare function createScriptTextMeasurement(measurement: TextMeasurement, profile: ScriptFontProfile, options?: ScriptFontsOptions): TextMeasurement;
|
|
86
101
|
export declare function openTypeLanguage(tag: string | undefined): string | undefined;
|
|
87
102
|
/** Heading or body role of a style from its OPF path; undefined without a slide path. */
|
|
88
103
|
export declare function textRole(style: { path?: string } | undefined): "heading" | "body" | undefined;
|
package/dist/fonts.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { create } from "fontkit";
|
|
2
|
-
import { FONT_COMPATIBILITY } from "./font-compatibility.js";
|
|
2
|
+
import { FONT_COMPATIBILITY, disabledFeaturesFor } from "./font-compatibility.js";
|
|
3
3
|
import { openTypeLanguage, scriptFontAliases } from "./script-fonts.js";
|
|
4
4
|
import { fontPolicyFor } from "./font-policy.js";
|
|
5
5
|
import { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
6
|
-
export { FONT_COMPATIBILITY, EXPERIMENTAL_FONT_CANDIDATES } from "./font-compatibility.js";
|
|
6
|
+
export { FONT_COMPATIBILITY, EXPERIMENTAL_FONT_CANDIDATES, disabledFeaturesFor } from "./font-compatibility.js";
|
|
7
7
|
export { FONT_POLICY, FONT_POLICY_DECISIONS, FONT_POLICY_SOURCE, fontPolicyFor } from "./font-policy.js";
|
|
8
|
-
export { SCRIPT_FONT_FAMILIES, SCRIPT_FONT_REPLACEMENTS, createScriptFonts, createScriptTextMeasurement, designatedFamilies, detectScripts, itemizeScripts, openTypeLanguage, scriptFontAliases, scriptFontRole, scriptOfCharacter, textRole } from "./script-fonts.js";
|
|
8
|
+
export { SCRIPT_FONT_FAMILIES, SCRIPT_FONT_REPLACEMENTS, createScriptFonts, createScriptTextMeasurement, designatedFamilies, glyphFallbackFamilies, detectScripts, itemizeScripts, openTypeLanguage, scriptFontAliases, scriptFontRole, scriptOfCharacter, textRole } from "./script-fonts.js";
|
|
9
9
|
|
|
10
10
|
export class OPFFontError extends Error {
|
|
11
11
|
constructor(code, message, details = {}) { super(message); this.name = "OPFFontError"; this.code = code; this.details = details; }
|
|
@@ -20,7 +20,8 @@ function validFamily(family) {
|
|
|
20
20
|
if (typeof family !== "string" || !family.trim() || /[\u0000-\u001f"'\\<>;]/.test(family)) throw new OPFFontError("invalid-font-family", "Font family must be a nonempty plain name.");
|
|
21
21
|
return family;
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
// The open families load with the office pack (prepareNodeFonts({pack:"office"})).
|
|
24
|
+
const packFor = family => { const pack = BUNDLED_FONT_MANIFEST.packages.find(pkg=>pkg.renamedFrom?.toLowerCase()===family.toLowerCase() || pkg.faces.some(face=>face.family.toLowerCase()===family.toLowerCase()))?.pack; return pack==="open" ? "office" : pack; };
|
|
24
25
|
const percent = value => `${(value*100).toFixed(1)}%`;
|
|
25
26
|
/** FF-31: say why a family has no face and what the caller can do, from the OPF font policy. */
|
|
26
27
|
function unavailableFontError(family, style, policy) {
|
|
@@ -41,12 +42,12 @@ function unavailableFontError(family, style, policy) {
|
|
|
41
42
|
return new OPFFontError("font-unavailable", `No local font face for '${family}'. ${owner} Its declared replacement ${replacement} is visual only${delta}, which substitutionPolicy '${policy}' does not allow. Pass substitutionPolicy: 'visual' to preview with ${candidates.join(" or ")}, or ${hook}. The PPTX names '${family}' either way.`, details);
|
|
42
43
|
if (policy === "none")
|
|
43
44
|
return new OPFFontError("font-unavailable", `No local font face for '${family}'. ${owner} Its declared ${compatibility} replacement is ${replacement}${delta}; substitutionPolicy 'none' does not allow it. Pass substitutionPolicy: '${compatibility}', or ${hook}.`, details);
|
|
44
|
-
return new OPFFontError("font-unavailable", `No local font face for '${family}'. ${owner} None of its replacements (${candidates.join(", ")}) is loaded${packs.length ? `; load the ${packs.map(pack=>"'"+pack+"'").join(" or ")} font pack` : ""}, or ${hook}.`, details);
|
|
45
|
+
return new OPFFontError("font-unavailable", `No local font face for '${family}'. ${owner} None of its replacements (${candidates.join(", ")}) is loaded${packs.length ? `; load the ${packs.map(pack=>"'"+pack+"'").join(" or ")} font pack` : ""}, or ${hook}. The PPTX names '${family}' either way.`, details);
|
|
45
46
|
}
|
|
46
47
|
/** Local font files only. The caller explicitly chooses aliases and fallback. */
|
|
47
48
|
export function createFontRegistry(entries, options = {}) {
|
|
48
49
|
if (!Array.isArray(entries) || !entries.length) throw new OPFFontError("empty-font-registry", "Supply at least one font file.");
|
|
49
|
-
const
|
|
50
|
+
const makeFace = entry => {
|
|
50
51
|
if (!(entry.data instanceof Uint8Array)) throw new OPFFontError("invalid-font-data", "Font data must be a Uint8Array.");
|
|
51
52
|
const data = entry.data.slice();
|
|
52
53
|
let font;
|
|
@@ -68,25 +69,40 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
68
69
|
const format = signature === "OTTO" ? "otf" : signature === "wOFF" ? "woff" : signature === "wOF2" ? "woff2" : "ttf";
|
|
69
70
|
// Script replacement faces (FF-19) declare the ISO 15924 scripts they serve.
|
|
70
71
|
const scripts = Array.isArray(entry.scripts) && entry.scripts.length ? Object.freeze(entry.scripts.map(String)) : undefined;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
72
|
+
// "used": a bundled face is embedded in an SVG only when the slide's text names its family.
|
|
73
|
+
if (entry.embed !== undefined && entry.embed !== "always" && entry.embed !== "used") throw new OPFFontError("invalid-font-embed", "Font embed must be 'always' or 'used'.");
|
|
74
|
+
// A fallback-only face (the default Noto Sans) serves glyph fallback and explicit requests by its own name,
|
|
75
|
+
// but never stands in for another family, so loading it does not change what any other font previews with.
|
|
76
|
+
return {family,familyGroup,fontFace,weight,italic,font,data,format,license:entry.license,embed:entry.embed,scripts,fallbackOnly:entry.fallbackOnly===true,cache:new Map()};
|
|
77
|
+
};
|
|
78
|
+
const faces = [], duplicates = new Set(), familySlots = new Map();
|
|
79
|
+
// Adds faces atomically: every check runs before any face is registered.
|
|
80
|
+
const register = added => {
|
|
81
|
+
const ids = new Set(duplicates), slots = new Map(familySlots);
|
|
82
|
+
for (const face of added) {
|
|
83
|
+
const id = key(face.family,face.weight,face.italic);
|
|
84
|
+
if (ids.has(id)) throw new OPFFontError("duplicate-font-face", `Duplicate face: ${id}`);
|
|
85
|
+
ids.add(id);
|
|
86
|
+
}
|
|
87
|
+
for (const face of added) for (const family of new Set([face.family,face.familyGroup])) {
|
|
88
|
+
const id=key(family,face.weight,face.italic),previous=slots.get(id);
|
|
89
|
+
if(previous && previous!==face) throw new OPFFontError('ambiguous-font-face', `Multiple physical faces match '${family}' at weight ${face.weight}.`, {fontFamily:family,fontWeight:face.weight,italic:face.italic});
|
|
90
|
+
slots.set(id,face);
|
|
91
|
+
}
|
|
92
|
+
faces.push(...added);
|
|
93
|
+
for (const id of ids) duplicates.add(id);
|
|
94
|
+
for (const [id,face] of slots) familySlots.set(id,face);
|
|
95
|
+
};
|
|
96
|
+
register(entries.map(makeFace));
|
|
85
97
|
const substitutions = new Map();
|
|
86
98
|
// Loaded script replacement faces stand in for the proprietary script fonts
|
|
87
99
|
// they replace (Meiryo -> Noto Sans JP). Caller aliases take precedence.
|
|
88
|
-
|
|
89
|
-
const
|
|
100
|
+
let aliases = new Map();
|
|
101
|
+
const buildAliases = () => {
|
|
102
|
+
const scriptAliases = scriptFontAliases(new Set(faces.filter(face=>face.scripts&&!face.fallbackOnly).map(face=>face.family)));
|
|
103
|
+
aliases = new Map(Object.entries({...scriptAliases,...options.aliases}).map(([from,to])=>[validFamily(from).toLowerCase(),validFamily(to)]));
|
|
104
|
+
};
|
|
105
|
+
buildAliases();
|
|
90
106
|
const policy = options.substitutionPolicy ?? "none";
|
|
91
107
|
if (!["none","metric","visual"].includes(policy)) throw new OPFFontError("invalid-font-policy", "substitutionPolicy must be none, metric, or visual.");
|
|
92
108
|
if (options.fallbackFamily) validFamily(options.fallbackFamily);
|
|
@@ -125,7 +141,7 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
125
141
|
const substituteTier = index===0 ? tier : "visual";
|
|
126
142
|
if (substituteTier==="visual" && !(policy==="visual" || metricFallback && index===0)) continue;
|
|
127
143
|
const exactWeight = substituteTier==="metric" || policy!=="visual";
|
|
128
|
-
const faces = findFamily(substitute).filter(face=>!exactWeight || face.weight===weight);
|
|
144
|
+
const faces = findFamily(substitute).filter(face=>!face.fallbackOnly && (!exactWeight || face.weight===weight));
|
|
129
145
|
let available = faces.filter(face=>face.italic===!!style.italic);
|
|
130
146
|
// Visual replacements without the requested style draw the other one and say so.
|
|
131
147
|
if (!available.length && substituteTier==="visual" && policy==="visual" && faces.length) { available = faces.filter(face=>!face.italic); styleFallback = available.length>0; }
|
|
@@ -175,7 +191,10 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
175
191
|
}
|
|
176
192
|
}
|
|
177
193
|
if(value===undefined||includeOutline&&!Object.hasOwn(value,'outline')) {
|
|
178
|
-
|
|
194
|
+
// FF-31: features a policy row turns off in its replacement (Gelasio for Georgia: liga, clig), the
|
|
195
|
+
// same ones the SVG output turns off, so measured and drawn advances agree.
|
|
196
|
+
const off=disabledFeaturesFor(face.family),policyFeatures=off&&Object.fromEntries(off.map(tag=>[tag,false]));
|
|
197
|
+
const shape=extra=>{const features=policyFeatures||extra?{...policyFeatures,...extra}:undefined;return language?face.font.layout(text,features,undefined,language):features?face.font.layout(text,features):face.font.layout(text);};
|
|
179
198
|
let run;
|
|
180
199
|
try { run=shape(); }
|
|
181
200
|
catch (error) {
|
|
@@ -209,11 +228,28 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
209
228
|
resolveFont(style) { return resolve(style).resolution; },
|
|
210
229
|
clearSubstitutions() { substitutions.clear(); },
|
|
211
230
|
get substitutions() { return [...substitutions.values()]; },
|
|
212
|
-
|
|
231
|
+
// Faces flagged embed:"used" (the open pack, FF-31) are not in this eager list: supplied with selectEmbeddedFonts (or
|
|
232
|
+
// prepareNodeFonts().options.embeddedFonts) they are embedded only in SVGs whose text names their family.
|
|
233
|
+
get embeddedFonts() { return embedded(face=>face.embed!=="used"); },
|
|
213
234
|
/** Parsed face metadata in entry order, without encoding font bytes. */
|
|
214
|
-
describeFaces: ()=>faces.map(face=>({family:face.family,weight:face.weight,italic:face.italic,...(face.scripts?{scripts:[...face.scripts]}:{})})),
|
|
235
|
+
describeFaces: ()=>faces.map(face=>({family:face.family,weight:face.weight,italic:face.italic,...(face.scripts?{scripts:[...face.scripts]}:{}),...(face.fallbackOnly?{fallbackOnly:true}:{})})),
|
|
215
236
|
/** Embedded faces for which `predicate({family,weight,italic,scripts})` holds; large script faces can be left to raster fontFiles. */
|
|
216
237
|
selectEmbeddedFonts: predicate=>embedded(predicate),
|
|
238
|
+
/** True when a loaded script-pack face has a glyph for the character (FF-19 glyph fallback planning). */
|
|
239
|
+
scriptFacesCover: character => faces.some(face => face.scripts && face.font.hasGlyphForCodePoint(character.codePointAt(0))),
|
|
240
|
+
/**
|
|
241
|
+
* Register more faces in this registry (FF-19: script faces are added once a document needs
|
|
242
|
+
* them). Atomic: on an error no face is added. Returns the added faces' metadata. Text
|
|
243
|
+
* measurements planned before the call may not know the new faces; plan again afterwards.
|
|
244
|
+
*/
|
|
245
|
+
addFaces(added) {
|
|
246
|
+
if (!Array.isArray(added)) throw new OPFFontError("invalid-font-data", "addFaces requires an array of font entries.");
|
|
247
|
+
const made = added.map(makeFace);
|
|
248
|
+
register(made);
|
|
249
|
+
buildAliases();
|
|
250
|
+
substitutions.clear();
|
|
251
|
+
return made.map(face=>({family:face.family,weight:face.weight,italic:face.italic,...(face.scripts?{scripts:[...face.scripts]}:{})}));
|
|
252
|
+
},
|
|
217
253
|
};
|
|
218
|
-
function embedded(predicate) { return faces.filter(face=>predicate({family:face.family,weight:face.weight,italic:face.italic,scripts:face.scripts})).map(face=>({family:face.family,weight:face.weight,italic:face.italic,...(face.license ? {license:face.license} : {}),dataUrl:`data:font/${face.format};base64,${base64(face.data)}`})); }
|
|
254
|
+
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==="used" ? {embed:"used"} : {}),dataUrl:`data:font/${face.format};base64,${base64(face.data)}`})); }
|
|
219
255
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,15 @@ export declare const engineDefaults: Readonly<{
|
|
|
32
32
|
}>;
|
|
33
33
|
|
|
34
34
|
export type RenderDiagnostic = LayoutDiagnostic | {
|
|
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
|
+
} | {
|
|
35
44
|
code: "unsupported-pattern" | "date-needs-value" | "language-preview-unavailable" | "language-preview-unresolved" | "paragraph-direction-unavailable";
|
|
36
45
|
path: string;
|
|
37
46
|
message: string;
|
|
@@ -55,6 +64,8 @@ export type RenderDiagnostic = LayoutDiagnostic | {
|
|
|
55
64
|
|
|
56
65
|
export interface RenderSvgOptions {
|
|
57
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";
|
|
58
69
|
/** Unscaled reference-pixel clearance around supplied vector text outlines; default 1. */
|
|
59
70
|
textRasterPadding?: number;
|
|
60
71
|
embeddedFonts?: EmbeddedFont[];
|
|
@@ -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()),
|