@openpresentation/opf-render 0.9.0 → 0.10.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 +62 -12
- package/dist/font-compatibility.js +32 -2
- package/dist/font-manifest.js +877 -0
- package/dist/font-policy.js +2463 -0
- package/dist/fonts-browser.d.ts +10 -0
- package/dist/fonts-browser.js +38 -16
- package/dist/fonts-node.d.ts +23 -9
- package/dist/fonts-node.js +47 -9
- package/dist/fonts.d.ts +71 -4
- package/dist/fonts.js +95 -21
- package/dist/index.d.ts +20 -2
- package/dist/script-font-pack.js +40 -0
- package/dist/script-fonts.js +426 -0
- package/dist/svg.d.ts +18 -2
- package/dist/svg.js +351 -38
- package/package.json +165 -5
package/dist/fonts-browser.d.ts
CHANGED
|
@@ -3,9 +3,12 @@ import type {
|
|
|
3
3
|
FontRegistry,
|
|
4
4
|
FontRegistryOptions,
|
|
5
5
|
} from "./fonts.js";
|
|
6
|
+
import type { BundledFontPackage, ScriptSelection } from "./fonts-node.js";
|
|
6
7
|
export interface BrowserFontInput extends Omit<FontFaceInput, "data"> {
|
|
7
8
|
data?: Uint8Array;
|
|
8
9
|
url?: string;
|
|
10
|
+
/** Reviewed SHA-256 (hex); verified with Web Crypto before the face is used. */
|
|
11
|
+
sha256?: string;
|
|
9
12
|
}
|
|
10
13
|
export declare function loadBrowserFontRegistry(
|
|
11
14
|
entries: BrowserFontInput[],
|
|
@@ -13,5 +16,12 @@ export declare function loadBrowserFontRegistry(
|
|
|
13
16
|
document?: Document;
|
|
14
17
|
fetch?: typeof fetch;
|
|
15
18
|
signal?: AbortSignal;
|
|
19
|
+
crypto?: { subtle: SubtleCrypto };
|
|
16
20
|
},
|
|
17
21
|
): Promise<FontRegistry & { dispose(): void }>;
|
|
22
|
+
export declare function scriptFontPackages(scripts: ScriptSelection): BundledFontPackage[];
|
|
23
|
+
/** Hash-pinned browser entries for the script pack, served by the host from `baseUrl`. */
|
|
24
|
+
export declare function scriptFontEntries(
|
|
25
|
+
scripts: ScriptSelection,
|
|
26
|
+
options: { baseUrl: string },
|
|
27
|
+
): (BrowserFontInput & { url: string; family: string; weight: number; italic: boolean; sha256: string; scripts: string[]; package: string; license: string })[];
|
package/dist/fonts-browser.js
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { createFontRegistry, OPFFontError } from "./fonts.js";
|
|
2
|
+
export { scriptFontEntries, scriptFontPackages } from "./script-font-pack.js";
|
|
3
|
+
|
|
4
|
+
async function verifyDigest(entry, data, subtle) {
|
|
5
|
+
if (entry.sha256 === undefined) return;
|
|
6
|
+
if (!subtle?.digest)
|
|
7
|
+
throw new OPFFontError("font-api-unavailable", "Verifying a font SHA-256 requires Web Crypto.");
|
|
8
|
+
const digest = new Uint8Array(await subtle.digest("SHA-256", data));
|
|
9
|
+
const actual = [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
10
|
+
if (actual !== String(entry.sha256).toLowerCase())
|
|
11
|
+
throw new OPFFontError(
|
|
12
|
+
"font-integrity-mismatch",
|
|
13
|
+
"The font file differs from its reviewed SHA-256; serve the pinned package.",
|
|
14
|
+
{ url: entry.url, expected: entry.sha256, actual },
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
2
18
|
/** Fetch only host-selected font files, then use those exact bytes for shaping and CSS. */
|
|
3
19
|
export async function loadBrowserFontRegistry(entries, options = {}) {
|
|
4
20
|
const documentRef = options.document ?? globalThis.document;
|
|
@@ -9,32 +25,38 @@ export async function loadBrowserFontRegistry(entries, options = {}) {
|
|
|
9
25
|
"The browser Font Loading API is required.",
|
|
10
26
|
);
|
|
11
27
|
const fetcher = options.fetch ?? globalThis.fetch;
|
|
28
|
+
const subtle = options.crypto?.subtle ?? globalThis.crypto?.subtle;
|
|
12
29
|
const faces = await Promise.all(
|
|
13
30
|
entries.map(async (entry) => {
|
|
14
|
-
|
|
15
|
-
if (!entry.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
let loaded = entry;
|
|
32
|
+
if (!entry.data) {
|
|
33
|
+
if (!entry.url)
|
|
34
|
+
throw new OPFFontError(
|
|
35
|
+
"invalid-font-source",
|
|
36
|
+
"Supply font bytes or a font-file URL.",
|
|
37
|
+
);
|
|
38
|
+
const response = await fetcher(entry.url, { signal: options.signal });
|
|
39
|
+
if (!response.ok)
|
|
40
|
+
throw new OPFFontError(
|
|
41
|
+
"font-fetch-failed",
|
|
42
|
+
`Could not load font (${response.status}).`,
|
|
43
|
+
{ url: entry.url },
|
|
44
|
+
);
|
|
45
|
+
loaded = { ...entry, data: new Uint8Array(await response.arrayBuffer()) };
|
|
46
|
+
}
|
|
47
|
+
// Script-pack entries carry the reviewed hash (FF-19); verify before use.
|
|
48
|
+
await verifyDigest(entry, loaded.data, subtle);
|
|
49
|
+
return loaded;
|
|
28
50
|
}),
|
|
29
51
|
);
|
|
30
52
|
const registry = createFontRegistry(faces, options),
|
|
31
53
|
loaded = [];
|
|
32
54
|
// Normalize family names using the registry's parsed font metadata.
|
|
33
|
-
const
|
|
55
|
+
const described = registry.describeFaces();
|
|
34
56
|
try {
|
|
35
57
|
await Promise.all(
|
|
36
58
|
faces.map(async (entry, index) => {
|
|
37
|
-
const descriptor =
|
|
59
|
+
const descriptor = described[index];
|
|
38
60
|
const face = new FontFaceRef(
|
|
39
61
|
descriptor.family,
|
|
40
62
|
entry.data.slice().buffer,
|
package/dist/fonts-node.d.ts
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
|
-
import type {FontRegistry,FontRegistryOptions,EmbeddedFont} from "./fonts.js";
|
|
2
|
-
|
|
1
|
+
import type {FontRegistry,FontRegistryOptions,EmbeddedFont,FontFaceInput} from "./fonts.js";
|
|
2
|
+
/** A caller-supplied face (FF-31), for example a licensed copy of the real font. Node callers may pass a file path. */
|
|
3
|
+
export type CallerFontFace = FontFaceInput | (Omit<FontFaceInput, "data"> & {path: string});
|
|
4
|
+
/** "all" or ISO 15924 codes (`Jpan`, `Hans`, `Hant`, `Kore`, `Arab`, `Hebr`, `Deva`, `Thai`, ...). */
|
|
5
|
+
export type ScriptSelection = "all" | string[];
|
|
6
|
+
export interface ScriptPackOptions {
|
|
7
|
+
/** Caller-supplied faces, loaded first so a licensed real font resolves as an exact face (FF-31). */
|
|
8
|
+
faces?: readonly CallerFontFace[];
|
|
9
|
+
/** Also load the optional, hash-pinned OFL Noto script pack for these scripts (FF-19). */
|
|
10
|
+
scripts?: ScriptSelection;
|
|
11
|
+
}
|
|
12
|
+
export declare function loadBundledFontRegistry(options?: FontRegistryOptions & ScriptPackOptions): Promise<FontRegistry & {fontFiles:string[]}>;
|
|
3
13
|
|
|
4
|
-
export declare function loadOfficeFontRegistry(options?: FontRegistryOptions & {includeBaseFonts?:boolean}): Promise<FontRegistry & {fontFiles:string[]}>;
|
|
14
|
+
export declare function loadOfficeFontRegistry(options?: FontRegistryOptions & ScriptPackOptions & {includeBaseFonts?:boolean}): Promise<FontRegistry & {fontFiles:string[]}>;
|
|
5
15
|
|
|
16
|
+
export type BundledFontPackage = Readonly<{
|
|
17
|
+
name:string; version:string; pack:"base"|"office"|"scripts"; source:string;
|
|
18
|
+
/** ISO 15924 scripts served by a script-pack package. */
|
|
19
|
+
scripts?: readonly string[];
|
|
20
|
+
license:string; licenseFile:string; licenseSha256:string;
|
|
21
|
+
faces:readonly Readonly<{file:string; family:string; weight:number; italic:boolean; sha256:string}>[];
|
|
22
|
+
}>;
|
|
6
23
|
export interface BundledFontManifest {
|
|
7
24
|
readonly version: number;
|
|
8
|
-
readonly packages: readonly
|
|
9
|
-
name:string; version:string; pack:"base"|"office"; source:string;
|
|
10
|
-
license:string; licenseFile:string; licenseSha256:string;
|
|
11
|
-
faces:readonly Readonly<{file:string; family:string; weight:number; italic:boolean; sha256:string}>[];
|
|
12
|
-
}>[];
|
|
25
|
+
readonly packages: readonly BundledFontPackage[];
|
|
13
26
|
}
|
|
14
27
|
export declare const BUNDLED_FONT_MANIFEST: BundledFontManifest;
|
|
28
|
+
export declare function scriptFontPackages(scripts: ScriptSelection): BundledFontPackage[];
|
|
15
29
|
export interface PreparedNodeFonts {
|
|
16
30
|
registry: FontRegistry & {fontFiles:string[]};
|
|
17
31
|
manifest: BundledFontManifest;
|
|
@@ -23,4 +37,4 @@ export interface PreparedNodeFonts {
|
|
|
23
37
|
loadSystemFonts: false;
|
|
24
38
|
};
|
|
25
39
|
}
|
|
26
|
-
export declare function prepareNodeFonts(options?: FontRegistryOptions & {pack?:"base"|"office"; includeBaseFonts?:boolean}): Promise<PreparedNodeFonts>;
|
|
40
|
+
export declare function prepareNodeFonts(options?: FontRegistryOptions & ScriptPackOptions & {pack?:"base"|"office"; includeBaseFonts?:boolean; /** Embed script faces in SVG (large); default false. */ embedScriptFonts?:boolean}): Promise<PreparedNodeFonts>;
|
package/dist/fonts-node.js
CHANGED
|
@@ -5,6 +5,8 @@ import { createHash } from "node:crypto";
|
|
|
5
5
|
import { createFontRegistry, OPFFontError } from "./fonts.js";
|
|
6
6
|
import { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
7
7
|
export { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
8
|
+
import { scriptFontPackages } from "./script-font-pack.js";
|
|
9
|
+
export { scriptFontPackages } from "./script-font-pack.js";
|
|
8
10
|
const require = createRequire(import.meta.url);
|
|
9
11
|
|
|
10
12
|
async function verifiedFile(file, expected, details) {
|
|
@@ -16,9 +18,9 @@ async function verifiedFile(file, expected, details) {
|
|
|
16
18
|
return bytes;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
async function
|
|
21
|
+
async function loadPackages(packages) {
|
|
20
22
|
const entries = [], fontFiles = [];
|
|
21
|
-
for (const pkg of
|
|
23
|
+
for (const pkg of packages) {
|
|
22
24
|
let manifestPath, installed;
|
|
23
25
|
try {
|
|
24
26
|
manifestPath = require.resolve(`${pkg.name}/package.json`);
|
|
@@ -33,39 +35,75 @@ async function loadPack(pack) {
|
|
|
33
35
|
const file = path.join(directory, face.file);
|
|
34
36
|
const data = await verifiedFile(file, face.sha256, {package:pkg.name, file:face.file});
|
|
35
37
|
fontFiles.push(file);
|
|
36
|
-
entries.push({data:new Uint8Array(data), family:face.family, weight:face.weight, italic:face.italic, license});
|
|
38
|
+
entries.push({data:new Uint8Array(data), family:face.family, weight:face.weight, italic:face.italic, license, ...(pkg.scripts ? {scripts:[...pkg.scripts]} : {})});
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
return {entries, fontFiles};
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
const loadPack = pack => loadPackages(BUNDLED_FONT_MANIFEST.packages.filter(item => item.pack === pack));
|
|
45
|
+
|
|
46
|
+
async function withScripts(loaded, scripts) {
|
|
47
|
+
if (scripts === undefined || (Array.isArray(scripts) && !scripts.length)) return loaded;
|
|
48
|
+
const extra = await loadPackages(scriptFontPackages(scripts));
|
|
49
|
+
return {entries:[...loaded.entries, ...extra.entries], fontFiles:[...loaded.fontFiles, ...extra.fontFiles]};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** FF-31: caller-supplied faces, for example the caller's own licensed Aptos files. Plain
|
|
53
|
+
* FontFaceInput entries; Node callers may pass `path` instead of `data`. They come first, so the
|
|
54
|
+
* real family resolves as an exact face ahead of any replacement. */
|
|
55
|
+
async function callerFaces(faces = []) {
|
|
56
|
+
if (!Array.isArray(faces)) throw new OPFFontError("invalid-font-data", "faces must be an array of font entries.");
|
|
57
|
+
const entries = [], fontFiles = [];
|
|
58
|
+
for (const face of faces) {
|
|
59
|
+
if (face?.data instanceof Uint8Array) { entries.push(face); continue; }
|
|
60
|
+
if (typeof face?.path !== "string") throw new OPFFontError("invalid-font-data", "Each caller face needs data (Uint8Array) or a file path.");
|
|
61
|
+
let data;
|
|
62
|
+
try { data = await readFile(face.path); } catch (error) { throw new OPFFontError("font-resource-unavailable", `Cannot read caller font file ${face.path}.`, {file:face.path, cause:error.code}); }
|
|
63
|
+
const {path:file, ...rest} = face;
|
|
64
|
+
entries.push({...rest, data:new Uint8Array(data)});
|
|
65
|
+
fontFiles.push(path.resolve(file));
|
|
66
|
+
}
|
|
67
|
+
return {entries, fontFiles};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function withFaces(loaded, faces) {
|
|
71
|
+
if (!faces?.length) return loaded;
|
|
72
|
+
const own = await callerFaces(faces);
|
|
73
|
+
return {entries:[...own.entries, ...loaded.entries], fontFiles:[...own.fontFiles, ...loaded.fontFiles]};
|
|
74
|
+
}
|
|
75
|
+
|
|
42
76
|
/** Bundled, openly licensed faces. No system font discovery or network requests. */
|
|
43
|
-
export async function loadBundledFontRegistry(options = {}) {
|
|
44
|
-
const {entries, fontFiles} = await loadPack("base");
|
|
77
|
+
export async function loadBundledFontRegistry({scripts, faces, ...options} = {}) {
|
|
78
|
+
const {entries, fontFiles} = await withFaces(await withScripts(await loadPack("base"), scripts), faces);
|
|
45
79
|
return Object.assign(createFontRegistry(entries,options),{fontFiles});
|
|
46
80
|
}
|
|
47
81
|
|
|
48
82
|
/** Six pinned open-source Office substitutes, optionally alongside the base Roboto pack. */
|
|
49
|
-
export async function loadOfficeFontRegistry(options = {}) {
|
|
83
|
+
export async function loadOfficeFontRegistry({scripts, faces, ...options} = {}) {
|
|
50
84
|
const {entries, fontFiles} = await loadPack("office");
|
|
51
85
|
if (options.includeBaseFonts !== false) {
|
|
52
86
|
const base = await loadPack("base");
|
|
53
87
|
fontFiles.push(...base.fontFiles);
|
|
54
88
|
entries.push(...base.entries);
|
|
55
89
|
}
|
|
56
|
-
|
|
90
|
+
const loaded = await withFaces(await withScripts({entries, fontFiles}, scripts), faces);
|
|
91
|
+
return Object.assign(createFontRegistry(loaded.entries,{substitutionPolicy:"metric",...options}),{fontFiles:loaded.fontFiles});
|
|
57
92
|
}
|
|
58
93
|
|
|
59
94
|
/** One set of verified font inputs for layout, SVG, editor, PPTX, and Node raster export. */
|
|
60
|
-
export async function prepareNodeFonts({pack = "base", ...options} = {}) {
|
|
95
|
+
export async function prepareNodeFonts({pack = "base", embedScriptFonts = false, ...options} = {}) {
|
|
61
96
|
if (pack !== "base" && pack !== "office") throw new OPFFontError("invalid-font-pack", "Choose the base or office font pack.", {pack});
|
|
62
97
|
const registry = await (pack === "base" ? loadBundledFontRegistry(options) : loadOfficeFontRegistry(options));
|
|
98
|
+
// 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
|
+
const embeddedFonts = registry.selectEmbeddedFonts(face => embedScriptFonts || !face.scripts);
|
|
63
101
|
return {
|
|
64
102
|
registry,
|
|
65
103
|
manifest:BUNDLED_FONT_MANIFEST,
|
|
66
104
|
options:{
|
|
67
105
|
textMeasurement:registry.textMeasurement,
|
|
68
|
-
embeddedFonts
|
|
106
|
+
embeddedFonts,
|
|
69
107
|
fontFiles:[...registry.fontFiles],
|
|
70
108
|
useBundledFonts:false,
|
|
71
109
|
loadSystemFonts:false,
|
package/dist/fonts.d.ts
CHANGED
|
@@ -1,21 +1,88 @@
|
|
|
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 }
|
|
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
3
|
export interface EmbeddedFont { family: string; weight: number; italic?: boolean; dataUrl: string; license?: string }
|
|
4
4
|
export type FontCompatibility = "exact" | "metric" | "visual" | "generic";
|
|
5
|
-
export interface
|
|
5
|
+
export interface FontReplacementMeasurement { replacement:string; meanAbsWidthDelta:number; meanWidthDelta:number; maxAbsWidthDelta:number; styles:number; reference:string }
|
|
6
|
+
export interface FontResolution {
|
|
7
|
+
requestedFamily:string; sourceFamily:string; resolvedFamily:string; requestedWeight:number; resolvedWeight:number; italic:boolean; compatibility:FontCompatibility;
|
|
8
|
+
/** True when the face is not the chosen family itself (policy replacement, alias, script replacement or fallback).
|
|
9
|
+
* Exporters keep writing sourceFamily; the face only drives measurement and drawing (FF-31). */
|
|
10
|
+
substitute:boolean;
|
|
11
|
+
/** A visual replacement without the requested italic/upright style draws the other style. */
|
|
12
|
+
styleFallback?:true;
|
|
13
|
+
fontFace?:TextStyle['fontFace']; path?:string; source?:string; note?:string;
|
|
14
|
+
/** The provisional owner decision the replacement follows, if any. */
|
|
15
|
+
decision?:string;
|
|
16
|
+
/** Measured width difference of the declared replacement against the real font. */
|
|
17
|
+
measured?:FontReplacementMeasurement;
|
|
18
|
+
licenseClass?:FontPolicyEntry['licenseClass']; availability?:FontPolicyEntry['availability'];
|
|
19
|
+
}
|
|
20
|
+
/** One OPF font policy row (snapshot of opf spec/reference/font-policy.json, decisions applied). */
|
|
21
|
+
export interface FontPolicyEntry {
|
|
22
|
+
family:string; licenseClass:"open"|"proprietary-standard"|"proprietary-nonstandard"; license:string;
|
|
23
|
+
availability:("windows"|"windows-optional"|"macos"|"office"|"office-cloud")[]; embeddableByOpf:boolean;
|
|
24
|
+
replacement:{family:string; compatibility:"metric"|"visual"; decision?:string; metricModeFallback?:true; weight?:number; measured:FontReplacementMeasurement|null; source?:string}|null;
|
|
25
|
+
alternates?:string[];
|
|
26
|
+
}
|
|
27
|
+
export declare const FONT_POLICY: readonly Readonly<FontPolicyEntry>[];
|
|
28
|
+
export declare const FONT_POLICY_SOURCE: Readonly<{version:number; path:string; sha256:string}>;
|
|
29
|
+
/** Provisional owner decisions applied to the snapshot (owner may revise). */
|
|
30
|
+
export declare const FONT_POLICY_DECISIONS: Readonly<Record<string, unknown>>;
|
|
31
|
+
export declare function fontPolicyFor(family:string): Readonly<FontPolicyEntry>|undefined;
|
|
6
32
|
export interface FontRegistryOptions {
|
|
7
33
|
aliases?: Record<string,string>; fallbackFamily?: string; strictGlyphs?: boolean;
|
|
8
34
|
substitutionPolicy?: "none" | "metric" | "visual";
|
|
9
35
|
themeFonts?: Partial<Record<"majorLatin"|"minorLatin"|"majorEastAsia"|"minorEastAsia"|"majorComplexScript"|"minorComplexScript",string>>;
|
|
10
36
|
}
|
|
11
|
-
export declare const FONT_COMPATIBILITY: readonly Readonly<{requestedFamily:string;substitutes:readonly string[];compatibility:"metric"|"visual";weights?:readonly number[];source?:string;note:string}>[];
|
|
37
|
+
export declare const FONT_COMPATIBILITY: readonly Readonly<{requestedFamily:string;substitutes:readonly string[];compatibility:"metric"|"visual";weights?:readonly number[];weight?:number;source?:string;measured?:FontReplacementMeasurement;decision?:string;metricModeFallback?:true;licenseClass?:FontPolicyEntry['licenseClass'];note:string}>[];
|
|
12
38
|
export declare const EXPERIMENTAL_FONT_CANDIDATES: readonly Readonly<{requestedFamily:string;substitute:string;source:string;note:string}>[];
|
|
39
|
+
export interface FaceDescription { family:string; weight:number; italic:boolean; scripts?:string[] }
|
|
13
40
|
export interface FontRegistry {
|
|
14
|
-
textMeasurement: TextMeasurement & {outlineBounds: NonNullable<TextMeasurement['outlineBounds']
|
|
41
|
+
textMeasurement: TextMeasurement & {outlineBounds: NonNullable<TextMeasurement['outlineBounds']>; resolveFont(style: TextStyle): FontResolution};
|
|
15
42
|
clearSubstitutions(): void;
|
|
16
43
|
resolveFont(style: TextStyle): FontResolution;
|
|
17
44
|
readonly embeddedFonts: EmbeddedFont[];
|
|
45
|
+
/** Embedded faces matching `predicate`; large script faces can stay with raster fontFiles. */
|
|
46
|
+
selectEmbeddedFonts(predicate: (face: FaceDescription) => boolean): EmbeddedFont[];
|
|
47
|
+
/** Parsed face metadata in entry order, without encoding font bytes. */
|
|
48
|
+
describeFaces(): FaceDescription[];
|
|
18
49
|
readonly substitutions: FontResolution[];
|
|
19
50
|
}
|
|
20
51
|
export declare class OPFFontError extends Error { code:string; details:Record<string,unknown>; constructor(code:string,message:string,details?:Record<string,unknown>) }
|
|
21
52
|
export declare function createFontRegistry(entries: FontFaceInput[], options?: FontRegistryOptions): FontRegistry;
|
|
53
|
+
|
|
54
|
+
/** OOXML script slot. */
|
|
55
|
+
export type ScriptRole = "latin" | "eastAsian" | "complexScript";
|
|
56
|
+
export interface ScriptFontSlots { latin: string; eastAsian: string; complexScript: string }
|
|
57
|
+
/** Core `resolveScriptFonts` output, or the same shape. `serif` prefers serif replacements. */
|
|
58
|
+
export interface ScriptFontProfile {
|
|
59
|
+
heading?: ScriptFontSlots; body?: ScriptFontSlots;
|
|
60
|
+
supplement?: { script: string; heading: string; body: string };
|
|
61
|
+
script?: string; bcp47?: string; lang?: string; direction?: "ltr" | "rtl"; rtl?: boolean;
|
|
62
|
+
languageSource?: "document" | "option" | "default"; scriptRole?: ScriptRole; serif?: boolean;
|
|
63
|
+
}
|
|
64
|
+
export interface ScriptRun { text: string; script: string; role: ScriptRole }
|
|
65
|
+
export interface PlannedRun { text: string; family: string; own: boolean; stack?: string[] }
|
|
66
|
+
export interface ScriptFonts {
|
|
67
|
+
readonly profile: ScriptFontProfile;
|
|
68
|
+
readonly rtl: boolean;
|
|
69
|
+
/** Font runs for text drawn in a resolved latin style. */
|
|
70
|
+
plan(text: string, style: TextStyle): PlannedRun[];
|
|
71
|
+
runWidths(runs: PlannedRun[], size: number, style: TextStyle): number[] | undefined;
|
|
72
|
+
/** Script-aware measurement; undefined without a measurement provider. */
|
|
73
|
+
readonly textMeasurement?: TextMeasurement;
|
|
74
|
+
}
|
|
75
|
+
export declare const SCRIPT_FONT_FAMILIES: Readonly<Record<string, Readonly<{ sans?: string; serif?: string }>>>;
|
|
76
|
+
export declare const SCRIPT_FONT_REPLACEMENTS: readonly Readonly<{requestedFamily:string;script:string;substitutes:readonly string[];compatibility:"visual";note:string}>[];
|
|
77
|
+
export declare function scriptOfCharacter(character: string): string;
|
|
78
|
+
export declare function scriptFontRole(script: string): ScriptRole;
|
|
79
|
+
export declare function itemizeScripts(text: string, profile?: ScriptFontProfile): ScriptRun[];
|
|
80
|
+
export declare function detectScripts(value: unknown, profile?: ScriptFontProfile): string[];
|
|
81
|
+
export declare function designatedFamilies(script: string, serif?: boolean): string[];
|
|
82
|
+
export declare function scriptFontAliases(families: Iterable<string>): Record<string, string>;
|
|
83
|
+
export declare function createScriptFonts(profile?: ScriptFontProfile, measurement?: TextMeasurement): ScriptFonts;
|
|
84
|
+
/** Wrap a measurement so pagination, the renderer and the editor itemize script runs identically. */
|
|
85
|
+
export declare function createScriptTextMeasurement(measurement: TextMeasurement, profile: ScriptFontProfile): TextMeasurement;
|
|
86
|
+
export declare function openTypeLanguage(tag: string | undefined): string | undefined;
|
|
87
|
+
/** Heading or body role of a style from its OPF path; undefined without a slide path. */
|
|
88
|
+
export declare function textRole(style: { path?: string } | undefined): "heading" | "body" | undefined;
|
package/dist/fonts.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { create } from "fontkit";
|
|
2
2
|
import { FONT_COMPATIBILITY } from "./font-compatibility.js";
|
|
3
|
+
import { openTypeLanguage, scriptFontAliases } from "./script-fonts.js";
|
|
4
|
+
import { fontPolicyFor } from "./font-policy.js";
|
|
5
|
+
import { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
3
6
|
export { FONT_COMPATIBILITY, EXPERIMENTAL_FONT_CANDIDATES } from "./font-compatibility.js";
|
|
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";
|
|
4
9
|
|
|
5
10
|
export class OPFFontError extends Error {
|
|
6
11
|
constructor(code, message, details = {}) { super(message); this.name = "OPFFontError"; this.code = code; this.details = details; }
|
|
@@ -15,6 +20,29 @@ function validFamily(family) {
|
|
|
15
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.");
|
|
16
21
|
return family;
|
|
17
22
|
}
|
|
23
|
+
const packFor = family => BUNDLED_FONT_MANIFEST.packages.find(pkg=>pkg.faces.some(face=>face.family.toLowerCase()===family.toLowerCase()))?.pack;
|
|
24
|
+
const percent = value => `${(value*100).toFixed(1)}%`;
|
|
25
|
+
/** FF-31: say why a family has no face and what the caller can do, from the OPF font policy. */
|
|
26
|
+
function unavailableFontError(family, style, policy) {
|
|
27
|
+
const row = fontPolicyFor(family), details = {path:style.path, fontFamily:family, substitutionPolicy:policy};
|
|
28
|
+
const hook = `supply licensed '${family}' font files (prepareNodeFonts({faces}) or createFontRegistry entries)`;
|
|
29
|
+
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"});
|
|
30
|
+
Object.assign(details, {licenseClass:row.licenseClass});
|
|
31
|
+
if (!row.replacement) {
|
|
32
|
+
const pack = packFor(family);
|
|
33
|
+
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}:{})});
|
|
34
|
+
}
|
|
35
|
+
const {family:replacement, compatibility, measured} = row.replacement;
|
|
36
|
+
const candidates = [replacement, ...(row.alternates ?? [])], packs = [...new Set(candidates.map(packFor).filter(Boolean))];
|
|
37
|
+
Object.assign(details, {replacement, replacementCompatibility:compatibility, candidates, ...(packs.length?{packs}:{}), ...(measured?{measured}:{}), ...(row.replacement.decision?{decision:row.replacement.decision}:{})});
|
|
38
|
+
const delta = measured ? ` (measured mean width difference ${percent(measured.meanAbsWidthDelta)}, max ${percent(measured.maxAbsWidthDelta)})` : " (not measured against the real font)";
|
|
39
|
+
const owner = `'${family}' is ${row.license} and OPF never bundles or embeds it.`;
|
|
40
|
+
if (compatibility === "visual" && policy !== "visual")
|
|
41
|
+
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
|
+
if (policy === "none")
|
|
43
|
+
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
|
+
}
|
|
18
46
|
/** Local font files only. The caller explicitly chooses aliases and fallback. */
|
|
19
47
|
export function createFontRegistry(entries, options = {}) {
|
|
20
48
|
if (!Array.isArray(entries) || !entries.length) throw new OPFFontError("empty-font-registry", "Supply at least one font file.");
|
|
@@ -38,7 +66,9 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
38
66
|
if (!Number.isInteger(weight) || weight < 1 || weight > 1000) throw new OPFFontError("invalid-font-weight", "Font weight must be between 1 and 1000.");
|
|
39
67
|
const signature = String.fromCharCode(...data.subarray(0,4));
|
|
40
68
|
const format = signature === "OTTO" ? "otf" : signature === "wOFF" ? "woff" : signature === "wOF2" ? "woff2" : "ttf";
|
|
41
|
-
|
|
69
|
+
// Script replacement faces (FF-19) declare the ISO 15924 scripts they serve.
|
|
70
|
+
const scripts = Array.isArray(entry.scripts) && entry.scripts.length ? Object.freeze(entry.scripts.map(String)) : undefined;
|
|
71
|
+
return {family,familyGroup,fontFace,weight,italic,font,data,format,license:entry.license,scripts,cache:new Map()};
|
|
42
72
|
});
|
|
43
73
|
const duplicates = new Set();
|
|
44
74
|
for (const face of faces) {
|
|
@@ -53,7 +83,10 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
53
83
|
familySlots.set(id,face);
|
|
54
84
|
}
|
|
55
85
|
const substitutions = new Map();
|
|
56
|
-
|
|
86
|
+
// Loaded script replacement faces stand in for the proprietary script fonts
|
|
87
|
+
// they replace (Meiryo -> Noto Sans JP). Caller aliases take precedence.
|
|
88
|
+
const scriptAliases = scriptFontAliases(new Set(faces.filter(face=>face.scripts).map(face=>face.family)));
|
|
89
|
+
const aliases = new Map(Object.entries({...scriptAliases,...options.aliases}).map(([from,to])=>[validFamily(from).toLowerCase(),validFamily(to)]));
|
|
57
90
|
const policy = options.substitutionPolicy ?? "none";
|
|
58
91
|
if (!["none","metric","visual"].includes(policy)) throw new OPFFontError("invalid-font-policy", "substitutionPolicy must be none, metric, or visual.");
|
|
59
92
|
if (options.fallbackFamily) validFamily(options.fallbackFamily);
|
|
@@ -68,33 +101,56 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
68
101
|
const family = themeKey ? options.themeFonts?.[themeKey] : requested;
|
|
69
102
|
if (!family || family.startsWith("+")) throw new OPFFontError("unresolved-theme-font", `Supply a concrete theme family for '${requested}'.`, {path:style.path});
|
|
70
103
|
validFamily(family);
|
|
71
|
-
let matching = findFamily(family), compatibility = "exact", rule;
|
|
104
|
+
let matching = findFamily(family), compatibility = "exact", rule, targetWeight = weight, styleFallback = false, via = "family";
|
|
72
105
|
const encodedSymbol = /^(wingdings(?: [23])?|webdings|symbol)$/i.test(family);
|
|
73
106
|
if (!matching.length && encodedSymbol) throw new OPFFontError("font-encoding-required", `Font '${family}' requires character mapping before substitution.`, {path:style.path,fontFamily:family});
|
|
74
107
|
if (!matching.length && aliases.has(family.toLowerCase())) {
|
|
75
|
-
matching = findFamily(aliases.get(family.toLowerCase())); compatibility = "visual";
|
|
108
|
+
matching = findFamily(aliases.get(family.toLowerCase())); compatibility = "visual"; via = "alias";
|
|
76
109
|
}
|
|
77
110
|
if (!matching.length && policy!=="none") {
|
|
78
111
|
const candidate = FONT_COMPATIBILITY.find(entry=>entry.requestedFamily.toLowerCase()===family.toLowerCase());
|
|
79
112
|
const tier = candidate?.compatibility==="metric" && !candidate.weights.includes(weight) ? "visual" : candidate?.compatibility;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
113
|
+
// A declared visual replacement is used in visual mode, and in metric mode only when the
|
|
114
|
+
// policy keeps it as a metric-mode fallback (Cambria -> Caladea); it is still reported visual.
|
|
115
|
+
// In metric mode the fallback applies only where the pre-FF-31 metric rule did: the declared
|
|
116
|
+
// replacement at weights 400 and 700, normal or italic, with an exact face weight.
|
|
117
|
+
const metricFallback = policy!=="visual" && candidate?.metricModeFallback===true && [400,700].includes(weight);
|
|
118
|
+
const allowVisual = policy==="visual" || metricFallback;
|
|
119
|
+
if (candidate && (allowVisual || tier==="metric")) {
|
|
120
|
+
// A family that names its weight (Segoe UI Semibold, Arial Black) selects that weight in
|
|
121
|
+
// the replacement; its bold style link still selects bold (FF-31).
|
|
122
|
+
const wanted = candidate.weight ? (weight>=600 ? Math.max(candidate.weight,700) : candidate.weight) : weight;
|
|
123
|
+
for (const [index, substitute] of candidate.substitutes.entries()) {
|
|
124
|
+
// Only the declared replacement can carry the row's metric claim; an alternate is visual.
|
|
125
|
+
const substituteTier = index===0 ? tier : "visual";
|
|
126
|
+
if (substituteTier==="visual" && !(policy==="visual" || metricFallback && index===0)) continue;
|
|
127
|
+
const exactWeight = substituteTier==="metric" || policy!=="visual";
|
|
128
|
+
const faces = findFamily(substitute).filter(face=>!exactWeight || face.weight===weight);
|
|
129
|
+
let available = faces.filter(face=>face.italic===!!style.italic);
|
|
130
|
+
// Visual replacements without the requested style draw the other one and say so.
|
|
131
|
+
if (!available.length && substituteTier==="visual" && policy==="visual" && faces.length) { available = faces.filter(face=>!face.italic); styleFallback = available.length>0; }
|
|
132
|
+
if (available.length) { matching=available; compatibility=substituteTier; rule={...candidate, substituteIndex:index}; targetWeight=wanted; via="replacement"; break; }
|
|
84
133
|
}
|
|
85
134
|
}
|
|
86
135
|
}
|
|
87
136
|
// Equations require an explicit math-aware choice; never fall through to body text.
|
|
88
137
|
if (!matching.length && /^(cambria math|stix two math|noto sans math)$/i.test(family)) throw new OPFFontError("math-font-required", `Supply '${family}' or an explicit math-font alias.`, {path:style.path});
|
|
89
|
-
if (!matching.length && options.fallbackFamily) { matching=findFamily(options.fallbackFamily); compatibility="generic"; }
|
|
90
|
-
if (!matching.length) throw
|
|
91
|
-
|
|
138
|
+
if (!matching.length && options.fallbackFamily) { matching=findFamily(options.fallbackFamily); compatibility="generic"; via="fallback"; }
|
|
139
|
+
if (!matching.length) throw unavailableFontError(family, style, policy);
|
|
140
|
+
const styled = styleFallback ? matching : matching.filter(face=>face.italic===!!style.italic);
|
|
141
|
+
// Script replacement faces have no italics; use upright glyphs and advances.
|
|
142
|
+
if (!styled.length && style.italic && matching.length && matching.every(face=>face.scripts)) { if (compatibility!=="generic") compatibility="visual"; }
|
|
143
|
+
else matching = styled;
|
|
92
144
|
if (!matching.length) throw new OPFFontError("font-style-unavailable", `No ${style.italic ? "italic" : "upright"} face for '${family}'.`, {path:style.path});
|
|
93
|
-
matching.sort((a,b)=>Math.abs(a.weight-
|
|
145
|
+
matching.sort((a,b)=>Math.abs(a.weight-targetWeight)-Math.abs(b.weight-targetWeight) || a.weight-b.weight);
|
|
94
146
|
const face = matching[0];
|
|
95
|
-
if (face.weight!==
|
|
96
|
-
|
|
97
|
-
|
|
147
|
+
if ((face.weight!==targetWeight || styleFallback) && compatibility!=="generic") compatibility="visual";
|
|
148
|
+
// FF-31: `substitute` is true whenever the face is not the chosen family itself (policy
|
|
149
|
+
// replacement, alias, script replacement or fallback). Exporters keep writing sourceFamily.
|
|
150
|
+
const substitute = via!=="family", policyRow = substitute ? fontPolicyFor(family) : undefined;
|
|
151
|
+
const measured = rule?.measured && rule.substituteIndex===0 ? rule.measured : undefined;
|
|
152
|
+
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]}:{})};
|
|
153
|
+
if (face.family.toLowerCase()!==family.toLowerCase() || face.weight!==weight || face.italic!==!!style.italic) substitutions.set(JSON.stringify([requested,weight,!!style.italic,style.path]),resolution);
|
|
98
154
|
return {face,resolution};
|
|
99
155
|
};
|
|
100
156
|
const resolveFace = style => resolve(style).face;
|
|
@@ -108,7 +164,10 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
108
164
|
const metrics = (text,size,style,includeOutline=false) => {
|
|
109
165
|
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.');
|
|
110
166
|
const face=resolveFace(style);
|
|
111
|
-
|
|
167
|
+
// A BCP-47 `lang` selects the font's OpenType language system, as a browser
|
|
168
|
+
// does for an element's lang (FF-19). Without it, shaping is unchanged.
|
|
169
|
+
const language=openTypeLanguage(style.lang),key=language?`${language}\u0000${text}`:text;
|
|
170
|
+
let value=face.cache.get(key);
|
|
112
171
|
if (value===undefined) {
|
|
113
172
|
if (options.strictGlyphs!==false) for(const character of text) {
|
|
114
173
|
if (/\p{Default_Ignorable_Code_Point}/u.test(character)) continue;
|
|
@@ -116,7 +175,16 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
116
175
|
}
|
|
117
176
|
}
|
|
118
177
|
if(value===undefined||includeOutline&&!Object.hasOwn(value,'outline')) {
|
|
119
|
-
const
|
|
178
|
+
const shape=features=>language?face.font.layout(text,features,undefined,language):features?face.font.layout(text,features):face.font.layout(text);
|
|
179
|
+
let run;
|
|
180
|
+
try { run=shape(); }
|
|
181
|
+
catch (error) {
|
|
182
|
+
// fontkit rejects some mark-attachment lookups (null anchors, for example
|
|
183
|
+
// Gurmukhi tippi in Noto Sans Gurmukhi). Mark positioning moves marks
|
|
184
|
+
// without changing advances, so measure again without it (FF-19).
|
|
185
|
+
try { run=shape({abvm:false,blwm:false,mark:false,mkmk:false}); }
|
|
186
|
+
catch { throw new OPFFontError("font-shaping-failed", `Font '${face.family}' cannot shape this text.`, {path:style.path,fontFamily:face.family,cause:error?.message}); }
|
|
187
|
+
}
|
|
120
188
|
value={width:run.positions.reduce((total,position)=>total+position.xAdvance,0)/face.font.unitsPerEm,...value};
|
|
121
189
|
if(includeOutline) {
|
|
122
190
|
const bounds=run.bbox,unit=face.font.unitsPerEm;
|
|
@@ -124,8 +192,8 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
124
192
|
?{x:bounds.minX/unit,y:-bounds.maxY/unit,width:bounds.width/unit,height:bounds.height/unit}:null;
|
|
125
193
|
}
|
|
126
194
|
if (text.length<=2048) {
|
|
127
|
-
if (face.cache.size>=512&&!face.cache.has(
|
|
128
|
-
face.cache.set(
|
|
195
|
+
if (face.cache.size>=512&&!face.cache.has(key)) face.cache.delete(face.cache.keys().next().value);
|
|
196
|
+
face.cache.set(key,value);
|
|
129
197
|
}
|
|
130
198
|
}
|
|
131
199
|
return value;
|
|
@@ -136,10 +204,16 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
136
204
|
return bounds===null?null:{x:bounds.x*size,y:bounds.y*size,width:bounds.width*size,height:bounds.height*size};
|
|
137
205
|
};
|
|
138
206
|
return {
|
|
139
|
-
|
|
207
|
+
// resolveFont lets exporters tell a substitute from the chosen family (FF-31).
|
|
208
|
+
textMeasurement: {measure,resolveStyle,outlineBounds,resolveFont:style=>resolve(style).resolution},
|
|
140
209
|
resolveFont(style) { return resolve(style).resolution; },
|
|
141
210
|
clearSubstitutions() { substitutions.clear(); },
|
|
142
211
|
get substitutions() { return [...substitutions.values()]; },
|
|
143
|
-
get embeddedFonts() { return
|
|
212
|
+
get embeddedFonts() { return embedded(()=>true); },
|
|
213
|
+
/** 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]}:{})})),
|
|
215
|
+
/** Embedded faces for which `predicate({family,weight,italic,scripts})` holds; large script faces can be left to raster fontFiles. */
|
|
216
|
+
selectEmbeddedFonts: predicate=>embedded(predicate),
|
|
144
217
|
};
|
|
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)}`})); }
|
|
145
219
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EmbeddedFont } from "./fonts.js";
|
|
1
|
+
import type { EmbeddedFont, ScriptFonts } from "./fonts.js";
|
|
2
2
|
import type { SlideComposition, LayoutDiagnostic, TextMeasurement } from "@openpresentation/opf/composition";
|
|
3
3
|
export declare const packageName = "@openpresentation/opf-render";
|
|
4
4
|
|
|
@@ -32,9 +32,16 @@ export declare const engineDefaults: Readonly<{
|
|
|
32
32
|
}>;
|
|
33
33
|
|
|
34
34
|
export type RenderDiagnostic = LayoutDiagnostic | {
|
|
35
|
-
code: "unsupported-pattern" | "date-needs-value";
|
|
35
|
+
code: "unsupported-pattern" | "date-needs-value" | "language-preview-unavailable" | "language-preview-unresolved" | "paragraph-direction-unavailable";
|
|
36
36
|
path: string;
|
|
37
37
|
message: string;
|
|
38
|
+
} | {
|
|
39
|
+
/** A font-scheme id matched no record; the default font scheme (`aptos`) was used as the base. */
|
|
40
|
+
code: "unresolved-font-scheme";
|
|
41
|
+
path: string;
|
|
42
|
+
message: string;
|
|
43
|
+
id: string;
|
|
44
|
+
fallback: string;
|
|
38
45
|
} | {
|
|
39
46
|
code: "unresolved-asset";
|
|
40
47
|
path: string;
|
|
@@ -48,12 +55,19 @@ export type RenderDiagnostic = LayoutDiagnostic | {
|
|
|
48
55
|
|
|
49
56
|
export interface RenderSvgOptions {
|
|
50
57
|
textMeasurement?: TextMeasurement;
|
|
58
|
+
/** Unscaled reference-pixel clearance around supplied vector text outlines; default 1. */
|
|
59
|
+
textRasterPadding?: number;
|
|
51
60
|
embeddedFonts?: EmbeddedFont[];
|
|
52
61
|
strictAssets?: boolean;
|
|
53
62
|
imageResolver?: (src: string | undefined, context: { asset: unknown; path: string }) => string | null | undefined;
|
|
54
63
|
onDiagnostic?: (diagnostic: RenderDiagnostic) => void;
|
|
55
64
|
validate?: boolean;
|
|
56
65
|
slideIndex?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Today's calendar date (ISO YYYY-MM-DD) for `date: true` header/footer furniture. The renderer
|
|
68
|
+
* never reads a clock; without it a current date is reported as unresolved content.
|
|
69
|
+
*/
|
|
70
|
+
date?: string;
|
|
57
71
|
trace?: boolean;
|
|
58
72
|
catalogs?: Record<string, { records?: unknown[] } | unknown[]>;
|
|
59
73
|
catalogSources?: Record<string, { records?: unknown[] } | unknown[]>;
|
|
@@ -82,6 +96,10 @@ export interface ResolvedPresentation {
|
|
|
82
96
|
|
|
83
97
|
export interface ResolvedSlide {
|
|
84
98
|
geometry: SlideComposition;
|
|
99
|
+
/** Script font plan for this slide (FF-19): slots, language tags and direction. */
|
|
100
|
+
scriptFonts: ScriptFonts;
|
|
101
|
+
/** The script-aware measurement composition used; undefined for estimated layout. */
|
|
102
|
+
textMeasurement?: TextMeasurement;
|
|
85
103
|
index: number;
|
|
86
104
|
path: string;
|
|
87
105
|
slide: unknown;
|