id-ocr-engine / engine /barcode_reader.py
Kevin-KES
Deploy current main to HF Space
50c6ee2
Raw
History Blame Contribute Delete
4.39 kB
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