parse static method

ScanResult parse(
  1. String rawData
)

Parses raw text, Secure QR XML, or compressed binary payload from an Aadhaar card.

Implementation

static ScanResult parse(String rawData) {
  if (rawData.contains('<?xml') ||
      rawData.contains('<PrintLetterBarcodeData')) {
    return _parseXmlQr(rawData);
  }

  if (isAadhaarPayload(rawData) && !rawData.contains('<?xml')) {
    final secureResult = _parseSecureQr(rawData);
    if (secureResult != null) return secureResult;
  }

  // Enhanced v3.0: Multi-candidate UID extraction with OCR error correction
  final allDigitSequences = _extractAllDigitSequences(rawData);
  final lines = rawData
      .split(_lineSplitRegex)
      .map((l) => l.trim())
      .where((l) => l.isNotEmpty)
      .toList();

  // Strategy 1: Direct regex match for formatted Aadhaar number
  String? bestUid;
  bool isVerhoeffValid = false;

  final uidMatch = _uidRegex.firstMatch(rawData);
  if (uidMatch != null) {
    final rawUid = uidMatch.group(0)!.replaceAll(' ', '');
    if (validateAadhaarVerhoeff(rawUid)) {
      bestUid = rawUid;
      isVerhoeffValid = true;
    } else {
      // Try OCR digit correction on this candidate
      final corrected = _tryOcrCorrection(rawUid);
      if (corrected != null) {
        bestUid = corrected;
        isVerhoeffValid = true;
      } else {
        bestUid = rawUid; // Use as-is even if Verhoeff fails
      }
    }
  }

  // Strategy 2: Find any 12-digit sequence that passes Verhoeff
  if (!isVerhoeffValid) {
    for (final seq in allDigitSequences) {
      if (seq.length >= 12) {
        // Try each 12-digit window
        for (int start = 0; start <= seq.length - 12; start++) {
          final candidate = seq.substring(start, start + 12);
          if (candidate.startsWith(RegExp(r'[2-9]'))) {
            if (validateAadhaarVerhoeff(candidate)) {
              bestUid = candidate;
              isVerhoeffValid = true;
              break;
            }
            // Try OCR correction
            final corrected = _tryOcrCorrection(candidate);
            if (corrected != null) {
              bestUid = corrected;
              isVerhoeffValid = true;
              break;
            }
          }
        }
        if (isVerhoeffValid) break;
      }
    }
  }

  // Strategy 3: Concatenate nearby 4-digit groups (XXXX XXXX XXXX format)
  if (!isVerhoeffValid) {
    final fourDigitGroups = RegExp(
      r'\b\d{4}\b',
    ).allMatches(rawData).map((m) => m.group(0)!).toList();
    if (fourDigitGroups.length >= 3) {
      for (int i = 0; i <= fourDigitGroups.length - 3; i++) {
        final candidate =
            fourDigitGroups[i] +
            fourDigitGroups[i + 1] +
            fourDigitGroups[i + 2];
        if (candidate.startsWith(RegExp(r'[2-9]')) &&
            candidate.length == 12) {
          if (validateAadhaarVerhoeff(candidate)) {
            bestUid = candidate;
            isVerhoeffValid = true;
            break;
          }
          final corrected = _tryOcrCorrection(candidate);
          if (corrected != null) {
            bestUid = corrected;
            isVerhoeffValid = true;
            break;
          }
          bestUid ??= candidate;
        }
      }
    }
  }

  if (bestUid != null && bestUid.length >= 12) {
    final cleanUid = bestUid.substring(0, 12);
    final formattedUid =
        '${cleanUid.substring(0, 4)} ${cleanUid.substring(4, 8)} ${cleanUid.substring(8, 12)}';

    final Map<String, String> fields = {
      'Card Type': 'Aadhaar Card (India)',
      'Aadhaar Number': formattedUid,
      'Verhoeff Checksum': isVerhoeffValid ? 'Valid ✓' : 'Invalid ✗',
      'Verification Status': isVerhoeffValid
          ? 'UIDAI Verhoeff D10 Validated ✓'
          : 'Checksum Mismatch ✗',
    };

    // Extract Name — look for lines with mostly uppercase alphabets
    // (typically the cardholder name line on Aadhaar)
    final nameCandidate = _extractName(lines);
    if (nameCandidate != null) {
      fields['Full Name'] = nameCandidate;
    }

    // Extract Date of Birth
    for (final line in lines) {
      if (_dobHeaderRegex.hasMatch(line)) {
        final dobMatch = _dobMatchRegex.firstMatch(line);
        if (dobMatch != null) {
          fields['Date of Birth'] = dobMatch.group(0)!;
          final yr = int.tryParse(dobMatch.group(0)!.substring(6));
          if (yr != null) {
            fields['Calculated Age'] = '${DateTime.now().year - yr} years';
          }
        }
      } else if (_yobHeaderRegex.hasMatch(line)) {
        final yobMatch = _yobMatchRegex.firstMatch(line);
        if (yobMatch != null) {
          fields['Year of Birth'] = yobMatch.group(0)!;
          final yr = int.tryParse(yobMatch.group(0)!);
          if (yr != null) {
            fields['Approximate Age'] = '${DateTime.now().year - yr} years';
          }
        }
      }
    }

    // Try extracting DOB from raw text if not found via headers
    if (!fields.containsKey('Date of Birth') &&
        !fields.containsKey('Year of Birth')) {
      final dobAnywhere = RegExp(
        r'\b\d{2}[/\-.]\d{2}[/\-.]\d{4}\b',
      ).firstMatch(rawData);
      if (dobAnywhere != null) {
        fields['Date of Birth'] = dobAnywhere.group(0)!;
        final parts = dobAnywhere.group(0)!.split(RegExp(r'[/\-.]'));
        if (parts.length == 3) {
          final yr = int.tryParse(parts[2]);
          if (yr != null && yr > 1900 && yr < DateTime.now().year) {
            fields['Calculated Age'] = '${DateTime.now().year - yr} years';
          }
        }
      }
    }

    // Extract Gender
    for (final line in lines) {
      if (_genderRegex.hasMatch(line)) {
        final match = _genderRegex.firstMatch(line);
        if (match != null) {
          fields['Gender'] = match.group(0)!;
        }
        break;
      }
    }
    // Also check for single M/F near gender keywords
    if (!fields.containsKey('Gender')) {
      final genderShort = RegExp(
        r'(?:Gender|Sex)\s*[:\-]?\s*([MF])\b',
        caseSensitive: false,
      ).firstMatch(rawData);
      if (genderShort != null) {
        final g = genderShort.group(1)!.toUpperCase();
        fields['Gender'] = g == 'M' ? 'Male' : 'Female';
      }
    }

    // Extract Pincode
    final pincodeMatch = _pincodeRegex.firstMatch(rawData);
    if (pincodeMatch != null) {
      fields['Pincode'] = pincodeMatch.group(0)!;
    }

    // Extract VID (Virtual ID) — 16-digit number
    final vidMatch = RegExp(
      r'\b\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\b',
    ).firstMatch(rawData);
    if (vidMatch != null) {
      final vidClean = vidMatch.group(0)!.replaceAll(' ', '');
      if (vidClean.length == 16 && vidClean != cleanUid) {
        fields['VID'] =
            '${vidClean.substring(0, 4)} ${vidClean.substring(4, 8)} ${vidClean.substring(8, 12)} ${vidClean.substring(12, 16)}';
      }
    }

    // Extract Address lines (lines below gender/DOB, above Aadhaar number)
    final addressLines = _extractAddressLines(lines, cleanUid);
    if (addressLines.isNotEmpty) {
      fields['Address'] = addressLines.join(', ');
    }

    final verifications = {
      'verhoeffChecksum': isVerhoeffValid,
      'uidSyntaxValid': true,
      'pincodeValid': fields.containsKey('Pincode'),
      'nameExtracted': fields.containsKey('Full Name'),
      'dobExtracted':
          fields.containsKey('Date of Birth') ||
          fields.containsKey('Year of Birth'),
    };

    return ScanResult(
      mode: ScanMode.aadhaar,
      rawValue: rawData,
      isValid: isVerhoeffValid,
      confidence: isVerhoeffValid ? 0.99 : 0.70,
      fields: fields,
      verifications: verifications,
    );
  }

  return ScanResult(
    mode: ScanMode.aadhaar,
    rawValue: rawData,
    isValid: false,
    confidence: 0.3,
    fields: {
      'Card Type': 'Aadhaar (Unparsed)',
      'Raw Input': rawData,
      'Detection Note':
          'No valid 12-digit Aadhaar number found. Ensure card is well-lit and in focus.',
    },
    verifications: {'verhoeffChecksum': false, 'uidSyntaxValid': false},
  );
}