@openpresentation/opf-render 0.13.1 → 0.14.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 +106 -76
- package/dist/chart-options.js +26 -21
- package/dist/charts.js +204 -117
- package/dist/deck-runtime.js +21 -34
- package/dist/element.d.ts +11 -6
- package/dist/element.js +28 -18
- package/dist/export-browser.d.ts +7 -1
- package/dist/export-browser.js +7 -6
- package/dist/font-compatibility.js +4 -4
- package/dist/font-registry.js +324 -0
- package/dist/fonts-browser.d.ts +25 -3
- package/dist/fonts-browser.js +54 -3
- package/dist/fonts-node.d.ts +32 -16
- package/dist/fonts-node.js +110 -50
- package/dist/fonts.d.ts +48 -27
- package/dist/fonts.js +7 -320
- package/dist/index.d.ts +36 -16
- package/dist/lazy-font-list.js +1 -1
- package/dist/lazy-fonts.js +7 -7
- package/dist/pdf-fonts.js +1 -1
- package/dist/player.d.ts +4 -4
- package/dist/player.js +5 -5
- package/dist/preview-fonts.d.ts +4 -4
- package/dist/preview-fonts.js +6 -5
- package/dist/raster.js +24 -9
- package/dist/script-font-pack.js +19 -26
- package/dist/script-fonts.js +6 -17
- package/dist/svg.d.ts +30 -14
- package/dist/svg.js +286 -266
- package/dist/symbol-fonts.js +22 -55
- package/package.json +2 -2
- package/dist/font-policy.js +0 -2734
- package/dist/symbol-encodings.js +0 -7
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { create } from "fontkit";
|
|
2
|
+
import { FONT_COMPATIBILITY, disabledFeaturesFor } from "./font-compatibility.js";
|
|
3
|
+
import { adjustedFontSize, openTypeLanguage, scriptFontAliases, sizeAdjustFor } from "./script-fonts.js";
|
|
4
|
+
import { fontPolicyFor } from "@openpresentation/opf/font-policy";
|
|
5
|
+
import { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
6
|
+
import { SYMBOL_SCRIPT, isSymbolEncodedFamily, symbolEncodingFamily, symbolPreviewFaces } from "./symbol-fonts.js";
|
|
7
|
+
|
|
8
|
+
// The font registry the loaders (`loadFonts` in fonts-node.js and fonts-browser.js) build: faces in, one text measurement out.
|
|
9
|
+
// It is not a public entry point; callers load fonts with `loadFonts` and read `handle.registry`. `/fonts` (fonts.js) re-exports
|
|
10
|
+
// the data and helpers a host may read.
|
|
11
|
+
export class OPFFontError extends Error {
|
|
12
|
+
constructor(code, message, details = {}) { super(message); this.name = "OPFFontError"; this.code = code; this.details = details; }
|
|
13
|
+
}
|
|
14
|
+
const key = (family, weight, italic) => `${family.toLowerCase()}:${weight}:${!!italic}`;
|
|
15
|
+
function base64(data) {
|
|
16
|
+
let binary = "";
|
|
17
|
+
for (let i = 0; i < data.length; i += 32768) binary += String.fromCharCode(...data.subarray(i, i + 32768));
|
|
18
|
+
return btoa(binary);
|
|
19
|
+
}
|
|
20
|
+
function validFamily(family) {
|
|
21
|
+
if (typeof family !== "string" || !family.trim() || /[\u0000-\u001f"'\\<>;]/.test(family)) throw new OPFFontError("invalid-font-family", "Font family must be a nonempty plain name.");
|
|
22
|
+
return family;
|
|
23
|
+
}
|
|
24
|
+
// The open families load with the office pack (loadFonts({pack:"office"})).
|
|
25
|
+
|
|
26
|
+
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; };
|
|
27
|
+
const percent = value => `${(value*100).toFixed(1)}%`;
|
|
28
|
+
/** FF-31: say why a family has no face and what the caller can do, from the OPF font policy. */
|
|
29
|
+
function unavailableFontError(family, style, policy) {
|
|
30
|
+
const row = fontPolicyFor(family), details = {path:style.path, fontFamily:family, substitutionPolicy:policy};
|
|
31
|
+
const hook = `supply licensed '${family}' font files (loadFonts({faces}))`;
|
|
32
|
+
if (!row) return new OPFFontError("font-unavailable", `No local font face for '${family}'. '${family}' is not in the OPF font policy table, so there is no declared replacement: ${hook}, add an alias, or set fallbackFamily.`, {...details, licenseClass:"unknown"});
|
|
33
|
+
Object.assign(details, {licenseClass:row.licenseClass});
|
|
34
|
+
if (!row.replacement) {
|
|
35
|
+
const pack = packFor(family);
|
|
36
|
+
return new OPFFontError("font-unavailable", `No local font face for '${family}'. '${family}' is ${row.licenseClass === "open" ? `openly licensed (${row.license})` : row.license}${pack ? `; load the '${pack}' font pack` : `; no pinned renderer pack ships it yet, so supply its font files (for example from @expo-google-fonts/${family.toLowerCase().replace(/ +/g,"-")})`}.`, {...details, ...(pack?{pack}:{})});
|
|
37
|
+
}
|
|
38
|
+
const {family:replacement, compatibility, measured} = row.replacement;
|
|
39
|
+
const candidates = [replacement, ...(row.alternates ?? [])], packs = [...new Set(candidates.map(packFor).filter(Boolean))];
|
|
40
|
+
Object.assign(details, {replacement, replacementCompatibility:compatibility, candidates, ...(packs.length?{packs}:{}), ...(measured?{measured}:{}), ...(row.replacement.decision?{decision:row.replacement.decision}:{})});
|
|
41
|
+
const delta = measured ? ` (measured mean width difference ${percent(measured.meanAbsWidthDelta)}, max ${percent(measured.maxAbsWidthDelta)})` : " (not measured against the real font)";
|
|
42
|
+
const owner = `'${family}' is ${row.license} and OPF never bundles or embeds it.`;
|
|
43
|
+
if (compatibility === "visual" && policy !== "visual")
|
|
44
|
+
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);
|
|
45
|
+
if (policy === "none")
|
|
46
|
+
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);
|
|
47
|
+
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);
|
|
48
|
+
}
|
|
49
|
+
/** Local font files only. The caller explicitly chooses aliases and fallback. */
|
|
50
|
+
// fontkit 2.0.4 cannot decode every OpenType lookup: its GSUB type 8 (reverse chaining contextual single substitution) struct lacks the
|
|
51
|
+
// backtrackGlyphCount field, so the lookup fails with "Not a fixed size" when a feature that references it is applied, and it has no processor
|
|
52
|
+
// for the type anyway. Noto Sans Mongolian 3.x has such a lookup under calt and rclt in every script, so every shaping of the face threw (FF-44).
|
|
53
|
+
// A lookup that cannot be decoded is treated as one that applies nothing (no subtables), so the rest of the feature still runs: advances equal
|
|
54
|
+
// a browser's for the face's Latin and Mongolian samples (test/mongolian-shaping.mjs). Applied only after shaping failed, once per font.
|
|
55
|
+
const lookupsGuarded = new WeakSet();
|
|
56
|
+
export function skipUndecodableLookups(font) {
|
|
57
|
+
if (lookupsGuarded.has(font)) return false;
|
|
58
|
+
lookupsGuarded.add(font);
|
|
59
|
+
let guarded = false;
|
|
60
|
+
for (const tag of ["GSUB", "GPOS"]) {
|
|
61
|
+
let list;
|
|
62
|
+
try { list = font[tag]?.lookupList; } catch { continue; }
|
|
63
|
+
if (typeof list?.get !== "function") continue;
|
|
64
|
+
const get = list.get.bind(list);
|
|
65
|
+
list.get = index => { try { return get(index); } catch { return { lookupType: 1, flags: {}, subTables: [] }; } };
|
|
66
|
+
guarded = true;
|
|
67
|
+
}
|
|
68
|
+
return guarded;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The face a registry draws for `family` (a validated concrete family: theme tokens are already resolved) in `style`, chosen
|
|
73
|
+
* from `faces` ({family, familyGroup, weight, italic, fallbackOnly?, scripts?}) under the substitution policy, aliases and
|
|
74
|
+
* fallback family, or an OPFFontError when none applies. Shared by the registry's resolution and by the lazy loader's face
|
|
75
|
+
* planning (lazy-fonts.js), so the browser loads exactly the faces Node would draw with everything loaded.
|
|
76
|
+
*/
|
|
77
|
+
export function pickFace(faces, family, style, {policy = "none", aliases = new Map(), fallbackFamily} = {}) {
|
|
78
|
+
const findFamily = name => faces.filter(face=>face.family.toLowerCase()===name.toLowerCase() || face.familyGroup.toLowerCase()===name.toLowerCase());
|
|
79
|
+
const weight = style.fontWeight ?? 400;
|
|
80
|
+
let matching = findFamily(family), compatibility = "exact", rule, targetWeight = weight, styleFallback = false, via = "family", encoding;
|
|
81
|
+
// FF-45: a symbol-encoded family (Symbol, Wingdings, Wingdings 2, Wingdings 3, Webdings) whose own face is not loaded
|
|
82
|
+
// draws the Unicode equivalent of each code with the first loaded open symbol face (symbol-fonts.js: Noto Sans Symbols 2,
|
|
83
|
+
// Noto Sans Symbols, Noto Sans Math, or the office pack's Noto Sans). The resolved style carries the encoding so the
|
|
84
|
+
// planner maps every character; the PPTX keeps the family and the original codes. Without any such face the caller's
|
|
85
|
+
// fallback family applies, else the request fails as before (font-encoding-required) and names the pack to load.
|
|
86
|
+
if (!matching.length && isSymbolEncodedFamily(family)) {
|
|
87
|
+
for (const candidate of symbolPreviewFaces(family)) { const pool = findFamily(candidate); if (pool.length) { matching = pool; break; } }
|
|
88
|
+
if (matching.length) { compatibility = "visual"; via = "encoding"; encoding = symbolEncodingFamily(family); }
|
|
89
|
+
else if (!fallbackFamily) throw new OPFFontError("font-encoding-required", `Font '${family}' is symbol-encoded and no open symbol face is loaded: load the symbol faces (loadFonts with scripts: ['${SYMBOL_SCRIPT}'], or scripts: 'auto' with the presentation) or supply '${family}' itself.`, {path:style.path,fontFamily:family,scripts:[SYMBOL_SCRIPT],faces:[...symbolPreviewFaces(family)]});
|
|
90
|
+
}
|
|
91
|
+
if (!matching.length && aliases.has(family.toLowerCase())) {
|
|
92
|
+
matching = findFamily(aliases.get(family.toLowerCase())); compatibility = "visual"; via = "alias";
|
|
93
|
+
}
|
|
94
|
+
if (!matching.length && policy!=="none") {
|
|
95
|
+
const candidate = FONT_COMPATIBILITY.find(entry=>entry.requestedFamily.toLowerCase()===family.toLowerCase());
|
|
96
|
+
const tier = candidate?.compatibility==="metric" && !candidate.weights.includes(weight) ? "visual" : candidate?.compatibility;
|
|
97
|
+
// A declared visual replacement is used in visual mode, and in metric mode only when the
|
|
98
|
+
// policy keeps it as a metric-mode fallback (Cambria -> Caladea); it is still reported visual.
|
|
99
|
+
// In metric mode the fallback applies only where the pre-FF-31 metric rule did: the declared
|
|
100
|
+
// replacement at weights 400 and 700, normal or italic, with an exact face weight.
|
|
101
|
+
const metricFallback = policy!=="visual" && candidate?.metricModeFallback===true && [400,700].includes(weight);
|
|
102
|
+
const allowVisual = policy==="visual" || metricFallback;
|
|
103
|
+
if (candidate && (allowVisual || tier==="metric")) {
|
|
104
|
+
// A family that names its weight (Segoe UI Semibold, Arial Black) selects that weight in
|
|
105
|
+
// the replacement; its bold style link still selects bold (FF-31).
|
|
106
|
+
const wanted = candidate.weight ? (weight>=600 ? Math.max(candidate.weight,700) : candidate.weight) : weight;
|
|
107
|
+
for (const [index, substitute] of candidate.substitutes.entries()) {
|
|
108
|
+
// Only the declared replacement can carry the row's metric claim; an alternate is visual.
|
|
109
|
+
const substituteTier = index===0 ? tier : "visual";
|
|
110
|
+
if (substituteTier==="visual" && !(policy==="visual" || metricFallback && index===0)) continue;
|
|
111
|
+
const exactWeight = substituteTier==="metric" || policy!=="visual";
|
|
112
|
+
const pool = findFamily(substitute).filter(face=>!face.fallbackOnly && (!exactWeight || face.weight===weight));
|
|
113
|
+
let available = pool.filter(face=>face.italic===!!style.italic);
|
|
114
|
+
// Visual replacements without the requested style draw the other one and say so.
|
|
115
|
+
if (!available.length && substituteTier==="visual" && policy==="visual" && pool.length) { available = pool.filter(face=>!face.italic); styleFallback = available.length>0; }
|
|
116
|
+
if (available.length) { matching=available; compatibility=substituteTier; rule={...candidate, substituteIndex:index}; targetWeight=wanted; via="replacement"; break; }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// FF-45: Cambria Math previews with STIX Two Math (the math pack, loaded as a script face: the alias above resolves it
|
|
121
|
+
// under every policy). Without the pack the policy error below names the pack; the former `math-font-required` failure is gone.
|
|
122
|
+
if (!matching.length && fallbackFamily) { matching=findFamily(fallbackFamily); compatibility="generic"; via="fallback"; }
|
|
123
|
+
if (!matching.length) throw unavailableFontError(family, style, policy);
|
|
124
|
+
const styled = styleFallback ? matching : matching.filter(face=>face.italic===!!style.italic);
|
|
125
|
+
// Script replacement faces have no italics; use upright glyphs and advances. RR-17 (FF-41): so does a visual preview of a
|
|
126
|
+
// single-style display face (Anton, Bebas Neue: the families Impact and the open font schemes name), reported visual; PowerPoint
|
|
127
|
+
// synthesizes the slant of such a face too, so the advances agree and the preview no longer refuses the deck. A strict registry
|
|
128
|
+
// (policy none) still reports font-style-unavailable.
|
|
129
|
+
if (!styled.length && style.italic && matching.length && (matching.every(face=>face.scripts) || policy==="visual" && matching.every(face=>!face.italic))) { if (compatibility!=="generic") compatibility="visual"; }
|
|
130
|
+
else matching = styled;
|
|
131
|
+
if (!matching.length) throw new OPFFontError("font-style-unavailable", `No ${style.italic ? "italic" : "upright"} face for '${family}'.`, {path:style.path});
|
|
132
|
+
matching.sort((a,b)=>Math.abs(a.weight-targetWeight)-Math.abs(b.weight-targetWeight) || a.weight-b.weight);
|
|
133
|
+
const face = matching[0];
|
|
134
|
+
if ((face.weight!==targetWeight || styleFallback) && compatibility!=="generic") compatibility="visual";
|
|
135
|
+
return {face, compatibility, rule, styleFallback, via, ...(encoding ? {encoding} : {})};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function createFontRegistry(entries, options = {}) {
|
|
139
|
+
if (!Array.isArray(entries) || !entries.length) throw new OPFFontError("empty-font-registry", "Supply at least one font file.");
|
|
140
|
+
const makeFace = entry => {
|
|
141
|
+
if (!(entry.data instanceof Uint8Array)) throw new OPFFontError("invalid-font-data", "Font data must be a Uint8Array.");
|
|
142
|
+
const data = entry.data.slice();
|
|
143
|
+
let font;
|
|
144
|
+
try { font = create(data, entry.postscriptName); } catch (error) { throw new OPFFontError("invalid-font-data", error.message); }
|
|
145
|
+
if (!font?.layout || !font.unitsPerEm) throw new OPFFontError("font-collection", "Select one font from a collection with postscriptName.");
|
|
146
|
+
const family = validFamily(entry.family ?? font.familyName);
|
|
147
|
+
// OpenType groups Medium/SemiBold/ExtraBold under a preferred family, while
|
|
148
|
+
// retaining separate legacy families for four-style native font selectors.
|
|
149
|
+
// An explicit caller rename owns its namespace and must not add this alias.
|
|
150
|
+
const familyGroup = entry.family && entry.family.toLowerCase() !== font.familyName?.toLowerCase()
|
|
151
|
+
? family : validFamily(font.getName?.('preferredFamily', 'en') ?? family);
|
|
152
|
+
const linking = font['OS/2']?.fsSelection ?? font.head?.macStyle;
|
|
153
|
+
const fontFace = linking && typeof linking.bold === 'boolean' && typeof linking.italic === 'boolean'
|
|
154
|
+
? {family:validFamily(font.getName?.('fontFamily', 'en') ?? font.familyName ?? family),bold:linking.bold,italic:linking.italic} : undefined;
|
|
155
|
+
const weight = entry.weight ?? 400;
|
|
156
|
+
const italic = entry.italic ?? !!font.italicAngle;
|
|
157
|
+
if (!Number.isInteger(weight) || weight < 1 || weight > 1000) throw new OPFFontError("invalid-font-weight", "Font weight must be between 1 and 1000.");
|
|
158
|
+
const signature = String.fromCharCode(...data.subarray(0,4));
|
|
159
|
+
const format = signature === "OTTO" ? "otf" : signature === "wOFF" ? "woff" : signature === "wOF2" ? "woff2" : "ttf";
|
|
160
|
+
// Script replacement faces (FF-19) declare the ISO 15924 scripts they serve.
|
|
161
|
+
const scripts = Array.isArray(entry.scripts) && entry.scripts.length ? Object.freeze(entry.scripts.map(String)) : undefined;
|
|
162
|
+
// "used": a bundled face is embedded in an SVG only when the slide's text names its family.
|
|
163
|
+
if (entry.embed !== undefined && entry.embed !== "always" && entry.embed !== "used") throw new OPFFontError("invalid-font-embed", "Font embed must be 'always' or 'used'.");
|
|
164
|
+
// A fallback-only face (the default Noto Sans) serves glyph fallback and explicit requests by its own name,
|
|
165
|
+
// but never stands in for another family, so loading it does not change what any other font previews with.
|
|
166
|
+
return {family,familyGroup,fontFace,weight,italic,font,data,format,license:entry.license,embed:entry.embed,scripts,fallbackOnly:entry.fallbackOnly===true,cache:new Map()};
|
|
167
|
+
};
|
|
168
|
+
const faces = [], duplicates = new Set(), familySlots = new Map();
|
|
169
|
+
// Adds faces atomically: every check runs before any face is registered.
|
|
170
|
+
const register = added => {
|
|
171
|
+
const ids = new Set(duplicates), slots = new Map(familySlots);
|
|
172
|
+
for (const face of added) {
|
|
173
|
+
const id = key(face.family,face.weight,face.italic);
|
|
174
|
+
if (ids.has(id)) throw new OPFFontError("duplicate-font-face", `Duplicate face: ${id}`);
|
|
175
|
+
ids.add(id);
|
|
176
|
+
}
|
|
177
|
+
for (const face of added) for (const family of new Set([face.family,face.familyGroup])) {
|
|
178
|
+
const id=key(family,face.weight,face.italic),previous=slots.get(id);
|
|
179
|
+
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});
|
|
180
|
+
slots.set(id,face);
|
|
181
|
+
}
|
|
182
|
+
faces.push(...added);
|
|
183
|
+
for (const id of ids) duplicates.add(id);
|
|
184
|
+
for (const [id,face] of slots) familySlots.set(id,face);
|
|
185
|
+
};
|
|
186
|
+
register(entries.map(makeFace));
|
|
187
|
+
const substitutions = new Map();
|
|
188
|
+
// Loaded script replacement faces stand in for the proprietary script fonts
|
|
189
|
+
// they replace (Meiryo -> Noto Sans JP). Caller aliases take precedence.
|
|
190
|
+
let aliases = new Map();
|
|
191
|
+
const buildAliases = () => {
|
|
192
|
+
const scriptAliases = scriptFontAliases(new Set(faces.filter(face=>face.scripts&&!face.fallbackOnly).map(face=>face.family)));
|
|
193
|
+
aliases = new Map(Object.entries({...scriptAliases,...options.aliases}).map(([from,to])=>[validFamily(from).toLowerCase(),validFamily(to)]));
|
|
194
|
+
};
|
|
195
|
+
buildAliases();
|
|
196
|
+
const policy = options.substitutionPolicy ?? "none";
|
|
197
|
+
if (!["none","metric","visual"].includes(policy)) throw new OPFFontError("invalid-font-policy", "substitutionPolicy must be none, metric, or visual.");
|
|
198
|
+
if (options.fallbackFamily) validFamily(options.fallbackFamily);
|
|
199
|
+
const resolve = style => {
|
|
200
|
+
const requested = validFamily(style?.fontFamily);
|
|
201
|
+
const weight = style.fontWeight ?? 400;
|
|
202
|
+
if (!Number.isFinite(weight) || weight < 1 || weight > 1000) throw new OPFFontError("invalid-font-weight", "Font weight must be between 1 and 1000.");
|
|
203
|
+
// DrawingML theme tokens are resolved before family lookup, never as literal fonts.
|
|
204
|
+
const themeKey = {"+mj-lt":"majorLatin","+mn-lt":"minorLatin","+mj-ea":"majorEastAsia","+mn-ea":"minorEastAsia","+mj-cs":"majorComplexScript","+mn-cs":"minorComplexScript"}[requested.toLowerCase()];
|
|
205
|
+
if (requested.startsWith("+") && !themeKey) throw new OPFFontError("unresolved-theme-font", `Unknown theme font '${requested}'.`, {path:style.path});
|
|
206
|
+
const family = themeKey ? options.themeFonts?.[themeKey] : requested;
|
|
207
|
+
if (!family || family.startsWith("+")) throw new OPFFontError("unresolved-theme-font", `Supply a concrete theme family for '${requested}'.`, {path:style.path});
|
|
208
|
+
validFamily(family);
|
|
209
|
+
const {face, compatibility, rule, styleFallback, via, encoding} = pickFace(faces, family, style, {policy, aliases, fallbackFamily:options.fallbackFamily});
|
|
210
|
+
// FF-31: `substitute` is true whenever the face is not the chosen family itself (policy
|
|
211
|
+
// replacement, alias, script replacement or fallback). Exporters keep writing sourceFamily.
|
|
212
|
+
const substitute = via!=="family", policyRow = substitute ? fontPolicyFor(family) : undefined;
|
|
213
|
+
// RR-38: a policy row can scale its replacement's size (Arabic Typesetting -> Noto Naskh Arabic: 0.64) so measured and drawn
|
|
214
|
+
// advances approximate the real font's. It holds only for the face the multiplier was measured on.
|
|
215
|
+
const sizeAdjust = sizeAdjustFor(policyRow, face.family);
|
|
216
|
+
const measured = rule?.measured && rule.substituteIndex===0 ? rule.measured : undefined;
|
|
217
|
+
const resolution={requestedFamily:requested,sourceFamily:family,resolvedFamily:face.family,requestedWeight:weight,resolvedWeight:face.weight,italic:face.italic,compatibility,substitute,...(styleFallback?{styleFallback:true}:{}),...(face.fontFace?{fontFace:{...face.fontFace}}:{}),...(style.path?{path:style.path}:{}),...(rule?{source:rule.source,note:rule.note,...(rule.decision?{decision:rule.decision}:{})}:{}),...(measured?{measured:{...measured}}:{}),...(policyRow?{licenseClass:policyRow.licenseClass,availability:[...policyRow.availability]}:{}),...(sizeAdjust?{sizeAdjust}:{}),...(encoding?{symbolEncoding:encoding}:{})};
|
|
218
|
+
if (face.family.toLowerCase()!==family.toLowerCase() || face.weight!==weight || face.italic!==!!style.italic) substitutions.set(JSON.stringify([requested,weight,!!style.italic,style.path]),resolution);
|
|
219
|
+
return {face,resolution};
|
|
220
|
+
};
|
|
221
|
+
const resolveFace = style => resolve(style).face;
|
|
222
|
+
const resolveStyle = style => {
|
|
223
|
+
const {face,resolution}=resolve(style),resolved={...style,fontFamily:face.family,fontWeight:face.weight,italic:face.italic};
|
|
224
|
+
// Never carry a stale selection from a previously resolved style.
|
|
225
|
+
delete resolved.fontFace;
|
|
226
|
+
delete resolved.symbolEncoding;
|
|
227
|
+
if(face.fontFace) resolved.fontFace={...face.fontFace};
|
|
228
|
+
// FF-45: the planner maps a symbol-encoded family's codes (symbol-fonts.js); the face only lends its glyphs.
|
|
229
|
+
if(resolution.symbolEncoding) resolved.symbolEncoding=resolution.symbolEncoding;
|
|
230
|
+
return resolved;
|
|
231
|
+
};
|
|
232
|
+
const metrics = (text,size,style,includeOutline=false) => {
|
|
233
|
+
if (typeof text!=='string'||!Number.isFinite(size)||size<=0) throw new OPFFontError('invalid-text-measurement','Text measurement requires a string and a positive finite font size.');
|
|
234
|
+
const {face,resolution}=resolve(style);
|
|
235
|
+
// A BCP-47 `lang` selects the font's OpenType language system, as a browser
|
|
236
|
+
// does for an element's lang (FF-19). Without it, shaping is unchanged.
|
|
237
|
+
const language=openTypeLanguage(style.lang),key=language?`${language}\u0000${text}`:text;
|
|
238
|
+
let value=face.cache.get(key);
|
|
239
|
+
if (value===undefined) {
|
|
240
|
+
if (options.strictGlyphs!==false) for(const character of text) {
|
|
241
|
+
if (/\p{Default_Ignorable_Code_Point}/u.test(character)) continue;
|
|
242
|
+
const code=character.codePointAt(0);
|
|
243
|
+
if (!face.font.hasGlyphForCodePoint(code)) {
|
|
244
|
+
// No loaded face has it: the script faces for this text are not loaded (glyph fallback needs a face to fall back to).
|
|
245
|
+
const loadedElsewhere=faces.some(other=>other!==face&&other.font.hasGlyphForCodePoint(code));
|
|
246
|
+
throw new OPFFontError("missing-glyph", `Font '${face.family}' cannot display U+${code.toString(16).toUpperCase()}.${loadedElsewhere?"":" No loaded font has it, so glyph fallback has no face to fall back to; if it is a script character, load that script's font (loadFonts with scripts: 'auto' and the presentation, or fonts.ensure(presentation))."}`, {path:style.path,fontFamily:face.family,character,...(loadedElsewhere?{}:{loadedFaceHasGlyph:false})});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if(value===undefined||includeOutline&&!Object.hasOwn(value,'outline')) {
|
|
251
|
+
// FF-31: features a policy row turns off in its replacement (Gelasio for Georgia: liga, clig), the
|
|
252
|
+
// same ones the SVG output turns off, so measured and drawn advances agree.
|
|
253
|
+
const off=disabledFeaturesFor(face.family),policyFeatures=off&&Object.fromEntries(off.map(tag=>[tag,false]));
|
|
254
|
+
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);};
|
|
255
|
+
let run;
|
|
256
|
+
try { run=shape(); }
|
|
257
|
+
catch (error) {
|
|
258
|
+
// A lookup fontkit cannot decode (GSUB type 8 in Noto Sans Mongolian) applies nothing; shape again with the rest (FF-44).
|
|
259
|
+
if (skipUndecodableLookups(face.font)) { try { run=shape(); } catch { /* the mark retry below */ } }
|
|
260
|
+
// fontkit rejects some mark-attachment lookups (null anchors, for example
|
|
261
|
+
// Gurmukhi tippi in Noto Sans Gurmukhi). Mark positioning moves marks
|
|
262
|
+
// without changing advances, so measure again without it (FF-19).
|
|
263
|
+
if (!run) {
|
|
264
|
+
try { run=shape({abvm:false,blwm:false,mark:false,mkmk:false}); }
|
|
265
|
+
catch { throw new OPFFontError("font-shaping-failed", `Font '${face.family}' cannot shape this text.`, {path:style.path,fontFamily:face.family,cause:error?.message}); }
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
value={width:run.positions.reduce((total,position)=>total+position.xAdvance,0)/face.font.unitsPerEm,...value};
|
|
269
|
+
if(includeOutline) {
|
|
270
|
+
const unit=face.font.unitsPerEm;
|
|
271
|
+
let bounds=null;
|
|
272
|
+
// FF-45: fontkit reads COLR v0 layers only; a COLRv1 colour face (Noto Color Emoji) has no outlines it can bound (its
|
|
273
|
+
// glyf outlines are empty), so its ink box is the face's em box over the run: the colour image fills the em square.
|
|
274
|
+
try { bounds=run.bbox; } catch { bounds=null; }
|
|
275
|
+
if(bounds&&[bounds.minX,bounds.minY,bounds.maxX,bounds.maxY].every(Number.isFinite)) value.outline={x:bounds.minX/unit,y:-bounds.maxY/unit,width:bounds.width/unit,height:bounds.height/unit};
|
|
276
|
+
else if(face.font.directory?.tables?.COLR&&value.width>0) { const ascent=face.font.ascent/unit,descent=face.font.descent/unit; value.outline={x:0,y:-ascent,width:value.width,height:ascent-descent}; }
|
|
277
|
+
else value.outline=null;
|
|
278
|
+
}
|
|
279
|
+
if (text.length<=2048) {
|
|
280
|
+
if (face.cache.size>=512&&!face.cache.has(key)) face.cache.delete(face.cache.keys().next().value);
|
|
281
|
+
face.cache.set(key,value);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// RR-38: the replacement is measured at its adjusted size, the one the SVG draws it at.
|
|
285
|
+
return {value,scale:resolution.sizeAdjust??1};
|
|
286
|
+
};
|
|
287
|
+
const measure=(text,size,style)=>{const {value,scale}=metrics(text,size,style);return value.width*adjustedFontSize(size,scale===1?undefined:scale);};
|
|
288
|
+
const outlineBounds=(text,size,style)=>{
|
|
289
|
+
const {value,scale}=metrics(text,size,style,true),bounds=value.outline,drawn=adjustedFontSize(size,scale===1?undefined:scale);
|
|
290
|
+
return bounds===null?null:{x:bounds.x*drawn,y:bounds.y*drawn,width:bounds.width*drawn,height:bounds.height*drawn};
|
|
291
|
+
};
|
|
292
|
+
return {
|
|
293
|
+
// resolveFont lets exporters tell a substitute from the chosen family (FF-31).
|
|
294
|
+
textMeasurement: {measure,resolveStyle,outlineBounds,resolveFont:style=>resolve(style).resolution},
|
|
295
|
+
resolveFont(style) { return resolve(style).resolution; },
|
|
296
|
+
clearSubstitutions() { substitutions.clear(); },
|
|
297
|
+
get substitutions() { return [...substitutions.values()]; },
|
|
298
|
+
// Faces flagged embed:"used" (the open pack, FF-31) are not in this eager list: supplied with selectEmbeddedFonts (or
|
|
299
|
+
// the `loadFonts` handle's embeddedFonts) they are embedded only in SVGs whose text names their family.
|
|
300
|
+
get embeddedFonts() { return embedded(face=>face.embed!=="used"); },
|
|
301
|
+
/** Parsed face metadata in entry order, without encoding font bytes. */
|
|
302
|
+
describeFaces: ()=>faces.map(face=>({family:face.family,weight:face.weight,italic:face.italic,...(face.scripts?{scripts:[...face.scripts]}:{}),...(face.fallbackOnly?{fallbackOnly:true}:{})})),
|
|
303
|
+
/** Embedded faces for which `predicate({family,weight,italic,scripts})` holds; large script faces can be left to raster fontFiles. */
|
|
304
|
+
selectEmbeddedFonts: predicate=>embedded(predicate),
|
|
305
|
+
/** Every face as `{family, data}` (the bytes the registry measures with), for the browser PDF export to embed. */
|
|
306
|
+
exportFaces: ()=>faces.map(face=>({family:face.family,data:face.data})),
|
|
307
|
+
/** True when a loaded script-pack face has a glyph for the character (FF-19 glyph fallback planning). */
|
|
308
|
+
scriptFacesCover: character => faces.some(face => face.scripts && face.font.hasGlyphForCodePoint(character.codePointAt(0))),
|
|
309
|
+
/**
|
|
310
|
+
* Register more faces in this registry (FF-19: script faces are added once a document needs
|
|
311
|
+
* them). Atomic: on an error no face is added. Returns the added faces' metadata. Text
|
|
312
|
+
* measurements planned before the call may not know the new faces; plan again afterwards.
|
|
313
|
+
*/
|
|
314
|
+
addFaces(added) {
|
|
315
|
+
if (!Array.isArray(added)) throw new OPFFontError("invalid-font-data", "addFaces requires an array of font entries.");
|
|
316
|
+
const made = added.map(makeFace);
|
|
317
|
+
register(made);
|
|
318
|
+
buildAliases();
|
|
319
|
+
substitutions.clear();
|
|
320
|
+
return made.map(face=>({family:face.family,weight:face.weight,italic:face.italic,...(face.scripts?{scripts:[...face.scripts]}:{})}));
|
|
321
|
+
},
|
|
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==="used" ? {embed:"used"} : {}),dataUrl:`data:font/${face.format};base64,${base64(face.data)}`})); }
|
|
324
|
+
}
|
package/dist/fonts-browser.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
EnsureResult,
|
|
2
3
|
FontFaceInput,
|
|
3
4
|
FontRegistry,
|
|
4
5
|
FontRegistryOptions,
|
|
6
|
+
FontsHandle,
|
|
5
7
|
} from "./fonts.js";
|
|
6
8
|
import type { AutoScriptSelection, BundledFontPackage, LazyFont, ScriptSelection } from "./fonts-node.js";
|
|
7
9
|
import type { RenderSvgOptions } from "./svg.js";
|
|
@@ -53,9 +55,29 @@ export interface BrowserFontRegistry extends FontRegistry {
|
|
|
53
55
|
/** Names of the script-pack packages loaded so far. */
|
|
54
56
|
readonly loadedScriptPackages: string[];
|
|
55
57
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
/**
|
|
59
|
+
* The fonts handle of a browser: the faces you list, registered with the document, and on demand the script and vendored faces a deck
|
|
60
|
+
* draws. Pass it as `{ fonts }` to `renderSvg`, `renderSlideSvg`, `<opf-deck>` and the player, core `paginate` and `validate`, and the editor.
|
|
61
|
+
* The registry behind it keeps the lower-level loaders (`ensureLazyFonts`, `ensureScripts`, `loadScripts`, `pendingLazyFonts`, `pendingScripts`).
|
|
62
|
+
*/
|
|
63
|
+
export interface BrowserFontsHandle extends FontsHandle {
|
|
64
|
+
readonly registry: BrowserFontRegistry;
|
|
65
|
+
/** Remove every face this handle added from the document. */
|
|
66
|
+
dispose(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Load the vendored and script faces the presentation draws (each hash-verified, all or nothing), repeating while loading changes what
|
|
69
|
+
* is needed. Cheap when nothing is needed. Besides `signal`, the call takes the `renderSvg` options the document resolves with
|
|
70
|
+
* (`catalogs`, ...) over the loader's `renderOptions`. A document that does not resolve rejects with what `renderSvg` throws for it; after
|
|
71
|
+
* `dispose()` it rejects with `font-registry-disposed`.
|
|
72
|
+
*/
|
|
73
|
+
ensure(presentation: unknown, options?: { signal?: AbortSignal } & Partial<RenderSvgOptions>): Promise<EnsureResult>;
|
|
74
|
+
/** Synchronous: the vendored files and script packages `ensure` would fetch. Empty means a render can start now. Throws what `renderSvg` throws for a document that does not resolve. */
|
|
75
|
+
pending(presentation: unknown, renderOptions?: Partial<RenderSvgOptions>): string[];
|
|
76
|
+
}
|
|
77
|
+
export declare function loadFonts(
|
|
58
78
|
options?: FontRegistryOptions & {
|
|
79
|
+
/** The faces to register at startup (the host's startup set; the rest can be `extraLazyFonts`). Each is fetched from `url` or taken from `data`, and verified when it has a `sha256`. */
|
|
80
|
+
faces?: BrowserFontInput[];
|
|
59
81
|
document?: Document;
|
|
60
82
|
fetch?: typeof fetch;
|
|
61
83
|
signal?: AbortSignal;
|
|
@@ -84,7 +106,7 @@ export declare function loadBrowserFontRegistry(
|
|
|
84
106
|
*/
|
|
85
107
|
renderOptions?: Partial<RenderSvgOptions>;
|
|
86
108
|
},
|
|
87
|
-
): Promise<
|
|
109
|
+
): Promise<BrowserFontsHandle>;
|
|
88
110
|
export declare function scriptFontPackages(scripts: ScriptSelection): BundledFontPackage[];
|
|
89
111
|
/** Hash-pinned browser entries for the script pack, served by the host from `baseUrl`. */
|
|
90
112
|
export declare function scriptFontEntries(
|
package/dist/fonts-browser.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { createFontRegistry, OPFFontError } from "./
|
|
1
|
+
import { createFontRegistry, OPFFontError } from "./font-registry.js";
|
|
2
2
|
import { lazyFontEntries, lazyFontList, normalizeExtraLazyFonts } from "./lazy-font-list.js";
|
|
3
3
|
import { lazyFacesNeeded } from "./lazy-fonts.js";
|
|
4
|
+
import { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
4
5
|
import { analyzePresentationScripts, nextFallbackPackage, scriptFontPackages, scriptPackageEntries, scriptSelectionOf, uncoveredCjkCharacters } from "./script-font-pack.js";
|
|
5
6
|
export { autoScriptSelection, detectPresentationScripts, scriptFontEntries, scriptFontPackages } from "./script-font-pack.js";
|
|
6
7
|
export { lazyFontEntries, lazyFontList, splitStartupFaces } from "./lazy-font-list.js";
|
|
@@ -20,8 +21,8 @@ async function verifyDigest(entry, data, subtle) {
|
|
|
20
21
|
);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
// The registry behind `loadFonts`: fetch only host-selected font files, then use those exact bytes for shaping and CSS.
|
|
25
|
+
async function loadRegistry(entries, options = {}) {
|
|
25
26
|
// Validate the host's extra faces first: a bad list must not leave startup faces registered in the document with no registry to dispose.
|
|
26
27
|
const extraLazy = normalizeExtraLazyFonts(options.extraLazyFonts);
|
|
27
28
|
const documentRef = options.document ?? globalThis.document;
|
|
@@ -267,3 +268,53 @@ export async function loadBrowserFontRegistry(entries, options = {}) {
|
|
|
267
268
|
},
|
|
268
269
|
});
|
|
269
270
|
}
|
|
271
|
+
|
|
272
|
+
const MAX_ENSURE_ROUNDS = 4;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The fonts handle for a browser: the faces you list (`faces`, each `{ url | data, family, weight, italic, sha256 }`, fetched and
|
|
276
|
+
* hash-verified, then registered with the document) and, on demand, the script and vendored faces a deck draws. Pass it as `{ fonts }` to
|
|
277
|
+
* `renderSvg`, `renderSlideSvg`, `<opf-deck>` and the player, core `paginate` and `validate`, and the editor. `scripts`, `scriptBaseUrl`,
|
|
278
|
+
* `lazyFontsBaseUrl`, `extraLazyFonts`, `renderOptions`, `signal`, `document`, `fetch`, `crypto` and the registry options
|
|
279
|
+
* (`substitutionPolicy`, `aliases`, `fallbackFamily`, `themeFonts`, `strictGlyphs`) are as for the registry (see `fonts-browser.d.ts`).
|
|
280
|
+
* `dispose()` removes every face from the document.
|
|
281
|
+
*/
|
|
282
|
+
export async function loadFonts({ faces = [], ...options } = {}) {
|
|
283
|
+
const registry = await loadRegistry(faces, options);
|
|
284
|
+
// The embedded list is base64 of every eager face: compute it once, and again only when faces were added.
|
|
285
|
+
let embedded, stale = true;
|
|
286
|
+
const addFaces = registry.addFaces;
|
|
287
|
+
registry.addFaces = (added) => { const result = addFaces(added); stale = true; return result; };
|
|
288
|
+
const pending = (presentation, renderOptions) => [
|
|
289
|
+
...registry.pendingLazyFonts(presentation, renderOptions).map((face) => face.file),
|
|
290
|
+
...registry.pendingScripts(presentation, renderOptions),
|
|
291
|
+
];
|
|
292
|
+
return {
|
|
293
|
+
textMeasurement: registry.textMeasurement,
|
|
294
|
+
get embeddedFonts() { if (stale) { embedded = registry.embeddedFonts; stale = false; } return embedded; },
|
|
295
|
+
registry,
|
|
296
|
+
manifest: BUNDLED_FONT_MANIFEST,
|
|
297
|
+
get substitutions() { return registry.substitutions; },
|
|
298
|
+
/**
|
|
299
|
+
* Load the vendored and script faces the presentation draws, each hash-verified, all or nothing, and repeat while loading
|
|
300
|
+
* changes what is needed (a script face can need a vendored one). Cheap when nothing is needed. Besides `signal`, the call
|
|
301
|
+
* takes the `renderSvg` options the document resolves with (`catalogs`, ...) over the loader's `renderOptions`. A document that
|
|
302
|
+
* does not resolve rejects with what `renderSvg` throws for it. After `dispose()` it rejects with `font-registry-disposed`.
|
|
303
|
+
*/
|
|
304
|
+
async ensure(presentation, callOptions = {}) {
|
|
305
|
+
const lazy = [], scripts = [];
|
|
306
|
+
let uncovered = [];
|
|
307
|
+
for (let round = 0; round < MAX_ENSURE_ROUNDS; round++) {
|
|
308
|
+
const needLazy = registry.pendingLazyFonts(presentation, callOptions).length > 0, needScripts = registry.pendingScripts(presentation, callOptions).length > 0;
|
|
309
|
+
if (!needLazy && !needScripts) break;
|
|
310
|
+
callOptions.signal?.throwIfAborted?.();
|
|
311
|
+
if (needLazy) lazy.push(...(await registry.ensureLazyFonts(presentation, callOptions)));
|
|
312
|
+
if (needScripts) { const result = await registry.ensureScripts(presentation, callOptions); scripts.push(...result.loaded); uncovered = result.uncovered; }
|
|
313
|
+
}
|
|
314
|
+
return { scripts, lazy, uncovered };
|
|
315
|
+
},
|
|
316
|
+
/** Synchronous: the files and script packages `ensure` would fetch. Empty means a render can start now. Throws what `renderSvg` throws for a document that does not resolve. */
|
|
317
|
+
pending,
|
|
318
|
+
dispose: () => registry.dispose(),
|
|
319
|
+
};
|
|
320
|
+
}
|
package/dist/fonts-node.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {FontRegistry,FontRegistryOptions,
|
|
1
|
+
import type {FontRegistry,FontRegistryOptions,FontFaceInput,FontsHandle,EnsureResult,ScriptFontProfile} from "./fonts.js";
|
|
2
2
|
import type {RenderSvgOptions} from "./svg.js";
|
|
3
3
|
/** A caller-supplied face (FF-31), for example a licensed copy of the real font. Node callers may pass a file path. */
|
|
4
4
|
export type CallerFontFace = FontFaceInput | (Omit<FontFaceInput, "data"> & {path: string});
|
|
@@ -39,10 +39,6 @@ export interface ScriptPackOptions {
|
|
|
39
39
|
export interface LazyFont { readonly package: string; readonly family: string; readonly weight: number; readonly italic: boolean; /** Path relative to the package root, for example fonts/intos/Intos-Regular.ttf. */ readonly file: string; readonly sha256: string; readonly license: string; readonly renamedFrom?: string; /** Set on a host's extra lazy face (`extraLazyFonts`, `package: "host"`): where the host serves the file from. */ readonly url?: string }
|
|
40
40
|
export type ScriptFontRegistry = FontRegistry & {fontFiles:string[]; /** Set for `scripts: "auto"`. */ scriptSelection?: AppliedScriptSelection};
|
|
41
41
|
export type NodeFontRegistry = ScriptFontRegistry & {/** The embed "used" faces this registry holds, with the files a host copies to serve them itself. */ lazyFonts: readonly LazyFont[]};
|
|
42
|
-
export declare function loadBundledFontRegistry(options?: FontRegistryOptions & ScriptPackOptions): Promise<NodeFontRegistry>;
|
|
43
|
-
|
|
44
|
-
export declare function loadOfficeFontRegistry(options?: FontRegistryOptions & ScriptPackOptions & {includeBaseFonts?:boolean; /** Leave out the open families font schemes select (FF-31); default true. */ includeOpenFonts?:boolean}): Promise<NodeFontRegistry>;
|
|
45
|
-
|
|
46
42
|
export type BundledFontPackage = Readonly<{
|
|
47
43
|
/** An npm package name, or a plain id for a vendored entry of a git upstream. */
|
|
48
44
|
name:string;
|
|
@@ -74,15 +70,35 @@ export interface BundledFontManifest {
|
|
|
74
70
|
}
|
|
75
71
|
export declare const BUNDLED_FONT_MANIFEST: BundledFontManifest;
|
|
76
72
|
export declare function scriptFontPackages(scripts: ScriptSelection): BundledFontPackage[];
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
73
|
+
/** The fonts handle of Node: the verified bundled faces (and any you supply) for layout, SVG, editor, PPTX and Node raster export. */
|
|
74
|
+
export interface NodeFontsHandle extends FontsHandle {
|
|
75
|
+
readonly registry: NodeFontRegistry;
|
|
76
|
+
readonly manifest: BundledFontManifest;
|
|
77
|
+
/** Every font file the handle holds, for `svgToPng` and `svgToPdf`; includes the script faces `ensure` loaded. */
|
|
78
|
+
readonly fontFiles: string[];
|
|
79
|
+
/** The files are already in `fontFiles`, so a conversion adds no bundled faces. */
|
|
80
|
+
readonly useBundledFonts: false;
|
|
81
|
+
/** System fonts never load: the output does not depend on the machine. */
|
|
82
|
+
readonly loadSystemFonts: false;
|
|
83
|
+
/**
|
|
84
|
+
* Load the script faces the presentation's text needs that this handle does not hold (what `scripts: "auto"` does at load, for another
|
|
85
|
+
* presentation), including the CJK face a glyph fallback needs. Cheap when nothing is missing; `embeddedFonts` and `fontFiles` then include
|
|
86
|
+
* the new faces. A package that is not installed or a script no pinned font serves is reported once through `onDiagnostic`.
|
|
87
|
+
* Resolves with the packages this call added (`scripts`; `lazy` is empty: Node loads every vendored face with its pack).
|
|
88
|
+
*/
|
|
89
|
+
ensure(presentation: unknown, options?: { onDiagnostic?: ScriptPackOptions["onDiagnostic"]; renderOptions?: Partial<RenderSvgOptions> }): Promise<EnsureResult>;
|
|
90
|
+
/** Synchronous: the script packages the presentation needs that are not loaded yet. */
|
|
91
|
+
pending(presentation: unknown, renderOptions?: Partial<RenderSvgOptions>): string[];
|
|
92
|
+
}
|
|
93
|
+
export interface LoadFontsOptions extends FontRegistryOptions, ScriptPackOptions {
|
|
94
|
+
/** `base` (default): the bundled Roboto faces. `office`: the metric and visual substitutes for the Office families plus the open families font schemes select. `none`: only the `faces` you supply. */
|
|
95
|
+
pack?: "base" | "office" | "none";
|
|
96
|
+
/** With pack "office": add the base Roboto faces too; default true. */
|
|
97
|
+
includeBaseFonts?: boolean;
|
|
98
|
+
/** With pack "office": leave out the open families (FF-31); default true. */
|
|
99
|
+
includeOpenFonts?: boolean;
|
|
100
|
+
/** Embed script faces in SVG (large); default false. */
|
|
101
|
+
embedScriptFonts?: boolean;
|
|
87
102
|
}
|
|
88
|
-
|
|
103
|
+
/** Load verified, bundled, openly licensed fonts: no system font discovery, no network. Pass the handle as `{ fonts }` to every deck-level function. */
|
|
104
|
+
export declare function loadFonts(options?: LoadFontsOptions): Promise<NodeFontsHandle>;
|