Spaces:
Running
Running
| 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 | |