@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.
Files changed (42) hide show
  1. package/README.md +156 -0
  2. package/dist/compile/DocumentCompiler.d.ts +40 -0
  3. package/dist/index.browser.d.ts +28 -0
  4. package/dist/index.browser.js +18 -0
  5. package/dist/index.d.ts +30 -0
  6. package/dist/index.js +11 -147930
  7. package/dist/layout/AutoFitEngine.d.ts +44 -0
  8. package/dist/layout/LineBoxValidator.d.ts +25 -0
  9. package/dist/layout/ParagraphLayoutEngine.d.ts +46 -0
  10. package/dist/layout/PositioningEngine.d.ts +68 -0
  11. package/dist/layout/TextFrameLayoutEngine.d.ts +50 -0
  12. package/dist/layout/estimateWidth.d.ts +36 -0
  13. package/dist/measure/FontEngine.d.ts +47 -0
  14. package/dist/measure/FontMetricsProvider.d.ts +49 -0
  15. package/dist/measure/FontNotFoundError.d.ts +6 -0
  16. package/dist/measure/SystemFontRegistry.d.ts +46 -0
  17. package/dist/measure/canvas-polyfill.d.ts +30 -0
  18. package/dist/types/Document.d.ts +593 -0
  19. package/dist/types/FontTypes.d.ts +61 -0
  20. package/dist/types/LayoutTypes.d.ts +128 -0
  21. package/dist/utils/env.d.ts +11 -0
  22. package/dist/utils/font.d.ts +16 -0
  23. package/dist/utils/groupLinesByParagraph.d.ts +44 -0
  24. package/dist/utils/list.d.ts +41 -0
  25. package/dist/utils/textTransform.d.ts +32 -0
  26. package/package.json +30 -13
  27. package/src/compile/DocumentCompiler.ts +0 -136
  28. package/src/index.ts +0 -80
  29. package/src/layout/AutoFitEngine.ts +0 -101
  30. package/src/layout/LineBoxValidator.ts +0 -162
  31. package/src/layout/ParagraphLayoutEngine.ts +0 -202
  32. package/src/layout/PositioningEngine.ts +0 -401
  33. package/src/layout/TextFrameLayoutEngine.ts +0 -91
  34. package/src/measure/FontMetricsProvider.ts +0 -226
  35. package/src/measure/FontNotFoundError.ts +0 -16
  36. package/src/measure/SystemFontRegistry.ts +0 -151
  37. package/src/measure/canvas-polyfill.d.ts +0 -6
  38. package/src/measure/canvas-polyfill.ts +0 -235
  39. package/src/measure/fontkit.d.ts +0 -44
  40. package/src/types/Document.ts +0 -540
  41. package/src/types/FontTypes.ts +0 -74
  42. 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,6 +0,0 @@
1
- /**
2
- * Type declarations for @napi-rs/canvas used by canvas-polyfill.ts.
3
- */
4
- declare module '@napi-rs/canvas' {
5
- export function createCanvas(width: number, height: number): any;
6
- }
@@ -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
- }
@@ -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
- }