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.
- package/README.md +355 -255
- package/browser/canvas-dna.js +626 -0
- package/browser/demo.html +759 -0
- package/browser/image-pipeline.js +1182 -0
- package/browser/intellifont-inspector.js +299 -0
- package/index.d.ts +103 -0
- package/index.js +760 -194
- package/intellifont-engine.node +0 -0
- package/package.json +63 -58
|
@@ -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
|
+
}
|