2018-07-30 04:07:02 +02:00
|
|
|
package org.telegram.messenger;
|
|
|
|
|
|
|
|
import android.content.res.AssetManager;
|
|
|
|
import android.graphics.Bitmap;
|
|
|
|
import android.graphics.Canvas;
|
|
|
|
import android.graphics.Matrix;
|
|
|
|
import android.graphics.Paint;
|
|
|
|
import android.graphics.Point;
|
|
|
|
import android.graphics.Rect;
|
|
|
|
import android.text.TextUtils;
|
|
|
|
import android.util.Base64;
|
|
|
|
import android.util.SparseArray;
|
|
|
|
|
|
|
|
import com.google.android.gms.vision.Frame;
|
|
|
|
import com.google.android.gms.vision.barcode.Barcode;
|
|
|
|
import com.google.android.gms.vision.barcode.BarcodeDetector;
|
|
|
|
|
|
|
|
import java.util.Calendar;
|
|
|
|
import java.util.HashMap;
|
|
|
|
|
|
|
|
public class MrzRecognizer {
|
|
|
|
|
2019-12-31 14:08:08 +01:00
|
|
|
public static Result recognize(Bitmap bitmap, boolean tryDriverLicenseFirst) {
|
|
|
|
Result res;
|
|
|
|
if (tryDriverLicenseFirst) {
|
|
|
|
res = recognizeBarcode(bitmap);
|
|
|
|
if (res != null)
|
2018-07-30 04:07:02 +02:00
|
|
|
return res;
|
|
|
|
}
|
2019-12-31 14:08:08 +01:00
|
|
|
try {
|
|
|
|
res = recognizeMRZ(bitmap);
|
|
|
|
if (res != null)
|
2018-07-30 04:07:02 +02:00
|
|
|
return res;
|
2019-12-31 14:08:08 +01:00
|
|
|
} catch (Exception ignore) {
|
|
|
|
}
|
|
|
|
if (!tryDriverLicenseFirst) {
|
|
|
|
res = recognizeBarcode(bitmap);
|
|
|
|
if (res != null)
|
2018-07-30 04:07:02 +02:00
|
|
|
return res;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-12-31 14:08:08 +01:00
|
|
|
private static Result recognizeBarcode(Bitmap bitmap) {
|
|
|
|
BarcodeDetector detector = new BarcodeDetector.Builder(ApplicationLoader.applicationContext)/*.setBarcodeFormats(Barcode.PDF417)*/.build();
|
2018-07-30 04:07:02 +02:00
|
|
|
if (bitmap.getWidth() > 1500 || bitmap.getHeight() > 1500) {
|
|
|
|
float scale = 1500f / Math.max(bitmap.getWidth(), bitmap.getHeight());
|
|
|
|
bitmap = Bitmap.createScaledBitmap(bitmap, Math.round(bitmap.getWidth() * scale), Math.round(bitmap.getHeight() * scale), true);
|
|
|
|
}
|
2019-12-31 14:08:08 +01:00
|
|
|
SparseArray<Barcode> barcodes = detector.detect(new Frame.Builder().setBitmap(bitmap).build());
|
|
|
|
for (int i = 0; i < barcodes.size(); i++) {
|
|
|
|
Barcode code = barcodes.valueAt(i);
|
|
|
|
if (code.valueFormat == Barcode.DRIVER_LICENSE && code.driverLicense != null) { // BarcodeDetector has built-in support for North American driver licenses/IDs
|
|
|
|
Result res = new Result();
|
|
|
|
res.type = "ID".equals(code.driverLicense.documentType) ? Result.TYPE_ID : Result.TYPE_DRIVER_LICENSE;
|
|
|
|
switch (code.driverLicense.issuingCountry) {
|
2018-07-30 04:07:02 +02:00
|
|
|
case "USA":
|
2019-12-31 14:08:08 +01:00
|
|
|
res.nationality = res.issuingCountry = "US";
|
2018-07-30 04:07:02 +02:00
|
|
|
break;
|
|
|
|
case "CAN":
|
2019-12-31 14:08:08 +01:00
|
|
|
res.nationality = res.issuingCountry = "CA";
|
2018-07-30 04:07:02 +02:00
|
|
|
break;
|
|
|
|
}
|
2019-12-31 14:08:08 +01:00
|
|
|
res.firstName = capitalize(code.driverLicense.firstName);
|
|
|
|
res.lastName = capitalize(code.driverLicense.lastName);
|
|
|
|
res.middleName = capitalize(code.driverLicense.middleName);
|
|
|
|
res.number = code.driverLicense.licenseNumber;
|
|
|
|
if (code.driverLicense.gender != null) {
|
|
|
|
switch (code.driverLicense.gender) {
|
2018-07-30 04:07:02 +02:00
|
|
|
case "1":
|
2019-12-31 14:08:08 +01:00
|
|
|
res.gender = Result.GENDER_MALE;
|
2018-07-30 04:07:02 +02:00
|
|
|
break;
|
|
|
|
case "2":
|
2019-12-31 14:08:08 +01:00
|
|
|
res.gender = Result.GENDER_FEMALE;
|
2018-07-30 04:07:02 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
int yearOffset, monthOffset, dayOffset;
|
2019-12-31 14:08:08 +01:00
|
|
|
if ("USA".equals(res.issuingCountry)) {
|
|
|
|
yearOffset = 4;
|
|
|
|
dayOffset = 2;
|
|
|
|
monthOffset = 0;
|
|
|
|
} else {
|
|
|
|
yearOffset = 0;
|
|
|
|
monthOffset = 4;
|
|
|
|
dayOffset = 6;
|
2018-07-30 04:07:02 +02:00
|
|
|
}
|
2019-12-31 14:08:08 +01:00
|
|
|
try {
|
|
|
|
if (code.driverLicense.birthDate != null && code.driverLicense.birthDate.length() == 8) {
|
|
|
|
res.birthYear = Integer.parseInt(code.driverLicense.birthDate.substring(yearOffset, yearOffset + 4));
|
|
|
|
res.birthMonth = Integer.parseInt(code.driverLicense.birthDate.substring(monthOffset, monthOffset + 2));
|
|
|
|
res.birthDay = Integer.parseInt(code.driverLicense.birthDate.substring(dayOffset, dayOffset + 2));
|
2018-07-30 04:07:02 +02:00
|
|
|
}
|
2019-12-31 14:08:08 +01:00
|
|
|
if (code.driverLicense.expiryDate != null && code.driverLicense.expiryDate.length() == 8) {
|
|
|
|
res.expiryYear = Integer.parseInt(code.driverLicense.expiryDate.substring(yearOffset, yearOffset + 4));
|
|
|
|
res.expiryMonth = Integer.parseInt(code.driverLicense.expiryDate.substring(monthOffset, monthOffset + 2));
|
|
|
|
res.expiryDay = Integer.parseInt(code.driverLicense.expiryDate.substring(dayOffset, dayOffset + 2));
|
2018-07-30 04:07:02 +02:00
|
|
|
}
|
2019-12-31 14:08:08 +01:00
|
|
|
} catch (NumberFormatException ignore) {
|
|
|
|
}
|
2018-07-30 04:07:02 +02:00
|
|
|
|
|
|
|
return res;
|
2019-12-31 14:08:08 +01:00
|
|
|
} else if (code.valueFormat == Barcode.TEXT && code.format == Barcode.PDF417) { // Russian driver licenses (new-ish ones) use a non-very-much-documented format
|
2018-07-30 04:07:02 +02:00
|
|
|
// base64(number|issue date|expiry date|last name|first name|middle/father name|birth date|categories|???|???)
|
|
|
|
// all dates are YYYYMMDD, names are capital cyrillic letters in Windows-1251, categories are comma separated
|
2019-12-31 14:08:08 +01:00
|
|
|
if (code.rawValue.matches("^[A-Za-z0-9=]+$")) {
|
|
|
|
try {
|
|
|
|
byte[] _data = Base64.decode(code.rawValue, 0);
|
|
|
|
String[] data = new String(_data, "windows-1251").split("\\|");
|
|
|
|
if (data.length >= 10) {
|
|
|
|
Result res = new Result();
|
|
|
|
res.type = Result.TYPE_DRIVER_LICENSE;
|
|
|
|
res.nationality = res.issuingCountry = "RU";
|
|
|
|
res.number = data[0];
|
|
|
|
res.expiryYear = Integer.parseInt(data[2].substring(0, 4));
|
|
|
|
res.expiryMonth = Integer.parseInt(data[2].substring(4, 6));
|
|
|
|
res.expiryDay = Integer.parseInt(data[2].substring(6));
|
|
|
|
res.lastName = capitalize(cyrillicToLatin(data[3]));
|
|
|
|
res.firstName = capitalize(cyrillicToLatin(data[4]));
|
|
|
|
res.middleName = capitalize(cyrillicToLatin(data[5]));
|
|
|
|
res.birthYear = Integer.parseInt(data[6].substring(0, 4));
|
|
|
|
res.birthMonth = Integer.parseInt(data[6].substring(4, 6));
|
|
|
|
res.birthDay = Integer.parseInt(data[6].substring(6));
|
2018-07-30 04:07:02 +02:00
|
|
|
return res;
|
|
|
|
}
|
2019-12-31 14:08:08 +01:00
|
|
|
} catch (Exception ignore) {
|
|
|
|
}
|
2018-07-30 04:07:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static Result recognizeMRZ(Bitmap bitmap) {
|
|
|
|
Bitmap smallBitmap;
|
|
|
|
float scale = 1;
|
|
|
|
if (bitmap.getWidth() > 512 || bitmap.getHeight() > 512) {
|
|
|
|
scale = 512f / Math.max(bitmap.getWidth(), bitmap.getHeight());
|
|
|
|
smallBitmap = Bitmap.createScaledBitmap(bitmap, Math.round(bitmap.getWidth() * scale), Math.round(bitmap.getHeight() * scale), true);
|
|
|
|
} else {
|
|
|
|
smallBitmap = bitmap;
|
|
|
|
}
|
|
|
|
|
|
|
|
int[] points = findCornerPoints(smallBitmap);
|
|
|
|
float pointsScale = 1f / scale;
|
|
|
|
if (points != null) {
|
|
|
|
Point topLeft = new Point(points[0], points[1]), topRight = new Point(points[2], points[3]),
|
|
|
|
bottomLeft = new Point(points[4], points[5]), bottomRight = new Point(points[6], points[7]);
|
|
|
|
if (topRight.x < topLeft.x) {
|
|
|
|
Point tmp = topRight;
|
|
|
|
topRight = topLeft;
|
|
|
|
topLeft = tmp;
|
|
|
|
tmp = bottomRight;
|
|
|
|
bottomRight = bottomLeft;
|
|
|
|
bottomLeft = tmp;
|
|
|
|
}
|
|
|
|
double topLength = Math.hypot(topRight.x - topLeft.x, topRight.y - topLeft.y);
|
|
|
|
double bottomLength = Math.hypot(bottomRight.x - bottomLeft.x, bottomRight.y - bottomLeft.y);
|
|
|
|
double leftLength = Math.hypot(bottomLeft.x - topLeft.x, bottomLeft.y - topLeft.y);
|
|
|
|
double rightLength = Math.hypot(bottomRight.x - topRight.x, bottomRight.y - topRight.y);
|
|
|
|
double tlRatio = topLength / leftLength;
|
|
|
|
double trRatio = topLength / rightLength;
|
|
|
|
double blRatio = bottomLength / leftLength;
|
|
|
|
double brRatio = bottomLength / rightLength;
|
|
|
|
if ((tlRatio >= 1.35 && tlRatio <= 1.75) && (blRatio >= 1.35 && blRatio <= 1.75) && (trRatio >= 1.35 && trRatio <= 1.75) && (brRatio >= 1.35 && brRatio <= 1.75)) {
|
|
|
|
double avgRatio = (tlRatio + trRatio + blRatio + brRatio) / 4.0;
|
|
|
|
Bitmap tmp = Bitmap.createBitmap(1024, (int) Math.round(1024 / avgRatio), Bitmap.Config.ARGB_8888);
|
|
|
|
Canvas c = new Canvas(tmp);
|
|
|
|
float[] dst = new float[]{
|
|
|
|
0,
|
|
|
|
0,
|
|
|
|
tmp.getWidth(),
|
|
|
|
0,
|
|
|
|
tmp.getWidth(),
|
|
|
|
tmp.getHeight(),
|
|
|
|
0,
|
|
|
|
tmp.getHeight()
|
|
|
|
};
|
|
|
|
float[] src = new float[]{
|
|
|
|
topLeft.x * pointsScale,
|
|
|
|
topLeft.y * pointsScale,
|
|
|
|
topRight.x * pointsScale,
|
|
|
|
topRight.y * pointsScale,
|
|
|
|
bottomRight.x * pointsScale,
|
|
|
|
bottomRight.y * pointsScale,
|
|
|
|
bottomLeft.x * pointsScale,
|
|
|
|
bottomLeft.y * pointsScale
|
|
|
|
};
|
|
|
|
Matrix perspMatrix = new Matrix();
|
|
|
|
perspMatrix.setPolyToPoly(src, 0, dst, 0, src.length >> 1);
|
|
|
|
c.drawBitmap(bitmap, perspMatrix, new Paint(Paint.FILTER_BITMAP_FLAG));
|
|
|
|
bitmap = tmp;
|
|
|
|
}
|
|
|
|
} else if (bitmap.getWidth() > 1500 || bitmap.getHeight() > 1500) {
|
|
|
|
scale = 1500f / Math.max(bitmap.getWidth(), bitmap.getHeight());
|
|
|
|
bitmap = Bitmap.createScaledBitmap(bitmap, Math.round(bitmap.getWidth() * scale), Math.round(bitmap.getHeight() * scale), true);
|
|
|
|
}
|
|
|
|
|
2019-12-31 14:08:08 +01:00
|
|
|
Bitmap binaryBitmap = null;
|
|
|
|
Rect[][] charRects = null;
|
|
|
|
int maxLength = 0;
|
|
|
|
int lineCount = 0;
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
|
|
Matrix m = null;
|
|
|
|
Bitmap toProcess = bitmap;
|
|
|
|
switch (i) {
|
2018-07-30 04:07:02 +02:00
|
|
|
case 1:
|
2019-12-31 14:08:08 +01:00
|
|
|
m = new Matrix();
|
|
|
|
m.setRotate(1f, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
|
2018-07-30 04:07:02 +02:00
|
|
|
break;
|
|
|
|
case 2:
|
2019-12-31 14:08:08 +01:00
|
|
|
m = new Matrix();
|
|
|
|
m.setRotate(-1f, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
|
2018-07-30 04:07:02 +02:00
|
|
|
break;
|
|
|
|
}
|
2019-12-31 14:08:08 +01:00
|
|
|
if (m != null) {
|
|
|
|
toProcess = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m, true);
|
2018-07-30 04:07:02 +02:00
|
|
|
}
|
2019-12-31 14:08:08 +01:00
|
|
|
binaryBitmap = Bitmap.createBitmap(toProcess.getWidth(), toProcess.getHeight(), Bitmap.Config.ALPHA_8);
|
|
|
|
charRects = binarizeAndFindCharacters(toProcess, binaryBitmap);
|
|
|
|
if (charRects == null)
|
2018-07-30 04:07:02 +02:00
|
|
|
return null;
|
2019-12-31 14:08:08 +01:00
|
|
|
for (Rect[] rects : charRects) {
|
|
|
|
maxLength = Math.max(rects.length, maxLength);
|
|
|
|
if (rects.length > 0)
|
2018-07-30 04:07:02 +02:00
|
|
|
lineCount++;
|
|
|
|
}
|
2019-12-31 14:08:08 +01:00
|
|
|
if (lineCount >= 2 && maxLength >= 30) {
|
2018-07-30 04:07:02 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2019-12-31 14:08:08 +01:00
|
|
|
if (maxLength < 30 || lineCount < 2)
|
2018-07-30 04:07:02 +02:00
|
|
|
return null;
|
|
|
|
Bitmap chrBitmap = Bitmap.createBitmap(10 * charRects[0].length, 15 * charRects.length, Bitmap.Config.ALPHA_8);
|
|
|
|
Canvas chrCanvas = new Canvas(chrBitmap);
|
|
|
|
Paint aaPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
|
|
|
|
Rect dst = new Rect(0, 0, 10, 15);
|
|
|
|
int x, y = 0;
|
|
|
|
for (Rect[] line : charRects) {
|
|
|
|
x = 0;
|
|
|
|
for (Rect rect : line) {
|
|
|
|
dst.set(x * 10, y * 15, x * 10 + 10, y * 15 + 15);
|
|
|
|
chrCanvas.drawBitmap(binaryBitmap, rect, dst, aaPaint);
|
|
|
|
x++;
|
|
|
|
}
|
|
|
|
y++;
|
|
|
|
}
|
|
|
|
String mrz = performRecognition(chrBitmap, charRects.length, charRects[0].length, ApplicationLoader.applicationContext.getAssets());
|
|
|
|
if (mrz == null)
|
|
|
|
return null;
|
|
|
|
|
|
|
|
String[] mrzLines = TextUtils.split(mrz, "\n");
|
|
|
|
Result result = new Result();
|
|
|
|
if (mrzLines.length >= 2 && mrzLines[0].length() >= 30 && mrzLines[1].length() == mrzLines[0].length()) {
|
2019-12-31 14:08:08 +01:00
|
|
|
result.rawMRZ = TextUtils.join("\n", mrzLines);
|
2018-07-30 04:07:02 +02:00
|
|
|
HashMap<String, String> countries = getCountriesMap();
|
|
|
|
char type = mrzLines[0].charAt(0);
|
|
|
|
if (type == 'P') { // passport
|
|
|
|
result.type = Result.TYPE_PASSPORT;
|
|
|
|
if (mrzLines[0].length() == 44) {
|
|
|
|
result.issuingCountry = mrzLines[0].substring(2, 5);
|
2019-12-31 14:08:08 +01:00
|
|
|
int lastNameEnd = mrzLines[0].indexOf("<<", 6);
|
|
|
|
if (lastNameEnd != -1) {
|
|
|
|
result.lastName = mrzLines[0].substring(5, lastNameEnd).replace('<', ' ').replace('0', 'O').trim();
|
|
|
|
result.firstName = mrzLines[0].substring(lastNameEnd + 2).replace('<', ' ').replace('0', 'O').trim();
|
|
|
|
if (result.firstName.contains(" ")) {
|
|
|
|
result.firstName = result.firstName.substring(0, result.firstName.indexOf(" "));
|
2018-07-30 04:07:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
String number = mrzLines[1].substring(0, 9).replace('<', ' ').replace('O', '0').trim();
|
|
|
|
int numberChecksum = checksum(number);
|
|
|
|
if (numberChecksum == getNumber(mrzLines[1].charAt(9))) {
|
|
|
|
result.number = number;
|
|
|
|
}
|
|
|
|
result.nationality = mrzLines[1].substring(10, 13);
|
|
|
|
String birthDate = mrzLines[1].substring(13, 19).replace('O', '0').replace('I', '1');
|
|
|
|
int birthDateChecksum = checksum(birthDate);
|
|
|
|
if (birthDateChecksum == getNumber(mrzLines[1].charAt(19))) {
|
|
|
|
parseBirthDate(birthDate, result);
|
|
|
|
}
|
|
|
|
result.gender = parseGender(mrzLines[1].charAt(20));
|
|
|
|
String expiryDate = mrzLines[1].substring(21, 27).replace('O', '0').replace('I', '1');
|
|
|
|
int expiryDateChecksum = checksum(expiryDate);
|
|
|
|
if (expiryDateChecksum == getNumber(mrzLines[1].charAt(27)) || mrzLines[1].charAt(27) == '<') {
|
|
|
|
parseExpiryDate(expiryDate, result);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Russian internal passports use transliteration for the name and have a number one digit longer than fits into the standard MRZ format
|
|
|
|
if ("RUS".equals(result.issuingCountry) && mrzLines[0].charAt(1) == 'N') {
|
2019-12-31 14:08:08 +01:00
|
|
|
result.type = Result.TYPE_INTERNAL_PASSPORT;
|
|
|
|
String[] names = result.firstName.split(" ");
|
2018-08-27 10:33:11 +02:00
|
|
|
result.firstName = cyrillicToLatin(russianPassportTranslit(names[0]));
|
2019-12-31 14:08:08 +01:00
|
|
|
if (names.length > 1)
|
|
|
|
result.middleName = cyrillicToLatin(russianPassportTranslit(names[1]));
|
2018-07-30 04:07:02 +02:00
|
|
|
result.lastName = cyrillicToLatin(russianPassportTranslit(result.lastName));
|
|
|
|
if (result.number != null)
|
|
|
|
result.number = result.number.substring(0, 3) + mrzLines[1].charAt(28) + result.number.substring(3);
|
|
|
|
} else {
|
|
|
|
result.firstName = result.firstName.replace('8', 'B');
|
|
|
|
result.lastName = result.lastName.replace('8', 'B');
|
|
|
|
}
|
|
|
|
result.lastName = capitalize(result.lastName);
|
|
|
|
result.firstName = capitalize(result.firstName);
|
2018-08-27 10:33:11 +02:00
|
|
|
result.middleName = capitalize(result.middleName);
|
2018-07-30 04:07:02 +02:00
|
|
|
}
|
|
|
|
} else if (type == 'I' || type == 'A' || type == 'C') { // id
|
|
|
|
result.type = Result.TYPE_ID;
|
|
|
|
if (mrzLines.length == 3 && mrzLines[0].length() == 30 && mrzLines[2].length() == 30) {
|
|
|
|
result.issuingCountry = mrzLines[0].substring(2, 5);
|
|
|
|
String number = mrzLines[0].substring(5, 14).replace('<', ' ').replace('O', '0').trim();
|
|
|
|
int numberChecksum = checksum(number);
|
|
|
|
if (numberChecksum == mrzLines[0].charAt(14) - '0') {
|
|
|
|
result.number = number;
|
|
|
|
}
|
|
|
|
|
|
|
|
String birthDate = mrzLines[1].substring(0, 6).replace('O', '0').replace('I', '1');
|
|
|
|
int birthDateChecksum = checksum(birthDate);
|
|
|
|
if (birthDateChecksum == getNumber(mrzLines[1].charAt(6))) {
|
|
|
|
parseBirthDate(birthDate, result);
|
|
|
|
}
|
|
|
|
result.gender = parseGender(mrzLines[1].charAt(7));
|
|
|
|
String expiryDate = mrzLines[1].substring(8, 14).replace('O', '0').replace('I', '1');
|
|
|
|
int expiryDateChecksum = checksum(expiryDate);
|
|
|
|
if (expiryDateChecksum == getNumber(mrzLines[1].charAt(14)) || mrzLines[1].charAt(14) == '<') {
|
|
|
|
parseExpiryDate(expiryDate, result);
|
|
|
|
}
|
|
|
|
result.nationality = mrzLines[1].substring(15, 18);
|
|
|
|
int lastNameEnd = mrzLines[2].indexOf("<<");
|
|
|
|
if (lastNameEnd != -1) {
|
|
|
|
result.lastName = mrzLines[2].substring(0, lastNameEnd).replace('<', ' ').trim();
|
|
|
|
result.firstName = mrzLines[2].substring(lastNameEnd + 2).replace('<', ' ').trim();
|
|
|
|
}
|
|
|
|
} else if (mrzLines.length == 2 && mrzLines[0].length() == 36) {
|
|
|
|
result.issuingCountry = mrzLines[0].substring(2, 5);
|
|
|
|
if ("FRA".equals(result.issuingCountry) && type == 'I' && mrzLines[0].charAt(1) == 'D') { // French IDs use an entirely different format
|
|
|
|
result.nationality = "FRA";
|
|
|
|
result.lastName = mrzLines[0].substring(5, 30).replace('<', ' ').trim();
|
|
|
|
result.firstName = mrzLines[1].substring(13, 27).replace("<<", ", ").replace('<', ' ').trim();
|
|
|
|
String number = mrzLines[1].substring(0, 12).replace('O', '0');
|
|
|
|
if (checksum(number) == getNumber(mrzLines[1].charAt(12))) {
|
|
|
|
result.number = number;
|
|
|
|
}
|
|
|
|
String birthDate = mrzLines[1].substring(27, 33).replace('O', '0').replace('I', '1');
|
|
|
|
if (checksum(birthDate) == getNumber(mrzLines[1].charAt(33))) {
|
|
|
|
parseBirthDate(birthDate, result);
|
|
|
|
}
|
|
|
|
result.gender = parseGender(mrzLines[1].charAt(34));
|
|
|
|
result.doesNotExpire = true;
|
|
|
|
} else {
|
|
|
|
int lastNameEnd = mrzLines[0].indexOf("<<");
|
|
|
|
if (lastNameEnd != -1) {
|
|
|
|
result.lastName = mrzLines[0].substring(5, lastNameEnd).replace('<', ' ').trim();
|
|
|
|
result.firstName = mrzLines[0].substring(lastNameEnd + 2).replace('<', ' ').trim();
|
|
|
|
}
|
|
|
|
String number = mrzLines[1].substring(0, 9).replace('<', ' ').replace('O', '0').trim();
|
|
|
|
int numberChecksum = checksum(number);
|
|
|
|
if (numberChecksum == getNumber(mrzLines[1].charAt(9))) {
|
|
|
|
result.number = number;
|
|
|
|
}
|
|
|
|
result.nationality = mrzLines[1].substring(10, 13);
|
|
|
|
String birthDate = mrzLines[1].substring(13, 19).replace('O', '0').replace('I', '1');
|
|
|
|
if (checksum(birthDate) == getNumber(mrzLines[1].charAt(19))) {
|
|
|
|
parseBirthDate(birthDate, result);
|
|
|
|
}
|
|
|
|
result.gender = parseGender(mrzLines[1].charAt(20));
|
|
|
|
String expiryDate = mrzLines[1].substring(21, 27).replace('O', '0').replace('I', '1');
|
|
|
|
if (checksum(expiryDate) == getNumber(mrzLines[1].charAt(27)) || mrzLines[1].charAt(27) == '<') {
|
|
|
|
parseExpiryDate(expiryDate, result);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
result.firstName = capitalize(result.firstName.replace('0', 'O').replace('8', 'B'));
|
|
|
|
result.lastName = capitalize(result.lastName.replace('0', 'O').replace('8', 'B'));
|
2018-08-27 10:33:11 +02:00
|
|
|
} else {
|
|
|
|
return null;
|
2018-07-30 04:07:02 +02:00
|
|
|
}
|
2019-12-31 14:08:08 +01:00
|
|
|
if (TextUtils.isEmpty(result.firstName) && TextUtils.isEmpty(result.lastName))
|
2018-08-27 10:33:11 +02:00
|
|
|
return null;
|
2018-07-30 04:07:02 +02:00
|
|
|
result.issuingCountry = countries.get(result.issuingCountry);
|
|
|
|
result.nationality = countries.get(result.nationality);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-12-31 14:08:08 +01:00
|
|
|
public static Result recognize(byte[] yuvData, int width, int height, int rotation) {
|
|
|
|
Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
2018-07-30 04:07:02 +02:00
|
|
|
setYuvBitmapPixels(bmp, yuvData);
|
2019-12-31 14:08:08 +01:00
|
|
|
Matrix m = new Matrix();
|
2018-07-30 04:07:02 +02:00
|
|
|
m.setRotate(rotation);
|
2019-12-31 14:08:08 +01:00
|
|
|
int minSize = Math.min(width, height);
|
|
|
|
int dh = Math.round(minSize * 0.704f);
|
|
|
|
boolean swap = rotation == 90 || rotation == 270;
|
|
|
|
bmp = Bitmap.createBitmap(bmp, swap ? (width / 2 - dh / 2) : 0, swap ? 0 : (height / 2 - dh / 2), swap ? dh : minSize, swap ? minSize : dh, m, false);
|
2018-07-30 04:07:02 +02:00
|
|
|
return recognize(bmp, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
private static String capitalize(String s) {
|
2019-12-31 14:08:08 +01:00
|
|
|
if (s == null)
|
2018-08-27 10:33:11 +02:00
|
|
|
return null;
|
2018-07-30 04:07:02 +02:00
|
|
|
char[] chars = s.toCharArray();
|
|
|
|
boolean prevIsSpace = true;
|
|
|
|
for (int i = 0; i < chars.length; i++) {
|
|
|
|
if (!prevIsSpace && Character.isLetter(chars[i])) {
|
|
|
|
chars[i] = Character.toLowerCase(chars[i]);
|
|
|
|
} else {
|
|
|
|
prevIsSpace = chars[i] == ' ';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return new String(chars);
|
|
|
|
}
|
|
|
|
|
|
|
|
private static int checksum(String s) {
|
|
|
|
int val = 0;
|
|
|
|
char[] chars = s.toCharArray();
|
|
|
|
final int[] weights = {7, 3, 1};
|
|
|
|
for (int i = 0; i < chars.length; i++) {
|
|
|
|
int charVal = 0;
|
|
|
|
if (chars[i] >= '0' && chars[i] <= '9') {
|
|
|
|
charVal = chars[i] - '0';
|
|
|
|
} else if (chars[i] >= 'A' && chars[i] <= 'Z') {
|
|
|
|
charVal = chars[i] - 'A' + 10;
|
|
|
|
}
|
|
|
|
val += charVal * weights[i % weights.length];
|
|
|
|
}
|
|
|
|
return val % 10;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static void parseBirthDate(String birthDate, Result result) {
|
|
|
|
try {
|
|
|
|
result.birthYear = Integer.parseInt(birthDate.substring(0, 2));
|
|
|
|
result.birthYear = result.birthYear < Calendar.getInstance().get(Calendar.YEAR) % 100 - 5 ? (2000 + result.birthYear) : (1900 + result.birthYear);
|
|
|
|
result.birthMonth = Integer.parseInt(birthDate.substring(2, 4));
|
|
|
|
result.birthDay = Integer.parseInt(birthDate.substring(4));
|
|
|
|
} catch (NumberFormatException ignore) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private static void parseExpiryDate(String expiryDate, Result result) {
|
|
|
|
try {
|
|
|
|
if ("<<<<<<".equals(expiryDate)) {
|
|
|
|
result.doesNotExpire = true;
|
|
|
|
} else {
|
|
|
|
result.expiryYear = 2000 + Integer.parseInt(expiryDate.substring(0, 2));
|
|
|
|
result.expiryMonth = Integer.parseInt(expiryDate.substring(2, 4));
|
|
|
|
result.expiryDay = Integer.parseInt(expiryDate.substring(4));
|
|
|
|
}
|
|
|
|
} catch (NumberFormatException ignore) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private static int parseGender(char gender) {
|
|
|
|
switch (gender) {
|
|
|
|
case 'M':
|
|
|
|
return Result.GENDER_MALE;
|
|
|
|
case 'F':
|
|
|
|
return Result.GENDER_FEMALE;
|
|
|
|
default:
|
|
|
|
return Result.GENDER_UNKNOWN;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private static String russianPassportTranslit(String s) {
|
|
|
|
final String cyrillic = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ";
|
|
|
|
final String latin = "ABVGDE2JZIQKLMNOPRSTUFHC34WXY9678";
|
|
|
|
char[] chars = s.toCharArray();
|
|
|
|
for (int i = 0; i < chars.length; i++) {
|
|
|
|
int idx = latin.indexOf(chars[i]);
|
|
|
|
if (idx != -1)
|
|
|
|
chars[i] = cyrillic.charAt(idx);
|
|
|
|
}
|
|
|
|
return new String(chars);
|
|
|
|
}
|
|
|
|
|
2019-12-31 14:08:08 +01:00
|
|
|
private static String cyrillicToLatin(String s) {
|
|
|
|
final String alphabet = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ";
|
|
|
|
final String[] replacements = {"A", "B", "V", "G", "D", "E", "E", "ZH", "Z", "I", "I", "K", "L", "M", "N", "O", "P", "R", "S", "T", "U", "F", "KH", "TS", "CH", "SH", "SHCH", "IE", "Y", "", "E", "IU", "IA"};
|
|
|
|
for (int i = 0; i < replacements.length; i++) {
|
|
|
|
s = s.replace(alphabet.substring(i, i + 1), replacements[i]);
|
2018-07-30 04:07:02 +02:00
|
|
|
}
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static int getNumber(char c) {
|
|
|
|
if (c == 'O')
|
|
|
|
return 0;
|
|
|
|
if (c == 'I')
|
|
|
|
return 1;
|
2018-08-27 10:33:11 +02:00
|
|
|
if (c == 'B')
|
|
|
|
return 8;
|
2018-07-30 04:07:02 +02:00
|
|
|
return c - '0';
|
|
|
|
}
|
|
|
|
|
|
|
|
private static HashMap<String, String> getCountriesMap() {
|
|
|
|
HashMap<String, String> countries = new HashMap<>();
|
|
|
|
countries.put("AFG", "AF");
|
|
|
|
countries.put("ALA", "AX");
|
|
|
|
countries.put("ALB", "AL");
|
|
|
|
countries.put("DZA", "DZ");
|
|
|
|
countries.put("ASM", "AS");
|
|
|
|
countries.put("AND", "AD");
|
|
|
|
countries.put("AGO", "AO");
|
|
|
|
countries.put("AIA", "AI");
|
|
|
|
countries.put("ATA", "AQ");
|
|
|
|
countries.put("ATG", "AG");
|
|
|
|
countries.put("ARG", "AR");
|
|
|
|
countries.put("ARM", "AM");
|
|
|
|
countries.put("ABW", "AW");
|
|
|
|
countries.put("AUS", "AU");
|
|
|
|
countries.put("AUT", "AT");
|
|
|
|
countries.put("AZE", "AZ");
|
|
|
|
countries.put("BHS", "BS");
|
|
|
|
countries.put("BHR", "BH");
|
|
|
|
countries.put("BGD", "BD");
|
|
|
|
countries.put("BRB", "BB");
|
|
|
|
countries.put("BLR", "BY");
|
|
|
|
countries.put("BEL", "BE");
|
|
|
|
countries.put("BLZ", "BZ");
|
|
|
|
countries.put("BEN", "BJ");
|
|
|
|
countries.put("BMU", "BM");
|
|
|
|
countries.put("BTN", "BT");
|
|
|
|
countries.put("BOL", "BO");
|
|
|
|
countries.put("BES", "BQ");
|
|
|
|
countries.put("BIH", "BA");
|
|
|
|
countries.put("BWA", "BW");
|
|
|
|
countries.put("BVT", "BV");
|
|
|
|
countries.put("BRA", "BR");
|
|
|
|
countries.put("IOT", "IO");
|
|
|
|
countries.put("BRN", "BN");
|
|
|
|
countries.put("BGR", "BG");
|
|
|
|
countries.put("BFA", "BF");
|
|
|
|
countries.put("BDI", "BI");
|
|
|
|
countries.put("CPV", "CV");
|
|
|
|
countries.put("KHM", "KH");
|
|
|
|
countries.put("CMR", "CM");
|
|
|
|
countries.put("CAN", "CA");
|
|
|
|
countries.put("CYM", "KY");
|
|
|
|
countries.put("CAF", "CF");
|
|
|
|
countries.put("TCD", "TD");
|
|
|
|
countries.put("CHL", "CL");
|
|
|
|
countries.put("CHN", "CN");
|
|
|
|
countries.put("CXR", "CX");
|
|
|
|
countries.put("CCK", "CC");
|
|
|
|
countries.put("COL", "CO");
|
|
|
|
countries.put("COM", "KM");
|
|
|
|
countries.put("COG", "CG");
|
|
|
|
countries.put("COD", "CD");
|
|
|
|
countries.put("COK", "CK");
|
|
|
|
countries.put("CRI", "CR");
|
|
|
|
countries.put("CIV", "CI");
|
|
|
|
countries.put("HRV", "HR");
|
|
|
|
countries.put("CUB", "CU");
|
|
|
|
countries.put("CUW", "CW");
|
|
|
|
countries.put("CYP", "CY");
|
|
|
|
countries.put("CZE", "CZ");
|
|
|
|
countries.put("DNK", "DK");
|
|
|
|
countries.put("DJI", "DJ");
|
|
|
|
countries.put("DMA", "DM");
|
|
|
|
countries.put("DOM", "DO");
|
|
|
|
countries.put("ECU", "EC");
|
|
|
|
countries.put("EGY", "EG");
|
|
|
|
countries.put("SLV", "SV");
|
|
|
|
countries.put("GNQ", "GQ");
|
|
|
|
countries.put("ERI", "ER");
|
|
|
|
countries.put("EST", "EE");
|
|
|
|
countries.put("ETH", "ET");
|
|
|
|
countries.put("FLK", "FK");
|
|
|
|
countries.put("FRO", "FO");
|
|
|
|
countries.put("FJI", "FJ");
|
|
|
|
countries.put("FIN", "FI");
|
|
|
|
countries.put("FRA", "FR");
|
|
|
|
countries.put("GUF", "GF");
|
|
|
|
countries.put("PYF", "PF");
|
|
|
|
countries.put("ATF", "TF");
|
|
|
|
countries.put("GAB", "GA");
|
|
|
|
countries.put("GMB", "GM");
|
|
|
|
countries.put("GEO", "GE");
|
|
|
|
countries.put("D<<", "DE");
|
|
|
|
countries.put("GHA", "GH");
|
|
|
|
countries.put("GIB", "GI");
|
|
|
|
countries.put("GRC", "GR");
|
|
|
|
countries.put("GRL", "GL");
|
|
|
|
countries.put("GRD", "GD");
|
|
|
|
countries.put("GLP", "GP");
|
|
|
|
countries.put("GUM", "GU");
|
|
|
|
countries.put("GTM", "GT");
|
|
|
|
countries.put("GGY", "GG");
|
|
|
|
countries.put("GIN", "GN");
|
|
|
|
countries.put("GNB", "GW");
|
|
|
|
countries.put("GUY", "GY");
|
|
|
|
countries.put("HTI", "HT");
|
|
|
|
countries.put("HMD", "HM");
|
|
|
|
countries.put("VAT", "VA");
|
|
|
|
countries.put("HND", "HN");
|
|
|
|
countries.put("HKG", "HK");
|
|
|
|
countries.put("HUN", "HU");
|
|
|
|
countries.put("ISL", "IS");
|
|
|
|
countries.put("IND", "IN");
|
|
|
|
countries.put("IDN", "ID");
|
|
|
|
countries.put("IRN", "IR");
|
|
|
|
countries.put("IRQ", "IQ");
|
|
|
|
countries.put("IRL", "IE");
|
|
|
|
countries.put("IMN", "IM");
|
|
|
|
countries.put("ISR", "IL");
|
|
|
|
countries.put("ITA", "IT");
|
|
|
|
countries.put("JAM", "JM");
|
|
|
|
countries.put("JPN", "JP");
|
|
|
|
countries.put("JEY", "JE");
|
|
|
|
countries.put("JOR", "JO");
|
|
|
|
countries.put("KAZ", "KZ");
|
|
|
|
countries.put("KEN", "KE");
|
|
|
|
countries.put("KIR", "KI");
|
|
|
|
countries.put("PRK", "KP");
|
|
|
|
countries.put("KOR", "KR");
|
|
|
|
countries.put("KWT", "KW");
|
|
|
|
countries.put("KGZ", "KG");
|
|
|
|
countries.put("LAO", "LA");
|
|
|
|
countries.put("LVA", "LV");
|
|
|
|
countries.put("LBN", "LB");
|
|
|
|
countries.put("LSO", "LS");
|
|
|
|
countries.put("LBR", "LR");
|
|
|
|
countries.put("LBY", "LY");
|
|
|
|
countries.put("LIE", "LI");
|
|
|
|
countries.put("LTU", "LT");
|
|
|
|
countries.put("LUX", "LU");
|
|
|
|
countries.put("MAC", "MO");
|
|
|
|
countries.put("MKD", "MK");
|
|
|
|
countries.put("MDG", "MG");
|
|
|
|
countries.put("MWI", "MW");
|
|
|
|
countries.put("MYS", "MY");
|
|
|
|
countries.put("MDV", "MV");
|
|
|
|
countries.put("MLI", "ML");
|
|
|
|
countries.put("MLT", "MT");
|
|
|
|
countries.put("MHL", "MH");
|
|
|
|
countries.put("MTQ", "MQ");
|
|
|
|
countries.put("MRT", "MR");
|
|
|
|
countries.put("MUS", "MU");
|
|
|
|
countries.put("MYT", "YT");
|
|
|
|
countries.put("MEX", "MX");
|
|
|
|
countries.put("FSM", "FM");
|
|
|
|
countries.put("MDA", "MD");
|
|
|
|
countries.put("MCO", "MC");
|
|
|
|
countries.put("MNG", "MN");
|
|
|
|
countries.put("MNE", "ME");
|
|
|
|
countries.put("MSR", "MS");
|
|
|
|
countries.put("MAR", "MA");
|
|
|
|
countries.put("MOZ", "MZ");
|
|
|
|
countries.put("MMR", "MM");
|
|
|
|
countries.put("NAM", "NA");
|
|
|
|
countries.put("NRU", "NR");
|
|
|
|
countries.put("NPL", "NP");
|
|
|
|
countries.put("NLD", "NL");
|
|
|
|
countries.put("NCL", "NC");
|
|
|
|
countries.put("NZL", "NZ");
|
|
|
|
countries.put("NIC", "NI");
|
|
|
|
countries.put("NER", "NE");
|
|
|
|
countries.put("NGA", "NG");
|
|
|
|
countries.put("NIU", "NU");
|
|
|
|
countries.put("NFK", "NF");
|
|
|
|
countries.put("MNP", "MP");
|
|
|
|
countries.put("NOR", "NO");
|
|
|
|
countries.put("OMN", "OM");
|
|
|
|
countries.put("PAK", "PK");
|
|
|
|
countries.put("PLW", "PW");
|
|
|
|
countries.put("PSE", "PS");
|
|
|
|
countries.put("PAN", "PA");
|
|
|
|
countries.put("PNG", "PG");
|
|
|
|
countries.put("PRY", "PY");
|
|
|
|
countries.put("PER", "PE");
|
|
|
|
countries.put("PHL", "PH");
|
|
|
|
countries.put("PCN", "PN");
|
|
|
|
countries.put("POL", "PL");
|
|
|
|
countries.put("PRT", "PT");
|
|
|
|
countries.put("PRI", "PR");
|
|
|
|
countries.put("QAT", "QA");
|
|
|
|
countries.put("REU", "RE");
|
|
|
|
countries.put("ROU", "RO");
|
|
|
|
countries.put("RUS", "RU");
|
|
|
|
countries.put("RWA", "RW");
|
|
|
|
countries.put("BLM", "BL");
|
|
|
|
countries.put("SHN", "SH");
|
|
|
|
countries.put("KNA", "KN");
|
|
|
|
countries.put("LCA", "LC");
|
|
|
|
countries.put("MAF", "MF");
|
|
|
|
countries.put("SPM", "PM");
|
|
|
|
countries.put("VCT", "VC");
|
|
|
|
countries.put("WSM", "WS");
|
|
|
|
countries.put("SMR", "SM");
|
|
|
|
countries.put("STP", "ST");
|
|
|
|
countries.put("SAU", "SA");
|
|
|
|
countries.put("SEN", "SN");
|
|
|
|
countries.put("SRB", "RS");
|
|
|
|
countries.put("SYC", "SC");
|
|
|
|
countries.put("SLE", "SL");
|
|
|
|
countries.put("SGP", "SG");
|
|
|
|
countries.put("SXM", "SX");
|
|
|
|
countries.put("SVK", "SK");
|
|
|
|
countries.put("SVN", "SI");
|
|
|
|
countries.put("SLB", "SB");
|
|
|
|
countries.put("SOM", "SO");
|
|
|
|
countries.put("ZAF", "ZA");
|
|
|
|
countries.put("SGS", "GS");
|
|
|
|
countries.put("SSD", "SS");
|
|
|
|
countries.put("ESP", "ES");
|
|
|
|
countries.put("LKA", "LK");
|
|
|
|
countries.put("SDN", "SD");
|
|
|
|
countries.put("SUR", "SR");
|
|
|
|
countries.put("SJM", "SJ");
|
|
|
|
countries.put("SWZ", "SZ");
|
|
|
|
countries.put("SWE", "SE");
|
|
|
|
countries.put("CHE", "CH");
|
|
|
|
countries.put("SYR", "SY");
|
|
|
|
countries.put("TWN", "TW");
|
|
|
|
countries.put("TJK", "TJ");
|
|
|
|
countries.put("TZA", "TZ");
|
|
|
|
countries.put("THA", "TH");
|
|
|
|
countries.put("TLS", "TL");
|
|
|
|
countries.put("TGO", "TG");
|
|
|
|
countries.put("TKL", "TK");
|
|
|
|
countries.put("TON", "TO");
|
|
|
|
countries.put("TTO", "TT");
|
|
|
|
countries.put("TUN", "TN");
|
|
|
|
countries.put("TUR", "TR");
|
|
|
|
countries.put("TKM", "TM");
|
|
|
|
countries.put("TCA", "TC");
|
|
|
|
countries.put("TUV", "TV");
|
|
|
|
countries.put("UGA", "UG");
|
|
|
|
countries.put("UKR", "UA");
|
|
|
|
countries.put("ARE", "AE");
|
|
|
|
countries.put("GBR", "GB");
|
|
|
|
countries.put("USA", "US");
|
|
|
|
countries.put("UMI", "UM");
|
|
|
|
countries.put("URY", "UY");
|
|
|
|
countries.put("UZB", "UZ");
|
|
|
|
countries.put("VUT", "VU");
|
|
|
|
countries.put("VEN", "VE");
|
|
|
|
countries.put("VNM", "VN");
|
|
|
|
countries.put("VGB", "VG");
|
|
|
|
countries.put("VIR", "VI");
|
|
|
|
countries.put("WLF", "WF");
|
|
|
|
countries.put("ESH", "EH");
|
|
|
|
countries.put("YEM", "YE");
|
|
|
|
countries.put("ZMB", "ZM");
|
|
|
|
countries.put("ZWE", "ZW");
|
|
|
|
return countries;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static native int[] findCornerPoints(Bitmap bitmap);
|
|
|
|
|
|
|
|
private static native Rect[][] binarizeAndFindCharacters(Bitmap in, Bitmap put);
|
|
|
|
|
|
|
|
private static native String performRecognition(Bitmap bitmap, int numRows, int numCols, AssetManager assets);
|
2019-12-31 14:08:08 +01:00
|
|
|
|
2018-07-30 04:07:02 +02:00
|
|
|
private static native void setYuvBitmapPixels(Bitmap bitmap, byte[] pixels);
|
|
|
|
|
|
|
|
public static class Result {
|
|
|
|
public static final int TYPE_PASSPORT = 1;
|
|
|
|
public static final int TYPE_ID = 2;
|
2019-12-31 14:08:08 +01:00
|
|
|
public static final int TYPE_INTERNAL_PASSPORT = 3;
|
|
|
|
public static final int TYPE_DRIVER_LICENSE = 4;
|
2018-07-30 04:07:02 +02:00
|
|
|
|
|
|
|
public static final int GENDER_MALE = 1;
|
|
|
|
public static final int GENDER_FEMALE = 2;
|
|
|
|
public static final int GENDER_UNKNOWN = 0;
|
|
|
|
|
|
|
|
public int type;
|
|
|
|
public String firstName;
|
|
|
|
public String lastName;
|
2018-08-27 10:33:11 +02:00
|
|
|
public String middleName;
|
2018-07-30 04:07:02 +02:00
|
|
|
public String number;
|
|
|
|
public int expiryYear, expiryMonth, expiryDay;
|
|
|
|
public int birthYear, birthMonth, birthDay;
|
|
|
|
public String issuingCountry;
|
|
|
|
public String nationality;
|
|
|
|
public int gender;
|
|
|
|
public boolean doesNotExpire;
|
|
|
|
public boolean mainCheckDigitIsValid;
|
|
|
|
public String rawMRZ;
|
|
|
|
}
|
|
|
|
}
|