├── .gitignore ├── tests ├── bootstrap.php └── Deft │ └── MrzParser │ └── Tests │ └── ParserTest.php ├── src └── Deft │ └── MrzParser │ ├── Exception │ ├── Exception.php │ ├── ParseException.php │ └── UnsupportedDocumentException.php │ ├── Document │ ├── Sex.php │ ├── TravelDocumentType.php │ ├── TravelDocumentInterface.php │ └── TravelDocument.php │ ├── Parser │ ├── ParserInterface.php │ ├── ParserFactory.php │ ├── AbstractParser.php │ ├── PassportParser.php │ └── TravelDocumentType1Parser.php │ ├── MrzParserInterface.php │ └── MrzParser.php ├── phpunit.xml.dist ├── composer.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | add('Deft\\', __DIR__); 5 | -------------------------------------------------------------------------------- /src/Deft/MrzParser/Exception/Exception.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests 6 | 7 | 8 | 9 | 10 | ./ 11 | 12 | ./tests 13 | ./vendor 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deft/mrz-parser", 3 | "description": "Library to parse machine readable zones (MRZ) of passports and ID cards", 4 | "keywords": ["mrz", "parsing", "passport", "identity document"], 5 | "authors": [ 6 | { 7 | "name": "Jan Kramer", 8 | "email": "jan@jankramer.eu" 9 | } 10 | ], 11 | "minimum-stability": "dev", 12 | "require": { 13 | }, 14 | "autoload": { 15 | "psr-0": { 16 | "Deft": "src/" 17 | } 18 | }, 19 | "extra": { 20 | "branch-alias": { 21 | "dev-master": "1.0.x-dev" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Deft/MrzParser/MrzParserInterface.php: -------------------------------------------------------------------------------- 1 | parseString($mrz); 17 | 18 | // Use getters to access the parsed information 19 | print $travelDocument->getDocumentNumber(); // Will print 'L898902C' 20 | 21 | // Parse array of lines 22 | $mrz = [ 23 | "PparseLines($mrz); 27 | ``` -------------------------------------------------------------------------------- /src/Deft/MrzParser/MrzParser.php: -------------------------------------------------------------------------------- 1 | parserFactory = new Parser\ParserFactory(); 18 | } 19 | 20 | /** 21 | * Parse a MRZ string, i.e. the lines of a document are concatenated 22 | * 23 | * @param $string 24 | * @return TravelDocumentInterface 25 | */ 26 | public function parseString($string) 27 | { 28 | $parser = $this->parserFactory->create($string); 29 | 30 | return $parser->parse($string); 31 | } 32 | 33 | /** 34 | * Parse an array of MRZ lines of one document 35 | * 36 | * @param string[] $lines 37 | * @return TravelDocumentInterface 38 | */ 39 | public function parseLines(array $lines) 40 | { 41 | return $this->parseString(implode('', $lines)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Deft/MrzParser/Parser/AbstractParser.php: -------------------------------------------------------------------------------- 1 | getToken($string, $start, $start + 5); 26 | 27 | $centennialBorder = new \DateTime("+15 year"); 28 | $dateToken = (substr($dateToken, 0, 2) > $centennialBorder->format('y') ? '19' : 20) . $dateToken; 29 | 30 | return \DateTime::createFromFormat('Ymd', $dateToken); 31 | } 32 | 33 | protected function getNames($string, $start, $finish) 34 | { 35 | $token = substr($string, ($start-1), ($finish - $start)+1); 36 | $nameParts = explode('<<', $token); 37 | if (count($nameParts) < 2) throw new ParseException("Names could not be parsed."); 38 | return array_slice( 39 | array_map(function ($str) { return $this->clean($str); }, $nameParts), 40 | 0, 41 | 2 42 | ); 43 | } 44 | 45 | protected function clean($string) 46 | { 47 | return trim(str_replace('<', ' ', $string)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Deft/MrzParser/Document/TravelDocumentInterface.php: -------------------------------------------------------------------------------- 1 | getToken($string, 1) != 'P') { 42 | throw new ParseException("First character is not 'P'"); 43 | } 44 | 45 | $fields = [ 46 | 'type' => TravelDocumentType::PASSPORT, 47 | 'issuingCountry' => $this->getToken($string, 3, 5), 48 | 'documentNumber' => $this->getToken($string, 45, 53), 49 | 'nationality' => $this->getToken($string, 55, 57), 50 | 'dateOfBirth' => $this->getDateToken($string, 58), 51 | 'sex' => $this->getToken($string, 65), 52 | 'dateOfExpiry' => $this->getDateToken($string, 66), 53 | 'personalNumber' => $this->getToken($string, 73, 86) 54 | ]; 55 | 56 | $names = $this->getNames($string, 6, 44); 57 | $fields['primaryIdentifier'] = $names[0]; 58 | $fields['secondaryIdentifier'] = $names[1]; 59 | 60 | return new TravelDocument($fields); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Deft/MrzParser/Parser/TravelDocumentType1Parser.php: -------------------------------------------------------------------------------- 1 | getToken($string, 1), ['I', 'A', 'C'])) { 43 | throw new ParseException("First character is not 'I', 'A', or 'C'"); 44 | } 45 | 46 | $fields = [ 47 | 'type' => TravelDocumentType::ID_CARD, 48 | 'issuingCountry' => $this->getToken($string, 3, 5), 49 | 'documentNumber' => $this->getToken($string, 6, 14), 50 | 'personalNumber' => $this->getToken($string, 16, 29), 51 | 'dateOfBirth' => $this->getDateToken($string, 31), 52 | 'sex' => $this->getToken($string, 38), 53 | 'dateOfExpiry' => $this->getDateToken($string, 39), 54 | 'nationality' => $this->getToken($string, 46, 48) 55 | ]; 56 | 57 | $names = $this->getNames($string, 61, 90); 58 | $fields['primaryIdentifier'] = $names[0]; 59 | $fields['secondaryIdentifier'] = $names[1]; 60 | 61 | return new TravelDocument($fields); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Deft/MrzParser/Document/TravelDocument.php: -------------------------------------------------------------------------------- 1 | $value) { 24 | $this->{$fieldName} = $value; 25 | } 26 | } 27 | 28 | /** 29 | * The primary type of this document, one of the constants in TravelDocumentType. 30 | * 31 | * @return string 32 | */ 33 | public function getType() 34 | { 35 | return $this->type; 36 | } 37 | 38 | /** 39 | * The number that identifies this document. 40 | * 41 | * @return string 42 | */ 43 | public function getDocumentNumber() 44 | { 45 | return $this->documentNumber; 46 | } 47 | 48 | /** 49 | * The country that issued this travel document as 3-letter country code (ISO 3166-1 alpha-3). 50 | * 51 | * @return string 52 | */ 53 | public function getIssuingCountry() 54 | { 55 | return $this->issuingCountry; 56 | } 57 | 58 | /** 59 | * The date this document expires. 60 | * 61 | * @return \DateTime 62 | */ 63 | public function getDateOfExpiry() 64 | { 65 | return $this->dateOfExpiry; 66 | } 67 | 68 | /** 69 | * The primary identifier of the document holder, typically its surname or family name. 70 | * 71 | * @return string 72 | */ 73 | public function getPrimaryIdentifier() 74 | { 75 | return $this->primaryIdentifier; 76 | } 77 | 78 | /** 79 | * The secondary identifier of the document holder, commonly its given names. 80 | * 81 | * @return string 82 | */ 83 | public function getSecondaryIdentifier() 84 | { 85 | return $this->secondaryIdentifier; 86 | } 87 | 88 | /** 89 | * The sex of the document holder, one of the constants in Gender. 90 | * 91 | * @return string 92 | */ 93 | public function getSex() 94 | { 95 | return $this->sex; 96 | } 97 | 98 | /** 99 | * The date of birth of the document holder. 100 | * 101 | * @return \DateTime 102 | */ 103 | public function getDateOfBirth() 104 | { 105 | return $this->dateOfBirth; 106 | } 107 | 108 | /** 109 | * The nationality of the document holder as 3-letter country code (ISO 3166-1 alpha-3). 110 | * 111 | * @return string 112 | */ 113 | public function getNationality() 114 | { 115 | return $this->nationality; 116 | } 117 | 118 | /** 119 | * The personal number that identifies the document holder in its state. 120 | * 121 | * @return strinmg 122 | */ 123 | public function getPersonalNumber() 124 | { 125 | return $this->personalNumber; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /tests/Deft/MrzParser/Tests/ParserTest.php: -------------------------------------------------------------------------------- 1 | parser = new MrzParser(); 20 | } 21 | 22 | public function testParse_passportString() 23 | { 24 | $mrzString = "Pparser->parseString($mrzString); 27 | $fields = [ 28 | 'type' => TravelDocumentType::PASSPORT, 29 | 'number' => 'L898902C', 30 | 'issuingCountry' => 'UTO', 31 | 'dateOfExpiry' => '23-06-1994', 32 | 'primaryIdentifier' => 'ERIKSSON', 33 | 'secondaryIdentifier' => 'ANNA MARIA', 34 | 'sex' => Sex::FEMALE, 35 | 'dateOfBirth' => '06-08-1969', 36 | 'nationality' => 'UTO', 37 | 'personalNumber' => 'ZE184226B' 38 | ]; 39 | $this->assertValidDocument($document, $fields); 40 | } 41 | 42 | public function testParse_idcardArray() 43 | { 44 | $mrzString = [ 45 | "Iparser->parseLines($mrzString); 51 | $fields = [ 52 | 'type' => TravelDocumentType::ID_CARD, 53 | 'number' => 'IU4FC08Q1', 54 | 'issuingCountry' => 'UTO', 55 | 'dateOfExpiry' => '16-03-2016', 56 | 'primaryIdentifier' => 'DOE', 57 | 'secondaryIdentifier' => 'JOHN', 58 | 'sex' => Sex::MALE, 59 | 'dateOfBirth' => '03-06-1993', 60 | 'nationality' => 'UTO', 61 | 'personalNumber' => '219340341' 62 | ]; 63 | $this->assertValidDocument($document, $fields); 64 | } 65 | 66 | protected function assertValidDocument($document, $fields) 67 | { 68 | $this->assertEquals($fields['type'], $document->getType(), "Incorrect document type"); 69 | $this->assertEquals($fields['number'], $document->getDocumentNumber(), "Incorrect document number"); 70 | $this->assertEquals($fields['issuingCountry'], $document->getIssuingCountry(), "Incorrect issuing country"); 71 | $this->assertInstanceOf('DateTime', $document->getDateOfExpiry(), "Date of expiry is not a DateTime"); 72 | $this->assertEquals($fields['dateOfExpiry'], $document->getDateOfExpiry()->format('d-m-Y'), "Invalid date of expiry"); 73 | $this->assertEquals($fields['primaryIdentifier'], $document->getPrimaryIdentifier(), "Invalid primary identifier"); 74 | $this->assertEquals($fields['secondaryIdentifier'], $document->getSecondaryIdentifier(), "Invalid secondary identifier"); 75 | $this->assertEquals($fields['sex'], $document->getSex(), "Invalid sex"); 76 | $this->assertInstanceOf('DateTime', $document->getDateOfBirth(), "Date of birth is not a DateTime"); 77 | $this->assertEquals($fields['dateOfBirth'], $document->getDateOfBirth()->format('d-m-Y'), "Invalid date of birth"); 78 | $this->assertEquals($fields['nationality'], $document->getNationality(), "Invalid nationality"); 79 | $this->assertEquals($fields['personalNumber'], $document->getPersonalNumber(), "Invalid personal number"); 80 | } 81 | } 82 | --------------------------------------------------------------------------------