3using System.Diagnostics.CodeAnalysis;
6using System.Text.RegularExpressions;
126 private static readonly IReadOnlyDictionary<char, char> CheckDigitConfusions =
new Dictionary<char, char>
139 private static readonly Regex Td2MrzNumber9CharsPlus =
new(
@"^(?'DocType'.{1,2})<(?'Issuer'[A-Z<]{3})(?'PID'[^<]+(<[^<]+)*)<<(?'SID'[^<]+(<[^<]+)*)<*\n(?'Nr1'.{9})<(?'Nationality'[A-Z<]{3})(?'Birth'.{6})(?'BirthCheck'\d)(?'Gender'[MF<])(?'Expires'.{6})(?'ExpiryCheck'\d)(?'Tail'.{7})(?'OverallCheck'\d)$", RegexOptions.Multiline);
140 private static readonly Regex Td2MrzNumber9Chars =
new(
@"^(?'DocType'.{1,2})<(?'Issuer'[A-Z<]{3})(?'PID'[^<]+(<[^<]+)*)<<(?'SID'[^<]+(<[^<]+)*)<*\n(?'Nr'.{9})(?'NrCheck'\d)(?'Nationality'[A-Z<]{3})(?'Birth'.{6})(?'BirthCheck'\d)(?'Gender'[MF<])(?'Expires'.{6})(?'ExpiryCheck'\d)(?'Optional'.{7})(?'OverallCheck'\d)$", RegexOptions.Multiline);
141 private static readonly Regex Td1MrzNumber9CharsPlus =
new(
@"^(?'DocType'.{1,2})<(?'Issuer'[A-Z<]{3})(?'Nr1'.{9})<(?'Nr2'.{3})(?'NrCheck'\d)(?'Optional1'.{11})\n(?'Birth'.{6})(?'BirthCheck'\d)(?'Gender'[MF<])(?'Expires'.{6})(?'ExpiryCheck'\d)(?'Nationality'[A-Z<]{3})(?'Optional2'.{11})(?'OverallCheck'\d)\n(?'PID'[^<]+(<[^<]+)*)<<(?'SID'[^<]+(<[^<]+)*).*$", RegexOptions.Multiline);
142 private static readonly Regex Td1MrzNumber9Chars =
new(
@"^(?'DocType'.{1,2})<(?'Issuer'[A-Z<]{3})(?'Nr'.{9})(?'NrCheck'.)(?'Optional1'.{15})\n(?'Birth'[^<]{6})(?'BirthCheck'\d)(?'Gender'[MF<])(?'Expires'[^<]{6})(?'ExpiryCheck'\d)(?'Nationality'[A-Z<]{3})(?'Optional2'.{11})(?'OverallCheck'\d)\n(?'PID'[^<]+(<[^<]+)*)<<(?'SID'[^<]+(<[^<]+)*).*$", RegexOptions.Multiline);
143 private static readonly Regex Td3MrzNumber9Chars =
new(
@"^(?'DocType'.{1,2})<(?'Issuer'[A-Z<]{3})(?'PID'[^<]+(<[^<]+)*)<<(?'SID'[^<]+(<[^<]+)*)<*\n(?'Nr'.{9})(?'NrCheck'\d)(?'Nationality'[A-Z<]{3})(?'Birth'[^<]{6})(?'BirthCheck'\d)(?'Gender'[MF<])(?'Expires'[^<]{6})(?'ExpiryCheck'\d)(?'Optional'.{14})(?'OptionalCheck'\d)(?'OverallCheck'\d)$", RegexOptions.Multiline);
145 private static readonly ParserDefinition[] OrderedPatterns =
147 new ParserDefinition(
MrzLayout.Td2, Td2MrzNumber9CharsPlus, AssembleTd2Info2),
148 new ParserDefinition(
MrzLayout.Td2, Td2MrzNumber9Chars, AssembleTd2Info1),
149 new ParserDefinition(
MrzLayout.Td3, Td3MrzNumber9Chars, AssembleTd3Info1),
150 new ParserDefinition(
MrzLayout.Td1, Td1MrzNumber9CharsPlus, AssembleTd1Info2),
151 new ParserDefinition(
MrzLayout.Td1, Td1MrzNumber9Chars, AssembleTd1Info1)
174 ArgumentNullException.ThrowIfNull(Mrz);
176 string NormalizedMrz = NormalizeMrz(Mrz);
177 string[] Lines = SplitLines(NormalizedMrz);
178 MrzLayout? Layout = TryDetectLayout(Lines);
179 int RequiredCheckCount = Layout.HasValue ? GetRequiredCheckCount(Layout.Value) : 0;
180 int PassedRequiredCheckCount = 0;
181 bool DocumentNumberCheckPassed =
false;
182 bool DateOfBirthCheckPassed =
false;
183 bool ExpiryCheckPassed =
false;
184 bool CompositeCheckPassed =
false;
188 PassedRequiredCheckCount = CountPassedRequiredChecks(
191 out DocumentNumberCheckPassed,
192 out DateOfBirthCheckPassed,
193 out ExpiryCheckPassed,
194 out CompositeCheckPassed);
201 if (TryParseStandard(Layout.Value, Lines, out ParsedInformation)
202 && IsSemanticallyValid(Layout.Value, ParsedInformation))
209 DocumentNumberCheckPassed,
210 DateOfBirthCheckPassed,
212 CompositeCheckPassed,
214 PassedRequiredCheckCount,
218 if (TryParseFallback(NormalizedMrz, Layout, out ParsedInformation, out ParsedLayout)
219 && ParsedLayout.HasValue
220 && IsSemanticallyValid(ParsedLayout.Value, ParsedInformation))
227 DocumentNumberCheckPassed,
228 DateOfBirthCheckPassed,
230 CompositeCheckPassed,
232 PassedRequiredCheckCount,
241 DocumentNumberCheckPassed,
242 DateOfBirthCheckPassed,
244 CompositeCheckPassed,
246 PassedRequiredCheckCount,
247 "StrictLayoutValidationFailed");
250 if (TryParseFallback(NormalizedMrz,
null, out ParsedInformation, out ParsedLayout)
251 && ParsedLayout.HasValue
252 && IsSemanticallyValid(ParsedLayout.Value, ParsedInformation))
259 DocumentNumberCheckPassed,
260 DateOfBirthCheckPassed,
262 CompositeCheckPassed,
264 PassedRequiredCheckCount,
273 DocumentNumberCheckPassed,
274 DateOfBirthCheckPassed,
276 CompositeCheckPassed,
278 PassedRequiredCheckCount,
279 Layout.HasValue ?
"ValidationFailed" :
"LayoutNotRecognized");
289 string CheckDigit = CalculateCheckDigitText(Value);
290 return CheckDigit.Length == 1 ? CheckDigit[0] :
'\0';
293 private static bool TryParseFallback(
294 string NormalizedMrz,
299 foreach (ParserDefinition Definition
in OrderedPatterns)
301 if (ExpectedLayout.HasValue && Definition.Layout != ExpectedLayout.Value)
304 Match Match = Definition.Pattern.Match(NormalizedMrz);
309 if (Candidate is
null)
312 Layout = Definition.Layout;
322 private static bool IsSemanticallyValid(
MrzLayout Layout, DocumentInformation Info)
326 MrzLayout.Td1 => IsSemanticallyValidTd1(Info),
327 MrzLayout.Td2 => IsSemanticallyValidTd2(Info),
328 MrzLayout.Td3 => IsSemanticallyValidTd3(Info),
333 private static bool IsSemanticallyValidTd1(DocumentInformation Info)
335 return HasValidDocumentType(Info.DocumentType)
336 && HasValidCountryCode(Info.IssuingState)
337 && HasValidDocumentNumber(Info.DocumentNumber)
338 && HasValidMrzDate(Info.DateOfBirth)
339 && HasValidGender(Info.Gender)
340 && HasValidMrzDate(Info.ExpiryDate)
341 && HasValidCountryCode(Info.Nationality)
342 && HasValidIdentifiers(Info.PrimaryIdentifier, Info.SecondaryIdentifier)
343 && HasValidMrzInformation(Info.MRZ_Information);
346 private static bool IsSemanticallyValidTd2(DocumentInformation Info)
348 return HasValidDocumentType(Info.DocumentType)
349 && HasValidCountryCode(Info.IssuingState)
350 && HasValidDocumentNumber(Info.DocumentNumber)
351 && HasValidCountryCode(Info.Nationality)
352 && HasValidMrzDate(Info.DateOfBirth)
353 && HasValidGender(Info.Gender)
354 && HasValidMrzDate(Info.ExpiryDate)
355 && HasValidIdentifiers(Info.PrimaryIdentifier, Info.SecondaryIdentifier)
356 && HasValidMrzInformation(Info.MRZ_Information);
359 private static bool IsSemanticallyValidTd3(DocumentInformation Info)
361 return HasValidDocumentType(Info.DocumentType)
362 && HasValidCountryCode(Info.IssuingState)
363 && HasValidDocumentNumber(Info.DocumentNumber)
364 && HasValidCountryCode(Info.Nationality)
365 && HasValidMrzDate(Info.DateOfBirth)
366 && HasValidGender(Info.Gender)
367 && HasValidMrzDate(Info.ExpiryDate)
368 && HasValidIdentifiers(Info.PrimaryIdentifier, Info.SecondaryIdentifier)
369 && HasValidMrzInformation(Info.MRZ_Information);
372 private static bool HasValidDocumentType(
string? Value)
374 return !
string.IsNullOrWhiteSpace(Value)
376 && Value.All(
static Character => Character is >=
'A' and <=
'Z');
379 private static bool HasValidCountryCode(
string? Value)
381 return !
string.IsNullOrWhiteSpace(Value)
383 && Value.All(
static Character => Character is >=
'A' and <=
'Z');
386 private static bool HasValidDocumentNumber(
string? Value)
388 return !
string.IsNullOrWhiteSpace(Value)
389 && Value.All(
static Character => (Character is >=
'A' and <=
'Z') || (Character is >=
'0' and <=
'9'));
392 private static bool HasValidGender(
string? Value)
394 return Value is
"M" or
"F" or
"<";
397 private static bool HasValidMrzDate(
string? Value)
399 if (
string.IsNullOrWhiteSpace(Value) || Value.Length != 6 || !Value.All(
static Character => Character is >=
'0' and <=
'9'))
402 if (Value ==
"000000")
405 int Month =
int.Parse(Value.Substring(2, 2),
System.Globalization.CultureInfo.InvariantCulture);
406 int Day =
int.Parse(Value.Substring(4, 2),
System.Globalization.CultureInfo.InvariantCulture);
407 if (Month < 1 || Month > 12)
412 int MaxDay = Month
switch
414 4 or 6 or 9 or 11 => 30,
419 return Day <= MaxDay;
422 private static bool HasValidIdentifiers(
string[]? PrimaryIdentifier,
string[]? SecondaryIdentifier)
424 return HasValidIdentifierParts(PrimaryIdentifier,
true)
425 && HasValidIdentifierParts(SecondaryIdentifier,
false);
428 private static bool HasValidIdentifierParts(
string[]? Parts,
bool Required)
430 if (Parts is
null || Parts.Length == 0)
433 bool HasContent =
false;
434 foreach (
string Part
in Parts)
436 if (
string.IsNullOrWhiteSpace(Part))
439 if (!Part.All(
static Character => Character is >=
'A' and <=
'Z'))
445 return Required ? HasContent :
true;
448 private static bool HasValidMrzInformation(
string? Value)
450 return !
string.IsNullOrWhiteSpace(Value)
451 && Value.All(
static Character => (Character is >=
'A' and <=
'Z') || (Character is >=
'0' and <=
'9') || Character ==
'<');
454 private static bool TryParseStandard(
457 [NotNullWhen(
true)] out DocumentInformation? Info)
462 return TryParseTd1Standard(Lines, out Info);
465 return TryParseTd2Standard(Lines, out Info);
468 return TryParseTd3Standard(Lines, out Info);
476 private static bool TryParseTd1Standard(
string[] Lines, [NotNullWhen(
true)] out DocumentInformation? Info)
478 if (Lines.Length != 3 || Lines[0].Length != 30 || Lines[1].Length != 30 || Lines[2].Length != 30)
484 string Line1 = Lines[0];
485 string Line2 = Lines[1];
486 string Line3 = Lines[2];
487 if (Line1[14] ==
'<')
493 string DocumentNumber = Line1.Substring(5, 9);
494 if (!IsCheckDigitValid(DocumentNumber, Line1[14]))
500 string DateOfBirth = Line2.Substring(0, 6);
501 if (!IsCheckDigitValid(DateOfBirth, Line2[6]))
507 string ExpiryDate = Line2.Substring(8, 6);
508 if (!IsCheckDigitValid(ExpiryDate, Line2[14]))
514 string Composite = Line1.Substring(5, 10) + Line1.Substring(15, 15) + Line2.Substring(0, 7) + Line2.Substring(8, 7) + Line2.Substring(18, 11);
515 if (!IsCheckDigitValid(Composite, Line2[29]))
521 Info =
new DocumentInformation
523 DocumentType = ExtractDocumentType(Line1.Substring(0, 2)),
524 IssuingState = Line1.Substring(2, 3),
525 DocumentNumber = DocumentNumber.Replace(
"<",
string.Empty),
526 DateOfBirth = DateOfBirth,
527 Gender = Line2.Substring(7, 1),
528 ExpiryDate = ExpiryDate,
529 Nationality = Line2.Substring(15, 3),
530 OptionalData = (Line1.Substring(15, 15) + Line2.Substring(18, 11)).Replace(
"<",
string.Empty),
531 MRZ_Information = DocumentNumber + Line1[14] + DateOfBirth + Line2[6] + ExpiryDate + Line2[14]
533 PopulateNames(Info, Line3);
537 private static bool TryParseTd2Standard(
string[] Lines, [NotNullWhen(
true)] out DocumentInformation? Info)
539 if (Lines.Length != 2 || Lines[0].Length != 36 || Lines[1].Length != 36)
545 string Line1 = Lines[0];
546 string Line2 = Lines[1];
547 string DocumentNumber = Line2.Substring(0, 9);
548 string DateOfBirth = Line2.Substring(13, 6);
549 string ExpiryDate = Line2.Substring(21, 6);
550 if (!IsCheckDigitValid(DocumentNumber, Line2[9])
551 || !IsCheckDigitValid(DateOfBirth, Line2[19])
552 || !IsCheckDigitValid(ExpiryDate, Line2[27]))
558 string Composite = Line2.Substring(0, 10) + Line2.Substring(13, 7) + Line2.Substring(21, 7) + Line2.Substring(28, 7);
559 if (!IsCheckDigitValid(Composite, Line2[35]))
565 Info =
new DocumentInformation
567 DocumentType = ExtractDocumentType(Line1.Substring(0, 2)),
568 IssuingState = Line1.Substring(2, 3),
569 DocumentNumber = DocumentNumber.Replace(
"<",
string.Empty),
570 Nationality = Line2.Substring(10, 3),
571 DateOfBirth = DateOfBirth,
572 Gender = Line2.Substring(20, 1),
573 ExpiryDate = ExpiryDate,
574 OptionalData = Line2.Substring(28, 7).Replace(
"<",
string.Empty),
575 MRZ_Information = DocumentNumber + Line2[9] + DateOfBirth + Line2[19] + ExpiryDate + Line2[27]
577 PopulateNames(Info, Line1.Substring(5));
581 private static bool TryParseTd3Standard(
string[] Lines, [NotNullWhen(
true)] out DocumentInformation? Info)
583 if (Lines.Length != 2 || Lines[0].Length != 44 || Lines[1].Length != 44)
589 string Line1 = Lines[0];
590 string Line2 = Lines[1];
591 string DocumentNumber = Line2.Substring(0, 9);
592 string DateOfBirth = Line2.Substring(13, 6);
593 string ExpiryDate = Line2.Substring(21, 6);
594 string OptionalData = Line2.Substring(28, 14);
595 if (!IsCheckDigitValid(DocumentNumber, Line2[9])
596 || !IsCheckDigitValid(DateOfBirth, Line2[19])
597 || !IsCheckDigitValid(ExpiryDate, Line2[27])
598 || !IsCheckDigitValid(OptionalData, Line2[42]))
604 string Composite = Line2.Substring(0, 10) + Line2.Substring(13, 7) + Line2.Substring(21, 7) + Line2.Substring(28, 15);
605 if (!IsCheckDigitValid(Composite, Line2[43]))
611 Info =
new DocumentInformation
613 DocumentType = ExtractDocumentType(Line1.Substring(0, 2)),
614 IssuingState = Line1.Substring(2, 3),
615 DocumentNumber = DocumentNumber.Replace(
"<",
string.Empty),
616 Nationality = Line2.Substring(10, 3),
617 DateOfBirth = DateOfBirth,
618 Gender = Line2.Substring(20, 1),
619 ExpiryDate = ExpiryDate,
620 OptionalData = OptionalData.Replace(
"<",
string.Empty),
621 MRZ_Information = DocumentNumber + Line2[9] + DateOfBirth + Line2[19] + ExpiryDate + Line2[27]
623 PopulateNames(Info, Line1.Substring(5));
627 private static int CountPassedRequiredChecks(
630 out
bool DocumentNumberCheckPassed,
631 out
bool DateOfBirthCheckPassed,
632 out
bool ExpiryCheckPassed,
633 out
bool CompositeCheckPassed)
638 DocumentNumberCheckPassed = Lines.Length >= 1 && Lines[0].Length >= 15 && IsCheckDigitValid(Lines[0].Substring(5, 9), Lines[0][14]);
639 DateOfBirthCheckPassed = Lines.Length >= 2 && Lines[1].Length >= 7 && IsCheckDigitValid(Lines[1].Substring(0, 6), Lines[1][6]);
640 ExpiryCheckPassed = Lines.Length >= 2 && Lines[1].Length >= 15 && IsCheckDigitValid(Lines[1].Substring(8, 6), Lines[1][14]);
641 CompositeCheckPassed = Lines.Length >= 2 && Lines[0].Length == 30 && Lines[1].Length == 30
642 && IsCheckDigitValid(Lines[0].Substring(5, 10) + Lines[0].Substring(15, 15) + Lines[1].Substring(0, 7) + Lines[1].Substring(8, 7) + Lines[1].Substring(18, 11), Lines[1][29]);
646 DocumentNumberCheckPassed = Lines.Length >= 2 && Lines[1].Length >= 10 && IsCheckDigitValid(Lines[1].Substring(0, 9), Lines[1][9]);
647 DateOfBirthCheckPassed = Lines.Length >= 2 && Lines[1].Length >= 20 && IsCheckDigitValid(Lines[1].Substring(13, 6), Lines[1][19]);
648 ExpiryCheckPassed = Lines.Length >= 2 && Lines[1].Length >= 28 && IsCheckDigitValid(Lines[1].Substring(21, 6), Lines[1][27]);
649 CompositeCheckPassed = Lines.Length >= 2 && Lines[1].Length == 36
650 && IsCheckDigitValid(Lines[1].Substring(0, 10) + Lines[1].Substring(13, 7) + Lines[1].Substring(21, 7) + Lines[1].Substring(28, 7), Lines[1][35]);
654 DocumentNumberCheckPassed = Lines.Length >= 2 && Lines[1].Length >= 10 && IsCheckDigitValid(Lines[1].Substring(0, 9), Lines[1][9]);
655 DateOfBirthCheckPassed = Lines.Length >= 2 && Lines[1].Length >= 20 && IsCheckDigitValid(Lines[1].Substring(13, 6), Lines[1][19]);
656 ExpiryCheckPassed = Lines.Length >= 2 && Lines[1].Length >= 28 && IsCheckDigitValid(Lines[1].Substring(21, 6), Lines[1][27]);
657 CompositeCheckPassed = Lines.Length >= 2 && Lines[1].Length == 44
658 && IsCheckDigitValid(Lines[1].Substring(0, 10) + Lines[1].Substring(13, 7) + Lines[1].Substring(21, 7) + Lines[1].Substring(28, 15), Lines[1][43]);
663 if (DocumentNumberCheckPassed)
665 if (DateOfBirthCheckPassed)
667 if (ExpiryCheckPassed)
669 if (CompositeCheckPassed)
674 private static int GetRequiredCheckCount(
MrzLayout Layout)
685 private static MrzLayout? TryDetectLayout(
string[] Lines)
687 if (Lines.Length == 3 && Lines.All(
static Line => Line.Length == 30))
689 if (Lines.Length == 2 && Lines.All(
static Line => Line.Length == 36))
691 if (Lines.Length == 2 && Lines.All(
static Line => Line.Length == 44))
696 private static string NormalizeMrz(
string Mrz)
698 StringBuilder Builder =
new StringBuilder(Mrz.Length);
699 foreach (
char Character
in Mrz.ToUpperInvariant())
701 if (Character ==
'\r')
703 if (Character ==
'\n')
705 Builder.Append(
'\n');
709 Builder.Append(NormalizeMrzCharacter(Character));
712 string[] RawLines = Builder.ToString()
713 .Split(
'\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
714 if (RawLines.Length == 1)
716 string Flat = RawLines[0];
720 return string.Join(
"\n", [Flat.Substring(0, 30), Flat.Substring(30, 30), Flat.Substring(60, 30)]);
723 return string.Join(
"\n", [Flat.Substring(0, 36), Flat.Substring(36, 36)]);
726 return string.Join(
"\n", [Flat.Substring(0, 44), Flat.Substring(44, 44)]);
730 return string.Join(
"\n", RawLines);
733 private static string[] SplitLines(
string NormalizedMrz)
735 if (
string.IsNullOrWhiteSpace(NormalizedMrz))
738 return NormalizedMrz.Split(
'\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
741 private static char NormalizeMrzCharacter(
char Character)
743 if (
char.IsWhiteSpace(Character))
745 if (Character ==
'<' || Character ==
'«' || Character ==
'‹' || Character ==
'›')
747 if ((Character >=
'A' && Character <=
'Z') || (Character >=
'0' && Character <=
'9'))
752 private static bool IsCheckDigitValid(
string Field,
char ExpectedCheckDigit)
757 private static char NormalizeCheckDigitChar(
char Character)
759 char Normalized = NormalizeMrzCharacter(
char.ToUpperInvariant(Character));
760 if (Normalized >=
'0' && Normalized <=
'9')
762 return CheckDigitConfusions.TryGetValue(Normalized, out
char Replacement) ? Replacement :
'\0';
765 private static string CalculateCheckDigitText(
string Value)
769 foreach (
char Character
in Value)
771 int CharacterValue = GetMrzCharacterValue(Character);
772 if (CharacterValue < 0)
775 Sum += CharacterValue * Weights[Index++ % 3];
778 return new string((
char)(
'0' + (Sum % 10)), 1);
781 private static int GetMrzCharacterValue(
char Character)
783 if (Character >=
'0' && Character <=
'9')
784 return Character -
'0';
785 if (Character >=
'A' && Character <=
'Z')
786 return (Character -
'A') + 10;
787 if (Character ==
'<')
792 private static string ExtractDocumentType(
string Value)
794 return Value.Replace(
"<",
string.Empty);
797 private static void PopulateNames(DocumentInformation Info,
string Value)
799 string[] Parts = Value.TrimEnd(
'<').Split(
"<<", 2, StringSplitOptions.None);
800 Info.PrimaryIdentifier = SplitIdentifierPart(Parts[0]);
801 Info.SecondaryIdentifier = Parts.Length > 1 ? SplitIdentifierPart(Parts[1]) : [];
804 private static string[] SplitIdentifierPart(
string Value)
806 return Value.Split(
'<', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
809 private static DocumentInformation? AssembleTd2Info2(Match Match)
811 if (!TryParseTd2OverflowTail(
812 Match.Groups[
"Nr1"].Value,
813 Match.Groups[
"Tail"].Value,
814 Match.Groups[
"Birth"].Value,
815 Match.Groups[
"BirthCheck"].Value,
816 Match.Groups[
"Expires"].Value,
817 Match.Groups[
"ExpiryCheck"].Value,
818 Match.Groups[
"OverallCheck"].Value,
819 out
string? DocumentNumberSuffix,
820 out
string? NumberCheck,
821 out
string? OptionalData))
824 DocumentInformation Result = AssembleInfo(Match);
825 Result.DocumentNumber = Match.Groups[
"Nr1"].Value + DocumentNumberSuffix;
826 Result.OptionalData = OptionalData;
827 return CalcTd2OverflowMrzInfo(Result, Match, NumberCheck, OptionalData) ? Result :
null;
830 private static DocumentInformation? AssembleTd1Info2(Match Match)
832 DocumentInformation Result = AssembleInfo(Match);
833 Result.DocumentNumber = Match.Groups[
"Nr1"].Value + Match.Groups[
"Nr2"].Value;
834 return CalcTd1MrzInfo(Result, Match) ? Result :
null;
837 private static DocumentInformation? AssembleTd1Info1(Match Match)
839 DocumentInformation Result = AssembleInfo(Match);
840 Result.DocumentNumber = Match.Groups[
"Nr"].Value;
841 return CalcTd1MrzInfo(Result, Match) ? Result :
null;
844 private static DocumentInformation? AssembleTd2Info1(Match Match)
846 DocumentInformation Result = AssembleInfo(Match);
847 Result.DocumentNumber = Match.Groups[
"Nr"].Value;
848 return CalcMrzInfo(Result, Match,
MrzLayout.Td2) ? Result :
null;
851 private static DocumentInformation? AssembleTd3Info1(Match Match)
853 DocumentInformation Result = AssembleInfo(Match);
854 Result.DocumentNumber = Match.Groups[
"Nr"].Value;
855 return CalcMrzInfo(Result, Match,
MrzLayout.Td3) ? Result :
null;
858 private static bool CalcTd1MrzInfo(DocumentInformation Info, Match Match)
860 if (Info.DocumentNumber is
null || Info.DateOfBirth is
null || Info.ExpiryDate is
null)
863 string NumberCheck = Match.Groups[
"NrCheck"].Value;
864 if (NumberCheck != CalculateCheckDigitText(Info.DocumentNumber))
867 string BirthCheck = Match.Groups[
"BirthCheck"].Value;
868 if (BirthCheck != CalculateCheckDigitText(Info.DateOfBirth))
871 string ExpiryCheck = Match.Groups[
"ExpiryCheck"].Value;
872 if (ExpiryCheck != CalculateCheckDigitText(Info.ExpiryDate))
875 string OptionalLine1 = Match.Groups[
"Optional1"].Value;
876 string OptionalLine2 = Match.Groups[
"Optional2"].Value;
877 string OverallCheck = Match.Groups[
"OverallCheck"].Value;
878 string Composite = Info.DocumentNumber + NumberCheck + OptionalLine1 + Info.DateOfBirth + BirthCheck + Info.ExpiryDate + ExpiryCheck + OptionalLine2;
879 if (OverallCheck != CalculateCheckDigitText(Composite))
882 Info.OptionalData = (OptionalLine1 + OptionalLine2).Replace(
"<",
string.Empty);
883 Info.MRZ_Information = Info.DocumentNumber + NumberCheck + Info.DateOfBirth + BirthCheck + Info.ExpiryDate + ExpiryCheck;
884 Info.DocumentNumber = Info.DocumentNumber.Replace(
"<",
string.Empty);
888 private static bool CalcTd2OverflowMrzInfo(DocumentInformation Info, Match Match,
string NumberCheck,
string OptionalData)
890 if (Info.DocumentNumber is
null || Info.DateOfBirth is
null || Info.ExpiryDate is
null)
893 if (NumberCheck != CalculateCheckDigitText(Info.DocumentNumber))
896 string BirthCheck = Match.Groups[
"BirthCheck"].Value;
897 if (BirthCheck != CalculateCheckDigitText(Info.DateOfBirth))
900 string ExpiryCheck = Match.Groups[
"ExpiryCheck"].Value;
901 if (ExpiryCheck != CalculateCheckDigitText(Info.ExpiryDate))
904 string Composite = Info.DocumentNumber + NumberCheck + Info.DateOfBirth + BirthCheck + Info.ExpiryDate + ExpiryCheck + OptionalData;
905 if (Match.Groups[
"OverallCheck"].Value != CalculateCheckDigitText(Composite))
908 Info.OptionalData = OptionalData.Replace(
"<",
string.Empty);
909 Info.MRZ_Information = Info.DocumentNumber + NumberCheck + Info.DateOfBirth + BirthCheck + Info.ExpiryDate + ExpiryCheck;
910 Info.DocumentNumber = Info.DocumentNumber.Replace(
"<",
string.Empty);
914 private static bool CalcMrzInfo(DocumentInformation Info, Match Match,
MrzLayout Layout)
916 if (Info.DocumentNumber is
null || Info.DateOfBirth is
null || Info.ExpiryDate is
null)
919 string NumberCheck = Match.Groups[
"NrCheck"].Value;
920 if (NumberCheck != CalculateCheckDigitText(Info.DocumentNumber))
923 string BirthCheck = Match.Groups[
"BirthCheck"].Value;
924 if (BirthCheck != CalculateCheckDigitText(Info.DateOfBirth))
927 string ExpiryCheck = Match.Groups[
"ExpiryCheck"].Value;
928 if (ExpiryCheck != CalculateCheckDigitText(Info.ExpiryDate))
931 string OptionalValue = Match.Groups[
"Optional"].Value;
934 if (!Match.Groups[
"OptionalCheck"].Success)
937 string OptionalCheck = Match.Groups[
"OptionalCheck"].Value;
938 if (OptionalCheck != CalculateCheckDigitText(OptionalValue))
942 if (Match.Groups[
"OverallCheck"].Success)
944 string Composite = Layout
switch
946 MrzLayout.Td2 => Info.DocumentNumber + NumberCheck + Info.DateOfBirth + BirthCheck + Info.ExpiryDate + ExpiryCheck + OptionalValue,
947 MrzLayout.Td3 => Info.DocumentNumber + NumberCheck + Info.DateOfBirth + BirthCheck + Info.ExpiryDate + ExpiryCheck + OptionalValue + Match.Groups[
"OptionalCheck"].Value,
951 if (Match.Groups[
"OverallCheck"].Value != CalculateCheckDigitText(Composite))
955 Info.OptionalData = OptionalValue.Replace(
"<",
string.Empty);
956 Info.MRZ_Information = Info.DocumentNumber + NumberCheck + Info.DateOfBirth + BirthCheck + Info.ExpiryDate + ExpiryCheck;
957 Info.DocumentNumber = Info.DocumentNumber.Replace(
"<",
string.Empty);
961 private static DocumentInformation AssembleInfo(Match Match)
963 DocumentInformation Result =
new DocumentInformation
965 DocumentType = ExtractDocumentType(Match.Groups[
"DocType"].Value),
966 IssuingState = Match.Groups[
"Issuer"].Value,
967 Nationality = Match.Groups[
"Nationality"].Value,
968 Gender = Match.Groups[
"Gender"].Value,
969 DocumentNumber = Match.Groups[
"Nr"].Value,
970 DateOfBirth = Match.Groups[
"Birth"].Value,
971 ExpiryDate = Match.Groups[
"Expires"].Value,
972 OptionalData = Match.Groups[
"Optional"].Value
974 Result.PrimaryIdentifier = SplitIdentifierPart(Match.Groups[
"PID"].Value);
975 Result.SecondaryIdentifier = SplitIdentifierPart(Match.Groups[
"SID"].Value);
979 private static bool TryParseTd2OverflowTail(
980 string DocumentNumberPrefix,
987 [NotNullWhen(
true)] out
string? DocumentNumberSuffix,
988 [NotNullWhen(
true)] out
string? NumberCheck,
989 [NotNullWhen(
true)] out
string? OptionalData)
991 for (
int CheckDigitIndex = 0; CheckDigitIndex < Tail.Length; CheckDigitIndex++)
993 string CandidateSuffix = Tail.Substring(0, CheckDigitIndex);
994 string CandidateNumberCheck = Tail.Substring(CheckDigitIndex, 1);
995 string CandidateOptionalData = Tail.Substring(CheckDigitIndex + 1);
996 string CandidateDocumentNumber = DocumentNumberPrefix + CandidateSuffix;
998 if (CandidateNumberCheck != CalculateCheckDigitText(CandidateDocumentNumber))
1001 string Composite = CandidateDocumentNumber
1002 + CandidateNumberCheck
1007 + CandidateOptionalData;
1008 if (OverallCheck != CalculateCheckDigitText(Composite))
1011 DocumentNumberSuffix = CandidateSuffix;
1012 NumberCheck = CandidateNumberCheck;
1013 OptionalData = CandidateOptionalData;
1017 DocumentNumberSuffix =
null;
1019 OptionalData =
null;
1023 private static readonly
int[] Weights = [7, 3, 1];
1025 private delegate DocumentInformation? DocumentInformationFromMatch(Match Match);
1027 private readonly record
struct ParserDefinition(
1030 DocumentInformationFromMatch Factory);
Represents the outcome of MRZ parsing and validation.
string NormalizedMrz
Gets the normalized MRZ text used for validation.
bool IsParsed
Gets a value indicating whether the MRZ parsed into a document model.
int RequiredCheckCount
Gets the number of required checks for the detected layout.
MrzLayout? Layout
Gets the detected MRZ layout, when one could be determined.
int PassedRequiredCheckCount
Gets the number of required checks that passed.
bool CompositeCheckPassed
Gets a value indicating whether the layout composite or overall check passed.
string? FailureReason
Gets the validation failure reason when parsing was unsuccessful.
DocumentInformation? DocumentInformation
Gets the parsed document information when parsing succeeded.
bool DateOfBirthCheckPassed
Gets a value indicating whether the date-of-birth check digit passed.
bool DocumentNumberCheckPassed
Gets a value indicating whether the document-number check digit passed.
bool ExpiryCheckPassed
Gets a value indicating whether the expiry-date check digit passed.
Provides shared MRZ parsing and validation services for OCR and NFC flows.
static bool TryParse(string Mrz, [NotNullWhen(true)] out DocumentInformation? Info)
Attempts to parse MRZ text into document information.
static MrzValidationResult Validate(string Mrz)
Validates MRZ text and returns detailed check information.
static char CalculateCheckDigit(string Value)
Calculates the MRZ check digit for a value.
MrzLayout
Identifies the supported MRZ layouts.
DocumentType
Type of markdown document.
Gender
Gender classification in a legal ID.