intellifont-engine 1.2.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
@@ -109,9 +109,112 @@ export declare function extractGlyphSignature(fontPath: string, character: strin
109
109
  * Indexes the specified font files with Brotli-11 compression.
110
110
  */
111
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>
112
158
  /**
113
159
  * Compare the visual similarity of two font glyphs
114
160
  *
115
161
  * Returns a similarity score from 0.0 (different) to 1.0 (identical).
116
162
  */
117
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
+ }