hmfayyaz commited on
Commit
2ab1707
·
0 Parent(s):

init commit

Browse files
Files changed (15) hide show
  1. .gitattributes +11 -0
  2. .gitignore +8 -0
  3. README.md +110 -0
  4. app.py +150 -0
  5. config/settings.py +218 -0
  6. main.py +98 -0
  7. requirements.txt +14 -0
  8. setup.py +28 -0
  9. share_app.py +37 -0
  10. src/__init__.py +0 -0
  11. src/extractor.py +185 -0
  12. src/formats.py +41 -0
  13. src/utils.py +72 -0
  14. src/validators.py +34 -0
  15. tests/test_extractor.py +20 -0
.gitattributes ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.pth filter=lfs diff=lfs merge=lfs -text
2
+ *.bin filter=lfs diff=lfs merge=lfs -text
3
+ *.h5 filter=lfs diff=lfs merge=lfs -text
4
+ *.weights filter=lfs diff=lfs merge=lfs -text
5
+ *.pt filter=lfs diff=lfs merge=lfs -text
6
+ *.onnx filter=lfs diff=lfs merge=lfs -text
7
+ *.pdf filter=lfs diff=lfs merge=lfs -text
8
+ *.png filter=lfs diff=lfs merge=lfs -text
9
+ *.jpg filter=lfs diff=lfs merge=lfs -text
10
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
11
+ *.xlsx filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .DS_Store
4
+ venv/
5
+ .env
6
+ output/
7
+ data/output/
8
+
README.md ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Passport OCR Tool
2
+
3
+ A robust tool to extract data from passport images (PNG, JPG) and PDFs using OCR. It extracts MRZ (Machine Readable Zone) data and exports it to Excel or CSV formats.
4
+
5
+ ## Features
6
+
7
+ - **Multi-format Support**: Handles images (JPG, PNG) and PDFs.
8
+ - **Batch Processing**: Process single files or entire directories.
9
+ - **Data Export**: Save results to Excel (`.xlsx`) or CSV.
10
+ - **Validation**: Basic validation of extracted fields.
11
+ - **Web Interface**: User-friendly web app for easy demonstration.
12
+ - **Local Processing**: Runs entirely on your local machine.
13
+
14
+ ## Prerequisites
15
+
16
+ ### 1. Python
17
+ Ensure you have Python 3.7+ installed.
18
+
19
+ ### 2. System Dependencies (Poppler)
20
+ This tool requires `poppler` for PDF processing.
21
+
22
+ - **macOS** (using Homebrew):
23
+ ```bash
24
+ brew install poppler
25
+ ```
26
+
27
+ - **Ubuntu/Debian**:
28
+ ```bash
29
+ sudo apt-get install poppler-utils
30
+ ```
31
+
32
+ - **Windows**:
33
+ 1. Download the latest binary from [@oschwartz10612 version](https://github.com/oschwartz10612/poppler-windows/releases/).
34
+ 2. Extract the zip file.
35
+ 3. Add the `bin/` folder to your System PATH environment variable.
36
+
37
+ ## Installation
38
+
39
+ 1. Clone the repository:
40
+ ```bash
41
+ git clone <repository-url>
42
+ cd passport-ocr-tool
43
+ ```
44
+
45
+ 2. Create a virtual environment (recommended):
46
+ ```bash
47
+ python -m venv venv
48
+ source venv/bin/activate # On Windows: venv\Scripts\activate
49
+ ```
50
+
51
+ 3. Install Python dependencies:
52
+ ```bash
53
+ pip install -r requirements.txt
54
+ ```
55
+
56
+ ## Usage
57
+
58
+ ### 1. Web App (Recommended for Demos)
59
+ Launch the easy-to-use web interface:
60
+
61
+ ```bash
62
+ streamlit run app.py
63
+ ```
64
+ This will open the tool in your default web browser (usually at `http://localhost:8501`). You can drag and drop files and download the results as Excel/CSV.
65
+
66
+ ### 2. Command Line Interface (CLI)
67
+
68
+ Run the tool using `main.py`:
69
+
70
+ ```bash
71
+ # Process a single image
72
+ python main.py --input path/to/passport.jpg
73
+
74
+ # Process a directory of images/PDFs
75
+ python main.py --input path/to/folder --output my_results
76
+
77
+ # Export to CSV
78
+ python main.py --input path/to/folder --format csv
79
+ ```
80
+
81
+ ### Command Line Arguments
82
+
83
+ - `--input`, `-i`: Path to input file or directory (Required).
84
+ - `--output`, `-o`: Output filename (default: `passport_data`).
85
+ - `--format`, `-f`: Output format: `excel` or `csv` (default: `excel`).
86
+ - `--gpu`: Enable GPU acceleration for OCR (requires CUDA).
87
+
88
+ ## Project Structure
89
+
90
+ ```
91
+ passport-ocr-tool/
92
+ ├── src/
93
+ │ ├── extractor.py # Core extraction logic
94
+ │ ├── utils.py # Helper functions
95
+ │ ├── validators.py # Data validation
96
+ │ └── formats.py # Export handlers
97
+ ├── config/
98
+ │ └── settings.py # Configuration
99
+ ├── app.py # Streamlit Web App
100
+ ├── main.py # CLI Entry point
101
+ └── ...
102
+ ```
103
+
104
+ ## Troubleshooting
105
+
106
+ - **PDF Error**: "Unable to get page count" or "poppler is not installed".
107
+ - Ensure `poppler` is installed and in your PATH.
108
+ - **OCR Accuracy**:
109
+ - Ensure images are clear and high resolution.
110
+ - The MRZ zone (bottom lines) must be visible and unobstructed.
app.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+ import tempfile
4
+ import pandas as pd
5
+ import time
6
+ from src.extractor import PassportExtractor
7
+ from src.validators import validate_passport_data
8
+
9
+ # Set page configuration
10
+ st.set_page_config(
11
+ page_title="Passport OCR Tool",
12
+ page_icon="🛂",
13
+ layout="wide"
14
+ )
15
+
16
+ # Initialize Extractor (cached to avoid reloading model)
17
+ @st.cache_resource
18
+ def get_extractor():
19
+ return PassportExtractor(use_gpu=False)
20
+
21
+ extractor = get_extractor()
22
+
23
+ def save_uploaded_file(uploaded_file):
24
+ """Save uploaded file to a temporary location and return the path."""
25
+ try:
26
+ suffix = os.path.splitext(uploaded_file.name)[1]
27
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp_file:
28
+ tmp_file.write(uploaded_file.getvalue())
29
+ return tmp_file.name
30
+ except Exception as e:
31
+ st.error(f"Error saving file: {e}")
32
+ return None
33
+
34
+ def main():
35
+ st.title("🛂 Passport OCR Extractor")
36
+ st.markdown("""
37
+ Upload passport images (JPG, PNG) or PDFs to extract MRZ data automatically.
38
+ The tool extracts details like Name, Surname, Passport Number, and Date of Birth.
39
+ """)
40
+
41
+ # Sidebar for configuration
42
+ st.sidebar.header("Configuration")
43
+ enable_validation = st.sidebar.checkbox("Enable Data Validation", value=True)
44
+
45
+ # File Uploader
46
+ uploaded_files = st.file_uploader(
47
+ "Choose passport files",
48
+ type=['png', 'jpg', 'jpeg', 'pdf'],
49
+ accept_multiple_files=True
50
+ )
51
+
52
+ if uploaded_files:
53
+ st.info(f"Loaded {len(uploaded_files)} files. Click 'Process Files' to start.")
54
+
55
+ if st.button("Process Files"):
56
+ results = []
57
+ progress_bar = st.progress(0)
58
+ status_text = st.empty()
59
+
60
+ for idx, uploaded_file in enumerate(uploaded_files):
61
+ status_text.text(f"Processing {uploaded_file.name}...")
62
+
63
+ # Save to temp file because extractor needs a path
64
+ temp_path = save_uploaded_file(uploaded_file)
65
+
66
+ if temp_path:
67
+ try:
68
+ ext = os.path.splitext(temp_path)[1].lower()
69
+ file_results = []
70
+
71
+ if ext == '.pdf':
72
+ file_results = extractor.process_pdf(temp_path)
73
+ else:
74
+ data = extractor.get_data(temp_path)
75
+ if data:
76
+ file_results = [data]
77
+
78
+ # Add validation if enabled
79
+ if enable_validation:
80
+ for res in file_results:
81
+ errors = validate_passport_data(res)
82
+ res['validation_errors'] = "; ".join(errors) if errors else "Valid"
83
+
84
+ # Add original filename for reference (since we used a temp file)
85
+ for res in file_results:
86
+ res['original_filename'] = uploaded_file.name
87
+
88
+ results.extend(file_results)
89
+
90
+ except Exception as e:
91
+ st.error(f"Failed to process {uploaded_file.name}: {e}")
92
+ finally:
93
+ # Cleanup temp file
94
+ if os.path.exists(temp_path):
95
+ os.remove(temp_path)
96
+
97
+ # Update progress
98
+ progress_bar.progress((idx + 1) / len(uploaded_files))
99
+
100
+ status_text.text("Processing complete!")
101
+
102
+ if results:
103
+ st.success(f"Successfully extracted {len(results)} records.")
104
+
105
+ # Create DataFrame
106
+ df = pd.DataFrame(results)
107
+
108
+ # Reorder columns for better readability if possible
109
+ cols = ['surname', 'name', 'passport_number', 'nationality', 'date_of_birth', 'sex', 'expiration_date', 'validation_errors', 'original_filename']
110
+ # Add missing cols to list if they exist in df
111
+ all_cols = cols + [c for c in df.columns if c not in cols]
112
+ # Filter valid columns only
113
+ final_cols = [c for c in all_cols if c in df.columns]
114
+
115
+ df = df[final_cols]
116
+
117
+ # Display Data
118
+ st.dataframe(df)
119
+
120
+ # Download Buttons
121
+ col1, col2 = st.columns(2)
122
+
123
+ # CSV Download
124
+ csv = df.to_csv(index=False).encode('utf-8')
125
+ col1.download_button(
126
+ label="Download CSV",
127
+ data=csv,
128
+ file_name="passport_data.csv",
129
+ mime="text/csv",
130
+ )
131
+
132
+ # Excel Download (requires openpyxl)
133
+ # Streamlit might need a BytesIO buffer for Excel
134
+ import io
135
+ buffer = io.BytesIO()
136
+ with pd.ExcelWriter(buffer, engine='openpyxl') as writer:
137
+ df.to_excel(writer, index=False, sheet_name='Sheet1')
138
+
139
+ col2.download_button(
140
+ label="Download Excel",
141
+ data=buffer.getvalue(),
142
+ file_name="passport_data.xlsx",
143
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
144
+ )
145
+
146
+ else:
147
+ st.warning("No data extracted from the provided files.")
148
+
149
+ if __name__ == "__main__":
150
+ main()
config/settings.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ # Base directory
4
+ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
5
+
6
+ # Input/Output directories
7
+ INPUT_DIR = os.path.join(BASE_DIR, 'data', 'input')
8
+ OUTPUT_DIR = os.path.join(BASE_DIR, 'data', 'output')
9
+ TEMP_DIR = os.path.join(BASE_DIR, 'temp_processing')
10
+
11
+ # Ensure directories exist
12
+ os.makedirs(INPUT_DIR, exist_ok=True)
13
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
14
+
15
+ # Allowed file extensions
16
+ ALLOWED_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.pdf'}
17
+
18
+ # OCR Settings
19
+ OCR_LANGUAGES = ['en']
20
+ USE_GPU = False # Set to True if you have CUDA installed
21
+
22
+ # Country Codes (ISO 3166-1 alpha-3)
23
+ # This is a subset. In a real production app, consider loading this from a standard library or full JSON file.
24
+ COUNTRY_CODES = [
25
+ {"name": "Afghanistan", "alpha-3": "AFG"},
26
+ {"name": "Albania", "alpha-3": "ALB"},
27
+ {"name": "Algeria", "alpha-3": "DZA"},
28
+ {"name": "Andorra", "alpha-3": "AND"},
29
+ {"name": "Angola", "alpha-3": "AGO"},
30
+ {"name": "Argentina", "alpha-3": "ARG"},
31
+ {"name": "Armenia", "alpha-3": "ARM"},
32
+ {"name": "Australia", "alpha-3": "AUS"},
33
+ {"name": "Austria", "alpha-3": "AUT"},
34
+ {"name": "Azerbaijan", "alpha-3": "AZE"},
35
+ {"name": "Bahamas", "alpha-3": "BHS"},
36
+ {"name": "Bahrain", "alpha-3": "BHR"},
37
+ {"name": "Bangladesh", "alpha-3": "BGD"},
38
+ {"name": "Barbados", "alpha-3": "BRB"},
39
+ {"name": "Belarus", "alpha-3": "BLR"},
40
+ {"name": "Belgium", "alpha-3": "BEL"},
41
+ {"name": "Belize", "alpha-3": "BLZ"},
42
+ {"name": "Benin", "alpha-3": "BEN"},
43
+ {"name": "Bhutan", "alpha-3": "BTN"},
44
+ {"name": "Bolivia", "alpha-3": "BOL"},
45
+ {"name": "Bosnia and Herzegovina", "alpha-3": "BIH"},
46
+ {"name": "Botswana", "alpha-3": "BWA"},
47
+ {"name": "Brazil", "alpha-3": "BRA"},
48
+ {"name": "Brunei Darussalam", "alpha-3": "BRN"},
49
+ {"name": "Bulgaria", "alpha-3": "BGR"},
50
+ {"name": "Burkina Faso", "alpha-3": "BFA"},
51
+ {"name": "Burundi", "alpha-3": "BDI"},
52
+ {"name": "Cabo Verde", "alpha-3": "CPV"},
53
+ {"name": "Cambodia", "alpha-3": "KHM"},
54
+ {"name": "Cameroon", "alpha-3": "CMR"},
55
+ {"name": "Canada", "alpha-3": "CAN"},
56
+ {"name": "Central African Republic", "alpha-3": "CAF"},
57
+ {"name": "Chad", "alpha-3": "TCD"},
58
+ {"name": "Chile", "alpha-3": "CHL"},
59
+ {"name": "China", "alpha-3": "CHN"},
60
+ {"name": "Colombia", "alpha-3": "COL"},
61
+ {"name": "Comoros", "alpha-3": "COM"},
62
+ {"name": "Congo", "alpha-3": "COG"},
63
+ {"name": "Congo (Democratic Republic of the)", "alpha-3": "COD"},
64
+ {"name": "Costa Rica", "alpha-3": "CRI"},
65
+ {"name": "Croatia", "alpha-3": "HRV"},
66
+ {"name": "Cuba", "alpha-3": "CUB"},
67
+ {"name": "Cyprus", "alpha-3": "CYP"},
68
+ {"name": "Czechia", "alpha-3": "CZE"},
69
+ {"name": "Denmark", "alpha-3": "DNK"},
70
+ {"name": "Djibouti", "alpha-3": "DJI"},
71
+ {"name": "Dominica", "alpha-3": "DMA"},
72
+ {"name": "Dominican Republic", "alpha-3": "DOM"},
73
+ {"name": "Ecuador", "alpha-3": "ECU"},
74
+ {"name": "Egypt", "alpha-3": "EGY"},
75
+ {"name": "El Salvador", "alpha-3": "SLV"},
76
+ {"name": "Equatorial Guinea", "alpha-3": "GNQ"},
77
+ {"name": "Eritrea", "alpha-3": "ERI"},
78
+ {"name": "Estonia", "alpha-3": "EST"},
79
+ {"name": "Eswatini", "alpha-3": "SWZ"},
80
+ {"name": "Ethiopia", "alpha-3": "ETH"},
81
+ {"name": "Fiji", "alpha-3": "FJI"},
82
+ {"name": "Finland", "alpha-3": "FIN"},
83
+ {"name": "France", "alpha-3": "FRA"},
84
+ {"name": "Gabon", "alpha-3": "GAB"},
85
+ {"name": "Gambia", "alpha-3": "GMB"},
86
+ {"name": "Georgia", "alpha-3": "GEO"},
87
+ {"name": "Germany", "alpha-3": "DEU"},
88
+ {"name": "Ghana", "alpha-3": "GHA"},
89
+ {"name": "Greece", "alpha-3": "GRC"},
90
+ {"name": "Grenada", "alpha-3": "GRD"},
91
+ {"name": "Guatemala", "alpha-3": "GTM"},
92
+ {"name": "Guinea", "alpha-3": "GIN"},
93
+ {"name": "Guinea-Bissau", "alpha-3": "GNB"},
94
+ {"name": "Guyana", "alpha-3": "GUY"},
95
+ {"name": "Haiti", "alpha-3": "HTI"},
96
+ {"name": "Honduras", "alpha-3": "HND"},
97
+ {"name": "Hungary", "alpha-3": "HUN"},
98
+ {"name": "Iceland", "alpha-3": "ISL"},
99
+ {"name": "India", "alpha-3": "IND"},
100
+ {"name": "Indonesia", "alpha-3": "IDN"},
101
+ {"name": "Iran", "alpha-3": "IRN"},
102
+ {"name": "Iraq", "alpha-3": "IRQ"},
103
+ {"name": "Ireland", "alpha-3": "IRL"},
104
+ {"name": "Israel", "alpha-3": "ISR"},
105
+ {"name": "Italy", "alpha-3": "ITA"},
106
+ {"name": "Jamaica", "alpha-3": "JAM"},
107
+ {"name": "Japan", "alpha-3": "JPN"},
108
+ {"name": "Jordan", "alpha-3": "JOR"},
109
+ {"name": "Kazakhstan", "alpha-3": "KAZ"},
110
+ {"name": "Kenya", "alpha-3": "KEN"},
111
+ {"name": "Kiribati", "alpha-3": "KIR"},
112
+ {"name": "Korea (North)", "alpha-3": "PRK"},
113
+ {"name": "Korea (South)", "alpha-3": "KOR"},
114
+ {"name": "Kuwait", "alpha-3": "KWT"},
115
+ {"name": "Kyrgyzstan", "alpha-3": "KGZ"},
116
+ {"name": "Laos", "alpha-3": "LAO"},
117
+ {"name": "Latvia", "alpha-3": "LVA"},
118
+ {"name": "Lebanon", "alpha-3": "LBN"},
119
+ {"name": "Lesotho", "alpha-3": "LSO"},
120
+ {"name": "Liberia", "alpha-3": "LBR"},
121
+ {"name": "Libya", "alpha-3": "LBY"},
122
+ {"name": "Liechtenstein", "alpha-3": "LIE"},
123
+ {"name": "Lithuania", "alpha-3": "LTU"},
124
+ {"name": "Luxembourg", "alpha-3": "LUX"},
125
+ {"name": "Madagascar", "alpha-3": "MDG"},
126
+ {"name": "Malawi", "alpha-3": "MWI"},
127
+ {"name": "Malaysia", "alpha-3": "MYS"},
128
+ {"name": "Maldives", "alpha-3": "MDV"},
129
+ {"name": "Mali", "alpha-3": "MLI"},
130
+ {"name": "Malta", "alpha-3": "MLT"},
131
+ {"name": "Marshall Islands", "alpha-3": "MHL"},
132
+ {"name": "Mauritania", "alpha-3": "MRT"},
133
+ {"name": "Mauritius", "alpha-3": "MUS"},
134
+ {"name": "Mexico", "alpha-3": "MEX"},
135
+ {"name": "Micronesia", "alpha-3": "FSM"},
136
+ {"name": "Moldova", "alpha-3": "MDA"},
137
+ {"name": "Monaco", "alpha-3": "MCO"},
138
+ {"name": "Mongolia", "alpha-3": "MNG"},
139
+ {"name": "Montenegro", "alpha-3": "MNE"},
140
+ {"name": "Morocco", "alpha-3": "MAR"},
141
+ {"name": "Mozambique", "alpha-3": "MOZ"},
142
+ {"name": "Myanmar", "alpha-3": "MMR"},
143
+ {"name": "Namibia", "alpha-3": "NAM"},
144
+ {"name": "Nauru", "alpha-3": "NRU"},
145
+ {"name": "Nepal", "alpha-3": "NPL"},
146
+ {"name": "Netherlands", "alpha-3": "NLD"},
147
+ {"name": "New Zealand", "alpha-3": "NZL"},
148
+ {"name": "Nicaragua", "alpha-3": "NIC"},
149
+ {"name": "Niger", "alpha-3": "NER"},
150
+ {"name": "Nigeria", "alpha-3": "NGA"},
151
+ {"name": "North Macedonia", "alpha-3": "MKD"},
152
+ {"name": "Norway", "alpha-3": "NOR"},
153
+ {"name": "Oman", "alpha-3": "OMN"},
154
+ {"name": "Pakistan", "alpha-3": "PAK"},
155
+ {"name": "Palau", "alpha-3": "PLW"},
156
+ {"name": "Panama", "alpha-3": "PAN"},
157
+ {"name": "Papua New Guinea", "alpha-3": "PNG"},
158
+ {"name": "Paraguay", "alpha-3": "PRY"},
159
+ {"name": "Peru", "alpha-3": "PER"},
160
+ {"name": "Philippines", "alpha-3": "PHL"},
161
+ {"name": "Poland", "alpha-3": "POL"},
162
+ {"name": "Portugal", "alpha-3": "PRT"},
163
+ {"name": "Qatar", "alpha-3": "QAT"},
164
+ {"name": "Romania", "alpha-3": "ROU"},
165
+ {"name": "Russia", "alpha-3": "RUS"},
166
+ {"name": "Rwanda", "alpha-3": "RWA"},
167
+ {"name": "Saint Kitts and Nevis", "alpha-3": "KNA"},
168
+ {"name": "Saint Lucia", "alpha-3": "LCA"},
169
+ {"name": "Saint Vincent and the Grenadines", "alpha-3": "VCT"},
170
+ {"name": "Samoa", "alpha-3": "WSM"},
171
+ {"name": "San Marino", "alpha-3": "SMR"},
172
+ {"name": "Sao Tome and Principe", "alpha-3": "STP"},
173
+ {"name": "Saudi Arabia", "alpha-3": "SAU"},
174
+ {"name": "Senegal", "alpha-3": "SEN"},
175
+ {"name": "Serbia", "alpha-3": "SRB"},
176
+ {"name": "Seychelles", "alpha-3": "SYC"},
177
+ {"name": "Sierra Leone", "alpha-3": "SLE"},
178
+ {"name": "Singapore", "alpha-3": "SGP"},
179
+ {"name": "Slovakia", "alpha-3": "SVK"},
180
+ {"name": "Slovenia", "alpha-3": "SVN"},
181
+ {"name": "Solomon Islands", "alpha-3": "SLB"},
182
+ {"name": "Somalia", "alpha-3": "SOM"},
183
+ {"name": "South Africa", "alpha-3": "ZAF"},
184
+ {"name": "South Sudan", "alpha-3": "SSD"},
185
+ {"name": "Spain", "alpha-3": "ESP"},
186
+ {"name": "Sri Lanka", "alpha-3": "LKA"},
187
+ {"name": "Sudan", "alpha-3": "SDN"},
188
+ {"name": "Suriname", "alpha-3": "SUR"},
189
+ {"name": "Sweden", "alpha-3": "SWE"},
190
+ {"name": "Switzerland", "alpha-3": "CHE"},
191
+ {"name": "Syria", "alpha-3": "SYR"},
192
+ {"name": "Taiwan", "alpha-3": "TWN"},
193
+ {"name": "Tajikistan", "alpha-3": "TJK"},
194
+ {"name": "Tanzania", "alpha-3": "TZA"},
195
+ {"name": "Thailand", "alpha-3": "THA"},
196
+ {"name": "Timor-Leste", "alpha-3": "TLS"},
197
+ {"name": "Togo", "alpha-3": "TGO"},
198
+ {"name": "Tonga", "alpha-3": "TON"},
199
+ {"name": "Trinidad and Tobago", "alpha-3": "TTO"},
200
+ {"name": "Tunisia", "alpha-3": "TUN"},
201
+ {"name": "Turkey", "alpha-3": "TUR"},
202
+ {"name": "Turkmenistan", "alpha-3": "TKM"},
203
+ {"name": "Tuvalu", "alpha-3": "TUV"},
204
+ {"name": "Uganda", "alpha-3": "UGA"},
205
+ {"name": "Ukraine", "alpha-3": "UKR"},
206
+ {"name": "United Arab Emirates", "alpha-3": "ARE"},
207
+ {"name": "United Kingdom", "alpha-3": "GBR"},
208
+ {"name": "United States of America", "alpha-3": "USA"},
209
+ {"name": "Uruguay", "alpha-3": "URY"},
210
+ {"name": "Uzbekistan", "alpha-3": "UZB"},
211
+ {"name": "Vanuatu", "alpha-3": "VUT"},
212
+ {"name": "Vatican City", "alpha-3": "VAT"},
213
+ {"name": "Venezuela", "alpha-3": "VEN"},
214
+ {"name": "Vietnam", "alpha-3": "VNM"},
215
+ {"name": "Yemen", "alpha-3": "YEM"},
216
+ {"name": "Zambia", "alpha-3": "ZMB"},
217
+ {"name": "Zimbabwe", "alpha-3": "ZWE"}
218
+ ]
main.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os
3
+ import sys
4
+ from tqdm import tqdm
5
+ from src.extractor import PassportExtractor
6
+ from src.formats import export_to_spreadsheet
7
+ from src.validators import validate_passport_data
8
+ from src.utils import setup_logger
9
+ from config.settings import ALLOWED_EXTENSIONS
10
+
11
+ # Setup Logger
12
+ logger = setup_logger()
13
+
14
+ def is_valid_file(filename):
15
+ ext = os.path.splitext(filename)[1].lower()
16
+ return ext in ALLOWED_EXTENSIONS
17
+
18
+ def main():
19
+ parser = argparse.ArgumentParser(description="Passport OCR Tool - Extract data from passport images/PDFs.")
20
+ parser.add_argument('--input', '-i', required=True, help="Path to input file or directory")
21
+ parser.add_argument('--output', '-o', default='passport_data', help="Output filename (without extension)")
22
+ parser.add_argument('--format', '-f', choices=['excel', 'csv'], default='excel', help="Output format (excel or csv)")
23
+ parser.add_argument('--gpu', action='store_true', help="Use GPU for OCR")
24
+
25
+ args = parser.parse_args()
26
+
27
+ input_path = os.path.abspath(args.input)
28
+ output_path = os.path.abspath(os.path.join('data', 'output', args.output)) # Default to data/output if relative
29
+
30
+ # Adjust output path if user provided a specific path
31
+ if os.path.dirname(args.output):
32
+ output_path = os.path.abspath(args.output)
33
+
34
+ if not os.path.exists(input_path):
35
+ logger.error(f"Input path does not exist: {input_path}")
36
+ sys.exit(1)
37
+
38
+ files_to_process = []
39
+ if os.path.isfile(input_path):
40
+ if is_valid_file(input_path):
41
+ files_to_process.append(input_path)
42
+ else:
43
+ logger.error(f"Unsupported file format: {input_path}")
44
+ sys.exit(1)
45
+ else:
46
+ for root, _, files in os.walk(input_path):
47
+ for file in files:
48
+ if is_valid_file(file):
49
+ files_to_process.append(os.path.join(root, file))
50
+
51
+ if not files_to_process:
52
+ logger.warning("No valid files found to process.")
53
+ sys.exit(0)
54
+
55
+ logger.info(f"Found {len(files_to_process)} files to process.")
56
+
57
+ # Initialize Extractor
58
+ extractor = PassportExtractor(use_gpu=args.gpu)
59
+
60
+ extracted_results = []
61
+
62
+ # Process files
63
+ for file_path in tqdm(files_to_process, desc="Processing files"):
64
+ ext = os.path.splitext(file_path)[1].lower()
65
+
66
+ try:
67
+ if ext == '.pdf':
68
+ results = extractor.process_pdf(file_path)
69
+ extracted_results.extend(results)
70
+ else:
71
+ result = extractor.get_data(file_path)
72
+ if result:
73
+ extracted_results.append(result)
74
+ except Exception as e:
75
+ logger.error(f"Failed to process {file_path}: {e}")
76
+
77
+ # Validate and Summarize
78
+ valid_count = 0
79
+ for res in extracted_results:
80
+ errors = validate_passport_data(res)
81
+ if not errors:
82
+ valid_count += 1
83
+ else:
84
+ res['validation_errors'] = "; ".join(errors)
85
+ # logger.warning(f"Validation issues for {res.get('source_file')}: {errors}")
86
+
87
+ logger.info(f"Processing complete. Extracted {len(extracted_results)} records ({valid_count} valid).")
88
+
89
+ # Export
90
+ if extracted_results:
91
+ success = export_to_spreadsheet(extracted_results, output_path, format=args.format)
92
+ if success:
93
+ logger.info(f"Successfully saved data to {output_path}.{args.format if not output_path.endswith(args.format) else ''}")
94
+ else:
95
+ logger.warning("No data extracted.")
96
+
97
+ if __name__ == "__main__":
98
+ main()
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ passporteye>=2.1.0
2
+ easyocr>=1.6.0
3
+ matplotlib>=3.5.0
4
+ opencv-python>=4.5.0
5
+ python-dateutil>=2.8.0
6
+ pdf2image>=1.16.0
7
+ pandas>=1.3.0
8
+ openpyxl>=3.0.0
9
+ tqdm>=4.60.0
10
+ Pillow>=9.0.0
11
+ numpy>=1.21.0
12
+ streamlit>=1.25.0
13
+ watchdog>=3.0.0
14
+ pyngrok
setup.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="passport-ocr-tool",
5
+ version="1.0.0",
6
+ description="A tool to extract data from passports (Images/PDFs) and export to Excel/CSV.",
7
+ author="Your Name",
8
+ packages=find_packages(),
9
+ install_requires=[
10
+ 'passporteye',
11
+ 'easyocr',
12
+ 'matplotlib',
13
+ 'opencv-python',
14
+ 'python-dateutil',
15
+ 'pdf2image',
16
+ 'pandas',
17
+ 'openpyxl',
18
+ 'tqdm',
19
+ 'Pillow',
20
+ 'numpy'
21
+ ],
22
+ entry_points={
23
+ 'console_scripts': [
24
+ 'passport-ocr=main:main',
25
+ ],
26
+ },
27
+ python_requires='>=3.7',
28
+ )
share_app.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import threading
4
+ from pyngrok import ngrok
5
+
6
+ def run_streamlit():
7
+ os.system("python3 -m streamlit run app.py")
8
+
9
+ def start_ngrok():
10
+ print("Starting Streamlit app...")
11
+ # Give streamlit a moment to start
12
+ time.sleep(5)
13
+
14
+ try:
15
+ # Open a HTTP tunnel on the default port 8501
16
+ public_url = ngrok.connect(8501).public_url
17
+ print("\n" + "="*60)
18
+ print(f" \033[92m>>> YOUR LIVE LINK: {public_url} <<<\033[0m")
19
+ print(" Share this link with your client. The app must stay running.")
20
+ print("="*60 + "\n")
21
+
22
+ # Keep the script running
23
+ input("Press Enter to exit...\n")
24
+ except Exception as e:
25
+ print(f"\nError starting ngrok: {e}")
26
+ print("\nNOTE: You may need a free ngrok account.")
27
+ print("1. Go to https://dashboard.ngrok.com/signup")
28
+ print("2. Get your Authtoken")
29
+ print("3. Run: ngrok config add-authtoken <your_token>")
30
+
31
+ if __name__ == "__main__":
32
+ # Start Streamlit in a separate thread
33
+ thread = threading.Thread(target=run_streamlit)
34
+ thread.daemon = True
35
+ thread.start()
36
+
37
+ start_ngrok()
src/__init__.py ADDED
File without changes
src/extractor.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import cv2
3
+ import numpy as np
4
+ import easyocr
5
+ import warnings
6
+ import ssl
7
+ from passporteye import read_mrz
8
+ from pdf2image import convert_from_path
9
+ from PIL import Image
10
+ import string as st
11
+
12
+ # Fix for SSL certificate errors on Mac when downloading EasyOCR models
13
+ try:
14
+ _create_unverified_https_context = ssl._create_unverified_context
15
+ except AttributeError:
16
+ pass
17
+ else:
18
+ ssl._create_default_https_context = _create_unverified_https_context
19
+
20
+ from src.utils import (
21
+ clean_string,
22
+ clean_mrz_line,
23
+ parse_date,
24
+ get_country_name,
25
+ get_sex,
26
+ setup_logger
27
+ )
28
+ from config.settings import USE_GPU, OCR_LANGUAGES, TEMP_DIR
29
+
30
+ # Suppress warnings
31
+ warnings.filterwarnings('ignore')
32
+
33
+ logger = setup_logger(__name__)
34
+
35
+ class PassportExtractor:
36
+ def __init__(self, use_gpu=USE_GPU, languages=None):
37
+ self.languages = languages if languages else OCR_LANGUAGES
38
+
39
+ # Set model storage directory to project/data/models to avoid permission issues
40
+ base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
41
+ model_dir = os.path.join(base_dir, 'data', 'models')
42
+ os.makedirs(model_dir, exist_ok=True)
43
+
44
+ logger.info(f"Initializing EasyOCR Reader (GPU={use_gpu})...")
45
+ self.reader = easyocr.Reader(self.languages, gpu=use_gpu, model_storage_directory=model_dir)
46
+ logger.info("EasyOCR Reader initialized.")
47
+
48
+ def extract_mrz_from_roi(self, img_path):
49
+ """
50
+ Extracts MRZ lines using PassportEye to find ROI, then EasyOCR to read text.
51
+ Returns (line1, line2, mrz_object).
52
+ """
53
+ try:
54
+ # Analyze image with PassportEye
55
+ mrz = read_mrz(img_path, save_roi=True)
56
+
57
+ if not mrz:
58
+ logger.warning(f"PassportEye failed to detect MRZ in {img_path}")
59
+ return None, None, None
60
+
61
+ # Get ROI (Region of Interest)
62
+ roi = mrz.aux['roi']
63
+
64
+ # Ensure ROI is uint8 for OpenCV
65
+ if roi.dtype != np.uint8:
66
+ if roi.max() <= 1.0:
67
+ roi = (roi * 255).astype(np.uint8)
68
+ else:
69
+ roi = roi.astype(np.uint8)
70
+
71
+ # Preprocess ROI for EasyOCR
72
+ # Original code: saved to tmp.png (gray), read back, resized to (1110, 140)
73
+ # We will try to do this in memory using OpenCV
74
+
75
+ # roi is likely a numpy array (H, W) or (H, W, C). PassportEye usually returns grayscale for ROI?
76
+ # Let's normalize to BGR for OpenCV consistency if needed, or keep grayscale.
77
+
78
+ # Resize to improve OCR accuracy as per original code logic
79
+ # Note: (1110, 140) is the target size (Width, Height)
80
+ img_resized = cv2.resize(roi, (1110, 140))
81
+
82
+ # Define allowed characters for MRZ
83
+ allow = st.ascii_letters + st.digits + "< "
84
+
85
+ # Run EasyOCR
86
+ code = self.reader.readtext(img_resized, detail=0, allowlist=allow)
87
+
88
+ if len(code) < 2:
89
+ logger.warning(f"EasyOCR found fewer than 2 lines in ROI for {img_path}")
90
+ return None, None, mrz
91
+
92
+ line1 = clean_mrz_line(code[0])
93
+ line2 = clean_mrz_line(code[1])
94
+
95
+ # Correct sex at index 20 of line 2 if available from mrz object
96
+ # MRZ object might have parsed it correctly even if EasyOCR missed it
97
+ if mrz.sex and len(line2) > 20:
98
+ l2_list = list(line2)
99
+ l2_list[20] = mrz.sex
100
+ line2 = "".join(l2_list)
101
+
102
+ return line1, line2, mrz
103
+
104
+ except Exception as e:
105
+ logger.error(f"Error in extract_mrz_from_roi: {e}")
106
+ return None, None, None
107
+
108
+ def get_data(self, img_path):
109
+ """
110
+ Extracts full passport data from an image file.
111
+ Returns a dictionary of extracted fields.
112
+ """
113
+ if not os.path.exists(img_path):
114
+ logger.error(f"File not found: {img_path}")
115
+ return None
116
+
117
+ line1, line2, mrz = self.extract_mrz_from_roi(img_path)
118
+
119
+ if mrz is None:
120
+ return None
121
+
122
+ data = {}
123
+ # Use PassportEye's parsing where possible, fallback/clean as needed
124
+ data['surname'] = mrz.surname.replace("<<", " ").strip().upper() if mrz.surname else ""
125
+ data['name'] = mrz.names.replace("<<", " ").strip().upper() if mrz.names else ""
126
+ data['sex'] = get_sex(mrz.sex)
127
+ data['date_of_birth'] = parse_date(mrz.date_of_birth) if mrz.date_of_birth else ""
128
+ data['nationality'] = get_country_name(mrz.nationality)
129
+ data['passport_type'] = clean_string(mrz.type)
130
+ data['passport_number'] = clean_string(mrz.number)
131
+ data['issuing_country'] = get_country_name(mrz.country)
132
+ data['expiration_date'] = parse_date(mrz.expiration_date)
133
+ data['personal_number'] = clean_string(mrz.personal_number)
134
+
135
+ # Construct full MRZ string
136
+ # Prefer the OCR'd lines if they exist, otherwise fallback?
137
+ # The original code returned (line1 or "") + (line2 or "")
138
+ data['mrz_full_string'] = (line1 or "") + (line2 or "")
139
+ data['source_file'] = os.path.basename(img_path)
140
+
141
+ return data
142
+
143
+ def process_pdf(self, pdf_path):
144
+ """
145
+ Converts PDF pages to images and extracts data from each.
146
+ Returns a list of data dictionaries.
147
+ """
148
+ extracted_data = []
149
+
150
+ # Ensure temp dir exists
151
+ os.makedirs(TEMP_DIR, exist_ok=True)
152
+
153
+ logger.info(f"Processing PDF: {pdf_path}")
154
+ try:
155
+ pages = convert_from_path(pdf_path)
156
+ except Exception as e:
157
+ logger.error(f"Failed to convert PDF {pdf_path}: {e}")
158
+ return []
159
+
160
+ for i, page in enumerate(pages):
161
+ temp_img_path = os.path.join(TEMP_DIR, f"temp_page_{i+1}.png")
162
+ page.save(temp_img_path, "PNG")
163
+
164
+ logger.info(f"Processing page {i+1}...")
165
+ result = self.get_data(temp_img_path)
166
+
167
+ # Special MRZ fix for PDF from original code
168
+ # It seems to re-clean the combined string.
169
+ if result and result.get("mrz_full_string"):
170
+ full_mrz = result["mrz_full_string"]
171
+ if len(full_mrz) >= 88: # 44 * 2
172
+ l1 = full_mrz[:44]
173
+ l2 = full_mrz[44:]
174
+ l1 = clean_mrz_line(l1)
175
+ l2 = clean_mrz_line(l2)
176
+ result["mrz_full_string"] = l1 + l2
177
+
178
+ if result:
179
+ extracted_data.append(result)
180
+
181
+ # Cleanup temp file
182
+ if os.path.exists(temp_img_path):
183
+ os.remove(temp_img_path)
184
+
185
+ return extracted_data
src/formats.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import os
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ def export_to_spreadsheet(data_list, output_file, format='excel'):
8
+ """
9
+ Exports a list of dictionaries to a spreadsheet (Excel or CSV).
10
+ """
11
+ if not data_list:
12
+ logger.warning("No data to export.")
13
+ return False
14
+
15
+ try:
16
+ df = pd.DataFrame(data_list)
17
+
18
+ # Ensure output directory exists
19
+ os.makedirs(os.path.dirname(output_file), exist_ok=True)
20
+
21
+ if format.lower() == 'excel' or output_file.endswith('.xlsx'):
22
+ if not output_file.endswith('.xlsx'):
23
+ output_file += '.xlsx'
24
+ df.to_excel(output_file, index=False)
25
+ logger.info(f"Data exported to {output_file}")
26
+
27
+ elif format.lower() == 'csv' or output_file.endswith('.csv'):
28
+ if not output_file.endswith('.csv'):
29
+ output_file += '.csv'
30
+ df.to_csv(output_file, index=False)
31
+ logger.info(f"Data exported to {output_file}")
32
+
33
+ else:
34
+ logger.error(f"Unsupported format: {format}")
35
+ return False
36
+
37
+ return True
38
+
39
+ except Exception as e:
40
+ logger.error(f"Failed to export data: {e}")
41
+ return False
src/utils.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import string as st
2
+ from dateutil import parser
3
+ import logging
4
+ import sys
5
+ from config.settings import COUNTRY_CODES
6
+
7
+ def setup_logger(name=__name__):
8
+ """Sets up a logger with standard formatting."""
9
+ logger = logging.getLogger(name)
10
+ if not logger.handlers:
11
+ handler = logging.StreamHandler(sys.stdout)
12
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
13
+ handler.setFormatter(formatter)
14
+ logger.addHandler(handler)
15
+ logger.setLevel(logging.INFO)
16
+ return logger
17
+
18
+ logger = setup_logger(__name__)
19
+
20
+ def parse_date(date_obj, iob=True):
21
+ """Parses a date object or string into DD/MM/YYYY format."""
22
+ try:
23
+ date_str = date_obj.isoformat() if hasattr(date_obj, 'isoformat') else str(date_obj)
24
+ # Passport dates are often YYMMDD, but dateutil usually handles it if formatted correctly.
25
+ # However, MRZ dates are tricky. PassportEye usually returns YYMMDD.
26
+ # parser.parse might struggle with 2-digit years without context, but let's trust existing logic first.
27
+ date = parser.parse(date_str, yearfirst=True).date()
28
+ return date.strftime('%d/%m/%Y')
29
+ except (ValueError, TypeError) as e:
30
+ logger.debug(f"Date parsing failed for {date_obj}: {e}")
31
+ return str(date_obj)
32
+
33
+ def clean_string(text):
34
+ """Removes non-alphanumeric characters and converts to uppercase."""
35
+ if not text:
36
+ return ""
37
+ return ''.join(i for i in text if i.isalnum()).upper()
38
+
39
+ def clean_mrz_line(line: str) -> str:
40
+ """Fix bad spacing or bad OCR for MRZ lines."""
41
+ if not line:
42
+ return ""
43
+
44
+ line = line.upper().replace(" ", "")
45
+
46
+ # Remove accidental characters except allowed
47
+ allowed = set(st.ascii_uppercase + st.digits + "<")
48
+ line = "".join([c for c in line if c in allowed])
49
+
50
+ # Ensure 44 length (standard TD3 MRZ length)
51
+ # Note: TD1/TD2 might be different lengths (30 or 36), but this logic enforces 44.
52
+ # We will keep existing logic for consistency but be aware of other formats.
53
+ if len(line) < 44:
54
+ line += "<" * (44 - len(line))
55
+ return line[:44]
56
+
57
+ def get_country_name(country_code):
58
+ """Resolves 3-letter country code to full name."""
59
+ country_code = str(country_code).upper()
60
+ for c in COUNTRY_CODES:
61
+ if c['alpha-3'] == country_code:
62
+ return c['name'].upper()
63
+ return country_code
64
+
65
+ def get_sex(code):
66
+ """Standardizes sex code."""
67
+ code = str(code).upper() if code else ''
68
+ if code in ['M', 'F']:
69
+ return code
70
+ if code == '0':
71
+ return 'M' # Fallback based on existing logic
72
+ return code
src/validators.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+
3
+ def validate_passport_data(data):
4
+ """
5
+ Validates extracted passport data.
6
+ Returns a list of validation warnings/errors.
7
+ """
8
+ errors = []
9
+
10
+ if not data:
11
+ return ["No data to validate"]
12
+
13
+ # Check required fields
14
+ required_fields = ['surname', 'name', 'passport_number', 'nationality']
15
+ for field in required_fields:
16
+ if not data.get(field):
17
+ errors.append(f"Missing required field: {field}")
18
+
19
+ # Validate dates (basic check if they look like DD/MM/YYYY)
20
+ date_fields = ['date_of_birth', 'expiration_date']
21
+ for field in date_fields:
22
+ val = data.get(field)
23
+ if val:
24
+ try:
25
+ datetime.datetime.strptime(val, '%d/%m/%Y')
26
+ except ValueError:
27
+ errors.append(f"Invalid date format for {field}: {val} (expected DD/MM/YYYY)")
28
+
29
+ # Validate MRZ length roughly (should be around 88 chars for TD3)
30
+ mrz = data.get('mrz_full_string', '')
31
+ if len(mrz) < 80:
32
+ errors.append(f"MRZ string seems too short ({len(mrz)} chars)")
33
+
34
+ return errors
tests/test_extractor.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+ import os
3
+ from src.extractor import PassportExtractor
4
+
5
+ class TestPassportExtractor(unittest.TestCase):
6
+ def test_initialization(self):
7
+ """Test if the extractor can be initialized."""
8
+ try:
9
+ extractor = PassportExtractor(use_gpu=False)
10
+ self.assertIsNotNone(extractor.reader)
11
+ except Exception as e:
12
+ self.fail(f"Initialization failed: {e}")
13
+
14
+ def test_utils_import(self):
15
+ """Test if utils can be imported correctly."""
16
+ from src.utils import clean_string
17
+ self.assertEqual(clean_string("Hello 123!"), "HELLO123")
18
+
19
+ if __name__ == '__main__':
20
+ unittest.main()