@openpresentation/opf-render 0.7.0 → 0.8.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 +62 -6
- package/dist/font-manifest.js +326 -0
- package/dist/fonts-node.d.ts +23 -1
- package/dist/fonts-node.js +59 -29
- package/dist/fonts.d.ts +2 -2
- package/dist/fonts.js +46 -12
- package/dist/index.d.ts +16 -1
- package/dist/raster.js +6 -18
- package/dist/svg.d.ts +18 -1
- package/dist/svg.js +149 -124
- package/package.json +19 -8
package/dist/fonts.js
CHANGED
|
@@ -25,12 +25,20 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
25
25
|
try { font = create(data, entry.postscriptName); } catch (error) { throw new OPFFontError("invalid-font-data", error.message); }
|
|
26
26
|
if (!font?.layout || !font.unitsPerEm) throw new OPFFontError("font-collection", "Select one font from a collection with postscriptName.");
|
|
27
27
|
const family = validFamily(entry.family ?? font.familyName);
|
|
28
|
+
// OpenType groups Medium/SemiBold/ExtraBold under a preferred family, while
|
|
29
|
+
// retaining separate legacy families for four-style native font selectors.
|
|
30
|
+
// An explicit caller rename owns its namespace and must not add this alias.
|
|
31
|
+
const familyGroup = entry.family && entry.family.toLowerCase() !== font.familyName?.toLowerCase()
|
|
32
|
+
? family : validFamily(font.getName?.('preferredFamily', 'en') ?? family);
|
|
33
|
+
const linking = font['OS/2']?.fsSelection ?? font.head?.macStyle;
|
|
34
|
+
const fontFace = linking && typeof linking.bold === 'boolean' && typeof linking.italic === 'boolean'
|
|
35
|
+
? {family:validFamily(font.getName?.('fontFamily', 'en') ?? font.familyName ?? family),bold:linking.bold,italic:linking.italic} : undefined;
|
|
28
36
|
const weight = entry.weight ?? 400;
|
|
29
37
|
const italic = entry.italic ?? !!font.italicAngle;
|
|
30
38
|
if (!Number.isInteger(weight) || weight < 1 || weight > 1000) throw new OPFFontError("invalid-font-weight", "Font weight must be between 1 and 1000.");
|
|
31
39
|
const signature = String.fromCharCode(...data.subarray(0,4));
|
|
32
40
|
const format = signature === "OTTO" ? "otf" : signature === "wOFF" ? "woff" : signature === "wOF2" ? "woff2" : "ttf";
|
|
33
|
-
return {family,weight,italic,font,data,format,license:entry.license,cache:new Map()};
|
|
41
|
+
return {family,familyGroup,fontFace,weight,italic,font,data,format,license:entry.license,cache:new Map()};
|
|
34
42
|
});
|
|
35
43
|
const duplicates = new Set();
|
|
36
44
|
for (const face of faces) {
|
|
@@ -38,12 +46,18 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
38
46
|
if (duplicates.has(id)) throw new OPFFontError("duplicate-font-face", `Duplicate face: ${id}`);
|
|
39
47
|
duplicates.add(id);
|
|
40
48
|
}
|
|
49
|
+
const familySlots = new Map();
|
|
50
|
+
for (const face of faces) for (const family of new Set([face.family,face.familyGroup])) {
|
|
51
|
+
const id=key(family,face.weight,face.italic),previous=familySlots.get(id);
|
|
52
|
+
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});
|
|
53
|
+
familySlots.set(id,face);
|
|
54
|
+
}
|
|
41
55
|
const substitutions = new Map();
|
|
42
56
|
const aliases = new Map(Object.entries(options.aliases ?? {}).map(([from,to])=>[validFamily(from).toLowerCase(),validFamily(to)]));
|
|
43
57
|
const policy = options.substitutionPolicy ?? "none";
|
|
44
58
|
if (!["none","metric","visual"].includes(policy)) throw new OPFFontError("invalid-font-policy", "substitutionPolicy must be none, metric, or visual.");
|
|
45
59
|
if (options.fallbackFamily) validFamily(options.fallbackFamily);
|
|
46
|
-
const findFamily = family => faces.filter(face=>face.family.toLowerCase()===family.toLowerCase());
|
|
60
|
+
const findFamily = family => faces.filter(face=>face.family.toLowerCase()===family.toLowerCase() || face.familyGroup.toLowerCase()===family.toLowerCase());
|
|
47
61
|
const resolve = style => {
|
|
48
62
|
const requested = validFamily(style?.fontFamily);
|
|
49
63
|
const weight = style.fontWeight ?? 400;
|
|
@@ -79,30 +93,50 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
79
93
|
matching.sort((a,b)=>Math.abs(a.weight-weight)-Math.abs(b.weight-weight) || a.weight-b.weight);
|
|
80
94
|
const face = matching[0];
|
|
81
95
|
if (face.weight!==weight && compatibility!=="generic") compatibility="visual";
|
|
82
|
-
const resolution={requestedFamily:requested,sourceFamily:family,resolvedFamily:face.family,requestedWeight:weight,resolvedWeight:face.weight,italic:face.italic,compatibility,...(style.path?{path:style.path}:{}),...(rule?{source:rule.source,note:rule.note}:{})};
|
|
96
|
+
const resolution={requestedFamily:requested,sourceFamily:family,resolvedFamily:face.family,requestedWeight:weight,resolvedWeight:face.weight,italic:face.italic,compatibility,...(face.fontFace?{fontFace:{...face.fontFace}}:{}),...(style.path?{path:style.path}:{}),...(rule?{source:rule.source,note:rule.note}:{})};
|
|
83
97
|
if (face.family.toLowerCase()!==family.toLowerCase() || face.weight!==weight) substitutions.set(JSON.stringify([requested,weight,!!style.italic,style.path]),resolution);
|
|
84
98
|
return {face,resolution};
|
|
85
99
|
};
|
|
86
100
|
const resolveFace = style => resolve(style).face;
|
|
87
|
-
const resolveStyle = style => {
|
|
88
|
-
|
|
101
|
+
const resolveStyle = style => {
|
|
102
|
+
const face=resolveFace(style),resolved={...style,fontFamily:face.family,fontWeight:face.weight,italic:face.italic};
|
|
103
|
+
// Never carry a stale selection from a previously resolved style.
|
|
104
|
+
delete resolved.fontFace;
|
|
105
|
+
if(face.fontFace) resolved.fontFace={...face.fontFace};
|
|
106
|
+
return resolved;
|
|
107
|
+
};
|
|
108
|
+
const metrics = (text,size,style,includeOutline=false) => {
|
|
109
|
+
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.');
|
|
89
110
|
const face=resolveFace(style);
|
|
90
|
-
let
|
|
91
|
-
if (
|
|
111
|
+
let value=face.cache.get(text);
|
|
112
|
+
if (value===undefined) {
|
|
92
113
|
if (options.strictGlyphs!==false) for(const character of text) {
|
|
93
114
|
if (/\p{Default_Ignorable_Code_Point}/u.test(character)) continue;
|
|
94
115
|
if (!face.font.hasGlyphForCodePoint(character.codePointAt(0))) throw new OPFFontError("missing-glyph", `Font '${face.family}' cannot display U+${character.codePointAt(0).toString(16).toUpperCase()}.`, {path:style.path,fontFamily:face.family,character});
|
|
95
116
|
}
|
|
96
|
-
|
|
117
|
+
}
|
|
118
|
+
if(value===undefined||includeOutline&&!Object.hasOwn(value,'outline')) {
|
|
119
|
+
const run=face.font.layout(text);
|
|
120
|
+
value={width:run.positions.reduce((total,position)=>total+position.xAdvance,0)/face.font.unitsPerEm,...value};
|
|
121
|
+
if(includeOutline) {
|
|
122
|
+
const bounds=run.bbox,unit=face.font.unitsPerEm;
|
|
123
|
+
value.outline=[bounds.minX,bounds.minY,bounds.maxX,bounds.maxY].every(Number.isFinite)
|
|
124
|
+
?{x:bounds.minX/unit,y:-bounds.maxY/unit,width:bounds.width/unit,height:bounds.height/unit}:null;
|
|
125
|
+
}
|
|
97
126
|
if (text.length<=2048) {
|
|
98
|
-
if (face.cache.size>=512) face.cache.delete(face.cache.keys().next().value);
|
|
99
|
-
face.cache.set(text,
|
|
127
|
+
if (face.cache.size>=512&&!face.cache.has(text)) face.cache.delete(face.cache.keys().next().value);
|
|
128
|
+
face.cache.set(text,value);
|
|
100
129
|
}
|
|
101
130
|
}
|
|
102
|
-
return
|
|
131
|
+
return value;
|
|
132
|
+
};
|
|
133
|
+
const measure=(text,size,style)=>metrics(text,size,style).width*size;
|
|
134
|
+
const outlineBounds=(text,size,style)=>{
|
|
135
|
+
const bounds=metrics(text,size,style,true).outline;
|
|
136
|
+
return bounds===null?null:{x:bounds.x*size,y:bounds.y*size,width:bounds.width*size,height:bounds.height*size};
|
|
103
137
|
};
|
|
104
138
|
return {
|
|
105
|
-
textMeasurement: {measure,resolveStyle},
|
|
139
|
+
textMeasurement: {measure,resolveStyle,outlineBounds},
|
|
106
140
|
resolveFont(style) { return resolve(style).resolution; },
|
|
107
141
|
clearSubstitutions() { substitutions.clear(); },
|
|
108
142
|
get substitutions() { return [...substitutions.values()]; },
|
package/dist/index.d.ts
CHANGED
|
@@ -31,12 +31,27 @@ export declare const engineDefaults: Readonly<{
|
|
|
31
31
|
chartTypes: readonly ["stacked-column-3x", "stacked-area-3x", "line-with-markers-3x"];
|
|
32
32
|
}>;
|
|
33
33
|
|
|
34
|
+
export type RenderDiagnostic = LayoutDiagnostic | {
|
|
35
|
+
code: "unsupported-pattern" | "date-needs-value";
|
|
36
|
+
path: string;
|
|
37
|
+
message: string;
|
|
38
|
+
} | {
|
|
39
|
+
code: "unresolved-asset";
|
|
40
|
+
path: string;
|
|
41
|
+
message: string;
|
|
42
|
+
reason: "missing-reference" | "missing-source" | "unsupported-source";
|
|
43
|
+
source?: string;
|
|
44
|
+
assetId?: string;
|
|
45
|
+
description: string;
|
|
46
|
+
placeholder: "label" | "icon";
|
|
47
|
+
};
|
|
48
|
+
|
|
34
49
|
export interface RenderSvgOptions {
|
|
35
50
|
textMeasurement?: TextMeasurement;
|
|
36
51
|
embeddedFonts?: EmbeddedFont[];
|
|
37
52
|
strictAssets?: boolean;
|
|
38
53
|
imageResolver?: (src: string | undefined, context: { asset: unknown; path: string }) => string | null | undefined;
|
|
39
|
-
onDiagnostic?: (diagnostic:
|
|
54
|
+
onDiagnostic?: (diagnostic: RenderDiagnostic) => void;
|
|
40
55
|
slideIndex?: number;
|
|
41
56
|
trace?: boolean;
|
|
42
57
|
catalogs?: Record<string, { records?: unknown[] } | unknown[]>;
|
package/dist/raster.js
CHANGED
|
@@ -98,29 +98,17 @@ async function loadPdfLib() {
|
|
|
98
98
|
|
|
99
99
|
async function bundledFontFiles() {
|
|
100
100
|
if (!bundledFontFilesCache) {
|
|
101
|
-
bundledFontFilesCache = resolveBundledFontFiles()
|
|
101
|
+
bundledFontFilesCache = resolveBundledFontFiles().catch(error => {
|
|
102
|
+
bundledFontFilesCache = null;
|
|
103
|
+
throw error;
|
|
104
|
+
});
|
|
102
105
|
}
|
|
103
106
|
return bundledFontFilesCache;
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
async function resolveBundledFontFiles() {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const path = await import("node:path");
|
|
110
|
-
const require = createRequire(import.meta.url);
|
|
111
|
-
const roboto = path.dirname(require.resolve("@expo-google-fonts/roboto/package.json"));
|
|
112
|
-
const robotoMono = path.dirname(require.resolve("@expo-google-fonts/roboto-mono/package.json"));
|
|
113
|
-
return [
|
|
114
|
-
path.join(roboto, "400Regular/Roboto_400Regular.ttf"),
|
|
115
|
-
path.join(roboto, "500Medium/Roboto_500Medium.ttf"),
|
|
116
|
-
path.join(roboto, "700Bold/Roboto_700Bold.ttf"),
|
|
117
|
-
path.join(roboto, "800ExtraBold/Roboto_800ExtraBold.ttf"),
|
|
118
|
-
path.join(robotoMono, "400Regular/RobotoMono_400Regular.ttf"),
|
|
119
|
-
path.join(robotoMono, "700Bold/RobotoMono_700Bold.ttf")
|
|
120
|
-
];
|
|
121
|
-
} catch {
|
|
122
|
-
return [];
|
|
123
|
-
}
|
|
110
|
+
const {loadBundledFontRegistry} = await import('./fonts-node.js');
|
|
111
|
+
return (await loadBundledFontRegistry()).fontFiles;
|
|
124
112
|
}
|
|
125
113
|
|
|
126
114
|
function normalizeSvgList(value) {
|
package/dist/svg.d.ts
CHANGED
|
@@ -31,12 +31,29 @@ export declare const engineDefaults: Readonly<{
|
|
|
31
31
|
chartTypes: readonly ["stacked-column-3x", "stacked-area-3x", "line-with-markers-3x"];
|
|
32
32
|
}>;
|
|
33
33
|
|
|
34
|
+
export type RenderDiagnostic = LayoutDiagnostic | {
|
|
35
|
+
code: "unsupported-pattern" | "date-needs-value";
|
|
36
|
+
path: string;
|
|
37
|
+
message: string;
|
|
38
|
+
} | {
|
|
39
|
+
code: "unresolved-asset";
|
|
40
|
+
path: string;
|
|
41
|
+
message: string;
|
|
42
|
+
reason: "missing-reference" | "missing-source" | "unsupported-source";
|
|
43
|
+
source?: string;
|
|
44
|
+
assetId?: string;
|
|
45
|
+
description: string;
|
|
46
|
+
placeholder: "label" | "icon";
|
|
47
|
+
};
|
|
48
|
+
|
|
34
49
|
export interface RenderSvgOptions {
|
|
35
50
|
textMeasurement?: TextMeasurement;
|
|
51
|
+
/** Unscaled reference-pixel clearance around supplied vector text outlines; default 1. */
|
|
52
|
+
textRasterPadding?: number;
|
|
36
53
|
embeddedFonts?: EmbeddedFont[];
|
|
37
54
|
strictAssets?: boolean;
|
|
38
55
|
imageResolver?: (src: string | undefined, context: { asset: unknown; path: string }) => string | null | undefined;
|
|
39
|
-
onDiagnostic?: (diagnostic:
|
|
56
|
+
onDiagnostic?: (diagnostic: RenderDiagnostic) => void;
|
|
40
57
|
slideIndex?: number;
|
|
41
58
|
trace?: boolean;
|
|
42
59
|
catalogs?: Record<string, { records?: unknown[] } | unknown[]>;
|