import logging import re from engine.id_parser import validate_luhn logger = logging.getLogger(__name__) def _parse_pdf417(data: str) -> dict | None: """Parse SA smart card PDF417 barcode data. The SA smart card PDF417 contains identity fields separated by specific delimiters. Format varies but typically contains surname, names, ID number, sex, DOB, nationality, etc. """ if not data or len(data) < 13: return None result = { "source": "pdf417", "id_number": None, "surname": None, "names": None, "sex": None, "date_of_birth": None, "nationality": None, "country_of_birth": None, "citizenship_status": None, } # Try to find 13-digit ID number anywhere in the data id_matches = re.findall(r"\d{13}", data.replace(" ", "")) for candidate in id_matches: if validate_luhn(candidate): result["id_number"] = candidate break # SA smart card PDF417 typically uses field separators # Common formats: pipe-delimited, or fixed-position fields parts = None for sep in ["|", "\t", "\n", "\r"]: candidate_parts = [p.strip() for p in data.split(sep) if p.strip()] if len(candidate_parts) >= 4: parts = candidate_parts break if parts and len(parts) >= 4: # Try to map parts to fields based on content patterns for part in parts: cleaned = part.strip() if not cleaned: continue # ID number (13 digits) digits_only = re.sub(r"\D", "", cleaned) if len(digits_only) == 13 and validate_luhn(digits_only) and not result["id_number"]: result["id_number"] = digits_only continue # Sex upper = cleaned.upper() if upper in ("M", "MALE"): result["sex"] = "Male" continue if upper in ("F", "FEMALE"): result["sex"] = "Female" continue # Nationality / citizenship if upper in ("RSA", "SOUTH AFRICA", "SA CITIZEN", "CITIZEN"): if not result["nationality"]: result["nationality"] = "South African" if not result["citizenship_status"]: result["citizenship_status"] = "SA Citizen" continue # Date pattern (YYYY-MM-DD or DD-MM-YYYY or YYYYMMDD) date_match = re.match(r"(\d{4})[/-](\d{2})[/-](\d{2})", cleaned) if date_match and not result["date_of_birth"]: result["date_of_birth"] = f"{date_match.group(1)}-{date_match.group(2)}-{date_match.group(3)}" continue date_match = re.match(r"(\d{2})[/-](\d{2})[/-](\d{4})", cleaned) if date_match and not result["date_of_birth"]: result["date_of_birth"] = f"{date_match.group(3)}-{date_match.group(2)}-{date_match.group(1)}" continue # If we got an ID number, derive fields from it as validation/fallback if result["id_number"]: from engine.id_parser import parse_id_number parsed = parse_id_number(result["id_number"]) if not result["date_of_birth"] and parsed.get("date_of_birth"): result["date_of_birth"] = parsed["date_of_birth"] if not result["sex"] and parsed.get("sex"): result["sex"] = parsed["sex"] if not result["citizenship_status"] and parsed.get("citizenship"): result["citizenship_status"] = parsed["citizenship"] return result return None def _parse_code39(data: str) -> dict | None: """Parse Code 39 barcode — typically contains only the ID number.""" digits_only = re.sub(r"\D", "", data) if len(digits_only) == 13 and validate_luhn(digits_only): from engine.id_parser import parse_id_number parsed = parse_id_number(digits_only) return { "source": "code39", "id_number": digits_only, "surname": None, "names": None, "sex": parsed.get("sex"), "date_of_birth": parsed.get("date_of_birth"), "nationality": None, "country_of_birth": None, "citizenship_status": parsed.get("citizenship"), } return None