mrz-genius 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/LICENSE +21 -0
- package/README.md +270 -0
- package/package.json +47 -0
- package/src/detector/mrzDetector.js +214 -0
- package/src/index.d.ts +141 -0
- package/src/index.js +150 -0
- package/src/ocr/llmExtractor.js +146 -0
- package/src/ocr/mrzOCR.js +489 -0
- package/src/parser/checkDigit.js +84 -0
- package/src/parser/fieldPositions.js +122 -0
- package/src/parser/mrzParser.js +487 -0
- package/src/parser/ocrCorrector.js +172 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mrz-genius
|
|
3
|
+
* Node.js library for MRZ (Machine Readable Zone) detection, OCR, and parsing
|
|
4
|
+
*
|
|
5
|
+
* Supports: TD1 (ID Cards), TD2, TD3 (Passports), MRVA (Visa type A), MRVB (Visa type B)
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - MRZ region detection on document images
|
|
9
|
+
* - OCR (Optical Character Recognition) for MRZ text extraction
|
|
10
|
+
* - Full MRZ parsing with check digit validation
|
|
11
|
+
* - OCR error correction
|
|
12
|
+
* - BAC key generation for e-Passports
|
|
13
|
+
*
|
|
14
|
+
* @module mrz-genius
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const { parse, detectFormat, parseName, parseDate, formatDate, parseSex, parseDocumentType } = require('./parser/mrzParser');
|
|
20
|
+
const { calculateCheckDigit, isCheckDigitValid, isCompositeValid } = require('./parser/checkDigit');
|
|
21
|
+
const { correctOCR, findMatchingStrings } = require('./parser/ocrCorrector');
|
|
22
|
+
const { MRZ_FORMATS, getFieldPositions } = require('./parser/fieldPositions');
|
|
23
|
+
const { detectMRZRegion, optimizeForOCR, preprocessForOCR } = require('./detector/mrzDetector');
|
|
24
|
+
const { performOCR, hasMRZ, postProcessOCR, extractMRZFromFullText } = require('./ocr/mrzOCR');
|
|
25
|
+
const { extractWithLLM } = require('./ocr/llmExtractor');
|
|
26
|
+
|
|
27
|
+
// ──────────────────────────────────────────────────────────
|
|
28
|
+
// High-level API
|
|
29
|
+
// ──────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Complete MRZ processing pipeline:
|
|
33
|
+
* 1. Detect MRZ region in image
|
|
34
|
+
* 2. Perform OCR on the detected region
|
|
35
|
+
* 3. Parse the extracted MRZ text
|
|
36
|
+
*
|
|
37
|
+
* @param {Buffer|string} image - Image buffer or file path
|
|
38
|
+
* @param {Object} [options] - Processing options
|
|
39
|
+
* @param {boolean} [options.ocrCorrection=true] - Enable OCR error correction in parser
|
|
40
|
+
* @param {boolean} [options.detectRegion=true] - Auto-detect MRZ region in image
|
|
41
|
+
* @param {string} [options.lang='eng'] - Tesseract language code
|
|
42
|
+
* @returns {Promise<Object>} Complete result with OCR data and parsed MRZ
|
|
43
|
+
*/
|
|
44
|
+
async function processImage(image, options = {}) {
|
|
45
|
+
const {
|
|
46
|
+
ocrCorrection = true,
|
|
47
|
+
detectRegion = true,
|
|
48
|
+
lang = 'eng',
|
|
49
|
+
llm = null // LLM configuration object
|
|
50
|
+
} = options;
|
|
51
|
+
|
|
52
|
+
let ocrResult;
|
|
53
|
+
let useLLM = !!llm;
|
|
54
|
+
|
|
55
|
+
if (useLLM) {
|
|
56
|
+
try {
|
|
57
|
+
// Ensure image is a buffer for LLM consumption
|
|
58
|
+
const fs = require('fs');
|
|
59
|
+
const imageBuffer = Buffer.isBuffer(image) ? image : fs.readFileSync(image);
|
|
60
|
+
const rawLLMText = await extractWithLLM(imageBuffer, llm);
|
|
61
|
+
|
|
62
|
+
const extractedLines = extractMRZFromFullText(rawLLMText);
|
|
63
|
+
ocrResult = {
|
|
64
|
+
lines: extractedLines || rawLLMText.split('\n').filter(l => l.length > 5),
|
|
65
|
+
rawText: rawLLMText,
|
|
66
|
+
confidence: 99, // LLMs don't typically return confidence, assume high if parsable
|
|
67
|
+
method: `llm_${llm.provider}`
|
|
68
|
+
};
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.warn(`LLM extraction failed: ${err.message}. Falling back to Tesseract OCR.`);
|
|
71
|
+
useLLM = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Fallback or Primary OCR
|
|
76
|
+
if (!useLLM) {
|
|
77
|
+
ocrResult = await performOCR(image, {
|
|
78
|
+
detectRegion,
|
|
79
|
+
lang,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Step 3: Parse
|
|
84
|
+
const parsed = parse(ocrResult.lines, { ocrCorrection });
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
success: !!parsed && parsed.valid,
|
|
88
|
+
ocr: {
|
|
89
|
+
lines: ocrResult.lines,
|
|
90
|
+
rawText: ocrResult.rawText,
|
|
91
|
+
confidence: ocrResult.confidence,
|
|
92
|
+
method: ocrResult.method,
|
|
93
|
+
},
|
|
94
|
+
parsed,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Parse an MRZ string directly (no image processing)
|
|
100
|
+
* @param {string|string[]} mrzText - MRZ text (multi-line string or array of lines)
|
|
101
|
+
* @param {Object} [options] - Parser options
|
|
102
|
+
* @param {boolean} [options.ocrCorrection=false] - Enable OCR error correction
|
|
103
|
+
* @returns {Object|null} Parsed MRZ result or null
|
|
104
|
+
*/
|
|
105
|
+
function parseMRZ(mrzText, options = {}) {
|
|
106
|
+
return parse(mrzText, options);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ──────────────────────────────────────────────────────────
|
|
110
|
+
// Exports
|
|
111
|
+
// ──────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
// High-level API
|
|
115
|
+
processImage,
|
|
116
|
+
parseMRZ,
|
|
117
|
+
|
|
118
|
+
// Detection
|
|
119
|
+
detectMRZRegion,
|
|
120
|
+
optimizeForOCR,
|
|
121
|
+
preprocessForOCR,
|
|
122
|
+
|
|
123
|
+
// OCR
|
|
124
|
+
performOCR,
|
|
125
|
+
hasMRZ,
|
|
126
|
+
postProcessOCR,
|
|
127
|
+
extractMRZFromFullText,
|
|
128
|
+
|
|
129
|
+
// Parser
|
|
130
|
+
parse,
|
|
131
|
+
detectFormat,
|
|
132
|
+
parseName,
|
|
133
|
+
parseDate,
|
|
134
|
+
formatDate,
|
|
135
|
+
parseSex,
|
|
136
|
+
parseDocumentType,
|
|
137
|
+
|
|
138
|
+
// Validation
|
|
139
|
+
calculateCheckDigit,
|
|
140
|
+
isCheckDigitValid,
|
|
141
|
+
isCompositeValid,
|
|
142
|
+
|
|
143
|
+
// OCR Correction
|
|
144
|
+
correctOCR,
|
|
145
|
+
findMatchingStrings,
|
|
146
|
+
|
|
147
|
+
// Constants
|
|
148
|
+
MRZ_FORMATS,
|
|
149
|
+
getFieldPositions,
|
|
150
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Perform a generic HTTP/HTTPS JSON post request
|
|
8
|
+
*/
|
|
9
|
+
function jsonPost(urlStr, headers, body) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const url = new URL(urlStr);
|
|
12
|
+
const lib = url.protocol === 'http:' ? http : https;
|
|
13
|
+
|
|
14
|
+
const options = {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
...headers
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const req = lib.request(url, options, (res) => {
|
|
23
|
+
let data = '';
|
|
24
|
+
res.on('data', chunk => data += chunk);
|
|
25
|
+
res.on('end', () => {
|
|
26
|
+
try {
|
|
27
|
+
const json = JSON.parse(data);
|
|
28
|
+
if (res.statusCode >= 400) {
|
|
29
|
+
reject(new Error(`API Error ${res.statusCode}: ${JSON.stringify(json)}`));
|
|
30
|
+
} else {
|
|
31
|
+
resolve(json);
|
|
32
|
+
}
|
|
33
|
+
} catch (e) {
|
|
34
|
+
reject(new Error('Invalid JSON response: ' + data));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
req.on('error', reject);
|
|
40
|
+
req.write(JSON.stringify(body));
|
|
41
|
+
req.end();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extract MRZ using LLM Vision APIs
|
|
47
|
+
* @param {Buffer} imageBuffer - Image containing MRZ
|
|
48
|
+
* @param {Object} llmConfig - LLM Configuration
|
|
49
|
+
* @returns {Promise<string>} - Extracted MRZ text
|
|
50
|
+
*/
|
|
51
|
+
async function extractWithLLM(imageBuffer, llmConfig) {
|
|
52
|
+
if (!llmConfig || !llmConfig.provider || !llmConfig.apiKey) {
|
|
53
|
+
throw new Error('LLM config requires provider and apiKey');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const base64Image = imageBuffer.toString('base64');
|
|
57
|
+
const prompt = `Extract only the raw MRZ (Machine Readable Zone) text from this identity document image.
|
|
58
|
+
Output nothing else but the exact MRZ text lines.
|
|
59
|
+
Do not add markdown formatting, do not add introductions. Only the raw uppercase letters, numbers, and '<' characters.
|
|
60
|
+
Preserve exact spacing and newlines.`;
|
|
61
|
+
|
|
62
|
+
const provider = llmConfig.provider.toLowerCase();
|
|
63
|
+
|
|
64
|
+
// OpenAI / ChatGPT / LiteLLM Compatible
|
|
65
|
+
if (provider === 'chatgpt' || provider === 'openai' || provider === 'litellm') {
|
|
66
|
+
const baseUrl = llmConfig.baseUrl || 'https://api.openai.com/v1';
|
|
67
|
+
const url = `${baseUrl.replace(/\/$/, '')}/chat/completions`;
|
|
68
|
+
const model = llmConfig.model || 'gpt-4o-mini';
|
|
69
|
+
|
|
70
|
+
const response = await jsonPost(url, {
|
|
71
|
+
'Authorization': `Bearer ${llmConfig.apiKey}`
|
|
72
|
+
}, {
|
|
73
|
+
model: model,
|
|
74
|
+
messages: [
|
|
75
|
+
{
|
|
76
|
+
role: 'user',
|
|
77
|
+
content: [
|
|
78
|
+
{ type: 'text', text: prompt },
|
|
79
|
+
{ type: 'image_url', image_url: { url: `data:image/jpeg;base64,${base64Image}` } }
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
max_tokens: 300,
|
|
84
|
+
temperature: 0.1
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return response.choices[0].message.content.trim();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Anthropic Claude
|
|
91
|
+
if (provider === 'anthropic') {
|
|
92
|
+
const url = 'https://api.anthropic.com/v1/messages';
|
|
93
|
+
const model = llmConfig.model || 'claude-3-haiku-20240307';
|
|
94
|
+
|
|
95
|
+
const response = await jsonPost(url, {
|
|
96
|
+
'x-api-key': llmConfig.apiKey,
|
|
97
|
+
'anthropic-version': '2023-06-01'
|
|
98
|
+
}, {
|
|
99
|
+
model: model,
|
|
100
|
+
messages: [
|
|
101
|
+
{
|
|
102
|
+
role: 'user',
|
|
103
|
+
content: [
|
|
104
|
+
{
|
|
105
|
+
type: 'image',
|
|
106
|
+
source: { type: 'base64', media_type: 'image/jpeg', data: base64Image }
|
|
107
|
+
},
|
|
108
|
+
{ type: 'text', text: prompt }
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
max_tokens: 300,
|
|
113
|
+
temperature: 0.1
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return response.content[0].text.trim();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Google Gemini
|
|
120
|
+
if (provider === 'gemini') {
|
|
121
|
+
const model = llmConfig.model || 'gemini-1.5-flash';
|
|
122
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${llmConfig.apiKey}`;
|
|
123
|
+
|
|
124
|
+
const response = await jsonPost(url, {}, {
|
|
125
|
+
contents: [{
|
|
126
|
+
parts: [
|
|
127
|
+
{ text: prompt },
|
|
128
|
+
{ inline_data: { mime_type: 'image/jpeg', data: base64Image } }
|
|
129
|
+
]
|
|
130
|
+
}],
|
|
131
|
+
generationConfig: {
|
|
132
|
+
temperature: 0.1,
|
|
133
|
+
maxOutputTokens: 300
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const candidate = response.candidates[0].content.parts[0].text;
|
|
138
|
+
return candidate.trim();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
throw new Error(`Unsupported LLM provider: ${provider}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
extractWithLLM
|
|
146
|
+
};
|