@vyaz/core 0.0.4 → 0.0.6
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 +156 -0
- package/dist/compile/DocumentCompiler.d.ts +40 -0
- package/dist/index.browser.d.ts +28 -0
- package/dist/index.browser.js +18 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +11 -147930
- package/dist/layout/AutoFitEngine.d.ts +44 -0
- package/dist/layout/LineBoxValidator.d.ts +25 -0
- package/dist/layout/ParagraphLayoutEngine.d.ts +46 -0
- package/dist/layout/PositioningEngine.d.ts +68 -0
- package/dist/layout/TextFrameLayoutEngine.d.ts +50 -0
- package/dist/layout/estimateWidth.d.ts +36 -0
- package/dist/measure/FontEngine.d.ts +47 -0
- package/dist/measure/FontMetricsProvider.d.ts +49 -0
- package/dist/measure/FontNotFoundError.d.ts +6 -0
- package/dist/measure/SystemFontRegistry.d.ts +46 -0
- package/dist/measure/canvas-polyfill.d.ts +30 -0
- package/dist/types/Document.d.ts +593 -0
- package/dist/types/FontTypes.d.ts +61 -0
- package/dist/types/LayoutTypes.d.ts +128 -0
- package/dist/utils/env.d.ts +11 -0
- package/dist/utils/font.d.ts +16 -0
- package/dist/utils/groupLinesByParagraph.d.ts +44 -0
- package/dist/utils/list.d.ts +41 -0
- package/dist/utils/textTransform.d.ts +32 -0
- package/package.json +30 -13
- package/src/compile/DocumentCompiler.ts +0 -136
- package/src/index.ts +0 -80
- package/src/layout/AutoFitEngine.ts +0 -101
- package/src/layout/LineBoxValidator.ts +0 -162
- package/src/layout/ParagraphLayoutEngine.ts +0 -202
- package/src/layout/PositioningEngine.ts +0 -401
- package/src/layout/TextFrameLayoutEngine.ts +0 -91
- package/src/measure/FontMetricsProvider.ts +0 -226
- package/src/measure/FontNotFoundError.ts +0 -16
- package/src/measure/SystemFontRegistry.ts +0 -151
- package/src/measure/canvas-polyfill.d.ts +0 -6
- package/src/measure/canvas-polyfill.ts +0 -235
- package/src/measure/fontkit.d.ts +0 -44
- package/src/types/Document.ts +0 -540
- package/src/types/FontTypes.ts +0 -74
- package/src/types/LayoutTypes.ts +0 -141
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FontMetricsProvider.ts — isomorphic font metrics provider.
|
|
3
|
-
*
|
|
4
|
-
* Strategy (priority):
|
|
5
|
-
* 1. fontkit (Node.js) — from registered buffer
|
|
6
|
-
* - 'browser' mode: font.hhea.ascent / font.hhea.descent
|
|
7
|
-
* - 'office' mode: font['OS/2'].usWinAscent / font['OS/2'].usWinDescent
|
|
8
|
-
* 2. Canvas TextMetrics (browser)
|
|
9
|
-
* 3. Fallback (fontSize * 0.85 / 0.15)
|
|
10
|
-
*
|
|
11
|
-
* Uses FontRegistry for font registration.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { FontMetrics, IFontMetricsProvider } from '../types/FontTypes.js';
|
|
15
|
-
import { enableOfficeTextMeasure, disableOfficeTextMeasure, registerCanvasFont } from './canvas-polyfill.js';
|
|
16
|
-
import { FontNotFoundError } from './FontNotFoundError.js';
|
|
17
|
-
|
|
18
|
-
// ── Weight normalisation ─────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Map named font weights to numeric strings.
|
|
22
|
-
* Both directions work: 'bold' → '700', 700 → '700', '700' → '700'.
|
|
23
|
-
*/
|
|
24
|
-
const WEIGHT_TO_NUM: Record<string, string> = {
|
|
25
|
-
thin: '100',
|
|
26
|
-
hairline: '100',
|
|
27
|
-
ultralight: '200',
|
|
28
|
-
extralight: '200',
|
|
29
|
-
light: '300',
|
|
30
|
-
normal: '400',
|
|
31
|
-
medium: '500',
|
|
32
|
-
semibold: '600',
|
|
33
|
-
demibold: '600',
|
|
34
|
-
bold: '700',
|
|
35
|
-
ultrabold: '800',
|
|
36
|
-
extrabold: '800',
|
|
37
|
-
heavy: '900',
|
|
38
|
-
black: '900',
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/** Normalise weight to a numeric string ("400", "700", etc.). */
|
|
42
|
-
function normaliseWeight(weight: string): string {
|
|
43
|
-
const lower = weight.toLowerCase();
|
|
44
|
-
const mapped = WEIGHT_TO_NUM[lower];
|
|
45
|
-
if (mapped) return mapped;
|
|
46
|
-
return weight; // already numeric or unrecognised — pass through
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Cache key: "${family}_${weight}_${style}" with normalised weight. */
|
|
50
|
-
function cacheKey(family: string, weight: string, style: string): string {
|
|
51
|
-
return `${family}_${normaliseWeight(weight)}_${style}`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export class FontMetricsProvider implements IFontMetricsProvider {
|
|
55
|
-
private cache = new Map<string, any>(); // fontkit.Font | undefined
|
|
56
|
-
private metricsCache = new Map<string, FontMetrics>();
|
|
57
|
-
private mode: 'browser' | 'office' = 'browser';
|
|
58
|
-
|
|
59
|
-
// ── Mode ──────────────────────────────────────────────────────────
|
|
60
|
-
|
|
61
|
-
setMode(mode: 'browser' | 'office'): void {
|
|
62
|
-
if (this.mode === mode) return;
|
|
63
|
-
this.mode = mode;
|
|
64
|
-
// Invalidate metrics cache on mode change
|
|
65
|
-
this.metricsCache.clear();
|
|
66
|
-
|
|
67
|
-
// Toggle ctx.measureText for pretext (canvas-based line breaking)
|
|
68
|
-
if (mode === 'office') {
|
|
69
|
-
enableOfficeTextMeasure(this.cache); // fontkit-based hmtx advance widths
|
|
70
|
-
} else {
|
|
71
|
-
disableOfficeTextMeasure(); // original Canvas 2D measureText
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
getMode(): 'browser' | 'office' {
|
|
76
|
-
return this.mode;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ── FontRegistry ──────────────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Register a binary font for use with fontkit.
|
|
83
|
-
* In browser — no-op (fonts are registered via CSS @font-face).
|
|
84
|
-
*
|
|
85
|
-
* @param sourcePath — if provided, also registers with @napi-rs/canvas for Node.js canvas measureText
|
|
86
|
-
*/
|
|
87
|
-
async registerFont(
|
|
88
|
-
family: string,
|
|
89
|
-
options: { weight?: string; style?: string },
|
|
90
|
-
source: string | Buffer,
|
|
91
|
-
sourcePath?: string,
|
|
92
|
-
): Promise<void> {
|
|
93
|
-
try {
|
|
94
|
-
// Dynamic ESM import — fontkit may not be available in browser
|
|
95
|
-
const fontkit = await import('fontkit');
|
|
96
|
-
const buffer = typeof source === 'string' ? Buffer.from(source) : source;
|
|
97
|
-
// @ts-ignore fontkit CJS/ESM compatibility
|
|
98
|
-
const fk = fontkit.default || fontkit;
|
|
99
|
-
const font = fk.create(buffer);
|
|
100
|
-
const key = cacheKey(
|
|
101
|
-
family,
|
|
102
|
-
options.weight || 'normal',
|
|
103
|
-
options.style || 'normal',
|
|
104
|
-
);
|
|
105
|
-
this.cache.set(key, font);
|
|
106
|
-
// Invalidate metrics for this font
|
|
107
|
-
this.metricsCache.delete(key);
|
|
108
|
-
|
|
109
|
-
// Also register with @napi-rs/canvas so ctx.measureText() uses real fonts
|
|
110
|
-
if (sourcePath) {
|
|
111
|
-
registerCanvasFont(sourcePath, family);
|
|
112
|
-
}
|
|
113
|
-
} catch {
|
|
114
|
-
// fontkit not available (browser) — no-op
|
|
115
|
-
}
|
|
116
|
-
return Promise.resolve();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ── Font object access (for per-glyph advance) ────────────────────
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Get fontkit font object for per-character calculations.
|
|
123
|
-
* Returns undefined if font is not registered or fontkit unavailable.
|
|
124
|
-
*/
|
|
125
|
-
getFont(family: string, weight = 'normal', style = 'normal'): any | undefined {
|
|
126
|
-
const key = cacheKey(family, weight, style);
|
|
127
|
-
return this.cache.get(key);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ── Metrics retrieval ─────────────────────────────────────────────
|
|
131
|
-
|
|
132
|
-
getMetrics(
|
|
133
|
-
fontFamily: string,
|
|
134
|
-
fontSize: number,
|
|
135
|
-
weight = 'normal',
|
|
136
|
-
style = 'normal',
|
|
137
|
-
): FontMetrics {
|
|
138
|
-
const key = cacheKey(fontFamily, weight, style);
|
|
139
|
-
|
|
140
|
-
// Metrics cache (depends on fontSize, so include in key)
|
|
141
|
-
const metricsKey = `${key}_${fontSize}_${this.mode}`;
|
|
142
|
-
const cached = this.metricsCache.get(metricsKey);
|
|
143
|
-
if (cached) return cached;
|
|
144
|
-
|
|
145
|
-
let metrics: FontMetrics;
|
|
146
|
-
|
|
147
|
-
// Strategy 1: fontkit
|
|
148
|
-
const font = this.cache.get(key);
|
|
149
|
-
|
|
150
|
-
if (font) {
|
|
151
|
-
const scale = fontSize / font.unitsPerEm;
|
|
152
|
-
|
|
153
|
-
if (this.mode === 'office') {
|
|
154
|
-
// Office mode: OS/2.usWinAscent + usWinDescent
|
|
155
|
-
const os2 = font['OS/2'];
|
|
156
|
-
let ascent: number;
|
|
157
|
-
let descent: number;
|
|
158
|
-
let sourceTable: 'OS/2' | 'hhea';
|
|
159
|
-
|
|
160
|
-
if (os2 && os2.winAscent != null && os2.winDescent != null) {
|
|
161
|
-
ascent = os2.winAscent * scale * 1.078;
|
|
162
|
-
descent = Math.abs(os2.winDescent) * scale * 1.078;
|
|
163
|
-
sourceTable = 'OS/2';
|
|
164
|
-
} else {
|
|
165
|
-
// Fallback to hhea if OS/2 is absent
|
|
166
|
-
ascent = font.ascent * scale;
|
|
167
|
-
descent = Math.abs(font.descent) * scale;
|
|
168
|
-
sourceTable = 'hhea';
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
metrics = {
|
|
172
|
-
ascent,
|
|
173
|
-
descent,
|
|
174
|
-
capHeight: (font.capHeight ?? ascent) * scale,
|
|
175
|
-
unitsPerEm: font.unitsPerEm,
|
|
176
|
-
sourceTable,
|
|
177
|
-
};
|
|
178
|
-
} else {
|
|
179
|
-
// Browser mode: hhea.ascender/descender
|
|
180
|
-
metrics = {
|
|
181
|
-
ascent: font.ascent * scale,
|
|
182
|
-
descent: Math.abs(font.descent) * scale,
|
|
183
|
-
capHeight: (font.capHeight ?? font.ascent) * scale,
|
|
184
|
-
unitsPerEm: font.unitsPerEm,
|
|
185
|
-
sourceTable: 'hhea',
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
this.metricsCache.set(metricsKey, metrics);
|
|
190
|
-
return metrics;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Strategy 2: If fontkit cache is non-empty, fontkit is available but font not found → throw
|
|
194
|
-
if (this.cache.size > 0) {
|
|
195
|
-
throw new FontNotFoundError(fontFamily, weight, style);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Strategy 3: Canvas TextMetrics (browser, fallback when fontkit unavailable)
|
|
199
|
-
if (typeof document !== 'undefined') {
|
|
200
|
-
try {
|
|
201
|
-
const canvas = document.createElement('canvas');
|
|
202
|
-
const ctx = canvas.getContext('2d')!;
|
|
203
|
-
ctx.font = `${style} ${weight} ${fontSize}px ${fontFamily}`;
|
|
204
|
-
const m = ctx.measureText('M');
|
|
205
|
-
|
|
206
|
-
metrics = {
|
|
207
|
-
ascent: m.fontBoundingBoxAscent || fontSize * 0.85,
|
|
208
|
-
descent: m.fontBoundingBoxDescent || fontSize * 0.15,
|
|
209
|
-
capHeight: m.actualBoundingBoxAscent || fontSize * 0.7,
|
|
210
|
-
unitsPerEm: 1000,
|
|
211
|
-
sourceTable: 'canvas',
|
|
212
|
-
};
|
|
213
|
-
this.metricsCache.set(metricsKey, metrics);
|
|
214
|
-
return metrics;
|
|
215
|
-
} catch {
|
|
216
|
-
// Fall through to fallback
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Font not found — throw error with clear message
|
|
221
|
-
throw new FontNotFoundError(fontFamily, weight, style);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** Singleton */
|
|
226
|
-
export const fontMetricsProvider = new FontMetricsProvider();
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FontNotFoundError.ts — thrown when a requested font is not registered.
|
|
3
|
-
*/
|
|
4
|
-
export class FontNotFoundError extends Error {
|
|
5
|
-
constructor(
|
|
6
|
-
family: string,
|
|
7
|
-
weight = 'normal',
|
|
8
|
-
style = 'normal',
|
|
9
|
-
) {
|
|
10
|
-
super(
|
|
11
|
-
`Font not found: "${family}" (weight: ${weight}, style: ${style}). ` +
|
|
12
|
-
`Use SystemFontRegistry.scan() to register system fonts.`,
|
|
13
|
-
);
|
|
14
|
-
this.name = 'FontNotFoundError';
|
|
15
|
-
}
|
|
16
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SystemFontRegistry.ts — singleton that scans system fonts using `get-system-fonts`
|
|
3
|
-
* and registers them in FontMetricsProvider for both fontkit and @napi-rs/canvas.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* import { systemFontRegistry } from './SystemFontRegistry.js';
|
|
7
|
-
* await systemFontRegistry.scan();
|
|
8
|
-
* console.log(systemFontRegistry.getRegisteredFamilies());
|
|
9
|
-
*
|
|
10
|
-
* ⚠️ Node.js built-in module imports are dynamic (lazy) to avoid
|
|
11
|
-
* Vite/Webpack externalization errors in browser builds.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { fontMetricsProvider } from './FontMetricsProvider.js';
|
|
15
|
-
|
|
16
|
-
interface ScanResult {
|
|
17
|
-
/** Total font files found on the system */
|
|
18
|
-
total: number;
|
|
19
|
-
/** Number of successfully registered font files */
|
|
20
|
-
registered: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Parse font subfamily name into weight/style.
|
|
25
|
-
* Examples:
|
|
26
|
-
* "Regular" → { weight: 'normal', style: 'normal' }
|
|
27
|
-
* "Bold" → { weight: 'bold', style: 'normal' }
|
|
28
|
-
* "Italic" → { weight: 'normal', style: 'italic' }
|
|
29
|
-
* "Bold Italic" → { weight: 'bold', style: 'italic' }
|
|
30
|
-
* "Light" → { weight: 'light', style: 'normal' }
|
|
31
|
-
* "Medium" → { weight: 'medium', style: 'normal' }
|
|
32
|
-
* "Semi Bold" → { weight: 'semibold', style: 'normal' }
|
|
33
|
-
* "Black" → { weight: 'black', style: 'normal' }
|
|
34
|
-
*/
|
|
35
|
-
function parseSubfamily(subfamily: string): { weight: string; style: string } {
|
|
36
|
-
const lower = subfamily.toLowerCase();
|
|
37
|
-
|
|
38
|
-
let style: string;
|
|
39
|
-
if (lower.includes('italic')) {
|
|
40
|
-
style = 'italic';
|
|
41
|
-
} else {
|
|
42
|
-
style = 'normal';
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let weight: string;
|
|
46
|
-
if (lower.includes('thin') || lower.includes('hairline')) {
|
|
47
|
-
weight = 'thin';
|
|
48
|
-
} else if (lower.includes('extralight') || lower.includes('ultralight')) {
|
|
49
|
-
weight = 'extralight';
|
|
50
|
-
} else if (lower.includes('light')) {
|
|
51
|
-
weight = 'light';
|
|
52
|
-
} else if (lower.includes('semibold') || lower.includes('demibold')) {
|
|
53
|
-
weight = 'semibold';
|
|
54
|
-
} else if (lower.includes('bold') || lower.includes('heavy') || lower.includes('black')) {
|
|
55
|
-
weight = 'bold';
|
|
56
|
-
} else if (lower.includes('medium') || lower.includes('medium')) {
|
|
57
|
-
weight = 'medium';
|
|
58
|
-
} else {
|
|
59
|
-
weight = 'normal';
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return { weight, style };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export class SystemFontRegistry {
|
|
66
|
-
private static _instance: SystemFontRegistry;
|
|
67
|
-
|
|
68
|
-
/** Map of registered family names → true */
|
|
69
|
-
private registered = new Map<string, true>();
|
|
70
|
-
|
|
71
|
-
private constructor() {}
|
|
72
|
-
|
|
73
|
-
static get instance(): SystemFontRegistry {
|
|
74
|
-
if (!SystemFontRegistry._instance) {
|
|
75
|
-
SystemFontRegistry._instance = new SystemFontRegistry();
|
|
76
|
-
}
|
|
77
|
-
return SystemFontRegistry._instance;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Scan the system for all fonts and register them in FontMetricsProvider.
|
|
82
|
-
*
|
|
83
|
-
* - Uses `get-system-fonts` to find all .ttf/.otf files
|
|
84
|
-
* - Opens each with fontkit to extract familyName/subfamilyName
|
|
85
|
-
* - Registers in fontMetricsProvider (fontkit buffer + canvas path)
|
|
86
|
-
*
|
|
87
|
-
* @returns stats about what was found and registered
|
|
88
|
-
*/
|
|
89
|
-
async scan(): Promise<ScanResult> {
|
|
90
|
-
// Dynamic imports — hidden from bundler static analysis.
|
|
91
|
-
// Only resolves on Node.js. No-op in browser.
|
|
92
|
-
const [{ readFileSync }, getSystemFontsModule] = await Promise.all([
|
|
93
|
-
// @ts-ignore — 'node:fs' is a Node.js built-in; not resolvable with moduleResolution:bundler.
|
|
94
|
-
import('node:fs'),
|
|
95
|
-
import('get-system-fonts') as any,
|
|
96
|
-
]);
|
|
97
|
-
const getSystemFonts = (getSystemFontsModule.default || getSystemFontsModule) as (opts?: any) => Promise<string[]>;
|
|
98
|
-
|
|
99
|
-
const paths: string[] = await getSystemFonts();
|
|
100
|
-
let registered = 0;
|
|
101
|
-
|
|
102
|
-
for (const fontPath of paths) {
|
|
103
|
-
try {
|
|
104
|
-
const buffer = readFileSync(fontPath);
|
|
105
|
-
|
|
106
|
-
// Dynamic import — fontkit may not be available in browser
|
|
107
|
-
let fontkit: any;
|
|
108
|
-
try {
|
|
109
|
-
fontkit = await import('fontkit');
|
|
110
|
-
// @ts-ignore fontkit CJS/ESM compatibility
|
|
111
|
-
fontkit = fontkit.default || fontkit;
|
|
112
|
-
} catch {
|
|
113
|
-
// fontkit not available — skip font registration
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const font = fontkit.create(buffer);
|
|
118
|
-
const family = font.familyName;
|
|
119
|
-
if (!family) continue;
|
|
120
|
-
|
|
121
|
-
const { weight, style } = parseSubfamily(font.subfamilyName || 'Regular');
|
|
122
|
-
|
|
123
|
-
await fontMetricsProvider.registerFont(family, { weight, style }, buffer, fontPath);
|
|
124
|
-
this.registered.set(family, true);
|
|
125
|
-
registered++;
|
|
126
|
-
} catch {
|
|
127
|
-
// Skip unreadable / invalid font files
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return { total: paths.length, registered };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Check if a font family is registered.
|
|
137
|
-
*/
|
|
138
|
-
isRegistered(family: string): boolean {
|
|
139
|
-
return this.registered.has(family);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Get list of all registered font families.
|
|
144
|
-
*/
|
|
145
|
-
getRegisteredFamilies(): string[] {
|
|
146
|
-
return Array.from(this.registered.keys());
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Singleton instance */
|
|
151
|
-
export const systemFontRegistry = SystemFontRegistry.instance;
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Canvas API polyfill for Bun/Node.js via @napi-rs/canvas.
|
|
3
|
-
* Required by @chenglou/pretext in server environments (Bun/Node.js without DOM).
|
|
4
|
-
*
|
|
5
|
-
* Two levels of polyfill:
|
|
6
|
-
* 1. globalThis.document.createElement('canvas') — pretext uses this
|
|
7
|
-
* inside prepare() to create a temporary Canvas and call measureText.
|
|
8
|
-
* 2. OffscreenCanvas — for some libraries and early versions.
|
|
9
|
-
*
|
|
10
|
-
* Exports enableOfficeTextMeasure / disableOfficeTextMeasure —
|
|
11
|
-
* ctx.measureText override for fontkit-based measurements in Office mode.
|
|
12
|
-
*
|
|
13
|
-
* ⚠️ All Node.js built-in module imports are dynamic (lazy) to avoid
|
|
14
|
-
* Vite/Webpack externalization errors in browser builds.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
// ── Lazy Node.js module loader ─────────────────────────────────────
|
|
18
|
-
// Dynamic import('module') — hidden from bundler static analysis.
|
|
19
|
-
// Only resolves on Node.js / Bun. No-op in browser.
|
|
20
|
-
// Use 'any' for process to avoid requiring @types/node in browser contexts.
|
|
21
|
-
const _process: any = typeof globalThis !== 'undefined'
|
|
22
|
-
? (globalThis as any).process
|
|
23
|
-
: undefined;
|
|
24
|
-
|
|
25
|
-
let _require: ((id: string) => any) | null = null;
|
|
26
|
-
let _createCanvas: ((w: number, h: number) => any) | null = null;
|
|
27
|
-
|
|
28
|
-
async function _initNodeDeps(): Promise<void> {
|
|
29
|
-
try {
|
|
30
|
-
// @ts-ignore — 'module' is a Node.js built-in; not resolvable with moduleResolution:bundler.
|
|
31
|
-
// This dynamic import is guarded by a runtime check and never executes in browser.
|
|
32
|
-
const m: any = await import('module');
|
|
33
|
-
_require = m.createRequire(import.meta.url);
|
|
34
|
-
const canvas: any = _require!('@napi-rs/canvas');
|
|
35
|
-
_createCanvas = canvas.createCanvas;
|
|
36
|
-
} catch {
|
|
37
|
-
// Browser or server without @napi-rs/canvas
|
|
38
|
-
_createCanvas = null;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ESM top-level await — guarded by runtime check so bundlers don't
|
|
43
|
-
// attempt to resolve 'module' at compile time.
|
|
44
|
-
if (_process && (_process.versions?.node || _process.versions?.bun)) {
|
|
45
|
-
await _initNodeDeps();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ── Polyfill document.createElement('canvas') ────────────────────────────
|
|
49
|
-
// Pretext internally calls document.createElement('canvas') during prepare(),
|
|
50
|
-
// to create a temporary Canvas and obtain a 2d context for measureText.
|
|
51
|
-
// Without this polyfill, prepare() crashes with "document is not defined" in Bun.
|
|
52
|
-
|
|
53
|
-
function needsCanvasPolyfill(): boolean {
|
|
54
|
-
if (typeof globalThis.document === 'undefined') return true;
|
|
55
|
-
try {
|
|
56
|
-
const el = globalThis.document.createElement('canvas');
|
|
57
|
-
if (!el || typeof el.getContext !== 'function') return true;
|
|
58
|
-
return false;
|
|
59
|
-
} catch {
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (needsCanvasPolyfill()) {
|
|
65
|
-
(globalThis as any).document = {
|
|
66
|
-
createElement: (tag: string) => {
|
|
67
|
-
if (tag === 'canvas' && _createCanvas) return _createCanvas(1, 1);
|
|
68
|
-
// For other elements — empty stub
|
|
69
|
-
return {};
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ── Polyfill OffscreenCanvas ─────────────────────────────────────────────
|
|
75
|
-
// @napi-rs/canvas does not provide OffscreenCanvas, so we create a shim.
|
|
76
|
-
|
|
77
|
-
if (typeof globalThis.OffscreenCanvas === 'undefined' && _createCanvas) {
|
|
78
|
-
(globalThis as any).OffscreenCanvas = class OffscreenCanvasShim {
|
|
79
|
-
private _canvas: any;
|
|
80
|
-
private _w: number;
|
|
81
|
-
private _h: number;
|
|
82
|
-
|
|
83
|
-
constructor(width: number, height: number) {
|
|
84
|
-
this._w = width;
|
|
85
|
-
this._h = height;
|
|
86
|
-
this._canvas = _createCanvas!(width, height);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
get width(): number { return this._w; }
|
|
90
|
-
set width(v: number) { this._w = v; this._canvas.width = v; }
|
|
91
|
-
get height(): number { return this._h; }
|
|
92
|
-
set height(v: number) { this._h = v; this._canvas.height = v; }
|
|
93
|
-
|
|
94
|
-
getContext(type: string, attrs?: any): any {
|
|
95
|
-
return this._canvas.getContext(type, attrs);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async convertToBlob({ type: _type }: { type?: string } = {}): Promise<Blob> {
|
|
99
|
-
// @napi-rs/canvas toBuffer always returns PNG buffer
|
|
100
|
-
const buffer = this._canvas.toBuffer('image/png');
|
|
101
|
-
return new Blob([buffer], { type: 'image/png' });
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ── Office measureText override ─────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
const originalMeasureText = (globalThis as any).CanvasRenderingContext2D
|
|
109
|
-
?.prototype?.measureText as
|
|
110
|
-
| ((text: string) => TextMetrics)
|
|
111
|
-
| undefined;
|
|
112
|
-
|
|
113
|
-
/** fontCache: Map<family_weight_style, fontkit.Font> */
|
|
114
|
-
let officeFontCache: Map<string, any> | null = null;
|
|
115
|
-
let officeEnabled = false;
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Parse ctx.font string like "italic bold 16px Arial" → { family, size, weight }
|
|
119
|
-
*/
|
|
120
|
-
function parseFont(fontStr: string): { family: string; size: number; weight: string } | null {
|
|
121
|
-
// "italic bold 16px Arial" or "bold 16px Inter" or "16px Arial"
|
|
122
|
-
const pxMatch = fontStr.match(/(\d+(?:\.\d+)?)px\s+(.+)/);
|
|
123
|
-
if (!pxMatch) return null;
|
|
124
|
-
const size = parseFloat(pxMatch[1]);
|
|
125
|
-
// Simplified: everything after px is the family
|
|
126
|
-
const family = pxMatch[2].trim();
|
|
127
|
-
// Parse weight from beginning
|
|
128
|
-
const weightMatch = fontStr.match(/\b(bold|italic|\d{3})\b/);
|
|
129
|
-
const weight = weightMatch ? (weightMatch[1] === 'bold' ? 'bold' : weightMatch[1]) : 'normal';
|
|
130
|
-
return { family, size, weight };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Cache key: "${family}_${weight}_normal" */
|
|
134
|
-
function cacheKey(family: string, weight: string): string {
|
|
135
|
-
return `${family}_${weight}_normal`;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function officeMeasureText(this: any, text: string): TextMetrics {
|
|
139
|
-
if (!officeFontCache) {
|
|
140
|
-
return originalMeasureText?.call(this, text) ?? createEmptyMetrics();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const parsed = parseFont(this.font);
|
|
144
|
-
if (!parsed) {
|
|
145
|
-
return originalMeasureText?.call(this, text) ?? createEmptyMetrics();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const key = cacheKey(parsed.family, parsed.weight);
|
|
149
|
-
const font = officeFontCache.get(key);
|
|
150
|
-
if (!font) {
|
|
151
|
-
return originalMeasureText?.call(this, text) ?? createEmptyMetrics();
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const scale = parsed.size / font.unitsPerEm;
|
|
155
|
-
let totalWidth = 0;
|
|
156
|
-
|
|
157
|
-
for (let i = 0; i < text.length; i++) {
|
|
158
|
-
const codePoint = text.codePointAt(i)!;
|
|
159
|
-
const glyph = font.glyphForCodePoint(codePoint);
|
|
160
|
-
if (glyph) {
|
|
161
|
-
totalWidth += glyph.advanceWidth * scale;
|
|
162
|
-
} else {
|
|
163
|
-
// Fallback for missing glyph: use original
|
|
164
|
-
if (originalMeasureText) {
|
|
165
|
-
return originalMeasureText.call(this, text);
|
|
166
|
-
}
|
|
167
|
-
totalWidth += parsed.size * 0.5; // rough estimate
|
|
168
|
-
}
|
|
169
|
-
// Skip surrogate pairs
|
|
170
|
-
if (codePoint > 0xffff) i++;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return createMetricsObject(totalWidth);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function createMetricsObject(width: number): TextMetrics {
|
|
177
|
-
return {
|
|
178
|
-
width,
|
|
179
|
-
actualBoundingBoxAscent: 0,
|
|
180
|
-
actualBoundingBoxDescent: 0,
|
|
181
|
-
fontBoundingBoxAscent: 0,
|
|
182
|
-
fontBoundingBoxDescent: 0,
|
|
183
|
-
actualBoundingBoxLeft: 0,
|
|
184
|
-
actualBoundingBoxRight: width,
|
|
185
|
-
} as unknown as TextMetrics;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function createEmptyMetrics(): TextMetrics {
|
|
189
|
-
return createMetricsObject(0);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Register a font with @napi-rs/canvas so ctx.measureText() works in Node.js.
|
|
194
|
-
* No-op in browser or when @napi-rs/canvas is not available.
|
|
195
|
-
*/
|
|
196
|
-
export function registerCanvasFont(fontPath: string, family: string): void {
|
|
197
|
-
if (!_require) return; // @napi-rs/canvas not available
|
|
198
|
-
try {
|
|
199
|
-
const mod = _require('@napi-rs/canvas');
|
|
200
|
-
if (mod?.registerFont) {
|
|
201
|
-
mod.registerFont(fontPath, { family });
|
|
202
|
-
}
|
|
203
|
-
} catch {
|
|
204
|
-
// @napi-rs/canvas not available — no-op
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Enable Office measurement: replaces ctx.measureText with fontkit-based version.
|
|
210
|
-
* @param fontCache — Map key->font from FontMetricsProvider
|
|
211
|
-
*/
|
|
212
|
-
export function enableOfficeTextMeasure(fontCache: Map<string, any>): void {
|
|
213
|
-
if (officeEnabled) return;
|
|
214
|
-
officeFontCache = fontCache;
|
|
215
|
-
officeEnabled = true;
|
|
216
|
-
|
|
217
|
-
const CtxProto = (globalThis as any).CanvasRenderingContext2D?.prototype;
|
|
218
|
-
if (CtxProto && originalMeasureText) {
|
|
219
|
-
CtxProto.measureText = officeMeasureText;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Restore original ctx.measureText.
|
|
225
|
-
*/
|
|
226
|
-
export function disableOfficeTextMeasure(): void {
|
|
227
|
-
if (!officeEnabled) return;
|
|
228
|
-
officeEnabled = false;
|
|
229
|
-
officeFontCache = null;
|
|
230
|
-
|
|
231
|
-
const CtxProto = (globalThis as any).CanvasRenderingContext2D?.prototype;
|
|
232
|
-
if (CtxProto && originalMeasureText) {
|
|
233
|
-
CtxProto.measureText = originalMeasureText;
|
|
234
|
-
}
|
|
235
|
-
}
|
package/src/measure/fontkit.d.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal type declarations for fontkit.
|
|
3
|
-
* fontkit does not ship native TypeScript types.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
declare module 'fontkit' {
|
|
7
|
-
interface BBox {
|
|
8
|
-
minX: number;
|
|
9
|
-
minY: number;
|
|
10
|
-
maxX: number;
|
|
11
|
-
maxY: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface Glyph {
|
|
15
|
-
id: number;
|
|
16
|
-
advanceWidth: number;
|
|
17
|
-
bbox: BBox;
|
|
18
|
-
name: string;
|
|
19
|
-
path: any;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface TTFFont {
|
|
23
|
-
unitsPerEm: number;
|
|
24
|
-
ascent: number;
|
|
25
|
-
descent: number;
|
|
26
|
-
capHeight: number;
|
|
27
|
-
xHeight: number;
|
|
28
|
-
lineGap: number;
|
|
29
|
-
underlinePosition: number;
|
|
30
|
-
underlineThickness: number;
|
|
31
|
-
familyName: string;
|
|
32
|
-
subfamilyName: string;
|
|
33
|
-
postscriptName: string;
|
|
34
|
-
format: string;
|
|
35
|
-
glyphsForString(text: string): Glyph[];
|
|
36
|
-
getGlyph(glyphId: number): Glyph;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function create(buffer: Buffer, postscriptName?: string): TTFFont;
|
|
40
|
-
function open(filename: string, postscriptName?: string): Promise<TTFFont>;
|
|
41
|
-
function openSync(filename: string, postscriptName?: string): TTFFont;
|
|
42
|
-
|
|
43
|
-
export { create, open, openSync, TTFFont, Glyph, BBox };
|
|
44
|
-
}
|