Comparing version 2.0.0 to 3.0.0
{ | ||
"name": "mrz", | ||
"version": "2.0.0", | ||
"description": "Create and parse MRZ (Machine Readable Zone) in TD1 and TD3 format", | ||
"version": "3.0.0", | ||
"description": "Parse MRZ (Machine Readable Zone) from identity documents", | ||
"main": "./src/index.js", | ||
"files": [ | ||
"runkit.js", | ||
"src" | ||
], | ||
"scripts": { | ||
"eslint": "eslint src __tests__", | ||
"eslint": "eslint builder src", | ||
"eslint-fix": "npm run eslint -- --fix", | ||
@@ -17,5 +16,5 @@ "test": "npm run test-only && npm run eslint", | ||
"test-only": "jest", | ||
"build": "npm run buildCountries && cheminfo build", | ||
"prepublish": "npm run buildCountries", | ||
"buildCountries": "node builder/createCountriesJs.js" | ||
"build": "npm run buildStates && cheminfo build", | ||
"prepublish": "npm run buildStates", | ||
"buildStates": "node builder/createStatesJs.js" | ||
}, | ||
@@ -27,7 +26,10 @@ "repository": { | ||
"keywords": [ | ||
"mrz", | ||
"machine", | ||
"learning", | ||
"data", | ||
"mining", | ||
"datamining" | ||
"readable", | ||
"zone", | ||
"identity", | ||
"documents", | ||
"card", | ||
"passport" | ||
], | ||
@@ -42,8 +44,7 @@ "author": "Luc Patiny <luc@patiny.com>", | ||
"cheminfo-tools": "^1.20.2", | ||
"eslint": "^4.15.0", | ||
"eslint": "^4.16.0", | ||
"eslint-config-cheminfo": "^1.14.0", | ||
"eslint-plugin-jest": "^21.7.0", | ||
"jest": "^22.1.3", | ||
"should": "^11.1.0" | ||
"jest": "^22.1.4" | ||
} | ||
} |
@@ -9,7 +9,7 @@ # mrz | ||
Parse MRZ (Machine Readable Zone) in TD1, TD2, TD3 or CH driving licence format | ||
Parse MRZ (Machine Readable Zone) from identity documents. | ||
## Installation | ||
`$ npm install mrz` | ||
`$ npm install --save mrz` | ||
@@ -19,7 +19,9 @@ ## Example | ||
```js | ||
const parse = require("mrz").parse; | ||
const parse = require('mrz').parse; | ||
let mrz = `I<UTOD23145890<1233<<<<<<<<<<< | ||
7408122F1204159UTO<<<<<<<<<<<6 | ||
ERIKSSON<<ANNA<MARIA<<<<<<<<<<`; | ||
let mrz = [ | ||
'I<UTOD23145890<1233<<<<<<<<<<<', | ||
'7408122F1204159UTO<<<<<<<<<<<6', | ||
'ERIKSSON<<ANNA<MARIA<<<<<<<<<<' | ||
]; | ||
@@ -30,11 +32,60 @@ var result = parse(mrz); | ||
Or test it in [Runkit](https://runkit.com/npm/mrz) | ||
## API | ||
### parse(mrz) | ||
Parses the provided MRZ. The argument can be an array of lines or a single string | ||
including line breaks. This function throws an error if the input is in an | ||
unsupported format. It will never throw an error when there are invalid fields | ||
in the MRZ. Instead, the `result.valid` value will be `false` and | ||
details about the invalid fields can be found in `result.details`. | ||
#### result.format | ||
String identifying the format of the parsed MRZ. Supported formats are: | ||
* TD1 (identity card with three MRZ lines) | ||
* TD2 (identity card with two MRZ lines) | ||
* TD3 (passport) | ||
* SWISS_DRIVING_LICENSE | ||
#### result.valid | ||
`true` if all fields are valid. `false` otherwise. | ||
#### result.fields | ||
Object mapping field names to their respective value. The value is set to `null` | ||
if it is invalid. The value may be different than the raw value. For example | ||
`result.fields.sex` will be "male" when the raw value was "M". | ||
#### result.details | ||
Array of objects describing all parsed fields. Its structure is: | ||
* label {string} - Full english term for the field. | ||
* field {string} - Name of the field in `result.fields`. | ||
* value {string} - Value of the field or `null`. | ||
* valid {boolean} | ||
* ranges {Array} - Array of ranges that are necessary to compute this field. | ||
Ranges are objects with `line`, `start`, `end` and `raw`. | ||
* line {number} - Index of the line where the field is located. | ||
* start {number} - Index of the start of the field in `line`. | ||
* end {number} - Index of the end of the field in `line`. | ||
### formats | ||
Static mapping of supported formats. | ||
### states | ||
Static mapping of state code to state name. | ||
## Specifications | ||
## TD1, TD2 and TD3 | ||
### TD1, TD2 and TD3 | ||
https://www.icao.int/publications/pages/publication.aspx?docnum=9303 | ||
## Swiss driving license | ||
### Swiss driving license | ||
@@ -41,0 +92,0 @@ http://www.astra2.admin.ch/media/pdfpub/2003-10-15_2262_f.pdf |
'use strict'; | ||
const COUNTRIES = require('./generated/countries'); | ||
const states = require('./generated/states'); | ||
const formats = require('./formats'); | ||
const parse = require('./parse/parse'); | ||
module.exports = { | ||
COUNTRIES, | ||
states, | ||
formats, | ||
parse | ||
}; |
'use strict'; | ||
module.exports = function (fieldOptions) { | ||
const result = Object.assign({}, fieldOptions, { parser: undefined }); | ||
const parser = fieldOptions.parser; | ||
if ( | ||
!fieldOptions.line === undefined || | ||
!fieldOptions.start === undefined || | ||
!fieldOptions.end === undefined || | ||
!fieldOptions.parser | ||
) { | ||
throw new Error('field must have a line, start, stop and parser'); | ||
checkType(fieldOptions, 'label', 'string'); | ||
if (fieldOptions.field !== null) { | ||
checkType(fieldOptions, 'field', 'string'); | ||
} | ||
return function (lines) { | ||
checkType(fieldOptions, 'line', 'number'); | ||
checkType(fieldOptions, 'start', 'number'); | ||
checkType(fieldOptions, 'end', 'number'); | ||
checkType(fieldOptions, 'parser', 'function'); | ||
const ranges = [ | ||
{ | ||
line: fieldOptions.line, | ||
start: fieldOptions.start, | ||
end: fieldOptions.end | ||
} | ||
]; | ||
if (Array.isArray(fieldOptions.related)) { | ||
for (const related of fieldOptions.related) { | ||
checkType(related, 'line', 'number'); | ||
checkType(related, 'start', 'number'); | ||
checkType(related, 'end', 'number'); | ||
ranges.push(related); | ||
} | ||
} | ||
return function parseField(lines) { | ||
const source = getText(lines, fieldOptions); | ||
let related = fieldOptions.related || []; | ||
related = related.map((r) => getText(lines, r)); | ||
const result = { | ||
label: fieldOptions.label, | ||
field: fieldOptions.field, | ||
value: null, | ||
valid: false, | ||
ranges: ranges.map((range) => ({ | ||
line: range.line, | ||
start: range.start, | ||
end: range.end, | ||
raw: getText(lines, range) | ||
})) | ||
}; | ||
try { | ||
let parsed = parser(source, ...related); | ||
result.parsed = parsed; | ||
let parsed = fieldOptions.parser(source, ...related); | ||
result.value = typeof parsed === 'object' ? parsed.value : parsed; | ||
result.valid = true; | ||
const range = result.ranges[0]; | ||
result.line = range.line; | ||
result.start = range.start; | ||
result.end = range.end; | ||
if (typeof parsed === 'object') { | ||
result.start = range.start + parsed.start; | ||
result.end = range.start + parsed.end; | ||
} | ||
} catch (e) { | ||
result.parsed = null; | ||
result.valid = false; | ||
result.message = e.message; | ||
result.error = e.message; | ||
} | ||
@@ -36,1 +68,7 @@ return result; | ||
} | ||
function checkType(options, name, type) { | ||
if (typeof options[name] !== type) { | ||
throw new TypeError(`${name} must be a ${type}`); | ||
} | ||
} |
'use strict'; | ||
var parseTD1 = require('./td1'); | ||
var parseTD2 = require('./td2'); | ||
var parseTD3 = require('./td3'); | ||
var parsePCC = require('./pcc'); | ||
const checkLines = require('./checkLines'); | ||
const formats = require('../formats'); | ||
const parsers = require('./parsers'); | ||
module.exports = function parse(lines) { | ||
let result = {}; | ||
if (typeof lines === 'string') { | ||
lines = lines.split(/[\r\n]+/); | ||
} | ||
function parseMRZ(lines) { | ||
lines = checkLines(lines); | ||
switch (lines.length) { | ||
case 2: | ||
if (lines[0].length < 41) { | ||
result = parseTD2(lines); | ||
} else { | ||
result = parseTD3(lines); | ||
case 3: { | ||
switch (lines[0].length) { | ||
case 30: | ||
return parsers.TD1(lines); | ||
case 36: | ||
return parsers.TD2(lines); | ||
case 44: | ||
return parsers.TD3(lines); | ||
case 9: | ||
return parsers.SWISS_DRIVING_LICENSE(lines); | ||
default: | ||
throw new Error( | ||
'unrecognized document format. First line of input must have 30 (TD1), 36 (TD2), 44 (TD3) or 9 (Swiss Driving License) characters' | ||
); | ||
} | ||
break; | ||
case 3: | ||
if (lines[0].length < 15) { | ||
// in fact it should be 9 | ||
result = parsePCC(lines); | ||
} else { | ||
result = parseTD1(lines); | ||
} | ||
break; | ||
} | ||
default: | ||
throw new Error('input must be an array of 2 or 3 elements'); | ||
throw new Error( | ||
`unrecognized document format. Input must have two or three lines, found${ | ||
lines.length | ||
}` | ||
); | ||
} | ||
} | ||
return result; | ||
}; | ||
for (const format in formats) { | ||
parseMRZ[format] = parsers[format]; | ||
} | ||
module.exports = parseMRZ; |
'use strict'; | ||
const { getAnnotations, completeResult } = require('./fieldHelper'); | ||
const { TD1: TD1Fields } = require('./fields'); | ||
const checkLines = require('./checkLines'); | ||
const getResult = require('./getResult'); | ||
const { TD1 } = require('../formats'); | ||
const TD1Fields = require('./td1Fields'); | ||
module.exports = function parseTD1(lines) { | ||
lines.forEach((line) => { | ||
lines = checkLines(lines); | ||
lines.forEach((line, index) => { | ||
if (line.length !== 30) { | ||
throw new Error('each line should have a length of 30 in TD1'); | ||
throw new Error( | ||
`invalid number of characters for line ${index + 1}: ${ | ||
line.length | ||
}. Must be 30 for TD1` | ||
); | ||
} | ||
}); | ||
const result = { | ||
format: 'TD1', | ||
annotations: getAnnotations(lines, TD1Fields) | ||
}; | ||
completeResult(result); | ||
return result; | ||
return getResult(TD1, lines, TD1Fields); | ||
}; |
'use strict'; | ||
const { getAnnotations, completeResult } = require('./fieldHelper'); | ||
const { TD2: TD2Fields } = require('./fields'); | ||
const checkLines = require('./checkLines'); | ||
const getResult = require('./getResult'); | ||
const { TD2 } = require('../formats'); | ||
const TD2Fields = require('./td2Fields'); | ||
module.exports = function parseTD1(lines) { | ||
lines.forEach((line) => { | ||
module.exports = function parseTD2(lines) { | ||
lines = checkLines(lines); | ||
lines.forEach((line, index) => { | ||
if (line.length !== 36) { | ||
throw new Error('each line should have a length of 36 in TD2'); | ||
throw new Error( | ||
`invalid number of characters for line ${index + 1}: ${ | ||
line.length | ||
}. Must be 36 for TD2` | ||
); | ||
} | ||
}); | ||
const result = { | ||
format: 'TD2', | ||
annotations: getAnnotations(lines, TD2Fields) | ||
}; | ||
completeResult(result); | ||
return result; | ||
return getResult(TD2, lines, TD2Fields); | ||
}; |
'use strict'; | ||
const { getAnnotations, completeResult } = require('./fieldHelper'); | ||
const { TD3: TD3Fields } = require('./fields'); | ||
const checkLines = require('./checkLines'); | ||
const getResult = require('./getResult'); | ||
const { TD3 } = require('../formats'); | ||
const TD3Fields = require('./td3Fields'); | ||
module.exports = function parseTD1(lines) { | ||
lines.forEach((line) => { | ||
module.exports = function parseTD3(lines) { | ||
lines = checkLines(lines); | ||
lines.forEach((line, index) => { | ||
if (line.length !== 44) { | ||
throw new Error('each line should have a length of 30 in TD1'); | ||
throw new Error( | ||
`invalid number of characters for line ${index + 1}: ${ | ||
line.length | ||
}. Must be 44 for TD3` | ||
); | ||
} | ||
}); | ||
const result = { | ||
format: 'TD3', | ||
annotations: getAnnotations(lines, TD3Fields) | ||
}; | ||
completeResult(result); | ||
return result; | ||
return getResult(TD3, lines, TD3Fields); | ||
}; |
@@ -6,5 +6,5 @@ 'use strict'; | ||
test('test check digits', () => { | ||
expect(check('592166117<231', 8)).toBe(true); | ||
expect(check('592166111<773', 5)).toBe(true); | ||
expect(() => check('592166117<231', 8)).not.toThrow(); | ||
expect(() => check('592166111<773', 5)).not.toThrow(); | ||
expect(() => check('592166111<773', 4)).toThrow(/invalid check digit/); | ||
}); | ||
@@ -14,3 +14,6 @@ 'use strict'; | ||
} | ||
return code % 10 === Number(value); | ||
code %= 10; | ||
if (code !== Number(value)) { | ||
throw new Error(`invalid check digit: ${value}. Must be ${code}`); | ||
} | ||
}; |
'use strict'; | ||
module.exports = function parseDate(value) { | ||
const year = value.substring(0, 2); | ||
if (!value.match(/^[0-9<]{6}$/)) { | ||
throw new Error(`invalid date: ${value}`); | ||
} | ||
const month = value.substring(2, 4); | ||
const day = value.substring(4, 6); | ||
if (month < 1 || month > 12) { | ||
throw new Error(`Month "${month}" not valid`); | ||
if (month !== '<<' && (month < 1 || month > 12)) { | ||
throw new Error(`invalid date month: ${month}`); | ||
} | ||
if (day < 1 || day > 31) { | ||
throw new Error(`Day "${day}" not valid`); | ||
if (day !== '<<' && (day < 1 || day > 31)) { | ||
throw new Error(`invalid date day: ${day}`); | ||
} | ||
return `${day}.${month}.${year}`; | ||
return value; | ||
}; |
@@ -5,7 +5,7 @@ 'use strict'; | ||
module.exports = function (checkDigit, value) { | ||
if (checkDigit !== false && !check(value, checkDigit)) { | ||
throw new Error(`Check digit "${checkDigit}" not valid`); | ||
module.exports = function parseCheckDigit(checkDigit, value) { | ||
if (checkDigit !== false) { | ||
check(value, checkDigit); | ||
} | ||
return checkDigit; | ||
}; |
@@ -6,7 +6,16 @@ 'use strict'; | ||
module.exports = function parseDocumentNumber(source, checkDigit, optional) { | ||
let end, value; | ||
if (checkDigit === '<' && optional) { | ||
optional = cleanText(optional); | ||
source += optional.substring(0, optional.length - 1); | ||
value = source + optional.substring(0, optional.length - 1); | ||
end = value.length + 1; | ||
} else { | ||
value = cleanText(source); | ||
end = value.length; | ||
} | ||
return source.replace(/</g); | ||
return { | ||
value, | ||
start: 0, | ||
end | ||
}; | ||
}; |
@@ -6,3 +6,7 @@ 'use strict'; | ||
module.exports = function (checkDigit, source, optional) { | ||
module.exports = function parseDocumentNumberCheckDigit( | ||
checkDigit, | ||
source, | ||
optional | ||
) { | ||
if (checkDigit === '<' && optional) { | ||
@@ -14,6 +18,4 @@ optional = cleanText(optional); | ||
if (!check(source, checkDigit)) { | ||
throw new Error(`document number check digit "${checkDigit}" not valid`); | ||
} | ||
check(source, checkDigit); | ||
return checkDigit; | ||
}; |
@@ -5,3 +5,3 @@ 'use strict'; | ||
if (!source.match(/^[0-9]+$/)) { | ||
throw new Error('It may only be composed of numbers'); | ||
throw new Error(`invalid number: ${source}`); | ||
} | ||
@@ -8,0 +8,0 @@ |
@@ -5,4 +5,9 @@ 'use strict'; | ||
module.exports = function (value) { | ||
return parseText(value, /^[A-Z0-9<]+<*$/); | ||
module.exports = function parsePersonalNumber(source) { | ||
const value = parseText(source, /^[A-Z0-9<]+<*$/); | ||
return { | ||
value, | ||
start: 0, | ||
end: value.length | ||
}; | ||
}; |
@@ -5,4 +5,2 @@ 'use strict'; | ||
switch (source) { | ||
case '<': | ||
return 'unknown'; | ||
case 'M': | ||
@@ -12,7 +10,7 @@ return 'male'; | ||
return 'female'; | ||
case '<': | ||
return 'nonspecified'; | ||
default: | ||
throw new Error( | ||
`The sex "${source}" is incorrect. Allowed values: M, F or <.` | ||
); | ||
throw new Error(`invalid sex: ${source}. Must be M, F or <.`); | ||
} | ||
}; |
@@ -7,5 +7,7 @@ 'use strict'; | ||
if (!source.match(regexp)) { | ||
throw new Error(`it must match the following regexp: ${regexp}`); | ||
throw new Error( | ||
`invalid text: ${source}. Must match the following regular expression: ${regexp}` | ||
); | ||
} | ||
return cleanText(source); | ||
}; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
48738
5
48
1738
105