intellifont-engine 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,299 @@
1
+ /**
2
+ * intelliFont Web Inspector
3
+ *
4
+ * Click-to-inspect browser module. Given a DOM element (or a CSS font name),
5
+ * computes Canvas DNA metrics and queries the intelliFont backend to identify
6
+ * the font and return ranked suggestions.
7
+ *
8
+ * Usage (in your web app):
9
+ * import { IntellifontInspector } from 'intellifont-engine/browser/intellifont-inspector.js';
10
+ * const inspector = new IntellifontInspector({ serverUrl: 'http://localhost:3000' });
11
+ * const result = await inspector.inspectElement(document.querySelector('h1'));
12
+ * // result.matches → [{ family: "Inter", confidence: 0.95, matchQuality: "exact" }, ...]
13
+ *
14
+ * Usage (bookmarklet / standalone — no backend needed):
15
+ * Paste the contents of intellifont-bookmarklet.js into your browser console.
16
+ * Click any text on the page. Results shown as an overlay.
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ // =============================================================================
22
+ // CONSTANTS
23
+ // =============================================================================
24
+ const DEFAULT_SERVER_URL = 'http://localhost:3000';
25
+ const DEFAULT_CHARS = 'RQWM';
26
+ const DEFAULT_LIMIT = 8;
27
+
28
+ // =============================================================================
29
+ // MAIN INSPECTOR CLASS
30
+ // =============================================================================
31
+ class IntellifontInspector {
32
+ /**
33
+ * @param {object} options
34
+ * @param {string} [options.serverUrl='http://localhost:3000'] - intellifont serve endpoint
35
+ * @param {string} [options.characters='RQWM'] - Characters to analyze
36
+ * @param {number} [options.limit=8] - Max suggestions to return
37
+ * @param {boolean} [options.showOverlay=true] - Show built-in result overlay
38
+ */
39
+ constructor(options = {}) {
40
+ this.serverUrl = options.serverUrl || DEFAULT_SERVER_URL;
41
+ this.characters = options.characters || DEFAULT_CHARS;
42
+ this.limit = options.limit || DEFAULT_LIMIT;
43
+ this.showOverlay = options.showOverlay !== false;
44
+ this._overlay = null;
45
+ this._canvasDNA = null;
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Load canvas-dna module lazily
50
+ // ---------------------------------------------------------------------------
51
+ async _getCanvasDNA() {
52
+ if (this._canvasDNA) return this._canvasDNA;
53
+ if (typeof CanvasDNA !== 'undefined') {
54
+ this._canvasDNA = CanvasDNA;
55
+ return this._canvasDNA;
56
+ }
57
+ // Dynamic import if available
58
+ const mod = await import('./canvas-dna.js');
59
+ this._canvasDNA = mod.default || mod;
60
+ return this._canvasDNA;
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Core inspection function — given a DOM element
65
+ // ---------------------------------------------------------------------------
66
+ async inspectElement(element) {
67
+ const dna = await this._getCanvasDNA();
68
+ const { fontFamily, cssRaw, metrics } = dna.analyzeElement(element, this.characters);
69
+
70
+ if (metrics.length === 0) {
71
+ return {
72
+ cssName: cssRaw,
73
+ resolvedName: fontFamily,
74
+ matches: [],
75
+ error: 'Could not extract any glyph metrics for this font.'
76
+ };
77
+ }
78
+
79
+ return this._queryBackend(fontFamily, cssRaw, metrics);
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Inspect using a CSS font name string (e.g. from computed style)
84
+ // ---------------------------------------------------------------------------
85
+ async inspectFromCssName(cssName) {
86
+ const dna = await this._getCanvasDNA();
87
+ const fontFamily = cssName.split(',')[0].trim().replace(/['"]/g, '');
88
+ const metrics = dna.analyzeFont(fontFamily, this.characters);
89
+
90
+ return this._queryBackend(fontFamily, cssName, metrics);
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Send metrics to the intellifont backend
95
+ // ---------------------------------------------------------------------------
96
+ async _queryBackend(fontFamily, cssRaw, metrics) {
97
+ const url = `${this.serverUrl}/api/identify-from-metrics`;
98
+
99
+ let matches = [];
100
+ let backendError = null;
101
+
102
+ try {
103
+ const response = await fetch(url, {
104
+ method: 'POST',
105
+ headers: { 'Content-Type': 'application/json' },
106
+ body: JSON.stringify({ metrics, limit: this.limit }),
107
+ });
108
+
109
+ if (!response.ok) {
110
+ throw new Error(`Backend responded ${response.status}`);
111
+ }
112
+
113
+ const data = await response.json();
114
+ matches = data.matches || [];
115
+ } catch (err) {
116
+ backendError = err.message;
117
+ // Graceful degradation: return the raw computed metrics so they're useful
118
+ }
119
+
120
+ const result = {
121
+ cssName: cssRaw,
122
+ resolvedName: fontFamily,
123
+ matches,
124
+ metrics,
125
+ backendError: backendError || undefined,
126
+ };
127
+
128
+ if (this.showOverlay) {
129
+ this._renderOverlay(result);
130
+ }
131
+
132
+ return result;
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Built-in result overlay
137
+ // ---------------------------------------------------------------------------
138
+ _renderOverlay(result) {
139
+ this._removeOverlay();
140
+
141
+ const overlay = document.createElement('div');
142
+ overlay.id = 'intellifont-overlay';
143
+ overlay.setAttribute('style', `
144
+ position: fixed;
145
+ top: 20px;
146
+ right: 20px;
147
+ width: 320px;
148
+ max-height: 480px;
149
+ overflow-y: auto;
150
+ background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 100%);
151
+ border: 1px solid rgba(99, 102, 241, 0.4);
152
+ border-radius: 16px;
153
+ padding: 20px;
154
+ font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;
155
+ font-size: 13px;
156
+ color: #e2e8f0;
157
+ box-shadow: 0 25px 60px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.05);
158
+ z-index: 2147483647;
159
+ animation: intellifont-slide-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
160
+ `);
161
+
162
+ // Inject animation keyframes
163
+ if (!document.getElementById('intellifont-styles')) {
164
+ const style = document.createElement('style');
165
+ style.id = 'intellifont-styles';
166
+ style.textContent = `
167
+ @keyframes intellifont-slide-in {
168
+ from { opacity: 0; transform: translateY(-12px) scale(0.96); }
169
+ to { opacity: 1; transform: translateY(0) scale(1); }
170
+ }
171
+ #intellifont-overlay .if-match:hover {
172
+ background: rgba(99, 102, 241, 0.12) !important;
173
+ }
174
+ `;
175
+ document.head.appendChild(style);
176
+ }
177
+
178
+ const confidence_color = (score) => {
179
+ if (score >= 0.90) return '#6ee7b7'; // green
180
+ if (score >= 0.75) return '#fbbf24'; // yellow
181
+ return '#f87171'; // red
182
+ };
183
+
184
+ const matchHtml = result.matches.length > 0
185
+ ? result.matches.map((m, i) => `
186
+ <div class="if-match" style="
187
+ display: flex; align-items: center; justify-content: space-between;
188
+ padding: 10px 12px; margin: 6px 0;
189
+ background: rgba(255,255,255,0.04);
190
+ border-radius: 10px; cursor: default;
191
+ border: 1px solid rgba(255,255,255,0.06);
192
+ transition: background 0.15s;
193
+ ">
194
+ <div>
195
+ <div style="font-weight: 600; color: #f1f5f9; font-size: 13.5px;">
196
+ ${i === 0 ? '🏆 ' : ''}${m.family}
197
+ </div>
198
+ <div style="color: #94a3b8; font-size: 11px; margin-top: 2px;">
199
+ ${m.subfamily || 'Regular'} · ${m.matchQuality || ''}
200
+ </div>
201
+ </div>
202
+ <div style="
203
+ font-weight: 700; font-size: 14px;
204
+ color: ${confidence_color(m.confidence || m.score || 0)};
205
+ ">
206
+ ${((m.confidence || m.score || 0) * 100).toFixed(0)}%
207
+ </div>
208
+ </div>`)
209
+ .join('')
210
+ : `<div style="color: #64748b; padding: 12px 0; text-align: center;">
211
+ ${result.backendError
212
+ ? `⚠️ Could not reach intelliFont server.<br><span style="font-size:11px;">${result.backendError}</span>`
213
+ : 'No matches found in database.'}
214
+ </div>`;
215
+
216
+ overlay.innerHTML = `
217
+ <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px;">
218
+ <div style="display: flex; align-items: center; gap: 8px;">
219
+ <div style="
220
+ width: 28px; height: 28px; border-radius: 8px;
221
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
222
+ display: flex; align-items: center; justify-content: center;
223
+ font-size: 14px;
224
+ ">𝔽</div>
225
+ <span style="font-weight: 700; font-size: 15px; color: #f8fafc;">intelliFont</span>
226
+ </div>
227
+ <button onclick="document.getElementById('intellifont-overlay').remove()" style="
228
+ background: none; border: none; color: #64748b; font-size: 18px;
229
+ cursor: pointer; padding: 0; line-height: 1;
230
+ ">✕</button>
231
+ </div>
232
+
233
+ <div style="margin-bottom: 12px;">
234
+ <div style="color: #64748b; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 4px;">Detected CSS Font</div>
235
+ <div style="
236
+ background: rgba(255,255,255,0.06); border-radius: 8px;
237
+ padding: 8px 12px; font-family: monospace; font-size: 12px; color: #a5b4fc;
238
+ ">${result.cssName || result.resolvedName}</div>
239
+ </div>
240
+
241
+ <div style="color: #64748b; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px;">
242
+ Canvas DNA Matches
243
+ </div>
244
+
245
+ ${matchHtml}
246
+
247
+ <div style="
248
+ margin-top: 14px; padding-top: 12px;
249
+ border-top: 1px solid rgba(255,255,255,0.08);
250
+ color: #475569; font-size: 10px; text-align: center;
251
+ ">
252
+ Analyzed via pixel metrics · ${result.metrics?.length || 0} glyphs
253
+ </div>
254
+ `;
255
+
256
+ document.body.appendChild(overlay);
257
+ this._overlay = overlay;
258
+ }
259
+
260
+ _removeOverlay() {
261
+ const existing = document.getElementById('intellifont-overlay');
262
+ if (existing) existing.remove();
263
+ this._overlay = null;
264
+ }
265
+
266
+ // ---------------------------------------------------------------------------
267
+ // Enable click-to-inspect mode on the current page
268
+ // ---------------------------------------------------------------------------
269
+ enableClickInspect() {
270
+ this._clickHandler = async (e) => {
271
+ e.preventDefault();
272
+ e.stopPropagation();
273
+ const element = e.target;
274
+ element.style.outline = '2px solid rgba(99,102,241,0.8)';
275
+ setTimeout(() => { element.style.outline = ''; }, 1200);
276
+ await this.inspectElement(element);
277
+ };
278
+ document.addEventListener('click', this._clickHandler, true);
279
+ console.log('[intelliFont] Click any text to inspect its font. Call disableClickInspect() to stop.');
280
+ return this;
281
+ }
282
+
283
+ disableClickInspect() {
284
+ if (this._clickHandler) {
285
+ document.removeEventListener('click', this._clickHandler, true);
286
+ this._clickHandler = null;
287
+ }
288
+ return this;
289
+ }
290
+ }
291
+
292
+ // =============================================================================
293
+ // EXPORTS
294
+ // =============================================================================
295
+ if (typeof module !== 'undefined' && module.exports) {
296
+ module.exports = { IntellifontInspector };
297
+ } else if (typeof window !== 'undefined') {
298
+ window.IntellifontInspector = IntellifontInspector;
299
+ }
package/index.d.ts CHANGED
@@ -41,3 +41,180 @@ export declare function getCacheStats(): JsCacheStats
41
41
  export declare function cleanupCache(aggressive: boolean): number
42
42
  export declare function listPinnedFonts(): Array<string>
43
43
  export declare function updateDatabase(): Promise<void>
44
+ export interface JsVisualMatch {
45
+ family: string
46
+ subfamily?: string
47
+ confidence: number
48
+ matchedChars: Array<string>
49
+ }
50
+ export interface JsGlyphSignature {
51
+ character: string
52
+ aspectRatio: number
53
+ density: number
54
+ curveRatio: number
55
+ pointCount: number
56
+ xBalance: number
57
+ yBalance: number
58
+ }
59
+ export interface JsGlyphDbStats {
60
+ fontCount: number
61
+ charCount: number
62
+ uncompressedSizeMb: number
63
+ compressedSizeMb: number
64
+ compressionRatio: number
65
+ buildTimeSeconds: number
66
+ }
67
+ /**
68
+ * Identify a font visually by analyzing glyph shapes
69
+ *
70
+ * Uses the pre-built glyph signature database to find matching fonts.
71
+ * Pass multiple characters (e.g., "RQWM") for higher accuracy.
72
+ */
73
+ export declare function identifyVisualFont(fontPath: string, characters: string, limit?: number | undefined | null): Array<JsVisualMatch>
74
+ /**
75
+ * Identify a font visually from a memory buffer (e.g. file upload)
76
+ *
77
+ * Excellent for web servers where the font file is in memory.
78
+ */
79
+ export declare function identifyVisualFontBuffer(fontData: Buffer, characters: string, limit?: number | undefined | null): Array<JsVisualMatch>
80
+ /** AI-powered result for font similarity suggestions */
81
+ export interface JsAiSuggestion {
82
+ /** The matched font family name */
83
+ family: string
84
+ /** The matched font subfamily (e.g., "Regular", "Bold") */
85
+ subfamily: string
86
+ /** AI confidence score (0.0 to 1.0) */
87
+ confidence: number
88
+ /** Similarity category: "exact", "high", "medium", "low" */
89
+ matchQuality: string
90
+ }
91
+ /**
92
+ * AI-powered font similarity suggestion
93
+ *
94
+ * Analyzes the visual DNA of a font file and finds similar fonts
95
+ * from the signature database using pattern recognition.
96
+ */
97
+ export declare function aiSuggestSimilar(fontPath: string, limit?: number | undefined | null): Array<JsAiSuggestion>
98
+ /** AI-powered font similarity from memory buffer */
99
+ export declare function aiSuggestSimilarBuffer(fontData: Buffer, limit?: number | undefined | null): Array<JsAiSuggestion>
100
+ /**
101
+ * Extract glyph signature from a font file
102
+ *
103
+ * Returns detailed signature information for a specific character.
104
+ */
105
+ export declare function extractGlyphSignature(fontPath: string, character: string): JsGlyphSignature
106
+ /**
107
+ * Build a glyph signature database from font files
108
+ *
109
+ * Indexes the specified font files with Brotli-11 compression.
110
+ */
111
+ export declare function buildGlyphDatabase(fontPaths: Array<string>, outputPath: string): JsGlyphDbStats
112
+ /**
113
+ * Pixel-derived glyph metrics computed by the browser's Canvas API.
114
+ *
115
+ * These fields match the Rust MicroSignature format (all u8 range 0-255).
116
+ * Computed by canvas-dna.js from rendered glyph pixel bitmaps.
117
+ */
118
+ export interface JsPixelMetrics {
119
+ /** The character that was analyzed (e.g. "R") */
120
+ character: string
121
+ /** Width ÷ Height × 64 (0-255) */
122
+ aspectRatio: number
123
+ /** Ink pixel density in bounding box (0-255) */
124
+ density: number
125
+ /** Ink density in NW quadrant (0-255) */
126
+ quadrantNw: number
127
+ /** Ink density in NE quadrant (0-255) */
128
+ quadrantNe: number
129
+ /** Ink density in SW quadrant (0-255) */
130
+ quadrantSw: number
131
+ /** Ink density in SE quadrant (0-255) */
132
+ quadrantSe: number
133
+ /** Estimated curviness of strokes (0=straight, 255=very curved) */
134
+ curveRatio: number
135
+ /** Outline complexity / direction changes (0-255) */
136
+ pointCount: number
137
+ /** Horizontal center of mass (0=left, 128=center, 255=right) */
138
+ xBalance: number
139
+ /** Vertical center of mass (0=top, 128=center, 255=bottom) */
140
+ yBalance: number
141
+ /** Estimated stroke thickness (0-255) */
142
+ strokeWidth: number
143
+ /** Serif vs sans classification (0=sans, 255=strong serif) */
144
+ serifScore: number
145
+ }
146
+ /**
147
+ * Identify a font from pixel metrics computed by the browser Canvas API.
148
+ *
149
+ * This is the core of the "Canvas DNA" approach — the browser renders
150
+ * characters using the web font and measures their pixel geometry. Those
151
+ * measurements are sent here to be matched against the signature database,
152
+ * without ever needing to fetch or upload the font file.
153
+ *
154
+ * Accepts an array of JsPixelMetrics (one per analyzed character, e.g. R/Q/W/M)
155
+ * for higher accuracy multi-character matching.
156
+ */
157
+ export declare function identifyFromPixelMetrics(metrics: Array<JsPixelMetrics>, limit?: number | undefined | null): Array<JsVisualMatch>
158
+ /**
159
+ * Compare the visual similarity of two font glyphs
160
+ *
161
+ * Returns a similarity score from 0.0 (different) to 1.0 (identical).
162
+ */
163
+ export declare function compareGlyphSignatures(fontPathA: string, fontPathB: string, character: string): number
164
+
165
+ // =============================================================================
166
+ // Image Pipeline (browser) — browser/image-pipeline.js
167
+ // Identifies fonts from arbitrary raster images without uploading them.
168
+ // =============================================================================
169
+
170
+ /** Detected style properties for the text in an image. */
171
+ export interface DetectedStyle {
172
+ /** True if the text appears to be italic (slant angle > 8°) */
173
+ italic: boolean
174
+ /** Measured slant angle in degrees (0 = upright, positive = slants right) */
175
+ italicAngle: number
176
+ /** Estimated CSS font-weight: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 */
177
+ cssWeight: number
178
+ /** Human-readable weight label e.g. "Regular", "Bold", "Light" */
179
+ weightLabel: string
180
+ }
181
+
182
+ /** Result of analyzing an image for font identification. */
183
+ export interface ImagePipelineResult {
184
+ /** JsPixelMetrics[] ready to pass to identifyFromPixelMetrics() */
185
+ metrics: JsPixelMetrics[]
186
+ /** Detected italic/bold/weight style aggregated across all glyph crops */
187
+ style: DetectedStyle
188
+ /** Laplacian variance sharpness score — higher is sharper */
189
+ quality: number
190
+ /** Degrees the image was rotated to straighten text (negative = clockwise) */
191
+ deskewAngle: number
192
+ /** Total text regions found before the MAX_GLYPHS cap */
193
+ regionCount: number
194
+ /** User-facing warning if image quality is too low, or null */
195
+ warning: string | null
196
+ }
197
+
198
+ /** Any image source accepted by the ImagePipeline */
199
+ export type ImageSource =
200
+ | string // URL or data URL
201
+ | File
202
+ | Blob
203
+ | HTMLImageElement
204
+ | HTMLCanvasElement
205
+
206
+ /**
207
+ * Identify fonts from any raster image (screenshot, photo, PDF frame).
208
+ * Runs entirely in the browser — no upload required.
209
+ *
210
+ * @example
211
+ * const { metrics, warning } = await analyzeImage(file);
212
+ * if (warning) console.warn(warning);
213
+ * const matches = identifyFromPixelMetrics(metrics, 8);
214
+ */
215
+ export declare function analyzeImage(imageSource: ImageSource): Promise<ImagePipelineResult>
216
+
217
+ /** Full pipeline class for advanced usage (e.g. reusing across multiple images) */
218
+ export declare class ImagePipeline {
219
+ analyze(imageSource: ImageSource): Promise<ImagePipelineResult>
220
+ }