@vyaz/core 0.0.5 → 0.0.7

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